Files
Aislo/resources/tester/test_stmate_ui.py
T
eomsangdonandClaude Opus 5.5 3d8fddfc34 feat(stmate-test): 보고서·제잡비·식언어·수량산출·화면 묶음 합침 · 재계산 후 저장 무변경
- 보고서: 원본 STMATE 엑셀과 셀 대조(목록표·자재단가대비·중기·일위대가·산근 목록·설계내역서·원가계산서) · 조달청 XML 공식 XSD 검증 · PM_Prt 화면
- 제잡비: 원본 계산 순서 4형식(T/S/TA/SA) · 실무 7건 저장 결과키 전수 1원 · 요율표 조회 · 선택지 계산 · 제잡비·제비율 화면
- 식언어·수량: 함수·조건식·금액변수·문법 검사·수량변수·QCODE · 산근·구성·중기공식 편집 화면 · 순환 알림
- 화면: 원본 메뉴 체계·단축키 · 설계내역서 편집(행 넣기·지우기·이동·레벨·보관함) · 초기값 · 단가목록표
- 저장: 초기값(Start8x.Ini) 바뀐 키 저장 · 노임 환산 직종 = 초기값 CONLD · 산근·구성 결과에 적용단가 칸 채우지 않음(원본 0)
- DBF 글 칸 앞 빈칸 보존 · 시험 도우미 표준입력 utf8(한글 조각 깨짐 원인)
- 실무 5건 재계산 후 저장 = 바뀐 칸 0 · 시험 332건 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 19:12:04 +09:00

141 lines
6.0 KiB
Python

"""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]
assert r["qty"]["hal"] == [1, "3", 3]
@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"]))