Files
Aislo/tools/stmate-test/conv_xls_res.py
T
eomsangdonandClaude Opus 5.5 47a04b6df8 feat(stmate-test): REA MCP(Ghidra) 파일 입출력 — JDL 탭 줄·묶음 내보내기·검사 · 구버전→Win8 변환 · 엑셀 역등록 구성 줄 · 자재 집계표 엑셀 · 그림 코드
- JDL 줄 = 탭 구분 + \x05/\x07 덧붙임(검사기 0x504d20 · 고정폭 표는 죽은 고리 — 34 §17.9 바로잡음) · 단가 목록 7종·Main·GongJong 머리·Global.Ini 상수 키 · zip 받기
- 구버전 판 가리기(Chk_Data 0x459280) · 7.x/DOS 표별 필드 대응 · B→X 조건 = 단위 X
- 엑셀 역등록 PRPT「속성」·PRIN「대상」·PROUT「저장」 = PR/RR 줄만 · 원본 BDQTY 4건 일치
- 자재 = 재료비 수량집계표 엑셀 내보내기(주요건설자재현황 시트)
- 그림 코드 = COSTN I 코드 IFORM · SIZE = 그림 클래스 이름
- 지식DB 37번

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 15:58:17 +09:00

259 lines
10 KiB
Python

# -*- coding: utf-8 -*-
"""엑셀 역등록 자원 쪽 (CONV_XLS_2_ST · XLS_2_BDREC) — 목록표 → COSTN 행 · 일위대가표/중기사용료 → BDQTY 구성 ·
단가산출근거 → COSTN.SMT 산출식.
근거: XLSX_INITIAL_구조.md · 24_모듈구조_전수.md §3(XLS_2_BDREC — 엑셀 수량산출 양식 ↔ BDQTY) · 23번(COSTN.SMT) ·
실무 xlsx 숨은 열 실측
목록표 INFO: COD · BQC(BQCODE) · EQC(EQCODE) · JDC(JDCODE) · WQC(WQCODE) · EDT(EDT_DATE) · ADJ(S_ADJ) · RXX
구성표 머리 줄 INFO: BDCOD(호표 코드) · POSS/POSE(구성 줄 범위)
구성 줄 INFO(ST_P_XLS 0x5f1078 — 지식DB REA 분석/37 §3): 목록표 DET = 구성표 머리 줄 · BDCODE = 목록 COD ·
모든 줄 IPR→PRPT(「속성」0~99) · DVD→DEVD1 · BMK→BIMOK · BLA→BL_AMT · D 줄 SPR·SBM·TPR ·
PR/RR 줄만 UNI→UNIT1 · EQC · PRI→PRIN(「대상」 0 빈칸·1 재료비·2 노무비·3 경비·4 합계) · PRO→PROUT(「저장」) ·
M 줄만 KWN→KWNGP(「관급」 0·1 관급·2 별산) · RCU 는 내역 행 키(구성 줄에 안 씀)
산출근거 숨은 열: 머리 줄 BDCOD·POSS·POSE · 그 아래 줄마다 0x07+원래 식 한 줄 → CRLF 로 이으면 COSTN.SMT 본문
"""
from __future__ import annotations
import re
from conv_xls_boq import _numv, _txt, _unx, find_sheet, parse_info
# 목록표 이름 → (머리글자, 단가 열 배치)
LISTS = {
"재료비목록표": "M",
"노무비목록표": "L",
"경비목록표": "S",
"일식목록표": "W",
"중기목록표": "X",
"일위대가목록표": "B",
"산출근거목록표": "D",
}
LIST_INFO = {
"BQC": "BQCODE",
"EQC": "EQCODE",
"JDC": "JDCODE",
"WQC": "WQCODE",
"EDT": "EDT_DATE",
"ADJ": "S_ADJ",
"RXX": "RXX",
}
def _sheet(wb, info: dict, kind: str):
spec = find_sheet(info, kind)
if not spec or spec["sheet"] not in wb.sheetnames:
return None, None
return wb[spec["sheet"]], spec["cols"]
def _info_col(ws, top: int = 3) -> int | None:
"""숨은 INFO 열 — 머리 아래 첫 몇 줄에서 `COD| 또는 `BDCOD| 가 든 열."""
for r in ws.iter_rows(min_row=top, max_row=min(ws.max_row, top + 30), values_only=True):
for j, v in enumerate(r, start=1):
if isinstance(v, str) and ("`COD|" in v or "`BDCOD|" in v):
return j
return None
def stored_unit(code: str, unit: str, size: str) -> str:
"""보이는 단위 → 저장 단위(report_core.unitText 의 역) — 실무 7건 COSTN 전수 꼴:
X 코드 '시간' = 'X' · '%' = 규격 글로 비목(노무·인건 → L% · 재료·연료 → M% · 경비 → S% · 그 밖 T%)."""
u = str(unit or "").strip()
if code[:1] == "X" and u in ("시간", "X"):
return "X"
if u != "%":
return u
s = str(size or "")
if re.search(r"노무|인건|,노", s):
return "L%"
if re.search(r"재료|연료", s):
return "M%"
return "S%" if "경비" in s else "T%"
def costn_from_lists(wb, info: dict) -> list[dict]:
"""목록표 시트들 → COSTN 행(CODE NAME SIZE UNIT ?AMT1 + INFO 칸) · 목록 차례대로."""
out, seen = [], set()
for kind, pre in LISTS.items():
ws, cols = _sheet(wb, info, kind)
if ws is None:
continue
ic = _info_col(ws) or cols.get("INFO")
for r in ws.iter_rows(
min_row=cols.get("SPOS", 4), max_row=cols.get("EPOS", ws.max_row), values_only=True
):
meta = parse_info(r[ic - 1] if ic and ic - 1 < len(r) else None)
code = meta.get("COD", "").strip()
if not code or code in seen:
continue
row = {
"CODE": code,
"NAME": _txt(r[cols["NAME"] - 1]) if cols.get("NAME") else "",
"SIZE": _txt(r[cols["SIZE"] - 1]) if cols.get("SIZE") else "",
"UNIT": _txt(r[cols["UNIT"] - 1]).strip() if cols.get("UNIT") else "",
}
row["UNIT"] = stored_unit(code, row["UNIT"], row["SIZE"])
t = _numv(r[cols["TAMT"] - 1]) if cols.get("TAMT") else 0.0
part = {
c: (_numv(r[cols[f"{c}AMT"] - 1]) if cols.get(f"{c}AMT") else None) for c in "MLS"
}
if all(v is None for v in part.values()): # 단가 한 칸 목록(재·노·경)
part = {c: (t if c == pre else 0.0) for c in "MLS"}
row.update({f"{c}AMT1": part[c] or 0.0 for c in "MLS"}, TAMT1=t)
for k, f in LIST_INFO.items():
if k in meta:
row[f] = meta[k]
out.append(row)
seen.add(code)
return out
def _blocks(ws, ic: int):
"""구성표·산출근거의 호표 묶음 — (코드, 시작 줄, 끝 줄) · POSS/POSE 는 엑셀 줄 번호."""
for line, r in enumerate(ws.iter_rows(values_only=True), start=1):
v = r[ic - 1] if ic - 1 < len(r) else None
if isinstance(v, str) and "`BDCOD|" in v:
m = parse_info(v)
yield (
m["BDCOD"].strip(),
int(_numv(m.get("POSS")) or line + 1),
int(_numv(m.get("POSE")) or line + 1),
)
def _list_det(wb, info: dict, kind: str) -> list[tuple[str, int]]:
"""목록표 줄 차례 (COD, DET=구성표 머리 줄 번호) — ST_P_XLS 0x5ef67b."""
ws, _cols = _sheet(wb, info, kind)
if ws is None:
return []
ic = _info_col(ws)
out = []
for r in ws.iter_rows(values_only=True):
m = parse_info(r[ic - 1] if ic and ic - 1 < len(r) else None)
if "COD" in m and _numv(m.get("DET")):
out.append((m["COD"].strip(), int(_numv(m["DET"]))))
return out
def _int(v) -> int:
return int(_numv(v)) if str(v or "").strip() else 0
def comp_row(bd: str, r: tuple, cols: dict, meta: dict) -> dict:
"""구성 줄 하나 → BDQTY 행 — ST_P_XLS 0x5f1078(SetFieldValue 차례 · 키 없으면 0/거짓/빈칸)."""
cell = lambda k: _txt(r[cols[k] - 1]) if cols.get(k) and cols[k] - 1 < len(r) else "" # noqa: E731
code = meta.get("COD", "").strip()
row = {"BDCODE": bd, "CODE": code, "NAME1": cell("NAME"), "SIZE1": cell("SIZE")}
row["EXIST1"] = _int(meta.get("EXI"))
row["QTY1"] = 0.0 # AppendBlank 기본값 — 아래 갈래에서만 씀
ratio = code in ("PR", "RR")
if not (ratio or code == "QTY" or code[1:2].isdigit()):
return row
k = code[:1]
if k in "CET" and not ratio and code != "QTY":
row["EXIST1"] = 0
return row
if not (ratio or code == "QTY" or k in "ABDLMPQRSWX"):
return row
q = r[cols["QTY"] - 1] if cols.get("QTY") and cols["QTY"] - 1 < len(r) else None
row["QTY1"] = round(_numv(q), 7) # MROUNDUD(x, 7, 1)
row["UNIT1"] = cell("UNIT")
row["DEVD1"] = meta.get("DVD", "") == "T"
row["BIMOK"] = meta.get("BMK", "")
row["PRPT"] = _int(meta.get("IPR"))
if k == "D":
row |= {
"SPRPT": _int(meta.get("SPR")),
"SBMOK": meta.get("SBM", ""),
"TPRPT": _int(meta.get("TPR")),
}
if ratio:
row |= {"UNIT1": meta.get("UNI", ""), "EQCODE": meta.get("EQC", "")}
row |= {"PRIN": _int(meta.get("PRI")), "PROUT": _int(meta.get("PRO"))}
if k == "M":
row["KWNGP"] = _int(meta.get("KWN"))
row["BL_AMT"] = meta.get("BLA", "") == "T"
return row
def comp_from_sheets(wb, info: dict) -> list[dict]:
"""일위대가표 · 중기사용료 → BDQTY 구성행 — 목록표 줄 차례 · BDCODE = 목록 COD ·
머리 줄(DET) 의 POSS~POSE 줄 중 INFO 가 0x07 로 시작하는 줄마다 한 행(ST_P_XLS 0x5f97c2~0x5f9864)."""
out = []
for kind, lst in (("중기사용료", "중기목록표"), ("일위대가표", "일위대가목록표")):
ws, cols = _sheet(wb, info, kind)
if ws is None:
continue
ic = _info_col(ws)
rows = list(ws.iter_rows(values_only=True))
for bd, det in _list_det(wb, info, lst):
hm = parse_info(rows[det - 1][ic - 1]) if 0 < det <= len(rows) else {}
if "POSS" not in hm:
continue
for line in range(int(_numv(hm["POSS"])), int(_numv(hm.get("POSE"))) + 1):
r = rows[line - 1]
v = r[ic - 1] if ic - 1 < len(r) else None
if not (isinstance(v, str) and _unx(v).startswith("")):
continue
out.append(comp_row(bd, r, cols, parse_info(v)))
return out
REF_RE = re.compile(
r"(PR|RR)[A-Z]?_\(.*\)_1|([A-Z][0-9A-Z]{0,5})_1"
) # 'RRC_(25)_1' · 'PRS_(2)_1' = 비율 줄
def comp_from_san(wb, info: dict) -> list[dict]:
"""단가산출근거 → D 코드 BDQTY 구성행 — 코드 열 「코드_1」 줄(수량 = 숨은 QTY 열) · 소계/계 줄 = T1·T2…
(묶음 뿌리 T7 = 총계 줄은 저장 안 함 — 봉화 D00014 등 복호 대조)."""
ws, _cols = _sheet(wb, info, "단가산출근거")
if ws is None:
return []
ic = _info_col(ws)
rows = list(ws.iter_rows(values_only=True))
qc = next(
(j for r in rows[:6] for j, v in enumerate(r, start=1) if str(v or "").strip() == "QTY"),
None,
)
out = []
for bd, a, b in _blocks(ws, ic):
for line in range(a, b + 1):
r = rows[line - 1]
ref = next(
(
m.group(1) or m.group(2)
for v in r
if isinstance(v, str)
for m in [REF_RE.fullmatch(v)]
if m
),
None,
)
if not ref or ref == "T7" or ref == bd:
continue
q = _numv(r[qc - 1]) if qc and not ref.startswith("T") else 0.0
out.append({"BDCODE": bd, "CODE": ref, "QTY1": round(q, 11)})
return out
def memos_from_sheet(wb, info: dict) -> dict[str, str]:
"""단가산출근거 → {D코드: COSTN.SMT 본문} — 숨은 열 줄을 CRLF 로 이음."""
ws, _cols = _sheet(wb, info, "단가산출근거")
if ws is None:
return {}
ic = _info_col(ws)
rows = list(ws.iter_rows(values_only=True))
out = {}
for bd, a, b in _blocks(ws, ic):
lines = []
for line in range(a, b + 1):
v = rows[line - 1][ic - 1] if ic - 1 < len(rows[line - 1]) else None
if isinstance(v, str):
v = re.sub(r"_x0007_", "\x07", v)
if v.startswith("\x07"):
lines.append(v[1:])
out[bd] = "\r\n".join(lines)
return out