"""STMATE 재현 앱 일위대가(B)·중기(X) 재계산과 연쇄 재계산(계산완료) — 실무 STC 저장 단가와 1원 대조. 구성 = BDQTY.DBF 복호(tools/stmate-test/bdqty.py · 사용자 승인) · 정답 = STC 의 COSTN 저장 단가. 연쇄 시험은 기초단가(L·M·S)만 두고 중기 → 산근 → 일위대가 → 내역 → 원가계산서를 다시 셈. """ import json 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 server # noqa: E402 HERE = Path(__file__).parent DOCS = ROOT / "resources/knowledge/original/실무문서" ORIGINAL = { "봉화2024": "04.산불 봉화/01. 내역서/13-2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.STC", "울진2025": "25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC", "영월2024": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC", "대흥2024": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).STC", "소광원설계": "25년 산불진화임도(기번8)(울진소광)/1. 설계원본/03-내역/22-2025년 산불진화임도사업(울진 금강송 소광).STC", } CHANGED_OK = { "영덕변경": "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).STC", } def load(rel): return server.api_load(server.LoadReq(source_path=str(DOCS / rel))) def node(helper, payload): out = subprocess.check_output( ["node", str(HERE / helper)], input=json.dumps(payload, ensure_ascii=False), text=True, encoding="utf-8", ) return json.loads(out) def stored(row, slot): return [float(row[f"{k}AMT{slot}"] or 0) for k in "MLS"] @pytest.mark.parametrize("name", list(ORIGINAL) + list(CHANGED_OK)) def test_b_and_x_compositions_match_stored(name): d = load({**ORIGINAL, **CHANGED_OK}[name]) out = node( "helper_stmate_ilwi.cjs", {k: d[k] for k in ("costn_rows", "bdqty_rows", "start_ini")} ) rows = {r["CODE"].strip(): r for r in d["costn_rows"]} assert len(out) > 20, f"{name} 구성 호표 {len(out)}" diffs = {} for code, got in out.items(): slot = 2 if float(rows[code].get("NAGCD") or 0) > 0 else 1 if got != stored(rows[code], slot): diffs[code] = (got, stored(rows[code], slot)) assert not diffs, f"{name} 불일치 {len(diffs)}/{len(out)} {list(diffs.items())[:4]}" @pytest.mark.parametrize("name", list(ORIGINAL)) def test_full_cascade_keeps_every_unit_price_and_total(name): d = load(ORIGINAL[name]) before = {r["CODE"].strip(): stored(r, 1) for r in d["costn_rows"]} out = node("helper_stmate_cascade.cjs", d) assert out["count"]["xCount"] > 0 and out["count"]["dCount"] > 0 and out["count"]["bCount"] > 0 moved = {c: (v[:3], before[c]) for c, v in out["prices"].items() if v[:3] != before[c]} assert not moved, f"{name} 재계산 뒤 바뀐 단가 {list(moved.items())[:4]}" assert abs(out["grand"] - float(d["prjlst"]["COSTW1"])) < 0.5