"""구조물 터파기·되메우기·잔토가 인계 축에 오르나 (2026-09-08). ⚠⚠ **빠뜨렸던 자리다.** 성분으로만 있고 아무도 안 받아 **내역서에 한 줄도 안 나갔다** — 두께 식·기초 몫을 아무리 맞춰도 금액이 0원이었다(B09 매김에서 드러남). ⚠ 겨누는 것 여섯 ① 터파기가 **공종 줄로 선다** ② 심도로 갈린다 — 품셈 9-13 이 18구분(토질 3 × 육상/용수 × 심도 3)이라서 ③ ⚠ 토질·용수를 **지어내지 않는다** — 상위 코드 + 사유 ④ 되메우기는 코드가 붙고 **금액이 선다** ⑤ ⚠ 잔토는 **사토로 갈 몫**이라 내역에 안 서되 값과 사유가 실린다 ⑥ 구조물이 없으면 줄도 없다 """ from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff # noqa: E402 from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table # noqa: E402 def 구조물(**options: object) -> dict: return { "structure_id": "s1", "type_id": "masonry_wet", "start_m": 0.0, "end_m": 10.0, "options": {"height_m": 2.5, "length_m": 10.0, "back_len_cm": 45, **options}, } def 줄(**options: object) -> dict: unit = build_table([구조물(**options)]) rows = build_handoff(unit_quantity_table=unit)["work_items"] return {row["name"]: row for row in rows} def test_터파기가_공종_줄로_선다() -> None: row = 줄(foundation="기초유")["구조물터파기"] assert row["unit"] == "㎥" assert row["quantity"] > 0 assert row["work_item_code"] == "FP-09-13" def test_심도로_갈린다() -> None: """직고 2.5 + 기초 0.5 = 3.0m → `2~3m` 칸. 기초를 안 고르면 2.5m 라 같은 칸이다.""" assert 줄(foundation="기초유")["구조물터파기"]["spec"] == "심도 2~3m" 깊은 = build_table( [ { "structure_id": "s9", "type_id": "masonry_wet", "start_m": 0.0, "end_m": 10.0, "options": { "height_m": 3.5, "length_m": 10.0, "back_len_cm": 45, "foundation": "기초유", }, } ] ) rows = build_handoff(unit_quantity_table=깊은)["work_items"] assert next(r for r in rows if r["name"] == "구조물터파기")["spec"] == "심도 3m 초과" def test_토질을_못_가르면_그렇게_적는다() -> None: """⚠ 지반 갈래 표가 없으면 「토질을 못 가름」이다 — 토사로 눅이지 않는다.""" row = 줄(foundation="기초유")["구조물터파기"] assert row["blocked_kind"] == "input_missing" assert "토질을 못 가름" in row["blocked_reason"] assert row["ground_class"] is None assert row["in_bill"] is False def test_토질은_측점_설계값에서_온다() -> None: """⚠ 새 칸을 만들지 않는다 — 이미 저장되는 `design.ground_type` 을 읽는다.""" unit = build_table( [구조물(foundation="기초유")], None, None, {0.0: "ripping_rock", 5.0: "ripping_rock", 10.0: "ripping_rock"}, ) row = next( r for r in build_handoff(unit_quantity_table=unit)["work_items"] if r["name"] == "구조물터파기" ) assert row["ground_class"] == "암절취" assert row["spec"] == "암절취 · 심도 2~3m" # 남은 축은 용수 하나다 — 그것만 사유에 남는다. assert "용수" in row["blocked_reason"] and "토질" not in row["blocked_reason"] def test_걸친_측점이_섞이면_안_고른다() -> None: """⚠ 다수결로 고르면 임의값이 금액으로 굳는다 — 암 단가가 몇 배다.""" unit = build_table( [구조물(foundation="기초유")], None, None, {0.0: "soil", 5.0: "soil", 10.0: "blasting_rock"}, ) row = next( r for r in build_handoff(unit_quantity_table=unit)["work_items"] if r["name"] == "구조물터파기" ) assert row["ground_class"] is None assert "섞여" in row["blocked_reason"] assert "토사 2곳" in row["blocked_reason"] and "발파암 1곳" in row["blocked_reason"] def test_되메우기는_금액이_선다() -> None: row = 줄(foundation="기초유")["되메우기"] assert row["work_item_code"] == "FP-09-14-01" assert row["in_bill"] is True assert abs(row["quantity"] - (0.5 + 2.5) * 0.2 * 10.0) < 1e-9 def test_잔토는_사토로_갈_몫이라_내역에_안_선다() -> None: """⚠ 유토곡선이 세야 겹치지 않는다 — 다만 그 통로가 아직 없다는 사실을 사유로 남긴다.""" row = 줄(foundation="기초유")["잔토처리"] assert row["in_bill"] is False # ⚠ 「통로가 없어 안 실림」이던 문구가 **바뀐 자리** — 2026-09-09 유토곡선이 그 값을 # 받아 사토에 얹고 「사토 운반」 줄로 세우게 됐다. 사유도 그 사실로 옮겼다. assert "사토" in row["in_bill_reason"] and "사토 운반" in row["in_bill_reason"] assert row["quantity"] > 0 def test_구조물이_없으면_줄도_없다() -> None: rows = build_handoff(unit_quantity_table={"structures": []})["work_items"] assert all(row["name"] not in {"구조물터파기", "되메우기", "잔토처리"} for row in rows) def test_같은_토질_같은_심도는_한_줄이다() -> None: """⚠ 근거 문구가 측점마다 달라 **두 줄로 갈리던 자리**(2026-09-08 실화면). 사람이 보는 문구가 다르다는 이유로 내역 줄이 쪼개지면 같은 공종이 두 번 선다. """ unit = build_table( [ { "structure_id": "a", "type_id": "masonry_wet", "start_m": 0.0, "end_m": 10.0, "options": { "height_m": 2.5, "length_m": 10.0, "back_len_cm": 45, "foundation": "기초유", }, }, { "structure_id": "b", "type_id": "masonry_wet", "start_m": 100.0, "end_m": 110.0, "options": { "height_m": 2.5, "length_m": 10.0, "back_len_cm": 45, "foundation": "기초유", }, }, ], None, None, {0.0: "soil", 10.0: "soil", 105.0: "soil"}, ) rows = [ r for r in build_handoff(unit_quantity_table=unit)["work_items"] if r["name"] == "구조물터파기" ] assert len(rows) == 1 assert rows[0]["ground_class"] == "토사" # ── 용수 축 · 18구분 (2026-09-09 사용자 확정 3차 ④) ────────────────── def test_18구분_차례가_원문_그대로다() -> None: """⚠ 한 칸 밀리면 **다른 지반·다른 심도 단가**가 붙는다.""" from B08_Quantity.B08_Quantity_Engine_Handoff_Trench import trench_child_code assert trench_child_code("FP-09-13", "soil", "육상", "0~1m") == "FP-09-13-01" assert trench_child_code("FP-09-13", "soil", "용수", "0~1m") == "FP-09-13-04" assert trench_child_code("FP-09-13", "ripping_rock", "육상", "0~1m") == "FP-09-13-07" assert trench_child_code("FP-09-13", "ripping_rock", "용수", "2~3m") == "FP-09-13-12" assert trench_child_code("FP-09-13", "blasting_rock", "용수", "2~3m") == "FP-09-13-18" def test_축이_없으면_상위_코드로_둔다() -> None: """⚠ 3m 를 넘는 칸은 **원문에 없다** — 지어내지 않는다.""" from B08_Quantity.B08_Quantity_Engine_Handoff_Trench import trench_child_code assert trench_child_code("FP-09-13", "soil", "", "0~1m") is None assert trench_child_code("FP-09-13", None, "육상", "0~1m") is None assert trench_child_code("FP-09-13", "soil", "육상", "3m 초과") is None def 물줄(water: str | None, ground: str = "ripping_rock") -> dict: unit = build_table( [구조물(foundation="기초유")], None, None, {0.0: ground, 10.0: ground}, ) rows = build_handoff(unit_quantity_table=unit, structure_trench_water=water)["work_items"] return next(row for row in rows if row["name"] == "구조물터파기") def test_용수를_정하면_자식_코드로_내려가_금액이_선다() -> None: row = 물줄("육상") assert row["work_item_code"] == "FP-09-13-09" # 육상 암절취 2~3m assert row["in_bill"] is True and row["blocked_kind"] is None def test_통상값이라는_사실이_줄에_적힌다() -> None: """⚠ 사용자 확정이 아니라 **통상값**이다 — 안 적으면 뒤집을 근거가 사라진다.""" assert "통상값" in 물줄("육상")["spec_detail"] def test_용수로_뒤집으면_코드가_옮겨_간다() -> None: assert 물줄("용수")["work_item_code"] == "FP-09-13-12" def test_안_정하면_상위_코드로_막힌다() -> None: row = 물줄(None) assert row["work_item_code"] == "FP-09-13" assert row["in_bill"] is False and "용수" in row["blocked_reason"] def test_상태_사실이_값_옆에_적힌다() -> None: """⚠ 코드 주석에만 두면 화면에서 안 보인다 — 되메우기·잔토 줄에 함께 실린다.""" rows = 줄(foundation="기초유") for name in ("되메우기", "잔토처리"): assert "다짐부피 ÷ C" in rows[name]["spec_detail"], name