"""㉯ 양식이 안 품은 토공·버림 성분은 전개 값을 남김 (2026-09-14 브레인 판정 · 목록으로 못박음). 실측(936be972): STmate 호표(고정형)를 가져오니 전개 성분이 통째로 갈음돼 구조물터파기 85.25→25㎥ (−6,037,712원) · 되메우기·잔토·버림 타설도 줄었음. STmate 호표는 원래 토공을 안 품음(토공 따로 셈). 규칙 — `KEEP_ENGINE_COMPONENTS` 목록에 있는 이름은 **양식에 그 이름 줄이 없을 때** 전개 값을 보존, 목록에 없는 성분은 양식으로 갈음. 양식형(돌쌓기 찰)은 그 줄을 스스로 품어 겹치지 않음. """ from __future__ import annotations from collections import Counter from B05_Profile.B05_Profile_Structures_Schema import StructureInstance from B08_Quantity.B08_Quantity_Engine_StmateRecipe import recipe_item from B08_Quantity.B08_Quantity_Engine_StructureSheet import build_standard_sheets from B08_Quantity.B08_Quantity_Engine_StructureTemplate import ( KEEP_ENGINE_COMPONENTS, apply_templates, ) from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table HOPYO = { "no": 6, "source_code": "B00010", "name": "기슭막이", "spec": "H=2.0m", "unit": "M", "basis": [], "contract_rows": 0, "rows": [ { "name": "깬잡석찰쌓기", "spec": "L3=45", "amount": 2.09, "unit": "M2", "remark": "", "source_code": "", }, ], } WALL = StructureInstance.model_validate( { "structure_id": "a", "type_id": "masonry_wet", "placement": "interval", "start_m": 0.0, "end_m": 10.0, "options": {"height_m": 2.5, "back_len_cm": 45, "foundation": "기초유"}, } ).model_dump() NAMES = {"masonry_wet": "돌쌓기(찰)"} def _fixed() -> dict: return { **recipe_item(HOPYO, type_id="masonry_wet", file_name="a", project="p"), "code": "AX-ST-0000abcd", } def test_목록이_코드에_못박혀_있다() -> None: assert set(KEEP_ENGINE_COMPONENTS) >= {"터파기", "되메우기", "잔토처리", "버림콘크리트"} # ② 원문 빈도(벽·돌쌓기 호표 57) — 기초잡석 0/57 · 채집석(사토 공제 부피)은 호표 채집 줄(품)과 다른 것 assert {"기초잡석", "채집석"} <= set(KEEP_ENGINE_COMPONENTS) def test_고정형을_가져와도_토공_버림_성분은_남는다() -> None: engine = build_table([WALL], NAMES, {}, {}, None, use_templates=False)["structures"][0] fixed = build_table([WALL], NAMES, {}, {}, None, structure_templates={"masonry_wet": _fixed()}) components = {c["name"]: c for c in fixed["structures"][0]["components"]} engine_names = {c["name"] for c in engine["components"]} assert {"기초잡석", "채집석"} <= engine_names # 시험 벽에서 둘 다 서야 보존을 잼 for name in ("터파기", "되메우기", "잔토처리", "버림콘크리트", "기초잡석", "채집석"): before = next(c for c in engine["components"] if c["name"] == name) assert components[name]["amount"] == before["amount"], name assert "깬잡석찰쌓기" in components and "돌" not in components # 목록 밖은 양식으로 갈음 def test_양식형은_그_줄을_스스로_품어_겹치지_않는다() -> None: table = build_table([WALL], NAMES, {}, {}, None) counts = Counter(c["name"] for c in table["structures"][0]["components"]) assert all(count == 1 for count in counts.values()), counts def test_구조물도_장에도_남긴_줄이_보인다() -> None: templates = {"masonry_wet": _fixed()} table = build_table([WALL], NAMES, {}, {}, None, structure_templates=templates) payload = build_standard_sheets(table, {}) apply_templates(payload, {}, templates) names = [row["name"] for row in payload["sheets"][0]["rows"]] assert names[0] == "깬잡석찰쌓기" and "터파기" in names and "버림콘크리트" in names numbers = [row["no"] for row in payload["sheets"][0]["rows"]] assert len(numbers) == len(set(numbers)) # 남긴 줄 차례가 양식 줄과 안 겹침