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:
@@ -80,9 +80,9 @@ def _out(value: Decimal | None) -> int | float | None:
|
||||
|
||||
|
||||
# ── 파일 → 시트 표(줄마다 값 목록) ────────────────────────────────────────
|
||||
def _sheets(name: str, data: bytes) -> dict[str, list[list[Any]]]:
|
||||
"""시트 이름 → 줄 목록 — 일위대가 · 총괄표 · 단가산출만 읽음."""
|
||||
wanted = (SHEET, SUMMARY, hopyo_basis.SHEET)
|
||||
def _sheets(name: str, data: bytes, wanted: tuple[str, ...] = ()) -> dict[str, list[list[Any]]]:
|
||||
"""시트 이름 → 줄 목록 — 일위대가 · 총괄표 · 단가산출만 읽음(`wanted` = 더 읽을 시트)."""
|
||||
keep = (SHEET, SUMMARY, hopyo_basis.SHEET, *wanted)
|
||||
if not data:
|
||||
raise ExcelError("빈 파일")
|
||||
if b"DRMONE" in data[:256] and data[:4] not in (b"PK\x03\x04", _OLE):
|
||||
@@ -93,11 +93,11 @@ def _sheets(name: str, data: bytes) -> dict[str, list[list[Any]]]:
|
||||
|
||||
book = openpyxl.load_workbook(io.BytesIO(data), read_only=True, data_only=True)
|
||||
if hopyo_stmate.is_stmate([t.strip() for t in book.sheetnames]):
|
||||
wanted = hopyo_stmate.SHEETS
|
||||
keep = (*hopyo_stmate.SHEETS, *wanted)
|
||||
out = {
|
||||
ws.title.strip(): [list(row) for row in ws.iter_rows(values_only=True)]
|
||||
for ws in book.worksheets
|
||||
if ws.title.strip() in wanted
|
||||
if ws.title.strip() in keep
|
||||
}
|
||||
book.close()
|
||||
return out
|
||||
@@ -107,7 +107,7 @@ def _sheets(name: str, data: bytes) -> dict[str, list[list[Any]]]:
|
||||
return {
|
||||
sh.name.strip(): [sh.row_values(r) for r in range(sh.nrows)]
|
||||
for sh in book.sheets()
|
||||
if sh.name.strip() in wanted
|
||||
if sh.name.strip() in keep
|
||||
}
|
||||
except ExcelError:
|
||||
raise
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""779-1 격자 세 보기 — 여덟 실무 파일: 보기 셋(두 꼴 같은 역할) · 목록 짝 100 % · 줄 행 대응.
|
||||
|
||||
`M02_Hopyo_Grid.read` 응답 `보기 · 원본 · 목록 · 줄` — 서버 몫만(화면 없음).
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hx
|
||||
from M02_MasterTemplete import M02_Hopyo_Grid as hg
|
||||
from resources.tester.helper_m02_hopyo_golden import FILES
|
||||
|
||||
warnings.filterwarnings("ignore", category=UserWarning, module="openpyxl")
|
||||
ROLES = {"일위대가표", "단가산출표", "단가산출상세"}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", params=sorted(FILES))
|
||||
def one(request):
|
||||
path = FILES[request.param]
|
||||
data = path.read_bytes()
|
||||
parsed = hx.read_book(path.name, data)
|
||||
return parsed, hg.read(path.name, data, parsed)
|
||||
|
||||
|
||||
def test_보기_셋(one):
|
||||
parsed, grid = one
|
||||
assert set(grid["보기"]) == set(grid["원본"]) == ROLES
|
||||
names = {s["id"]: s["이름"] for s in grid["책"]["시트"]}
|
||||
assert [names[grid["보기"][r]] for r in ("일위대가표", "단가산출표", "단가산출상세")] == [
|
||||
grid["원본"][r] for r in ("일위대가표", "단가산출표", "단가산출상세")
|
||||
]
|
||||
assert list(names) == [grid["보기"][r] for r in ("일위대가표", "단가산출표", "단가산출상세")]
|
||||
assert len(grid["책"]["시트"]) == 3
|
||||
assert parsed["꼴"] in ("엑셀꼴", "STmate")
|
||||
|
||||
|
||||
def test_목록_짝_100(one):
|
||||
parsed, grid = one
|
||||
for key in ("호표", "단산"):
|
||||
got = grid["목록"][key]
|
||||
assert sorted(x["번"] for x in got) == sorted(x["번"] for x in parsed[key]), key
|
||||
rows = [x["행"] for x in got]
|
||||
assert len(set(rows)) == len(rows) and min(rows) >= 4
|
||||
|
||||
|
||||
def test_줄_행_대응(one):
|
||||
parsed, grid = one
|
||||
lines = [(h["번"], x) for h in parsed["호표"] for x in h["줄"]]
|
||||
assert [(x["번"], x["차례"], x["행"]) for x in grid["줄"]] == [
|
||||
(n, x["차례"], x["행"]) for n, x in lines
|
||||
]
|
||||
span = {h["번"]: h["행"] for h in parsed["호표"]}
|
||||
block = {b["번"]: b["행"] for b in parsed["단산"]}
|
||||
for one_, (no, line) in zip(grid["줄"], lines, strict=True):
|
||||
assert span[no][0] < one_["행"] <= span[no][1]
|
||||
ref = line.get("참조") or {}
|
||||
if ref.get("종류") == "단산" and ref["번"] in block:
|
||||
assert one_["단산"] == {"번": ref["번"], "행": block[ref["번"]]}
|
||||
else:
|
||||
assert "단산" not in one_
|
||||
if ref.get("종류") == "일위" and ref.get("번") in span:
|
||||
assert one_["일위"] == {"번": ref["번"], "행": span[ref["번"]]}
|
||||
else:
|
||||
assert "일위" not in one_
|
||||
assert any("단산" in x for x in grid["줄"])
|
||||
|
||||
|
||||
def test_두_꼴_같은_역할_셋():
|
||||
"""역할마다 엑셀꼴 · STmate 시트 이름 하나씩 · 탭 차례 = 역할 차례(꼴별로)."""
|
||||
assert {r: len(n) for r, n in hg.ROLES.items()} == dict.fromkeys(ROLES, 2)
|
||||
first = [hg.SHEETS.index(n[0]) for n in hg.ROLES.values()]
|
||||
second = [hg.SHEETS.index(n[1]) for n in hg.ROLES.values()]
|
||||
assert first == sorted(first) and second == sorted(second)
|
||||
@@ -38,7 +38,7 @@ def _sheet(grid: dict, name: str) -> dict:
|
||||
def test_격자_모양(north):
|
||||
book = north["책"]
|
||||
assert (book["종류"], book["판"], book["활성"]) == ("통합문서", 1, "s1")
|
||||
assert [s["이름"] for s in book["시트"]] == ["일위대가", "단가산출"]
|
||||
assert [s["이름"] for s in book["시트"]] == ["일위대가", "단가산출총괄표", "단가산출"]
|
||||
sheet = _sheet(north, "일위대가")
|
||||
assert len(sheet["병합"]) == 11 and "B1:O2" in sheet["병합"]
|
||||
assert len(sheet["열"]) == 15 and sheet["열"]["C"]["폭"] == round(7208 / 256, 2)
|
||||
@@ -143,7 +143,11 @@ def test_STmate_격자():
|
||||
data = path.read_bytes()
|
||||
parsed = hx.read_book(path.name, data)
|
||||
grid = hg.read(path.name, data, parsed)
|
||||
assert [s["이름"] for s in grid["책"]["시트"]] == ["일위대가표", "단가산출근거"]
|
||||
assert [s["이름"] for s in grid["책"]["시트"]] == [
|
||||
"일위대가표",
|
||||
"단가산출근거목록표",
|
||||
"단가산출근거",
|
||||
]
|
||||
cells = _sheet(grid, "일위대가표")["칸"]
|
||||
one = grid["호표"][0]
|
||||
assert one == {"번": 1, "이름": "녹생토(종자배합)", "행": [5, 10]}
|
||||
|
||||
Reference in New Issue
Block a user