Files
Aislo/resources/tester/test_stmate_jdl_rea.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

196 lines
7.1 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.
"""STMATE 재현 앱 — 조달청 JDL TEXT 형식(지식DB REA 분석/37 §1 · 34 §17.9 바로잡음) 지킴 시험 · 할증율 4바이트 계수 ·
변경 4차 칸.
근거: JDLCHK.Tab_33.dll 0x5051e0(고정폭 탭 넣기 고리를 0x505218 jmp 가 건너뜀) · 0x504d20(첫 0x05 앞을 탭으로 ·
뒤를 0x07 로 나눔 · 열마다 Trim) · 열 개수 검사(0x405bfc 뒤 cmp) · FieldDefs(0x491dec) · L.Txt 오류 203002
만드는 쪽 ST_U_Convert 0x4b0904(코드 줄) · 0x4bc1a9(Main.TXT) · DP_Common EXTTOSTR 0x453e4c
할증율 = ST_Boq 0x4e9b20 fstp dword · 0x4e9bde fmul dword · 변경 4차 = PRJLST MCOST5·XX5NM · ST_Prt 0x4b3183
"""
import json
import math
import struct
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tools/stmate-test"))
import jdl_format as J # noqa: E402
from dbf_rw import DbfTable # noqa: E402
from jdl_export import code_line, export_bundle # noqa: E402
from kanjub_cfg import stage_slots # noqa: E402
from stc_read import Source # noqa: E402
# 검사기 cmp 값(열 개수)
CMP = {
"Main.Txt": 3,
"M.Txt": 0x13,
"L.Txt": 7,
"E.Txt": 9,
"W.Txt": 9,
"J.Txt": 9,
"GongJong.Txt": 4,
}
CMP |= {
"Unit.Txt": 10,
"JUnit.Txt": 10,
"Danga.Txt": 10,
"MainPart.Txt": 0x1F,
"UnitPart.Txt": 0x1C,
}
CMP |= {"JPart.Txt": 0x13}
STC = next((ROOT / "tools/stmate-test/work_dbf").glob("*.STC"), None)
TAB, EXT, SUB, CRLF = "\t", "\x05", "\x07", b"\r\n"
def test_counts_and_column_tables():
assert J.COUNTS == CMP
for name, cols in J.COLUMNS.items():
assert len(cols) == CMP[name], name
def test_split_follows_checker():
"""탭 나눔 · 끝 탭 = 빈 열 · 0x05 뒤는 덧붙임 칸(0x07) · 열마다 #0~#32 걷음."""
raw = f"A{TAB} 이름 {TAB}\x01{TAB}{EXT}B{SUB}True{SUB} "
cols, ext = J.split_line(raw.encode("cp949"))
assert cols == ["A", "이름", "", ""] and ext == ["B", "True", ""]
assert J.split_line(f"X{TAB}Y") == (["X", "Y"], [])
line = J.build_line(["M00001", "철근", "", "TON", 1665.0, "", 0, ""], ["Q", True])
assert J.split_line(line) == (["M00001", "철근", "", "TON", "1665", "", "0", ""], ["Q", "True"])
def test_check_lines_counts_and_code_chars():
good = J.build_line(["L00001", "보통인부", "", "인", 150000, "", ""], ["", False, J.Raw(" ")])
assert J.check_lines("L.Txt", good + CRLF) == []
bad = J.build_line(["l0001", f"가{TAB}나", "", "인", 1, "", ""])
errs = J.check_lines("L.Txt", good + CRLF + bad)
assert any("203002" in e and "Line : 2" in e for e in errs) and any(
"코드 글자" in e for e in errs
)
def test_num_text_exttostr():
got = [J.num_text(v) for v in (268058.0, 0, 0.1 + 0.2, 1e20, 1.5e-7, -12.5)]
assert got == ["268058", "0", "0.3", "100000000000000000000", "0.00000015", "-12.5"]
def test_export_bundle_lines_follow_rules():
if STC is None:
pytest.skip("표본 STC 없음")
src = Source(str(STC))
r = export_bundle(str(STC))
assert r["series"] == 1 and all(not v for v in r["checks"].values())
t = DbfTable(src.files["COSTN.DBF"])
want = {
"L": "L.Txt",
"M": "M.Txt",
"S": "E.Txt",
"W": "W.Txt",
"B": "Unit.Txt",
"X": "JUnit.Txt",
}
want["D"] = "Danga.Txt"
lines = {k: v.split(CRLF) for k, v in r["files"].items()}
seen: dict[str, int] = {}
for i in range(len(t)):
code = t.get(i, "CODE")
name = want.get(code[:1])
if not name or t.deleted(i):
continue
cols, ext = J.split_line(lines[name][seen.get(name, 0)])
seen[name] = seen.get(name, 0) + 1
assert cols[0] == code and len(cols) == CMP[name]
k = code[0]
lam, mam, sam = (float(t.get(i, f) or 0) for f in ("LAMT1", "MAMT1", "SAMT1"))
if k == "L":
assert cols[4] == J.num_text(lam) and ext[0] == t.get(i, "BQCODE")
if k == "M":
assert cols[4] == J.num_text(mam) and cols[6] == J.num_text(
float(t.get(i, "AMT_11") or 0)
)
if k == "S":
assert cols[4:7] == ["0", "0", J.num_text(sam)]
if k in "BXD":
assert cols[4:7] == [J.num_text(mam), J.num_text(lam), J.num_text(sam)]
if k == "X":
assert cols[3] == "시간"
assert sum(seen.values()) == sum(r["counts"].get(n, 0) for n in set(want.values()))
main = J.split_line(r["files"]["Main.Txt"].split(CRLF)[0])[0]
p = DbfTable(src.files["PRJLST.DBF"])
assert main == [p.get(0, "PNUM"), p.get(0, "PNAME"), "02"]
ini = r["files"]["Global.Ini"].decode("cp949")
assert "MAKER=STM" in ini and "JDL VERSION=3.3" in ini and f"TCOST={p.get(0, 'TCOST1')}" in ini
def test_unit_rules():
rec = {
"CODE": "B00001",
"NAME": "a",
"SIZE": "",
"UNIT": "l.s.",
"EQCODE": "",
"BQCODE": "",
"UC": False,
}
rec |= {"L": 1.0, "M": 2.0, "E": 3.0, "AMT": [0] * 5, "PG": [""] * 5, "PG_J": "", "PG_S": ""}
assert J.split_line(code_line(rec, False, "")[1])[0][3] == "l.s.3"
assert (
J.split_line(code_line(rec | {"CODE": "W00001", "UNIT": "T%"}, False, "")[1])[0][3]
== "T%*100"
)
assert (
J.split_line(code_line(rec | {"CODE": "S00001", "UNIT": " $ "}, False, "")[1])[0][3] == "$"
)
assert code_line(rec | {"CODE": "I00001"}, False, "") is None
def _f32(x):
return struct.unpack("<f", struct.pack("<f", x))[0]
def test_hal_rate_float32_factor():
"""할증 계수 4바이트 실수 — 70%·115% 에서 십진 계산보다 1원 적음."""
helper = Path(__file__).with_name("helper_stmate_kanjub_rea.cjs")
rows = [{"BDCODE1": "M00001", "_recno": 1, "SORTCODE": "1", "ENDCODE": "0"}]
for pct, base in ((70, 1000), (115, 1000), (107.5, 2000)):
want = math.trunc(base * _f32(pct / 100))
out = json.loads(
subprocess.check_output(
["node", str(helper)],
input=json.dumps({"mode": "halbase", "rows": rows, "pct": pct, "base": base}),
text=True,
encoding="utf-8",
)
)
assert out == [want] * 3, (pct, out)
assert math.trunc(1000 * _f32(0.7)) == 699 and math.trunc(1000 * _f32(1.15)) == 1149
def test_fifth_series_slot():
"""변경 4차 = 계열 5 — 저장 칸 5 를 차수로 읽음."""
assert stage_slots({"1_C_KANNO": 1, "5_C_KANNO": 2}) == [1, 5]
def test_jdl_api_export_and_check():
if STC is None:
pytest.skip("표본 STC 없음")
import base64
import api_jdl as A
r = A.jdl_export(A.JdlReq(source_path=str(STC)))
assert (
r["series"] == 1
and r["missing"]
and "L.Txt" in r["first"]
and r["first"]["L.Txt"]["F_CODE"]
)
z = A.jdl_export_zip(A.JdlReq(source_path=str(STC)))
assert z.body[:2] == b"PK"
bad = base64.b64encode(J.build_line(["L1", "x\ty", "", "", "", "", ""])).decode()
c = A.jdl_check(A.JdlCheckReq(files={"L.TXT": bad, "readme.md": ""}))["checks"]
assert c["L.TXT"] and c["readme.md"] == ["검사 대상 아님"]