Files
Aislo/resources/tester/test_810_1_stc.py
T
eomsangdonandClaude Opus 5.5 e794e0490c feat(810-1): 경쟁사 원파일 파서 — EST(.estx) · STmate(.stc) 구조 줄 · node · smt_parser 거울 대조
- M02_San_Estx: DBISAM 표 · BLOB 읽기(import-estx.cjs 거울) · 산근 · 일위대가 블록 → 구조 줄(LinkIndex 뜻 · SCheck 비목 · LineData 표지 · 기계 세 줄 · 산근 속 산근 · 자리) · 줄합 = 저장 결과만
- M02_San_Stc: COSTN.DBF · COSTN.SMT · BOQ11.DBF 읽기(smt_parser 거울) · 줄 풀이 = M02_San_Stmate 재사용 · BDQTY 난독화 표지 · Start8x.Ini 옵션 그대로
- 시험 test_810_1_estx · test_810_1_stc 34 통과 — 거울 다름 0(estx 6 · stc 7) · 블록 금액 = 저장값 100 %(EST 3,378 · STC 371) · 엑셀 엔진 대조(거창 줄 갈래 547 중 265 = 엑셀이 작업량 Q 를 상수로 봄)
- 꼴 문서 resources/master_data/_원파일파서_틀.md · 대조 목록 docs/raw/요구_1104/_목록_810-1_원파일대조.xlsx

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 19:22:25 +09:00

188 lines
8.6 KiB
Python

"""810-1 지킴 — STmate 원파일 파서(`M02_San_Stc`) · 실무 .STC 일곱(읽기만).
(a) 거울 — `tools/stmate-test/smt_parser.py`(parse_dbf_bytes · extract_costn_memos)와
COSTN · BOQ11 행 ·
칸 · 산근 원문 1:1(다름 0) · 줄 풀이 = `M02_San_Stmate.parse_line`
(b) 블록 다시 셈(`M02_San_Eval.stmate_block` · 차수 1 단가 · 낙찰 · 결과 조정 · 끝전 갈래 = 옵션
SAN_TYP 1 비목버림 / 0 합버림_큰나머지) = COSTN 저장 금액(TAMT1 · LAMT1 · MAMT1 · SAMT1) 100 %
(c) 엑셀 짝 다섯(`helper_m02_hopyo_golden`) = 지금 엑셀 엔진 블록 금액 · 숨은 G 열 원문 ·
목록표 단가와
대조 — 다른 곳 = 엑셀 추정이 틀린 곳(수만 지킴) · 엑셀에 안 찍힌 블록 수 기록
원파일 없으면 skip(다른 PC).
"""
from __future__ import annotations
import sys
from functools import lru_cache
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
for p in (ROOT, ROOT / "tools" / "stmate-test", ROOT / "resources" / "tester"):
if str(p) not in sys.path:
sys.path.insert(0, str(p))
from M02_MasterTemplete import M02_San_Eval as ev # noqa: E402
from M02_MasterTemplete import M02_San_Stc as S # noqa: E402
from M02_MasterTemplete import M02_San_Stmate as stm # noqa: E402
K = ROOT / "resources" / "knowledge" / "original" / "실무문서"
SO = K / "25년 산불진화임도(기번8)(울진소광)"
FILES = {
"봉화": K / "04.산불 봉화/01. 내역서/13-2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.STC", # noqa: E501
"동부청영월": K / "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC", # noqa: E501
"울진대흥": K / "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).STC", # noqa: E501
"영덕계류": K / "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).STC", # noqa: E501
"울진소광": SO / "2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC", # noqa: E501
"소광22": SO / "1. 설계원본/03-내역/22-2025년 산불진화임도사업(울진 금강송 소광).STC", # noqa: E501
"소광변경1": SO / "변경 1차분-2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC", # noqa: E501
} # fmt: skip
#: 파일 → (산근 블록, 일위 블록 · 구성 못 읽음, SAN_TYP)
WANT = {
"봉화": (65, 45, "1"),
"동부청영월": (69, 23, "0"),
"울진대흥": (46, 34, "1"),
"영덕계류": (35, 13, "1"),
"울진소광": (62, 37, "1"),
"소광22": (40, 25, "1"),
"소광변경1": (54, 39, "1"),
}
RULE = {"1": "비목버림", "0": "합버림_큰나머지"}
@lru_cache(maxsize=2)
def raw(tag: str) -> bytes:
path = FILES[tag]
if not path.exists():
pytest.skip(f"원파일 없음 {path.name}")
return path.read_bytes()
@lru_cache(maxsize=2)
def parsed(tag: str) -> dict:
return S.read(raw(tag))
def _members(tag: str) -> dict[str, bytes]:
z, names = S._zip(raw(tag))
return {k: z.read(v) for k, v in names.items()}
@pytest.mark.parametrize("tag", list(FILES))
def test_mirror_smt_parser(tag: str) -> None:
import smt_parser as sp
m = _members(tag)
for name in ("COSTN.DBF", "BOQ11.DBF"):
_, theirs, _ = sp.parse_dbf_bytes(m[name])
mine = S.dbf(m[name])
assert len(mine) == len(theirs), name
for a, b in zip(mine, theirs, strict=True):
b = {k: v for k, v in b.items() if k != "_deleted"}
if "IFORM" in a:
assert a.pop("IFORM").hex() == b.pop("IFORM_HEX")
b.pop("IFORM")
assert a == b
memos = sp.extract_costn_memos(m["COSTN.DBF"], m["COSTN.SMT"])
d = parsed(tag)
blocks = {b["코드"]: b for b in d["산근"]}
assert sorted(blocks) == sorted(memos)
for code, mm in memos.items():
assert [ln["글"] for ln in blocks[code]["줄"]] == mm["text"].splitlines()
assert all(ln["풀이"] == stm.parse_line(ln["글"]) for ln in blocks[code]["줄"])
@pytest.mark.parametrize("tag", list(FILES))
def test_saved_amounts(tag: str) -> None:
d = parsed(tag)
n_cost, n_head, san = WANT[tag]
assert (len(d["산근"]), len(d["일위"]), d["옵션"]["SAN_TYP"]) == (n_cost, n_head, san)
assert all(b["표지"] == [S.UNREAD[0]] and not b["줄"] for b in d["일위"])
bad = []
for b in d["산근"]:
got = ev.stmate_block(b, [ln["풀이"] for ln in b["줄"]], d["단가"], RULE[san])["금액"]
if any(abs(got[c] - b["저장"][c]) >= 0.5 for c in "TLMS"):
bad.append((b["코드"], got, b["저장"]))
assert bad == []
assert all(not b["표지"] for b in d["산근"])
def test_structure_line() -> None:
"""봉화 D00014 — 참조 줄 자원 · 비목 · 계 · 산근 블록 머리."""
b = next(b for b in parsed("봉화")["산근"] if b["코드"] == "D00014")
assert (b["이름"], b["단위"], b["저장"]["T"]) == ("수평규준틀(변곡점)", "개소", 26144)
refs = [ln for ln in b["줄"] if ln["자원"]]
assert [ln["자원"]["코드"] for ln in refs] == ["M00080", "M00002", "M00001", "L00001", "L00007"]
assert refs[0]["자원"]["종류"] == "자재" and refs[3]["자원"]["종류"] == "노임"
assert [ln["단"] for ln in b["줄"] if ln["합계"]] == [1, 1, 2]
# ── (c) 엑셀 엔진 대조 ─────────────────────────────────────────────────
PAIRED = ("봉화", "동부청영월", "울진대흥", "영덕계류", "울진소광")
def excel_diffs(tag: str) -> tuple[list[dict], dict]:
"""엑셀 엔진(숨은 G 열 · 목록표 단가) 대 원파일 → (다른 곳, 셈)."""
import helper_m02_hopyo_golden as golden
from M02_MasterTemplete import M02_San_Book as book
if not golden.FILES[tag].exists():
pytest.skip("엑셀 없음")
xl = book.load(golden.FILES[tag])
d = parsed(tag)
rule = RULE[d["옵션"]["SAN_TYP"]]
mine = {b["코드"]: b for b in d["산근"]}
out = []
cnt = {"엑셀블록": len(xl["블록"]), "원파일블록": len(mine), "같이": 0, "금액같음": 0}
cnt |= {"글같음": 0, "단가": 0, "단가같음": 0}
for x in xl["블록"]:
b = mine.get(x["코드"])
if b is None:
out.append({"코드": x["코드"], "항목": "원파일에 블록 없음"})
continue
cnt["같이"] += 1
res = ev.stmate_block(x, stm.parse_block(x["몸"]), xl["단가"], rule)["금액"]
if all(abs(res[c] - b["저장"][c]) < 0.5 for c in "TLMS"):
cnt["금액같음"] += 1
else:
out.append({"코드": x["코드"], "항목": "금액", "엑셀": res, "원파일": b["저장"]})
g = [ln["G"].strip() for ln in x["몸"] if ln["G"].strip()]
if g == [ln["글"].strip() for ln in b["줄"] if ln["글"].strip()]:
cnt["글같음"] += 1
else:
out.append({"코드": x["코드"], "항목": "원문 글"})
for code, p in xl["단가"].items():
q = d["단가"].get(code)
cnt["단가"] += 1
if q and all(abs((p[c] or 0) - q[c]) < 0.5 for c in "TLMS"):
cnt["단가같음"] += 1
else:
out.append({"코드": code, "항목": "단가", "엑셀": p, "원파일": q})
for code in sorted(set(mine) - {x["코드"] for x in xl["블록"]}):
out.append({"코드": code, "항목": "엑셀에 안 찍힌 블록"})
return out, cnt
#: 엑셀 짝 → (엑셀 블록, 같이 있는 블록, 엑셀 단가 줄, 그중 같음, 엑셀에 안 찍힌 원파일 블록)
#: 울진대흥 = 엑셀(신설사업 판)과 원파일(설계원본 판)이 다른 판 — 코드 겹침 14 · 단가 40 빠짐
EXCEL = {
"봉화": (65, 65, 203, 203, 0),
"동부청영월": (69, 69, 201, 201, 0),
"울진대흥": (47, 14, 153, 113, 32),
"영덕계류": (15, 15, 54, 54, 20),
"울진소광": (40, 40, 138, 138, 22),
}
@pytest.mark.parametrize("tag", PAIRED)
def test_excel_engine(tag: str) -> None:
out, cnt = excel_diffs(tag)
n_xl, both, n_price, same_price, extra = EXCEL[tag]
assert (cnt["엑셀블록"], cnt["같이"], cnt["단가"]) == (n_xl, both, n_price)
assert cnt["금액같음"] == cnt["같이"] and cnt["글같음"] == cnt["같이"]
assert cnt["단가같음"] == same_price
assert sum(o["항목"] == "엑셀에 안 찍힌 블록" for o in out) == extra