feat(814-1 1·2): 올리기 파일 하나 · 확장자 파일 = 단가산출표 판 + 산출근거

- 같은 공사 엑셀 · 원파일 붙이기 걷음(keep · upload_native · same 지움) — 엑셀이든 .estx · .STC 든 하나만
- 원파일 = 새 HP · 산출근거 머리(코드 · 이름 · 규격 · 단위 · 저장 금액) 단가산출표 격자 · 일위대가 안 만듦(HeadMainDat 안 씀)
- 화면 = 일위대가 없으면 단가산출 목록 먼저 · 올림 안내 글 하나로
- 시험 810-2: 붙음 · EST 호표 → SC 기대 지움(요구 742-1 · 743-2 로 걷은 길) → 파일 하나 · 단가산출표 판 기대

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-10 14:14:21 +09:00
co-authored by Claude Opus 5.5
parent 4bd9e1484c
commit 56a3e0538f
5 changed files with 69 additions and 155 deletions
+38 -76
View File
@@ -7,9 +7,8 @@
STmate = 원문 그대로 `M02_San_Emit.stmate`(원파일 원문 = 엑셀 숨은 G 열 · 단가 = COSTN ·
끝전 = 옵션 SAN_TYP 1 비목버림 / 0 합버림_큰나머지 · 810-1 저장값 대조로 확인).
블록 번 = 엑셀 길과 같음(EST = 산근 표 차례 1부터 = 「단산N」 · STmate = 코드 숫자).
같은 공사 = 엑셀 산근 블록이 모두 원파일 블록과 금액 · 이름이 같음(`same`) — 둘 다 있으면 원파일.
일위대가(EST HeadMainDat) → 실무자료 호표 꼴(`hopyo`) · STmate 일위대가 구성은 못 읽음(BDQTY 난독화) →
호표 없음(엑셀을 같이 올리면 엑셀 호표 + 원파일 산근).
올리기 = 파일 하나(814-1 · 요구 742-1 — 같은 공사 엑셀 붙이기 걷음) · 원파일 = 단가산출표 판(`grid` ·
산출근거 머리) + 산출근거 · 일위대가 안 만듦(요구 743-2 · `hopyo` = 단산만).
"""
from __future__ import annotations
@@ -154,89 +153,52 @@ def inner(saved: dict, logic: dict) -> list[tuple[dict, int | None, str | None]]
return out
# ── 같은 공사 · 일위대가 ───────────────────────────────────────────────
def same(saved: dict, sheets: dict) -> bool:
"""엑셀 산근 블록이 모두 원파일 같은 번 블록과 계 금액(1 원 안)이 같음 = 같은 공사."""
from M02_MasterTemplete import M02_San_Book as book
have = numbers(saved)
kind = book.kind_of(sheets)
if not kind or (kind == "STmate") != (saved["꼴"] == "STmate"):
return False
if kind == "STmate":
excel = {int(b["코드"][1:]): b["머리"].get("C") for b in book.stmate_blocks(sheets)}
else:
excel = {int(n): t.get("T") for n, t in book.est_prices(sheets)[1].items()}
if not excel:
return False
for n, total in excel.items():
blk = have.get(int(n))
if blk is None or abs(float(total or 0) - float((blk.get("저장") or {}).get("T") or 0)) > 1:
return False
return True
GALLAE = {6: "노임", 5: "자재", 105: "기계", 104: "단가산출", 103: "호표", 21: "기타", 101: "기타"}
# ── 단가산출표 판(814-1 · 요구 742-2 · 743-2) ───────────────────────────
LIST = "단가산출표"
_LIST_HEAD = ("코드", "이름", "규격", "단위", "재료비", "노무비", "경비", "합계")
_LIST_COST = ("M", "L", "S", "T")
def hopyo(saved: dict) -> dict[str, Any]:
"""EST 일위대가(HeadMainDat) → 실무자료 읽기 꼴(`M02_Hopyo_Excel.read_book` 짝 · 호표 · 단산) ·
STmate = 호표 없음(구성 못 읽음)."""
blocks = numbers(saved)
names = {str(b.get("코드")): n for n, b in blocks.items()}
heads = {str(h.get("코드")): n for n, h in enumerate(saved.get("일위") or [], 1)}
"""원파일 → 실무자료 읽기 꼴 — 단산(산출근거 머리)만 · 일위대가 안 만듦(요구 743-2 · HeadMainDat 안 씀)."""
out: dict[str, Any] = {"꼴": "원파일", "시트": saved["꼴"], "호표": [], "검산": {"어긋남": []}}
out["단산"] = [
{"번": n, "이름": b.get("이름"), "단위": b.get("단위"), "금액": _cost(b.get("저장"))}
for n, b in blocks.items()
for n, b in numbers(saved).items()
]
if saved["꼴"] != "EST":
return out
for n, h in enumerate(saved.get("일위") or [], 1):
lines = []
for k, ln in enumerate(h.get("줄") or []):
link = ln.get("연결")
code = str((ln.get("자원") or {}).get("코드") or "")
ref = None
if link == 104 and code in names:
ref = {"종류": "단산", "번": names[code]}
elif link == 103 and code in heads:
ref = {"종류": "일위", "번": heads[code]}
kind = "%" if ln.get("연결") in (52, 58, 62) else GALLAE.get(link or 0, "기타")
if kind == "자재" and str(ln.get("단위") or "") == "인":
kind = "노임"
if not any((ln.get("단가") or {}).values()) and kind not in ("%",):
kind = "별도계상" if not ref else kind
lines.append(
{
"차례": k,
"이름": ln.get("품명") or "",
"규격": ln.get("규격") or "",
"수량": _dec(ln.get("수량")),
"단위": ln.get("단위") or "",
"단가": _cost(ln.get("단가")),
"금액": _cost(ln.get("저장")),
"비고": "",
"참조": ref,
"갈래": kind,
"행": ln.get("행"),
"코드": code or None,
}
)
out["호표"].append(
{
"번": n,
"이름": h.get("이름"),
"규격": h.get("규격") or "",
"단위": h.get("단위") or "",
"금액": _cost(h.get("저장")),
"행": [n, n],
"줄": lines,
}
)
return out
def grid(saved: dict) -> dict[str, Any]:
"""원파일 → 단가산출표 한 시트 격자(`M02_Hopyo_Grid.read` 꼴) — 산출근거 머리(코드 · 이름 · 규격 ·
단위 · 저장 금액) 한 줄씩 · 행 = 1 머리 + 블록 차례."""
cells: dict[str, dict] = {}
cols = "ABCDEFGH"
for c, head in zip(cols, _LIST_HEAD):
cells[f"{c}1"] = {"값": head}
rows = []
for at, (n, b) in enumerate(numbers(saved).items(), 2):
cost = b.get("저장") or {}
vals = [b.get("코드"), b.get("이름"), b.get("규격"), b.get("단위")]
vals += [_dec(cost.get(k)) for k in _LIST_COST]
for c, v in zip(cols, vals):
if v not in (None, ""):
cells[f"{c}{at}"] = {"값": v if isinstance(v, (int, float)) else str(v)}
rows.append({"번": n, "이름": b.get("이름"), "행": [at, at]})
sheet = {"id": "s1", "이름": LIST, "칸": cells, "틀고정": {"행": 1}}
sheet["열"] = {c: {"폭": w} for c, w in zip(cols, (10, 40, 18, 6, 12, 12, 12, 12))}
book = {"종류": "통합문서", "판": 1, "열": "", "서식": [{}], "시트": [sheet], "활성": "s1"}
return {
"책": book,
"호표": [],
"단산": rows,
"보기": {LIST: "s1"},
"원본": {LIST: LIST},
"목록": {"호표": [], "단산": [{"번": r["번"], "행": r["행"][0]} for r in rows]},
"줄": [],
}
def _dec(v: Any) -> int | float | None:
"""수 → 엑셀 읽기와 같은 꼴(정수 = int · 아니면 float · 없음 = None)."""
if v is None: