"""철근 이름·규격 가르기(2026-09-14 · 목록 3+4) — **가르기 전후 물량이 그대로**인지 잼. 「이형철근 D13」 한 칸을 이름 「이형철근」 + 규격 「D13」 으로 가르면 셋이 같은 글자에 묶여 있어 하나만 고치면 조용히 어긋남(브레인 경고): ① 묶음 조각 `from_components` · `amounts[name]` (D13·D16 이 서로 덮어씀) ② 이중계상 경계 검사 `_pickers`·`destinations`(못 집으면 검사가 놂) ③ 자재총괄 줄·할증. 이 시험은 **가르기 전 코드에서 먼저 초록**으로 만들고 가른 뒤에도 초록이어야 함. 옹벽 반중력식 10m D13 13.45 · D16 30.42 ㎏/m → 조각 0.4387 ton · 자재 134.5 · 304.2 ㎏ 집수정 □형 Ø800 1개소 D13 4.776 ㎏ L형수로 H=0.2 10m D13 0.398 ㎏/m → 3.98 ㎏ """ from __future__ import annotations import copy import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff, load_mapping # noqa: E402 from B08_Quantity.B08_Quantity_Engine_Handoff_Boundaries import ( # noqa: E402 verify_double_count_boundaries, ) from B08_Quantity.B08_Quantity_Engine_MaterialSummary import build_table # noqa: E402 from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table as build_unit # noqa: E402 def _구조물들() -> dict: return build_unit( [ { "structure_id": "w1", "type_id": "retaining_wall", "start_m": 100.0, "end_m": 110.0, "options": {"form": "반중력식", "height_m": 2.0, "length_m": 10.0}, }, { "structure_id": "b1", "type_id": "pipe_inlet_basin", "start_m": 200.0, "end_m": 200.0, "options": { "inlet_basin_form": "□형(기본형)", "inlet_basin_material": "콘크리트", "pipe_diameter_mm": "800", }, }, { "structure_id": "d1", "type_id": "open_ditch", "start_m": 300.0, "end_m": 310.0, "options": {"ditch_spec": "L형수로 H=0.2", "length_m": 10.0}, }, ], {"retaining_wall": "옹벽", "pipe_inlet_basin": "집수정", "open_ditch": "개거"}, ) def test_옹벽_철근_조각은_D13_D16_을_다_모아_ton() -> None: row = next( r for r in build_handoff(unit_quantity_table=_구조물들())["work_items"] if r.get("composite_parts") ) rebar = next(p for p in row["composite_parts"] if "12-03" in str(p.get("code"))) assert rebar["quantity"] == pytest.approx(0.4387) and not rebar.get("not_ready") from B08_Quantity.B08_Quantity_Engine_Handoff import structure_kind wall = next(s for s in _구조물들()["structures"] if s["type_id"] == "retaining_wall") assert structure_kind(wall) == "철근구조물" def test_자재총괄_철근_줄_물량과_할증이_그대로() -> None: table = build_table(_구조물들()) rebar = { row["supply_key"]: (row["net_amount"], row["surcharge_pct"], row["total_amount"]) for row in table["rows"] if row["supply_key"].startswith("이형철근") } assert set(rebar) == {"이형철근 D13", "이형철근 D16"} # 관급구분·단가 키는 전후 같은 글자 assert rebar["이형철근 D13"][0] == pytest.approx(134.5 + 4.776 + 3.98) assert rebar["이형철근 D16"][0] == pytest.approx(304.2) assert rebar["이형철근 D13"][1] == 3 and rebar["이형철근 D16"][1] == 3 # 1-3-1 이형철근 assert "이형철근" not in " ".join(table["missing_rate_materials"]) def test_경계_검사가_철근을_여전히_집는다() -> None: """철근이 토공 축으로 잘못 가면 묶음 조각이 또 센다고 걸려야 함 — 못 집으면 검사가 놂.""" unit = copy.deepcopy(_구조물들()) wall = next(s for s in unit["structures"] if s["type_id"] == "retaining_wall") for component in wall["components"]: if str(component["name"]).startswith("이형철근"): component["destination"] = "earthwork" found = verify_double_count_boundaries(unit, None, load_mapping()) assert any("이형철근" in text and "묶음 조각" in text for text in found), found assert not verify_double_count_boundaries(_구조물들(), None, load_mapping()) def test_가른_뒤_이름과_규격이_따로_서고_할증은_별칭_없이_이어짐() -> None: table = build_table(_구조물들()) rows = [(r["name"], r["spec"]) for r in table["rows"] if r["name"].startswith("이형철근")] assert sorted(rows) == [("이형철근", "D13"), ("이형철근", "D16")] unit = _구조물들() specs = { (c["name"], c.get("spec")) for s in unit["structures"] for c in s["components"] if str(c["name"]).startswith("이형철근") } assert specs == {("이형철근", "D13"), ("이형철근", "D16")}