Files
Aislo/tools/stmate-test/jdl_export.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

180 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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,
}