Files
Aislo/M02_MasterTemplete/M02_Hopyo_Stmate.py
T
eomsangdonandClaude Opus 5.5 193e03927b feat(M02 일위대가): 338-2b STmate 꼴 읽기 · 거창 흩어진 칸 이음 · 이음 넓힘 · 노임 갈래 · 문서 보안 400 · STmate 격자
- 새 M02_Hopyo_Stmate — 일위대가표 · 단가산출근거 · 목록표 → 엑셀꼴과 같은 HP(줄 코드 = M~Z 열 코드 꼴 첫 것 · 블록 글 · 자원 품 · 절 · 출처 · 직종 · 호표 근거글)
- 여섯 파일 올라감 — 봉화 45 · 영월 23 · 울진대흥 설계 · 착공 34 · 영덕 9 · 울진소광 25 호표 · 노임 이름 어긋남 0
- Basis.read 거창 꼴 — 빈 산출근거 칸은 오른쪽 칸 이어 붙임 · 돈 머리 맨 오른쪽 묶음 · 단산 줄 21 · 출처 17 · 직종 21
- Excel 이음 넓힘(단산 · 산근 · 단가산출 / 일위 · 대가 · 일위대가 N · 참조 · 호표) · 빈 비고 줄 이름으로 이음 · 노임 갈래 · DRM 400 글(장수 · 진안)
- Grid STmate 시트 · 숨은 칸 글 뺌
- 시험 stmate · excel · basis · grid · match · build 68 통과 · 원본 엑셀 · master_data sha256 그대로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
2026-10-15 23:28:57 +09:00

265 lines
12 KiB
Python

"""M02 일위대가 실무자료 STmate 꼴 읽기(PLAN 338-2b) — 「일위대가표」 · 「단가산출근거」 시트 +
목록표(노무비 · 중기 · 재료비 · 경비 · 단가산출근거) → 엑셀꼴과 같은 HP 꼴
(`M02_Hopyo_Excel.read_book`).
일위대가표 = A 열 「제N호표」 줄(B~Z 열 「BDCOD|B…」 = 호표 코드) → 다음 줄 A 이름 · B 규격 ·
D 단위 → 구성 줄(A 이름 · B 규격 · C 수량 · D 단위 · E 계 · G 노무비 · I 재료비 · K 경비 단가 ·
다음 열 금액 · M 비고) → 「합계」 줄에서 끝.
줄 코드 = M~Z 열에서 `_CODE` 꼴 첫 것(판마다 열 수가 달라도 읽힘 · 하이퍼링크 안 읽음) ·
코드 없는 줄 = 호표 `근거글`.
갈래 = D 단가산출(참조 단산 · 번 = 코드 숫자) · B 호표(참조 일위 · 번 = 호표 코드 표) · L 노임 ·
X 기계 · M 자재(비고 「별산」 · 규격 「별도계상」 · 값 없음 = 별도계상) · S · W 별도계상 ·
PR · 단위 % = % · 그 밖 기타.
단가산출근거 = 「BDCOD|D…」 머리 → 다음 머리 · 「총계」 앞까지 한 블록 · 글 = B 열(머리 이름 줄 ·
소계 · 계 뺌) · 자원 = M 열 「X00000_n」 줄(품 = H 열 · 이름 · 규격 = X 열 「이름 / 규격」 ·
비면 목록표 · 기계 셋 줄(노무비 · 재료비 · 경비)은 하나로 · 절 = 그 앞 번호 절 제목) ·
머리 이름 · 규격 · 단위 = 단가산출근거목록표 · 출처 = 블록 글 `Basis.sources` ·
직종 = 자원 L 이름(우리 인력 · 옛이름 = 지금 이름).
검산 안 함 — `검산 {어긋남: [], 총괄표: 0}`.
"""
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 # 서로 부름 — 부를 때만 씀
SHEET = "일위대가표"
CALC = "단가산출근거"
LISTS = ("단가산출근거목록표", "노무비목록표", "중기목록표", "재료비목록표", "경비목록표")
SHEETS = (SHEET, CALC, *LISTS)
_CODE = re.compile(r"^(?:COD\|)?([A-Z]{1,2}\d{5}|PR[A-Z]?\d+|[TC]\d+)(?:_\d+)?(?: →)?$")
_META = re.compile(r"(?<![A-Z])COD\|([A-Z]{1,2}\d{5})") # 숨은 칸 「`COD|L00001」
_HEAD_B = re.compile(r"BDCOD\|(B\d{5})")
_HEAD_D = re.compile(r"BDCOD\|(D\d{5})")
_RES = re.compile(r"^([LMXS])(\d{5})_\d+$")
_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}
NOTE, CODE_COLS, RES_COL, QTY_COL, NAME_COL = 12, range(12, 26), 12, 7, 23
_KIND = {"D": "단가산출", "B": "호표", "L": "노임", "X": "기계", "S": "별도계상", "W": "별도계상"}
def is_stmate(names: Any) -> bool:
return SHEET in names and CALC in names
def _cell(row: list[Any], col: int) -> Any:
return row[col] if col < len(row) else None
def _s(value: Any) -> str:
return hopyo_excel._text(value)
def _flat(value: Any) -> str:
return re.sub(r"\s+", "", str(value or ""))
def _code(row: list[Any], cols: Any = CODE_COLS) -> str | None:
for col in cols:
hit = _CODE.match(_s(_cell(row, col)))
if hit:
return hit.group(1)
return None
def _lists(sheets: dict[str, list[list[Any]]]) -> dict[str, dict[str, str]]:
"""목록표 → {코드: {이름, 규격, 단위}} — B 이름 · C 규격 · D 단위 · 코드 = 숨은 칸
「COD|…」 · 없으면 코드 꼴 첫 칸(판마다 A 열 · 뒤 열)."""
out: dict[str, dict[str, str]] = {}
for title in LISTS:
for row in sheets.get(title) or []:
meta = _META.search(" ".join(_s(v) for v in row))
code = meta.group(1) if meta else _code(row, range(len(row)))
if code and _s(_cell(row, 1)):
out[code] = {
"이름": _s(row[1]),
"규격": _s(_cell(row, 2)),
"단위": _s(_cell(row, 3)),
}
return out
# ── 단가산출근거 블록 ──────────────────────────────────────────────────
def _blocks(rows: list[list[Any]], table: dict[str, dict[str, str]]) -> dict[str, dict[str, Any]]:
"""{D 코드: {번, 코드, 이름, 규격, 단위, 행, 출처, 직종, 글, 자원}}."""
names = hopyo_basis.trade_names()
out: dict[str, dict[str, Any]] = {}
block: dict[str, Any] | None = None
title = ""
for at, row in enumerate(rows, 1):
head = next(
(hit for v in row[:8] if (hit := _HEAD_D.search(_s(v)))), None
) # 머리 = G 열(판마다 B 열 글이 다름)
if head:
code = head.group(1)
info = table.get(code) or {}
block = out.setdefault(
code,
{
"번": int(code[1:]),
"코드": code,
"이름": info.get("이름", ""),
"규격": info.get("규격", ""),
"단위": info.get("단위", ""),
"행": [at, at],
"글": [],
"자원": [],
},
)
title = ""
continue
if block is None:
continue
first = _flat(_cell(row, 0))
if first.startswith("총계"):
block["행"][1] = at
block = None
continue
text = _s(_cell(row, 1))
res = _RES.match(_s(_cell(row, RES_COL)))
code = res.group(1) + res.group(2) if res else None
if not code:
alt = _RES_A.match(first) or _RES_G.search(_s(_cell(row, 6)))
code = alt.group(1) if alt else None
if not text and not code:
continue
block["행"][1] = at
if _s(_cell(row, RES_COL)) == block["코드"]: # 머리 이름 줄 「이름 규격 / 단위」
if not block["이름"]:
name, _, unit = text.rpartition("/")
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 (sec := _SECTION.match(text)) and not re.match(r"^\s*\d+\.\d", text):
title = sec.group(1).strip()
if not code:
continue
last = block["자원"][-1] if block["자원"] else None
if code[0] == "X" and last and last["코드"] == code and last["절"] == title:
continue # 기계 노무비 · 재료비 · 경비 셋 줄 = 하나
name, _, spec = _s(_cell(row, NAME_COL)).partition("/")
info = table.get(code) or {}
qty = hopyo_excel._num(_cell(row, QTY_COL))
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,
}
)
for block in out.values():
block["출처"] = hopyo_basis.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
# ── 일위대가표 ────────────────────────────────────────────────────────
def _kind(code: str, line: dict[str, Any]) -> str:
if code.startswith("PR") or _flat(line["단위"]) == "%":
return "%"
if code[0] == "M":
apart = line["비고"].startswith("별산") or line["규격"] == "별도계상"
return "별도계상" if apart or not any(line["단가"].values()) else "자재"
return _KIND.get(code[0], "기타")
def _hopyos(rows: list[list[Any]]) -> tuple[list[dict[str, Any]], list[list[dict[str, Any]]]]:
hopyos: list[dict[str, Any]] = []
raw: list[list[dict[str, Any]]] = []
want_head = done = False
for at, row in enumerate(rows, 1):
first = _s(_cell(row, 0))
hit = hopyo_excel._HOPYO.fullmatch(_flat(first))
if hit:
code = next((m.group(1) for v in row[1:26] if (m := _HEAD_B.search(_s(v)))), None)
hopyos.append({"번": int(hit.group(1)), "코드": code, "행": [at, at], "근거글": []})
raw.append([])
want_head, done = True, False
continue
if not hopyos or done:
continue
hopyo = hopyos[-1]
if want_head:
hopyo.update(이름=first, 규격=_s(_cell(row, 1)), 단위=_s(_cell(row, 3)))
hopyo["금액"] = {}
want_head = False
continue
if _flat(first).startswith("합계") or _flat(first) == "계":
hopyo["금액"] = hopyo_excel._money(row, _COLS, 1)
done = True # 끝 — 다음 호표까지 줄 안 받음
continue
if not first:
continue
code = _code(row)
hopyo["행"][1] = at
if not code:
if re.search(r"[0-9A-Za-z가-힣]", first):
hopyo["근거글"].append(f"{first} {_s(_cell(row, 1))}".strip())
continue
if code[0] in "TC" and code[1].isdigit():
continue # 계 · 소계 · 계약단가
line = {
"차례": len(raw[-1]),
"이름": first,
"규격": _s(_cell(row, 1)),
"수량": hopyo_excel._num(_cell(row, 2)),
"단위": _s(_cell(row, 3)),
"단가": hopyo_excel._money(row, _COLS, 0),
"금액": hopyo_excel._money(row, _COLS, 1),
"비고": _s(_cell(row, NOTE)),
"코드": code,
"행": at,
}
line["갈래"] = _kind(code, line)
line["참조"] = (
{"종류": "단산", "번": int(code[1:]), "코드": code}
if code[0] == "D"
else {"종류": "일위", "번": None, "코드": code}
if code[0] == "B"
else None
)
raw[-1].append(line)
return hopyos, raw
def read(sheets: dict[str, list[list[Any]]]) -> dict[str, Any]:
"""STmate 시트 → HP 꼴 `{꼴, 시트, 호표, 단산, 검산}`(값은 JSON 수)."""
hopyos, raw = _hopyos(sheets.get(SHEET) or [])
if not hopyos:
raise hopyo_excel.ExcelError(f"{SHEET} 시트에 호표(제N호표)가 없음")
table = _lists(sheets)
blocks = _blocks(sheets.get(CALC) or [], table)
by_code = {h["코드"]: h["번"] for h in hopyos if h["코드"]}
for line in (one for lines in raw for one in lines):
ref = line["참조"] or {}
if ref.get("종류") == "일위":
ref["번"] = by_code.get(ref["코드"])
elif ref.get("종류") == "단산" and (block := blocks.get(ref["코드"])):
line["근거"] = {k: block[k] for k in ("출처", "직종", "행")}
out = []
for hopyo, lines in zip(hopyos, raw, strict=True):
for key in ("이름", "규격", "단위"):
hopyo.setdefault(key, "")
if not hopyo["근거글"]:
del hopyo["근거글"]
out.append({**hopyo_excel._json(hopyo), "줄": [hopyo_excel._json(x) for x in lines]})
keep = ("번", "코드", "이름", "규격", "단위", "행", "출처", "직종", "글", "자원")
return {
"꼴": "STmate",
"시트": SHEET,
"호표": out,
"단산": [{k: b[k] for k in keep} for b in blocks.values()],
"검산": {"어긋남": [], "총괄표": 0},
}