"""124-1 B08 좌측 패널 할증 모래 · 자갈 · 시멘트 — 기본 12 · 5 · 3 · 패널 값 = 총괄 세 줄 · 저장 · 거울. 정본 = `project_settings.json › quantity › material_surcharge_pct`(받은 칸만 · 기본은 읽을 때 채움) · 총괄자재수량집계표 그 세 줄은 패널 값만 씀(표 사용자 값 무시 · 표에서 안 고침). 견본 = 121 시험의 2공구 꼴 구조물 집계표 계. """ import asyncio import json import subprocess from pathlib import Path import pytest from pydantic import ValidationError from B08_Quantity import B08_Quantity_Router_Material as material_router from B08_Quantity import B08_Quantity_Router_Options as options from B08_Quantity.B08_Quantity_Material_Calc import PANEL_SURCHARGE, compute from common_util.common_util_project_settings import settings_path from M02_MasterTemplete import M02_Template_Layers as layers from test_121_b08_material_sheets import EXCEL_MIX, IMPORTS, SUMMARY, _over ROOT = Path(__file__).resolve().parents[2] CALC_TS = ROOT / "B08_Quantity" / "B08_Quantity_Material_Calc.ts" def _ts(summary, overrides, panel): body = ( f"\nconst s = {json.dumps(summary, ensure_ascii=False)};" f"\nconst o = {json.dumps(overrides, ensure_ascii=False)};" f"\nconst p = {json.dumps(panel, ensure_ascii=False)};" "\nconsole.log(JSON.stringify({sheets: computeMaterialSheets(s, o, p)," " defaults: PANEL_SURCHARGE, filled: panelSurcharge(p)}));" ) script = IMPORTS.sub("", CALC_TS.read_text(encoding="utf-8")) + body result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript", "-e", script], capture_output=True, text=True, encoding="utf-8", timeout=60, ) # fmt: skip assert result.returncode == 0, result.stderr return json.loads(result.stdout) def _rows(sheets): return {row["key"]: row for row in sheets["material"]["rows"]} @pytest.fixture def root(tmp_path, monkeypatch) -> Path: system = tmp_path / "master" (system / "table").mkdir(parents=True) (system / "table/구조물집계표.json").write_text( json.dumps({"열": [], "변수": {"파형강관_1본_m": 8}}), encoding="utf-8" ) monkeypatch.setattr(layers, "SYSTEM_ROOT", system) project = tmp_path / "project" project.mkdir() return project def test_기본_12_5_3_파일에_안_씀(root): assert PANEL_SURCHARGE == {"sand": 12, "gravel": 5, "cement": 3} rows = _rows(compute(SUMMARY, _over(mix=EXCEL_MIX))) assert [rows[k]["surcharge"] for k in ("sand", "gravel", "cement")] == [12, 5, 3] # 올림(14.5886 × 1.12) · 올림(18.0245 × 1.05) · 올림(238.73525 × 1.03) assert [rows[k]["amount"] for k in ("sand", "gravel", "cement")] == [17, 19, 246] assert [k for k, row in rows.items() if row["panel"]] == ["sand", "gravel", "cement"] assert options.read_options(root)["material_surcharge_pct"] == PANEL_SURCHARGE assert not settings_path(root).exists() def test_패널_값이_총괄_세_줄을_바로_바꿈_표_값은_무시(): over = _over(mix=EXCEL_MIX, surcharge={"sand": 50, "sod": 20}) panel = {"sand": 6, "gravel": 4, "cement": 2} # 실무 엑셀 값 out = _ts(SUMMARY, over, panel) rows = _rows(out["sheets"]) assert [rows[k]["amount"] for k in ("sand", "gravel", "cement")] == [ 16, 19, 244, ] # 엑셀 G4 · G5 · G18 assert rows["sand"]["surcharge"] == 6 # 표 사용자 값 50 은 무시 assert rows["sod"]["surcharge"] == 20 # 나머지 줄은 표 값 그대로 assert out["sheets"] == compute(SUMMARY, over, panel) def test_거울_빈칸_깨진_값은_기본(): for panel in (None, {}, {"gravel": 0}, {"sand": "x", "cement": 7.5}): out = _ts(SUMMARY, _over(), panel) assert out["defaults"] == PANEL_SURCHARGE assert out["sheets"] == compute(SUMMARY, _over(), panel), panel assert _rows(compute(SUMMARY, _over(), {"gravel": 0}))["gravel"]["surcharge"] == 0 def test_저장_뒤_남고_받은_칸만_씀(root, monkeypatch): out = options.save_options(root, options.OptionsSave(material_surcharge_pct={"sand": 8})) assert out["material_surcharge_pct"] == {"sand": 8, "gravel": 5, "cement": 3} options.save_options(root, options.OptionsSave(material_surcharge_pct={"cement": 2.5})) stored = json.loads(settings_path(root).read_text(encoding="utf-8")) assert stored["quantity"]["material_surcharge_pct"] == {"sand": 8, "cement": 2.5} # 다른 옵션 저장은 할증을 안 건드림 options.save_options(root, options.OptionsSave(soft_rock_pct=70)) assert options.read_options(root)["material_surcharge_pct"]["sand"] == 8 async def summary(_project_id): return {**SUMMARY, "rows": [], "notes": []} async def project_root(_project_id): return root monkeypatch.setattr(material_router, "structure_summary", summary) monkeypatch.setattr(material_router, "_project_root", project_root) got = asyncio.run(material_router.material_sheets("p")) assert got["surcharge_pct"] == {"sand": 8, "gravel": 5, "cement": 2.5} assert _rows(got["sheets"])["sand"]["surcharge"] == 8 def test_범위_밖_모르는_칸은_거절(): for bad in ({"sand": 120}, {"sand": -1}, {"rebar_d13": 3}): with pytest.raises(ValidationError): options.OptionsSave(material_surcharge_pct=bad) def test_화면_연결(): ui = (ROOT / "B08_Quantity/B08_Quantity_UI_Material.ts").read_text(encoding="utf-8") + ( ROOT / "B08_Quantity/B08_Quantity_Material_Tables.ts" ).read_text(encoding="utf-8") assert "liveSurcharge(projectId, payload.surcharge_pct)" in ui assert "onOptionsChange(host," in ui assert 'key === "surcharge" && !rows[keep[i]].panel' in ui and "할증 = 좌측 패널" in ui panel = (ROOT / "B08_Quantity/B08_Quantity_UI_Options.ts").read_text(encoding="utf-8") assert "put({ material_surcharge_pct:" in panel and "body.material_surcharge_pct" in panel