Files
Aislo/resources/tester/test_810_1_stc.py
T
eomsangdonandClaude Opus 5.5 43e9455688 feat(828-1): 원파일 일위대가 구성 줄 → 일위대가 길 · 중기 구성 → 내 기계 씨앗 · 낙찰률 줄 2 칸 좁힘
- ㉯ STC 일위 = HP 호표(엑셀 일위대가표 길과 같은 꼴 · 「일위대가표」 시트) → 목록 · 만들기 · 고를 재료 판(구성 줄 · 「일위대가 N」)
- ㉰ 구성표 중기 → 기계구성(엑셀 중기사용료와 같은 값 · 달러 손료 = 천원) · 같은 파일 구성 대조 같은구성 → STmate 기계 찾기
- ㉱ 낙찰률 줄 2 칸(원단가) = 구성 셈 · 일위 머리만(엑셀 일위대가표와 같음) · 산근 · 자원 = 1 칸 그대로(엑셀 단가산출근거와 같음 · 812-1 짝 0)
- 별도계상 = 자재 이름 · 규격 「별도계상」 · 「별산」(엑셀 비고 「별산…」 짝 · 828-2 대조)
- MyMachine 엑셀 중기사용료 잡품 줄 = 잡재료 · 잡재료비 이름도(원파일과 같은 값)
- 시험 test_828_1_stc_composition 새로 · test_814_1 낡은 칸 고침(원파일 호표 0 → 45 · 격자 일위대가표 시트) · test_810_1 거울 대조 글 칸 앞 빈칸 걷음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:47:55 +09:00

119 lines
5.5 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) 엑셀 짝 대조는 812-1 엑셀 읽기기 골든(`test_812_1_xlsx_golden` · 옛 엑셀 엔진 걷음)
원파일 없으면 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
#: 파일 → (산근 블록, 일위 블록(828-1 구성표 줄), 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):
# 거울 판(stmate-test)은 글 칸 앞 빈칸을 남김 — 제품은 양끝을 걷음(이름 맞춤 그대로)
b = {k: v.strip() if isinstance(v, str) else 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["줄"] and not b["표지"] for b in d["일위"]) # 828-1 구성표 읽음
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]