feat(779-1): 격자 세 보기 — 목록표 둘 · 보기 · 원본 · 목록 · 줄 행 대응

- Grid SHEETS 에 단가산출총괄표 · 단가산출근거목록표 추가(탭 차례 일위대가표 · 단가산출표 · 상세)
- 응답에 보기 {역할: 시트 id} · 원본 · 목록 {호표, 단산: [{번, 행}]} · 줄 [{번, 차례, 행, 단산?, 일위?}] 더함(옛 칸 그대로)
- STmate 목록은 숨은 COD 코드로 짝(차례 번 ≠ 블록 번)
- Excel._sheets 에 wanted 인자만(기본 그대로)
- 시험: test_779_grid_views 새로(여덟 파일) · test_m02_hopyo_grid 시트 목록 기대값 세 장으로 고침(탭이 늘어서)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 08:46:50 +09:00
co-authored by Claude Sonnet 5.5
parent 4e0ea28aec
commit 1c1d756a1e
4 changed files with 172 additions and 15 deletions
+86 -7
View File
@@ -8,12 +8,18 @@ xls = xlrd `formatting_info` — 값 · 병합 · 열 폭(÷256 글자) ·
숫자 형식) · 틀고정(파일 값 · 없으면 머리 4행).
xlsx = 값 · 열 폭(`<cols>`) · 넓은 시트는 머리 글(앞 4 줄) 마지막 열까지 ·
STmate 숨은 칸 글(「_x0005_`BDCOD…」) 뺌.
결과 `{책, 호표: [{번, 이름, 행}], 단산: [{번, 이름, 행}]}` — 행 = 엑셀 1부터 [시작, 끝].
결과 `{책, 호표: [{번, 이름, 행}], 단산: [{번, 이름, 행}], 보기, 원본, 목록, 줄}` —
행 = 엑셀 1부터 [시작, 끝].
세 보기(779-1) = 탭 차례 일위대가표 · 단가산출표(목록표 — 엑셀꼴 단가산출총괄표 · STmate
단가산출근거목록표) · 단가산출상세 · `보기 {역할: 시트 id}` · `원본 {역할: 원본 시트 이름}` ·
`목록 {호표: [{번, 행}], 단산: [{번, 행}]}`(목록 시트의 행 1부터) ·
`줄 [{번, 차례, 행, 단산?: {번, 행: [시작, 끝]}, 일위?: {번, 행: [시작, 끝]}}]`.
"""
from __future__ import annotations
import io
import re
import zipfile
from typing import Any
@@ -24,7 +30,23 @@ from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel
from M02_MasterTemplete import M02_Hopyo_Stmate as hopyo_stmate
SHEETS = (hopyo_excel.SHEET, hopyo_basis.SHEET, hopyo_stmate.SHEET, hopyo_stmate.CALC)
LIST_E, LIST_S, HOPYO_S = "단가산출총괄표", hopyo_stmate.LISTS[0], "일위대가목록표"
SHEETS = (
hopyo_excel.SHEET,
LIST_E,
hopyo_basis.SHEET,
hopyo_stmate.SHEET,
LIST_S,
hopyo_stmate.CALC,
)
ROLES = {
"일위대가표": (hopyo_excel.SHEET, hopyo_stmate.SHEET),
"단가산출표": (LIST_E, LIST_S),
"단가산출상세": (hopyo_basis.SHEET, hopyo_stmate.CALC),
}
_B, _D, _NO = re.compile(r"(B\d{5}|제\d+호표)"), re.compile(r"(D\d{5})"), re.compile(r"(\d+)")
_HOPYO = hopyo_excel._HOPYO
EXTRA = (LIST_E, hopyo_excel.SUMMARY, HOPYO_S) # 격자 몫으로 더 읽는 시트
HEAD_ROWS = 4
_HALIGN = {1: "left", 2: "center", 3: "right", 6: "centerContinuous", 7: "distributed"}
_VALIGN = {0: "top", 1: "center"}
@@ -213,9 +235,11 @@ def _xlsx_widths(data: bytes) -> dict[str, dict[str, dict[str, Any]]]:
return out
def _xlsx(name: str, data: bytes) -> dict[str, Any]:
def _xlsx(
name: str, data: bytes, found: dict[str, list[list[Any]]] | None = None
) -> dict[str, Any]:
# ponytail: xlsx 는 값 · 열 폭만(병합 · 서식은 openpyxl 비 read_only 로 무거움) — 필요하면 그때
found = hopyo_excel._sheets(name, data)
found = found or hopyo_excel._sheets(name, data, EXTRA)
widths = _xlsx_widths(data)
sheets = []
for at, title in enumerate((n for n in SHEETS if n in found), 1):
@@ -244,14 +268,69 @@ def _xlsx(name: str, data: bytes) -> dict[str, Any]:
return {"서식": [{}], "시트": sheets}
def _pairs(
rows: list[list[Any]] | None, col: int, rx: re.Pattern[str], hidden: bool = False
) -> list[tuple[str, int]]:
"""목록 시트 → [(열쇠, 행)] — `col` 칸 글이 `rx` 에 꼭 맞는 줄(엑셀 1부터) · `hidden` =
STmate 숨은 칸 「COD|…」 코드 먼저(목록 차례 번 ≠ 진짜 번)."""
out = []
for at, row in enumerate(rows or [], 1):
meta = hopyo_stmate._META.search(" ".join(map(str, row))) if hidden else None
cell = hopyo_excel._flat(hopyo_excel._text(row[col] if col < len(row) else None))
if meta:
out.append((meta.group(1), at))
elif hit := rx.fullmatch(cell):
out.append((hit.group(1), at))
return out
def _lines(parsed: dict[str, Any]) -> list[dict[str, Any]]:
"""일위대가 줄 → 엑셀 행 · 단산 줄 → 블록 행 · 일위 줄 → 하위 호표 행."""
block = {b["번"]: b["행"] for b in parsed.get("단산") or []}
hopyo = {h["번"]: h["행"] for h in parsed.get("호표") or []}
out = []
for h in parsed.get("호표") or []:
for line in h.get("줄") or []:
one: dict[str, Any] = {"번": h["번"], "차례": line["차례"], "행": line.get("행")}
ref = line.get("참조") or {}
for key, rows in (("단산", block), ("일위", hopyo)):
if ref.get("종류") == key and ref.get("번") in rows:
one[key] = {"번": ref["번"], "행": rows[ref["번"]]}
out.append(one)
return out
def _lists(rows: dict[str, Any], parsed: dict[str, Any]) -> dict[str, list[dict[str, int]]]:
"""목록 시트 행 ↔ 호표 번 · 단산(블록) 번."""
if parsed.get("꼴") == "STmate": # 숨은 코드(없으면 A 열) — 호표 = 코드 → 번 · 단산 = 코드 숫자
code = {h["코드"]: h["번"] for h in parsed.get("호표") or [] if h.get("코드")}
hopyo = [
(int(k[1:-2]) if k[0] == "제" else code.get(k), at)
for k, at in _pairs(rows.get(HOPYO_S), 0, _B, True)
]
calc = [(int(k[1:]), at) for k, at in _pairs(rows.get(LIST_S), 0, _D, True)]
else: # B 열 「제N호표」 · 「No」
hopyo = [(int(k), at) for k, at in _pairs(rows.get(hopyo_excel.SUMMARY), 1, _HOPYO)]
calc = [(int(k), at) for k, at in _pairs(rows.get(LIST_E), 1, _NO)]
pair = lambda got: [{"번": k, "행": at} for k, at in got if k is not None] # noqa: E731
return {"호표": pair(hopyo), "단산": pair(calc)}
def read(name: str, data: bytes, parsed: dict[str, Any]) -> dict[str, Any]:
"""엑셀 → `{책: Workbook, 호표, 단산}` — `parsed` = `M02_Hopyo_Excel.read_book` 결과
(행 범위)."""
"""엑셀 → `{책: Workbook, 호표, 단산, 보기, 원본, 목록, 줄}` — `parsed` =
`M02_Hopyo_Excel.read_book` 결과(행 범위)."""
xlsx = data[:4] == b"PK\x03\x04" or name.lower().endswith(".xlsx")
book = _xlsx(name, data) if xlsx else _xls(data)
rows = hopyo_excel._sheets(name, data, EXTRA) # ponytail: xls 는 서식 없이 한 번 더 읽음
book = _xlsx(name, data, rows) if xlsx else _xls(data)
keep = ("번", "이름", "행")
ids = {s["이름"]: s["id"] for s in book["시트"]}
on = {role: next((n for n in names if n in ids), None) for role, names in ROLES.items()}
return {
"책": {"종류": "통합문서", "판": 1, "열": "", **book, "활성": "s1"},
"호표": [{k: h.get(k) for k in keep} for h in parsed.get("호표") or []],
"단산": [{k: b.get(k) for k in keep} for b in parsed.get("단산") or []],
"보기": {role: ids[n] for role, n in on.items() if n},
"원본": {role: n for role, n in on.items() if n},
"목록": _lists(rows, parsed),
"줄": _lines(parsed),
}