# -*- coding: utf-8 -*- """조달청 JDL TEXT 묶음 내보내기 — 프로젝트(STC/폴더) → 단가 목록 줄 · Main · GongJong 머리 · Global.Ini. 근거: ST_U_Convert.CPM Conv_ST8_Jodal 0x4b9f2c(지식DB REA 분석/37 §1) · 계열: PRJLST PCLASS 0→1 · 3→2 · 4~7·9→1(0x4ba06e switch) · 그 밖은 앞 화면 계열 그대로(미확인 → 지정 필요) · 코드 차례: COSTN 첫 글자 LMSWI(0x4bb622) 다음 BXD(0x4bb696) · 같은 표 안 차례는 열린 색인(미확인 → 물리 차례) · 코드 기록(FUN_00431758): CODE · NAME · SIZE · UNIT(X 면 「시간」) · EQCODE · BQCODE · 계열 s 의 노무 = M·S 코드면 0 아니면 LAMTs · 재료 = L·S 면 0 아니면 MAMTs · 경비 = L·M 면 0 아니면 SAMTs(0x432d7e~) · 줄(FUN_004b0904 · 첫 글자 → 파일 L→L · M→M · S→E · W→W · B→Unit · X→JUnit · D→Danga · I→IMG.DBF): 단위 「$」→「$」 · L·M·S·W 의 「T%」→「T%*100」 · B·X·D 의 「L.S.」「P.S.」(대문자 비교) → 단위+「3」 · Main.TXT = PNUM · PNAME(탭→빈칸 · TrimRight) · 「02」 · GongJong 머리 줄 = PRIV_PR 거짓일 때만 · Global.Ini [BASIC] MAKER=STM · CDATE=yyyyMMdd · JDL VERSION=3.3 · [COST] L·M·E(=SCOST)·TCOST · [CPART] 상수 비트 """ from __future__ import annotations import datetime from dbf_rw import DbfTable from jdl_format import COUNTS, Raw, build_line, check_lines from stc_read import Source SERIES_BY_PCLASS = {0: 1, 3: 2, 4: 1, 5: 1, 6: 1, 7: 1, 9: 1} FILE_OF = {"L": "L.Txt", "M": "M.Txt", "S": "E.Txt", "W": "W.Txt"} FILE_OF |= {"B": "Unit.Txt", "X": "JUnit.Txt", "D": "Danga.Txt"} CPART_CONST = [("PPBIT", "2C"), ("MQBIT", "4C"), ("MABIT", "0C"), ("MTBIT", "0C")] CPART_CONST += [("UQBIT", "6C"), ("JQBIT", "6C"), ("DRBIT", "11R"), ("DTBIT", "0C")] # 근거는 찾았으나 값 원천(설정 자리)을 못 푼 것 — 내보내지 않음 MISSING = [ "MainPart.Txt · GongJong.Txt 본문 · UnitPart.Txt · JPart.Txt(공종·구성 줄 — 0x4b6378 · 0x4b2a46~ 미해독)", "CostAccount.Txt(잡비계산 — PRT.DBF I_TYP·T_RAT·T_AMT·S_DET·BIGO 0x4bfac4~ 미해독)", "J.Txt(만드는 쪽 줄 없음 — 검사기 목록에만 있음)", "DANGA.DBF · IMG.DBF(참고 표 — FORM 식 변환 0x452e78 미해독)", "Global.Ini [BASIC] VERSION(프로그램 판 글) · [CPART] MCMETHOD·UABIT·UTBIT·UCMETHOD·JABIT·JTBIT·" "JCMETHOD·DQBIT·DABIT·DCMETHOD · [PPART] · 환율·자리수 키(설정 자리 미해독)", "W 코드 표준시장 덧붙임 칸(기록 +0x254≠0 때 · 0x4b2309) · 금액 칸 BAMT~ZAMT(필드 있을 때)", "압축 <이름>_….JDL(ST_U_Archive 압축 형식 미확인)", ] def _f(t: DbfTable, i: int, name: str) -> float: if name not in t.fields: return 0.0 try: return float(t.get(i, name) or 0) except ValueError: return 0.0 def _s(t: DbfTable, i: int, name: str) -> str: return t.get(i, name) if name in t.fields else "" def _l(t: DbfTable, i: int, name: str) -> bool: return name in t.fields and t.get(i, name).upper() in ("T", "Y") def code_record(t: DbfTable, i: int, s: int) -> dict: """FUN_00431758 꼴 코드 기록 — 계열 s 의 노무·재료·경비.""" code = _s(t, i, "CODE") k = code[:1] rec = { "CODE": code, "NAME": _s(t, i, "NAME"), "SIZE": _s(t, i, "SIZE"), "UNIT": _s(t, i, "UNIT"), } if k == "X": rec["UNIT"] = "시간" rec |= {"EQCODE": _s(t, i, "EQCODE"), "BQCODE": _s(t, i, "BQCODE"), "UC": _l(t, i, "UC")} rec["L"] = 0.0 if k in "MS" else _f(t, i, f"LAMT{s}") rec["M"] = 0.0 if k in "LS" else _f(t, i, f"MAMT{s}") rec["E"] = 0.0 if k in "LM" else _f(t, i, f"SAMT{s}") rec["AMT"] = [_f(t, i, f"AMT_{n}{s}") for n in range(1, 6)] rec["PG"] = [_s(t, i, f"PG_{n}{s}") for n in range(1, 6)] rec["PG_J"] = _s(t, i, "PG_J") rec["PG_S"] = _s(t, i, f"PG_{s}") return rec def code_line(rec: dict, gun_ok: bool, gun: str) -> tuple[str, bytes] | None: """FUN_004b0904 한 줄 — (파일, 줄) · I 코드·그 밖은 None.""" k = rec["CODE"][:1] name = FILE_OF.get(k) if not name: return None unit = rec["UNIT"].strip() if unit == "$": unit = "$" if k in "LMSW" and unit == "T%": unit = "T%*100" if k in "BXD" and unit.upper() in ("L.S.", "P.S."): unit = unit + "3" head = [rec["CODE"], rec["NAME"], rec["SIZE"], unit] last = Raw(gun if (gun_ok and k in "MS") else " ") if k == "L": return name, build_line( head + [rec["L"], rec["EQCODE"], ""], [rec["BQCODE"], rec["UC"], *rec["AMT"], last] ) if k == "M": pairs = [x for a, p in zip(rec["AMT"], rec["PG"]) for x in (a, p)] ext = [rec["BQCODE"], rec["UC"], *rec["AMT"], *rec["PG"], rec["PG_J"], rec["PG_S"], last] return name, build_line(head + [rec["M"], rec["EQCODE"], *pairs, "", "", ""], ext) if k == "S": return name, build_line( head + ["0", "0", rec["E"], rec["EQCODE"], ""], [rec["BQCODE"], rec["UC"], last] ) prices = [rec["M"], rec["L"], rec["E"], rec["EQCODE"]] if k == "W": return name, build_line(head + prices + [""], [rec["BQCODE"], rec["UC"]]) return name, build_line(head + prices + ["", ""], [rec["BQCODE"], rec["UC"]]) def _tabless(s: str) -> str: return s.replace("\t", " ").rstrip(" ") def _ini(sections: list[tuple[str, list[tuple[str, str]]]]) -> bytes: out = [] for sec, items in sections: out.append(f"[{sec}]") out += [f"{k}={v}" for k, v in items] return ("\r\n".join(out) + "\r\n").encode("cp949", "replace") def export_bundle(path: str, series: int | None = None, today: datetime.date | None = None) -> dict: """프로젝트 → {files: {이름: 바이트}, series, counts, checks, missing, warnings}.""" src = Source(path) if "COSTN.DBF" not in src.files or "PRJLST.DBF" not in src.files: raise ValueError("COSTN.DBF · PRJLST.DBF 가 없음") t = DbfTable(src.files["COSTN.DBF"]) p = DbfTable(src.files["PRJLST.DBF"]) pclass = int(float(p.get(0, "PCLASS") or 0)) if "PCLASS" in p.fields else 0 s = series or SERIES_BY_PCLASS.get(pclass) if not s: raise ValueError(f"PCLASS {pclass} 의 계열 미확인 — series(1~4) 지정 필요") gun_ok = all( f in t.fields for f in ("GUN", "BAMT", "CAMT", "DAMT", "EAMT", "FAMT", "KAMT", "ZAMT") ) files: dict[str, bytearray] = {} warnings: list[str] = [] rows = [i for i in range(len(t)) if not t.deleted(i)] for group in ("LMSWI", "BXD"): for i in rows: k = t.get(i, "CODE")[:1] if k not in group: continue rec = code_record(t, i, s) if k == "W" and "ACTTYP" in t.fields and _f(t, i, "ACTTYP"): warnings.append(f"{rec['CODE']} 표준시장 덧붙임 칸 미확인 — 기본 꼴로 씀") got = code_line(rec, gun_ok, _s(t, i, "GUN")) if got: files.setdefault(got[0], bytearray()).extend(got[1] + b"\r\n") pnum, pname = _tabless(p.get(0, "PNUM")), _tabless(p.get(0, "PNAME")) files["Main.Txt"] = bytearray(build_line([pnum, pname, Raw("02")]) + b"\r\n") if not _l(p, 0, "PRIV_PR"): head = build_line(["01", p.get(0, "PNAME"), 1, ""], ["", False, "", "", "", Raw("0")]) files["GongJong.Txt"] = bytearray(head + b"\r\n") day = (today or datetime.date.today()).strftime("%Y%m%d") cost = [ (k, p.get(0, f"{f}{s}")) for k, f in (("LCOST", "LCOST"), ("MCOST", "MCOST"), ("ECOST", "SCOST")) ] cost.append(("TCOST", p.get(0, f"TCOST{s}"))) ini = [("BASIC", [("MAKER", "STM"), ("CDATE", day), ("JDL VERSION", "3.3")]), ("COST", cost)] ini.append(("CPART", CPART_CONST)) out = {k: bytes(v) for k, v in files.items()} out["Global.Ini"] = _ini(ini) checks = {k: check_lines(k, v) for k, v in out.items() if k in COUNTS} counts = {k: v.count(b"\r\n") for k, v in out.items() if k != "Global.Ini"} return { "files": out, "series": s, "pclass": pclass, "counts": counts, "checks": checks, "missing": MISSING, "warnings": warnings, }