"""STMATE 재현 앱 화면 편집(설계내역서 wBoq · 도구 대화상자 · 초기값 wM_Set_Sys · 단가목록표 wM_JabPum · 메뉴) 지킴 시험. 계산 = tools/stmate-test/ui_*.js(화면 없이 node) · 자료 = 실무 STC 불러오기 결과(server.api_load). 행 넣기·지우기·이동·계표시·붙여넣기 뒤 저장 원천(boq_rows)과 계산 원천(boqItems)이 함께 움직이는지 직공비 증감으로 대조. """ 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/실무문서" PROJECTS = { "봉화2024": "04.산불 봉화/01. 내역서/13-2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.STC", "영월2024": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC", "대흥2024": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).STC", "울진2025": "25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC", "영덕변경": "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).STC", } # 공종번호 다시 매김 계획 — 원본 번호 꼴(봉화 T4 「1.」 · T2 「1.1」 / 영월 T3 「1」 · T2 「1.1」) ITNUM_PLAN = { "봉화2024": [ {"level": 4, "first": "", "last": ".", "prefix": "none"}, {"level": 2, "first": ".", "last": "", "prefix": "parent"}, ], "영월2024": [ {"level": 3, "first": "", "last": "", "prefix": "none"}, {"level": 2, "first": ".", "last": "", "prefix": "parent"}, ], } _CACHE: dict = {} def run(name): if name not in _CACHE: d = server.api_load(server.LoadReq(source_path=str(DOCS / PROJECTS[name]))) d["itnum_plan"] = ITNUM_PLAN.get(name, []) out = subprocess.check_output( ["node", str(HERE / "helper_stmate_ui.cjs")], input=json.dumps(d, ensure_ascii=False, default=str), text=True, encoding="utf-8", ) _CACHE[name] = (d, json.loads(out)) return _CACHE[name] @pytest.mark.parametrize("name", list(PROJECTS)) def test_boq_structure_untouched_and_items_in_step(name): _d, r = run(name) assert r["structure"] == {"same": True, "nest": True, "items": r["structure"]["items"]} assert r["structure"]["items"][0] == r["structure"]["items"][1] @pytest.mark.parametrize("name", list(PROJECTS)) def test_insert_delete_rows_move_direct_cost(name): _d, r = run(name) ins, dl = r["insert"], r["deleteOrig"] assert ins["ok"] and ins["rowT"] > 0 and ins["delta"] == ins["rowT"] assert ins["back"] == 0 and ins["deleted"] == [] # 새 행은 지워도 원본 지움 목록에 안 듦 assert dl["ok"] and dl["deleted"] == [dl["recno"]] assert dl["delta"] == -dl["leafT"] # 원본 행 지움 = 그 행 저장 금액만큼 직공비 감소 @pytest.mark.parametrize("name", list(PROJECTS)) def test_move_copy_paste_group_level(name): d, r = run(name) assert r["move"] == {"up": True, "down": True, "back": True, "badRejected": True, "nest": True} g = r["group"] ini = d["start_ini"] assert g["wrap"] and g["eTotal"] == g["sumLeaf"] > 0 assert g["eName"] == (ini.get("DISP_N1") or "소계").strip() assert g["shift"] and g["shiftName"] == (ini.get("DISP_N3") or "합계").strip() assert g["note"] and g["nest"] p = r["paste"] assert p["ok"] and p["added"] == p["clipRows"] and p["pairs"] >= 1 and p["nest"] assert p["delta"] == p["srcT"] # 붙여넣은 묶음 금액만큼 직공비 증가 @pytest.mark.parametrize("name", list(PROJECTS)) def test_expand_ilwi_components(name): _d, r = run(name) if "expand" not in r: pytest.skip("구성표 있는 일위대가 내역 행 없음") e = r["expand"] assert e["ok"] and e["made"] == e["want"] and e["deleted"] and e["closing"] == "E1" @pytest.mark.parametrize("name", list(ITNUM_PLAN)) def test_itnum_reproduces_original_sequence(name): _d, r = run(name) for lv in r["itnum"]: got, want = lv["got"], lv["want"] # 원본은 지운 공종 자리를 비워 둔 번호가 있음(영월 1.9) — 첫 빈자리 앞까지는 같아야 함 k = next((i for i, (g, w) in enumerate(zip(got, want)) if g != w), len(got)) assert k >= 3, (lv["level"], got[: k + 1], want[: k + 1]) assert len(got) == len(want) def test_qty_round_and_hal_rate(): _d, r = run("봉화2024") assert r["qty"]["first"] == [1, 12.34, 12.34, 12.34] assert r["qty"]["second"] == [1, 12] # 할증율 = 기준단가에 곱할 백분율(TwBoqRate · 0% = 100%) — 103% → 계산 원천 가산 3% assert r["qty"]["hal"] == [1, "103", 3] assert r["qty"]["zero"] == [1, 0, 0, 0] assert r["qty"]["w"] == [0, 1, 1, "110"] # 보호된 견적단가 수정안함(CBoxNotUC) hb = r["halBase"] assert hb["row"] == hb["want"] and hb["calc"] == hb["want"] # 저장 단가 = 화면 계산 단가 @pytest.mark.parametrize("name", list(PROJECTS)) def test_initial_values_roundtrip(name): d, r = run(name) s = r["sys"] assert s["changed"] == [] and s["errs"] == [] conld = s["conld"] assert s["cond"] == ", ".join(conld[i : i + 6] for i in range(0, len(conld), 6)) assert s["xml"] == ["0", "2", True] @pytest.mark.parametrize("name", list(PROJECTS)) def test_unit_price_list(name): _d, r = run(name) a = r["dan"] assert a["leafRecno"] in a["useBoq"] assert a["counts"]["ALL"] == a["total"] assert a["counts"]["BD"] == a["counts"]["B"] + a["counts"]["D"] assert a["halFirst"] > 0 and a["halSecond"] == 0 # 기준단가 기준 — 두 번 적용해도 같은 값 def test_menu_shortcuts_follow_original(): _d, r = run("봉화2024") m = r["menu"] assert m["dec"] == ["F9", "Ctrl+A", "Ctrl+Shift+P", "Alt+Q", "Ctrl+Shift+Alt+Up"] assert m["sc"]["F9"] == "Act_Calc_Grass" and m["sc"]["Ctrl+B"] == "Act_iLWi_San" assert m["sc"]["Ctrl+N"] == "Act_PrjLst" and m["sc"]["Ctrl+F5"] == "Act_Refresh" assert len(m["acts"]) == len(set(m["acts"]))