"""**코드가 없으면 반드시 막힘 표시가 붙는다** (2026-09-09 랩탑 메인 제보로 세운 불변식). ⚠⚠ 오늘 세운 규칙 「막혔다고 말하기 전에 `blocked_kind` 를 볼 것」의 **뒤집힌 얼굴**이다. 보는 쪽을 그렇게 고쳤으면 **다는 쪽도 빠짐없이 달아야** 한다. 안 그러면 받는 쪽이 「코드도 없고 막힘 표시도 없는 **멀쩡한 줄**」로 읽어 **금액이 조용히 빠진다.** 실제로 그랬다 — `측구터파기 · 굴삭기+브레카` 가 `work_item_code: null` · `blocked_kind: null` 이었고 화면에 아무 말도 안 떴다. 인계본의 `bill_flag_warnings`·`unmatched_work_items` 에는 **이미 실려 있었다** — 목록은 있는데 **줄에 표시가 없었던 것**이다. 줄 단위로 보는 쪽은 목록을 안 본다. ⚠ 예외 둘 — 이 둘만 코드 없이 서도 된다. ① **묶음 줄**(`composite_parts`) — 무엇으로 묶이는지 조각에 적혀 있다 ② **내역에 안 서는 줄**(`in_bill: False`) — 합계·무대처럼 **막힌 게 아니라 안 세는 것**. 다만 그쪽도 **까닭은 있어야** 한다(`in_bill_reason` 또는 `blocked_kind`). """ 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 집계(item: str | None, group: str = "흙깎기") -> dict: return {"rows": [{"group": group, "item": item, "unit": "㎥", "amount": 100.0}]} def 인계(**kwargs) -> list[dict]: return build_handoff(**kwargs)["work_items"] def test_시공법을_못_고른_줄에_막힘_표시가_붙는다() -> None: """⚠ 실측 자리 — 「굴삭기+브레카」는 지반 갈래가 아니라 **시공법**이라 매핑이 안 맞았다.""" rows = 인계(summary_table=집계("리핑암")) row = next(r for r in rows if r["name"] == "흙깎기") assert row["work_item_code"] is None assert row["blocked_kind"] == "input_missing" # 시공법을 고르면 풀린다 assert "시공법" in row["blocked_reason"] def test_시공법을_고르면_코드가_붙고_표시가_사라진다() -> None: # ⚠ 시공법 값은 **영문 키**다(`ripping`·`blasting`) — 한글을 넣으면 안 풀린다. rows = 인계(summary_table=집계("리핑암"), ground_methods={"리핑암": "ripping"}) row = next(r for r in rows if r["name"] == "흙깎기") assert row["work_item_code"] is not None assert row["blocked_kind"] is None def test_품셈을_못_이은_줄은_다른_갈래로_적힌다() -> None: """⚠ 「사용자가 고르면 풀림」과 「우리가 매핑을 못 이음」은 **다음에 할 일이 다르다**.""" rows = 인계(summary_table=집계(None, group="지장목제거")) row = next(r for r in rows if r["name"] == "지장목제거") assert row["work_item_code"] is None assert row["blocked_kind"] == "unit_data_missing" assert "못 이었습니다" in row["blocked_reason"] def test_불변식_모든_줄에_대해_선다() -> None: """⚠ 한 자리를 고치는 것으로 끝내지 않는다 — **줄 빌더 여덟 곳 전부**를 건다.""" unit = build_table( [ { "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}, }, { "structure_id": "w1", "type_id": "retaining_wall", "start_m": 20.0, "end_m": 30.0, "options": {"height_m": 2.0, "length_m": 10.0, "form": "반중력식"}, }, ] ) haul = { "rows": [ {"equipment": "dump_truck", "ground": "토사", "volume_m3": 50.0}, {"equipment": "free_haul", "ground": "토사", "volume_m3": 10.0}, {"equipment": "모르는장비", "ground": "토사", "volume_m3": 5.0}, ] } rows = 인계( summary_table=집계("리핑암"), haul_table=haul, unit_quantity_table=unit, preparation_table={"rows": []}, ) 나쁜 = [ r["name"] for r in rows if not r.get("work_item_code") and not r.get("composite_parts") and r.get("in_bill") and not r.get("blocked_kind") ] assert 나쁜 == [], f"코드도 막힘 표시도 없이 내역에 서는 줄: {나쁜}" # 내역에 안 서는 줄도 **까닭 없이** 있으면 안 된다. 까닭없음 = [ r["name"] for r in rows if not r.get("in_bill") and not r.get("blocked_kind") and not str(r.get("in_bill_reason") or "").strip() ] assert 까닭없음 == [], f"안 서는 까닭이 안 적힌 줄: {까닭없음}" def test_무대는_막힌_것이_아니다() -> None: """⚠ 무대(소운반 20m 이내)는 **품에 포함**이라 안 세우는 것이지 막힌 것이 아니다. 여기에 막힘 표시를 달면 「만들어야 할 것」 목록에 올라 없는 일이 생긴다. """ rows = 인계(haul_table={"rows": [{"equipment": "free_haul", "ground": "토사", "volume_m3": 10.0}]}) row = next(r for r in rows if r["haul_equipment"] == "free_haul") assert row["in_bill"] is False assert row["blocked_kind"] is None assert "품에 포함" in row["in_bill_reason"] def test_물량_0_인_구조물은_내역에_안_선다() -> None: """⚠ 물넘이포장이 면적을 안 받아 **`0.0 ㎡` 로 내역에 서고 있었다**(2026-09-09 감사). 코드가 붙어 있어 **0 원 줄**이 만들어지고 화면에는 「값이 있는 줄」로 보인다 — 0 은 「없음」과 구별이 안 된다(오늘 표토제거에서 겪은 자리와 같은 결). 줄은 그대로 넘기되 **내역에서 빼고 까닭을 적는다.** """ unit = build_table( [ { "structure_id": "f1", "type_id": "ford_pavement", "start_m": 0.0, "end_m": 10.0, "options": {"thickness_cm": 20, "length_m": 10.0}, } ] ) row = next(r for r in 인계(unit_quantity_table=unit) if r["name"] == "물넘이포장") assert row["quantity"] == 0.0 assert row["in_bill"] is False assert "물량이 0" in row["in_bill_reason"] assert row["blocked_kind"] == "input_missing"