# -*- coding: utf-8 -*- """PLAN 704-1 · 704-2 — M02 기준 목록(대표 규격 펼침 · 금액 = M01 엔진) · 계산서 보기. ① 세 항목(돌쌓기 메쌓기 · 파형강관 · 구조물터파기) 줄 금액 = M01 시험 계산(`store.calc`) ② 대표 규격 — 관경 고르기 11 · 뒷길이 표 줄 3 · 지역 = 자동값 한 벌 ③ 실무 낱말 — 「석축」 찾으면 돌쌓기 · 찰쌓기(서버 찾기 맞춤 `hit` = 화면 `matchLogic` 거울 · 208-1) ④ 계산서 — 계 = 엔진 값 · 비목 소계 합 = 계 · 규격 축 아닌 입력은 400 ⑤ 목록 저장본 — 임시 storage `_derived/` · 같은 지문이면 다시 안 셈 ⑥ 화면 — 고르기 창이 기준 목록 표 · 계산서 [담기] = 지금 담기 동작(`done(logic)`) """ from __future__ import annotations from pathlib import Path import pytest from M01_MasterData import M01_MasterData_Store as m01 from M01_MasterData.M01_MasterData_Store_Find import hit from M02_MasterTemplete import M02_Logic_Standard as st ROOT = Path(__file__).resolve().parents[2] THREE = ("GF000220", "GC000404", "GF000101") # 돌쌓기 메쌓기(장비) · 파형강관 부설 · 구조물터파기 def _rows(key: str) -> list[dict]: whole = m01.loaded()[1] return st._rows_of(whole, whole.logic(key), False) def _given(key: str, change: dict) -> dict: whole = m01.loaded()[1] return st._given(whole, whole.logic(key), change)[0] @pytest.mark.parametrize("key", THREE) def test_줄_금액은_M01_시험_계산에_끝수만_버림(key: str) -> None: rows = _rows(key) assert rows for row in rows: got = m01.calc(key, _given(key, row["바꿈"])) assert got["ok"], got gap = float(got["sums"]["계"]) - row["금액"] # 줄 · 비목 버림(B09 산근 규칙) — 최대 3 원 assert -0.01 <= gap < 3.2, row["규격"] def test_대표_규격_펼침() -> None: pipe = _rows("GC000404") assert len({r["규격"] for r in pipe}) == len(pipe) # 줄마다 규격이 다름 assert sum(1 for r in pipe if not r["바꿈"] or "관경" in r["바꿈"]) == 11 assert pipe[0]["규격"].startswith("관경 250") and pipe[0]["단위"] == "본" stone = _rows("GF000220") backs = [r for r in stone if not r["바꿈"] or "뒷길이" in r["바꿈"]] assert [r["규격"].split(" · ")[0] for r in backs] == [ "뒷길이 35㎝ 이하", "뒷길이 55㎝ 이하", "뒷길이 75㎝ 이하", ] assert all("지역" not in r["바꿈"] for r in stone) # 지역 = 자동값 한 벌 hand = _rows("GF000219") # 메쌓기(인력) — 견치돌 · 뒷길이 25 같은 표 밖 조합은 0 원 → 뺌 assert all(r["금액"] != 0 for r in hand[1:]) assert stone[0]["공종"] == "돌공" and stone[0]["단위"] == "㎡" assert _rows("GF000101")[0]["공종"] == "토공" def test_석축_찾으면_쌓기() -> None: hits = [ r["이름"] for _, data in m01._logic_files(m01.loaded()[0]) for logic in data.get("줄") or [] for r in [{**logic, "별칭": ", ".join(st.words_of(logic))}] if hit(r, "석축") ] assert "돌쌓기 메쌓기(장비)" in hits assert any("찰쌓기" in name for name in hits) assert "석축" in _rows("GF000220")[0]["별칭"] @pytest.mark.parametrize("key", THREE) def test_계산서_계는_엔진_값(key: str) -> None: row = _rows(key)[-1] got = st.sheet(key, row["바꿈"]) assert got["계"] == pytest.approx(row["금액"], abs=0.01) assert sum(g["소계"] for g in got["묶음"]) == pytest.approx(got["계"], abs=0.01) assert got["판"].endswith("노임") and got["출처"] assert all(line["글"] for g in got["묶음"] for line in g["줄"]) def test_계산서_규격_축_아닌_입력은_거절() -> None: with pytest.raises(m01.StoreError) as e: st.sheet("GF000220", {"지역": "서울"}) assert e.value.status == 400 with pytest.raises(m01.StoreError): st.sheet("GF000220", {"뒷길이": 40}) # 대표 값 아님 def test_목록_저장본_같은_지문이면_안_셈(tmp_path, monkeypatch) -> None: calls = [] def fake(files, workers=1): calls.append(workers) return {"판": "2026 상반기 노임", "공종": ["돌공"], "줄": [{"키": "GF000220"}]} monkeypatch.setattr(st.layers, "storage_root", lambda: tmp_path) monkeypatch.setattr(st, "build", fake) st._MEMO.clear() first = st.listing() assert first["준비"] is False and first["줄"] == [] # 셈 도중 = 빈 줄 st._MEMO["job"].join() assert st.listing()["줄"] == [{"키": "GF000220"}] st._MEMO.clear() # 서버 다시 켬 — 저장본에서 읽음 assert st.listing()["준비"] is True and len(calls) == 1 assert list((tmp_path / "_derived" / st.KIND).glob("*.json")) st._MEMO.clear() def test_화면_기준_목록_계산서_담기() -> None: pick = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts").read_text("utf-8") assert 'from "@ui/ui_template_data_table"' in pick and "fetchStandard()" in pick assert "openStandardSheet(row, () => {\n close();\n done(logic);" in pick assert "(!work.select.value || x.공종 === work.select.value)" in pick sheet = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor_Sheet.ts").read_text("utf-8") # 줄 · 소계 · 계 = 공용 단가산출 계산서(714-2 · 계산 줄 부품을 그 안에서 씀) assert 'from "../A00_Common/calc_sheet"' in sheet and "createSourceView(" in sheet for name in ("Labor_Pick", "Labor_Sheet"): text = (ROOT / f"M02_MasterTemplete/M02_MasterTemplete_{name}.ts").read_text("utf-8") assert len(text.splitlines()) <= 700 router = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py").read_text("utf-8") assert '@router.get("/logic-lib/standard", dependencies=[Depends(verify_session)])' in router assert '@router.post("/logic-lib/sheet", dependencies=[Depends(verify_session)])' in router def test_내역서_낱말_측구파기로_측구터파기를_찾음() -> None: logic = next( logic for _, data in m01._logic_files(m01.loaded()[0]) for logic in data.get("줄") or [] if logic.get("키") == "GF000099" # 9-12-1 측구터파기 토사 ) row = {**logic, "별칭": ", ".join(st.words_of(logic))} assert hit(row, "측구파기")