refactor(776-2): EST 단가산출 읽기 어댑터 떼어 냄 · 두 형식 블록 항목 · STmate 블록 단가 · 금액

- 새 M02_Hopyo_ReadEst.py — Basis.read(엑셀 「단가산출」 블록) · trade_names · sources 옮김 + 글 → 항목 풀이(L 직종 · X 장비 · M 자재 · 줄 율에 기대지 않는 것만)
- Basis — 근거 후보만 남김 · 옛 이름(read · SHEET · GLINES · GCHARS · sources · trade_names)은 그대로 다시 냄
- Stmate — 블록 금액(머리 줄 C~F) · 항목(자원 + 단가 I~L · 금액 C~F · 기계 셋 줄 더함 · 행) · 항목 단가 639 중 635
- 항목만으로 지문 인력 · 장비 · 자재 다시 세움 = 지금 지문과 544 쌍 모두 같음(tmp 대조 · 776-3 길)
- S1 · S2 해시 그대로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 08:48:10 +09:00
co-authored by Claude Opus 5.5
parent 9d26e5fcd6
commit 0c2f12c23f
3 changed files with 344 additions and 136 deletions
+64 -17
View File
@@ -13,8 +13,11 @@ PR · 단위 % = % · 그 밖 기타.
단가산출근거 = 「BDCOD|D…」 머리 → 다음 머리 · 「총계」 앞까지 한 블록 · 글 = B 열(머리 이름 줄 ·
소계 · 계 뺌) · 자원 = M 열 「X00000_n」 줄(품 = H 열 · 이름 · 규격 = X 열 「이름 / 규격」 ·
비면 목록표 · 기계 셋 줄(노무비 · 재료비 · 경비)은 하나로 · 절 = 그 앞 번호 절 제목) ·
머리 이름 · 규격 · 단위 = 단가산출근거목록표 · 출처 = 블록 글 `Basis.sources` ·
직종 = 자원 L 이름(우리 인력 · 옛이름 = 지금 이름).
머리 이름 · 규격 · 단위 = 단가산출근거목록표 · 출처 = 블록 글 `ReadEst.sources` ·
직종 = 자원 L 이름(우리 인력 · 옛이름 = 지금 이름) ·
금액 = 머리 줄 C~F(1 단위) · 항목(776-2 · Book 블록 항목 · EST 어댑터 `M02_Hopyo_ReadEst` 와
같은 꼴) = 자원 + `{직종(L), 밑품 = 품, 율 [], 인자 [], 단가 I~L, 금액 C~F, 어떻게 「QTY」, 행}`
(기계 셋 줄 = 더함).
검산 안 함 — `검산 {어긋남: [], 총괄표: 0}`.
"""
@@ -23,8 +26,8 @@ from __future__ import annotations
import re
from typing import Any
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_ReadEst as hopyo_est
SHEET = "일위대가표"
CALC = "단가산출근거"
@@ -39,6 +42,8 @@ _RES_A = re.compile(r"^([LMXS]\d{5})[LMS]$") # A 열 「L00002L」
_RES_G = re.compile(r"&([LMXS]\d{5})[LMS]&") # G 열 식 「&L00002L&」
_SECTION = re.compile(r"^\s*(?:\d+\s*[.)]|[①-⑩])\s*(.*\S)")
_COLS = {"합계": 4, "노무비": 6, "재료비": 8, "경비": 10}
_CALC_AMOUNT = {"합계": 2, "노무비": 3, "재료비": 4, "경비": 5} # 단가산출근거 C~F 금액
_CALC_PRICE = {"합계": 8, "노무비": 9, "재료비": 10, "경비": 11} # I~L 단가(T · L · M · S)
NOTE, CODE_COLS, RES_COL, QTY_COL, NAME_COL = 12, range(12, 26), 12, 7, 23
_KIND = {"D": "단가산출", "B": "호표", "L": "노임", "X": "기계", "S": "별도계상", "W": "별도계상"}
@@ -67,6 +72,17 @@ def _code(row: list[Any], cols: Any = CODE_COLS) -> str | None:
return None
def _calc_money(row: list[Any], cols: dict[str, int]) -> dict[str, int | float | None]:
return {k: hopyo_excel._out(v) for k, v in hopyo_excel._money(row, cols, 0).items()}
def _add(into: dict[str, Any], more: dict[str, Any]) -> None:
"""기계 셋 줄(노무비 · 재료비 · 경비) 단가 · 금액을 한 항목에 더함."""
for k, v in more.items():
if v is not None:
into[k] = hopyo_excel._out(hopyo_excel._num(into.get(k) or 0) + hopyo_excel._num(v))
def _lists(sheets: dict[str, list[list[Any]]]) -> dict[str, dict[str, str]]:
"""목록표 → {코드: {이름, 규격, 단위}} — B 이름 · C 규격 · D 단위 · 코드 = 숨은 칸
「COD|…」 · 없으면 코드 꼴 첫 칸(판마다 A 열 · 뒤 열)."""
@@ -86,8 +102,8 @@ def _lists(sheets: dict[str, list[list[Any]]]) -> dict[str, dict[str, str]]:
# ── 단가산출근거 블록 ──────────────────────────────────────────────────
def _blocks(rows: list[list[Any]], table: dict[str, dict[str, str]]) -> dict[str, dict[str, Any]]:
"""{D 코드: {번, 코드, 이름, 규격, 단위, 행, 출처, 직종, 글, 자원}}."""
names = hopyo_basis.trade_names()
"""{D 코드: {번, 코드, 이름, 규격, 단위, 행, 출처, 직종, 글, 자원, 금액, 항목}}."""
names = hopyo_est.trade_names()
out: dict[str, dict[str, Any]] = {}
block: dict[str, Any] | None = None
title = ""
@@ -109,6 +125,8 @@ def _blocks(rows: list[list[Any]], table: dict[str, dict[str, str]]) -> dict[str
"행": [at, at],
"글": [],
"자원": [],
"금액": _calc_money(row, _CALC_AMOUNT),
"항목": [],
},
)
title = ""
@@ -135,30 +153,46 @@ def _blocks(rows: list[list[Any]], table: dict[str, dict[str, str]]) -> dict[str
block["이름"], block["단위"] = name.strip() or text, unit.strip()
continue
if text and _flat(text) not in ("소계", "계"):
if len(block["글"]) < hopyo_basis.GLINES:
block["글"].append(text[: hopyo_basis.GCHARS])
if len(block["글"]) < hopyo_est.GLINES:
block["글"].append(text[: hopyo_est.GCHARS])
if (sec := _SECTION.match(text)) and not re.match(r"^\s*\d+\.\d", text):
title = sec.group(1).strip()
if not code:
continue
price, amount = _calc_money(row, _CALC_PRICE), _calc_money(row, _CALC_AMOUNT)
last = block["자원"][-1] if block["자원"] else None
if code[0] == "X" and last and last["코드"] == code and last["절"] == title:
continue # 기계 노무비 · 재료비 · 경비 셋 줄 = 하나
_add(block["항목"][-1]["단가"], price) # 기계 노무비 · 재료비 · 경비 셋 줄 = 하나
_add(block["항목"][-1]["금액"], amount)
continue
name, _, spec = _s(_cell(row, NAME_COL)).partition("/")
info = table.get(code) or {}
qty = hopyo_excel._num(_cell(row, QTY_COL))
block["자원"].append(
res = {
"갈래": code[0],
"코드": code,
"이름": name.strip() or info.get("이름", ""),
"규격": spec.strip() if name.strip() else info.get("규격", ""),
"품": float(qty) if qty is not None else None,
"절": title,
}
block["자원"].append(res)
who = hopyo_est.trade(res["이름"]) if code[0] == "L" else None
block["항목"].append(
{
"갈래": code[0],
"코드": code,
"이름": name.strip() or info.get("이름", ""),
"규격": spec.strip() if name.strip() else info.get("규격", ""),
"품": float(qty) if qty is not None else None,
"절": title,
**res,
**({"직종": who} if code[0] == "L" else {}),
"밑품": res["품"],
"율": [],
"인자": [],
"단가": price,
"금액": amount,
"어떻게": "QTY",
"행": at,
}
)
for block in out.values():
block["출처"] = hopyo_basis.sources(" ".join(block["글"]))
block["출처"] = hopyo_est.sources(" ".join(block["글"]))
trades = [names.get(_flat(r["이름"])) for r in block["자원"] if r["갈래"] == "L"]
block["직종"] = list(dict.fromkeys(t for t in trades if t))
return out
@@ -257,7 +291,20 @@ def read(sheets: dict[str, list[list[Any]]]) -> dict[str, Any]:
if not hopyo["근거글"]:
del hopyo["근거글"]
out.append({**hopyo_excel._json(hopyo), "줄": [hopyo_excel._json(x) for x in lines]})
keep = ("번", "코드", "이름", "규격", "단위", "행", "출처", "직종", "글", "자원")
keep = (
"번",
"코드",
"이름",
"규격",
"단위",
"행",
"출처",
"직종",
"글",
"자원",
"금액",
"항목",
)
return {
"꼴": "STmate",
"시트": SHEET,