"""보내는 단위가 품셈 밑수와 같은가 — 금액이 실제로 틀렸던 자리 (2026-09-08). ⚠ 겨누는 것 다섯 ① 돌쌓기가 **㎡ · 비탈면적**으로 나가는가 (m · 연장이면 금액이 2.6배) ② 성분이 안 서면 **연장으로 대신 세지 않는가** — 세면 다시 틀린 축이다 ③ 밑수가 m 인 공종(맹암거·측구)은 **종전대로** 연장인가 ④ 표기 차이(㎥/m3 · 개소/개)는 흡수하되 **뜻이 다른 단위는 드러내는가** ⑤ 인계본에 그 경고가 **실제로 실리는가** (만들어 두고 안 부르면 없는 것과 같다) """ 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_BasisUnit import ( # noqa: E402 basis_units, normalize_unit, verify_unit_matches_basis, ) from B08_Quantity.B08_Quantity_Engine_Handoff import ( # noqa: E402 build_handoff, load_mapping, ) MAPPING = load_mapping() def 돌쌓기(**options: object) -> dict: """전개까지 실제로 돌린 구조물 한 기 — 값을 손으로 적지 않는다.""" from B08_Quantity.B08_Quantity_Engine_UnitQuantity import expand item = expand( { "structure_id": "s1", "type_id": "masonry_wet", "start_m": 0.0, "end_m": 10.0, "options": {"height_m": 2.5, "length_m": 10.0, **options}, }, names={"masonry_wet": "돌쌓기(찰)"}, ) return { "structure_id": item.structure_id, "type_id": item.type_id, "name": item.name, "length_m": item.length_m, "height_m": item.height_m, "start_m": item.start_m, "end_m": item.end_m, "options": item.options, "notes": item.notes, "components": [ {"name": c.name, "unit": c.unit, "amount": c.amount, "basis": c.basis} for c in item.components ], } def 인계(structure: dict) -> dict: return build_handoff(unit_quantity_table={"structures": [structure]}, mapping=MAPPING) def test_돌쌓기는_면적으로_나간다() -> None: """⚠ 실측 — 10m 로 나가 529,389원이 섰다. 26.101㎡ 로 세야 1,381,753원이다.""" row = 인계(돌쌓기(back_len_cm=45))["work_items"][0] assert row["work_item_code"] == "FP-13-04-05" assert row["unit"] == "㎡" assert abs(row["quantity"] - 26.101) < 0.01, row["quantity"] assert row["in_bill"] is True def test_면적이_안_서면_연장으로_대신_세지_않는다() -> None: """뒷길이 40 은 품셈 표에 없어 전개가 안 선다 — 그때 10.0 m 로 서면 안 된다.""" row = 인계(돌쌓기(back_len_cm=40))["work_items"][0] assert row["quantity"] != 10.0 assert row["in_bill"] is False or row["blocked_kind"] assert row["blocked_reason"] def test_밑수가_m_인_공종은_종전대로_연장이다() -> None: row = 인계( { "structure_id": "s2", "type_id": "underdrain", "name": "맹암거", "length_m": 40.0, "start_m": 0.0, "end_m": 40.0, "components": [], "notes": [], "options": {}, } )["work_items"][0] assert (row["unit"], row["quantity"]) == ("m", 40.0) def test_표기_차이는_흡수한다() -> None: assert normalize_unit("m3") == normalize_unit("㎥") assert normalize_unit("개") == "개소" rows = [{"work_item_code": "FP-12-15", "unit": "개", "in_bill": True, "name": "집수정"}] assert verify_unit_matches_basis(rows) == [] def test_뜻이_다른_단위는_드러낸다() -> None: """⚠ 환산하지 않는다 — m 을 ㎡ 로 바꾸려면 두께를 지어내야 한다.""" rows = [{"work_item_code": "FP-13-04-05", "unit": "m", "in_bill": True, "name": "돌쌓기(찰)"}] warnings = verify_unit_matches_basis(rows) assert warnings and "㎡" in warnings[0] def test_마스터에_없는_코드는_트집_잡지_않는다() -> None: rows = [{"work_item_code": "FP-99-99", "unit": "포", "in_bill": True, "name": "가짜"}] assert verify_unit_matches_basis(rows) == [] def test_인계본이_그_경고를_싣는다() -> None: """⚠ 검사를 만들어 두고 안 부르면 없는 것과 같다(2026-09-08 자기 감사에서 둘이 놀고 있었다).""" handoff = 인계(돌쌓기(back_len_cm=45)) assert handoff["basis_unit_warnings"] == [] def test_품셈에_면적_밑수가_실제로_있다() -> None: """⚠ 우리 판단의 근거가 **원문**인지 확인한다 — 없으면 이 고침 자체가 근거를 잃는다.""" table = basis_units() assert table["FP-13-04-05"] == {"㎡"} assert table["FP-13-06-01"] == {"㎡"} assert "m" in table["FP-12-10"] # ── 마스터가 밑수를 모르는 공종 — 매핑이 원문에서 읽어 채운다 (2026-09-08 층따기) ── # ⚠ 층따기 9-18 은 절 머리에 「(단위: …)」가 없고 [주] 공식 `Q1 = … = ㎥/시간` 만 있다. # 마스터 `basis_unit` 이 비어 대조가 조용히 통과했고, ㎡ 2,645.21 × ㎥당 단가로 # 410만원이 서 있었다. def 토공(group: str, unit: str, amount: float, item: str | None = None) -> dict: row = {"group": group, "unit": unit, "amount": amount, "in_bill": True} if item: row["item"] = item return row def test_층따기_길이를_넣으면_체적으로_나간다() -> None: """⭐ 2026-09-09 확정 2차 ① — 면적이 정본이고 **부피는 사용자가 넣은 길이를 곱해** 쓴다. 그러면 품셈 ㎥ 단가를 그대로 쓸 수 있어 단위 불일치가 풀린다. ⚠ 면적은 없애지 않는다 — 횡단도 하단 표가 면적을 쓴다. **㎥ 를 덧붙이는 것**이다. """ row = build_handoff( summary_table={"rows": [토공("층따기", "㎡", 13696.75)]}, bench_cut_depth_m=0.5 )["work_items"][0] assert (row["unit"], row["quantity"]) == ("㎥", 13696.75 * 0.5) assert row["in_bill"] is True # 어디서 온 값인지 규격 칸에 남는다 — 면적을 못 되짚으면 검산이 안 된다. assert "13,696.75㎡" in row["spec_detail"] and "0.5m" in row["spec_detail"] def test_층따기_길이가_없으면_막히고_사유가_간다() -> None: """⚠ 길이 기본값을 **임의로 박지 않는다** — 교본이 「설계도서에 명시」라 설계 입력이다.""" row = build_handoff(summary_table={"rows": [토공("층따기", "㎡", 13696.75)]})["work_items"][0] assert row["in_bill"] is False assert row["blocked_kind"] == "input_missing" assert "길이" in row["blocked_reason"] # 면적은 규격 칸에 남는다 — 값이 사라지면 왜 막혔는지 못 짚는다. assert "13,696.75" in row["spec_detail"] def _옛_시험_층따기는_면적으로_나간다() -> None: """⭐ 2026-09-09 사용자 확정 ⑦ — 단위는 **㎡**(실무 관행). ⚠ 앞서는 「품셈 공식이 ㎥ 라 못 곱한다」로 막아 두었다(410만원 자리). 확정으로 ㎡ 가 정본이 되었으므로 **막지 않는다.** 대신 매핑에 그 결정과 까닭을 적어 두었고, ㎡ 단가를 세우는 것은 받는 쪽 몫이다. """ row = build_handoff(summary_table={"rows": [토공("층따기", "㎡", 2645.21)]})["work_items"][0] assert row["work_item_code"] == "FP-09-18" assert (row["unit"], row["quantity"]) == ("㎡", 2645.21) assert row["in_bill"] is True assert row["blocked_kind"] is None def test_성토면다짐은_면적이_맞다() -> None: """⚠ 좁게 — 같은 면적을 쓰는 형제 공종까지 막으면 멀쩡한 줄이 사라진다. 9-17-1 비탈면 다짐은 시공량이 **㎡/시간**이라 면적이 맞다(2026-09-08 B09 원문 확인). """ row = build_handoff(summary_table={"rows": [토공("성토면다짐", "㎡", 2645.21)]})["work_items"][ 0 ] assert (row["unit"], row["quantity"]) == ("㎡", 2645.21) assert row["in_bill"] is True def test_매핑이_적은_밑수가_대조에_쓰인다() -> None: """마스터가 비어 있어도 매핑이 적은 값으로 경고가 선다.""" rows = [{"work_item_code": "FP-09-18", "unit": "㎡", "in_bill": True, "name": "층따기"}] assert verify_unit_matches_basis(rows) == [] # 마스터만 보면 못 잡는다 assert verify_unit_matches_basis(rows, extra={"FP-09-18": "㎥"}) # ── 매핑이 적은 밑수는 **원문에 실제로 있는 글자**여야 한다 (2026-09-08) ──────────── # ⚠ 근거 없는 단위가 대조의 기준이 되면 가드가 거짓말을 한다. 그래서 원문을 연다. PUM_TEXT = ( ROOT / "resources" / "knowledge" / "original" / "행정규칙" / "임도 품셈 적용기준 (현 산림사업 표준품셈)" / "첨부" / "(산림청고시 제2025-82호) 산림사업 표준품셈.md" ) #: 매핑에 적은 밑수와 그 근거 줄(원문 줄번호는 1부터). DECLARED_SOURCE = { "FP-09-03-02": (4704, "㎥"), "FP-09-04": (4738, "㎥"), "FP-09-05": (4744, "㎥"), # 2026-09-08 실무 양식이 정해져 부모(FP-09-16 노체)에서 **자식 포설**로 내렸다 — # 실무 내역 셋에 「노체포설·노체다짐」으로 가른 줄이 없고 성토 본체가 한 줄이다. "FP-09-16-01": (5322, "㎥"), "FP-09-17-01": (5368, "㎡"), "FP-10-11": (5919, "㎥"), "FP-10-12": (5928, "㎥"), # ⚠ 「단위:」 글자 없이 **괄호만** 적힌 모양 — 마스터가 못 읽던 자리(2026-09-08). "FP-05-24": (2769, "㎡"), } def test_매핑이_적은_밑수가_원문_줄에_실제로_있다() -> None: if not PUM_TEXT.is_file(): return lines = PUM_TEXT.read_text(encoding="utf-8").splitlines() declared = MAPPING.declared_units() for code, (line_no, unit) in DECLARED_SOURCE.items(): assert declared.get(code) == unit, code assert unit in lines[line_no - 1], f"{code} L{line_no}: {lines[line_no - 1][:60]}" def test_품_단위는_밑수로_안_받는다() -> None: """⚠ 「일당」은 **하루에 얼마**라는 품의 단위다 — 받으면 틀린 단위로 검사를 통과시킨다.""" assert "FP-12-06" not in MAPPING.declared_units() def test_덤프_단위중량이_원문_줄에_있다() -> None: """⚠ 「현장값이라 미확보」로 두었던 γt 가 원문에 있었다(2026-09-08 정정).""" if not PUM_TEXT.is_file(): return line = PUM_TEXT.read_text(encoding="utf-8").splitlines()[5930 - 1] assert "1.9ton/㎥" in line and "2.4ton/㎥" in line # ── 공종 단위가 아닌 글자는 **안 받는다** (2026-09-08 B09 전수에서 나온 모양들) ──── # ⚠ `(㎥/1대, 1일)` 은 **시공량**이라 밑수의 **역수**다 — 받으면 그 공종이 조용히 # 뒤집힌 단위를 갖는다. 안 받으면 대조가 없을 뿐이라 **덜 나쁘다.** def test_시공량_모양은_밑수로_안_받는다() -> None: from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import is_quantity_unit for bad in ("㎥/1대, 1일", "㎥/1인, 1일", "대/ton", "1ha당, 100본당", "일당", "조", "인"): assert not is_quantity_unit(bad), bad for good in ("㎥", "㎡", "m", "개소", "㏊당", "10m당"): assert is_quantity_unit(good), good def test_매핑에_적힌_밑수가_전부_공종_단위다() -> None: """⚠ 사람이 손으로 적는 칸이라 **다음 사람이 시공량을 적을 수 있다.**""" from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import is_quantity_unit for code, unit in MAPPING.declared_units().items(): assert is_quantity_unit(unit), f"{code}: {unit}" def test_부대시설_가설창고도_대조된다() -> None: """⚠ 내역에 실리는 25 줄 중 **이 공종만** 밑수가 없어 가드가 못 보고 있었다(2026-09-09). 마스터는 절 머리에 「(단위: …)」가 있어야 밑수를 채우는데 11-1 은 그것이 없다. 매핑에 원문 근거([주]① · [주]③)와 함께 「개소」를 적어 대조가 서게 했다. """ from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import load_mapping declared = load_mapping().declared_units() assert declared.get("FP-11-01") == "개소" rows = [ { "work_item_code": "FP-11-01", "name": "가설창고(컨테이너)", "unit": "m", "in_bill": True, } ] warnings = verify_unit_matches_basis(rows, extra=declared) assert len(warnings) == 1 and "개소" in warnings[0] # 맞는 단위로 보내면 조용하다. ok = [dict(rows[0], unit="개소")] assert verify_unit_matches_basis(ok, extra=declared) == []