feat(b08): 계곡 통과 시설도 원단위 전개로 — 관 유입·유출부 기슭막이·집수정·독립 기슭막이·물넘이 (A1)

- 전개가 structures.json 만 읽던 구멍 — pipe_points.json 도 읽음(관 자체는 관 줄이 셈)
- 관 부설(품셈 12-11 m당)에 기슭막이 몫 없음 → 기슭막이는 전개 한 곳에서만 · 사유에 적음
- 안 적힌 벽 칸은 등록부 기본값(횡단도 그림과 같은 값) + 「기본값으로 섰음」 사유
- 구조물 목록에 남은 관 지점 종류 옛 저장분은 안 셈(두 번 방지)
- 기슭막이 공종 = 형태로 돌쌓기 찰·메 코드 · 독립 기슭막이 한쪽 설치에 두 칸이 다르면 사유만
- 산출식 없는 세월교가 「터파기 줄 없음」 거짓 사유를 안 내게 · 구조물도 제원 저장이 관 지점 시설엔 안 먹힘을 알림

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-14 07:52:55 +09:00
co-authored by Claude Opus 5
parent 1e9ff999a4
commit b97cb4e2e6
8 changed files with 295 additions and 21 deletions
@@ -220,6 +220,21 @@
"work_item_code": "FP-12-15",
"master_name": "집수정"
},
{
"type_id": "revetment",
"work_item_code": null,
"class_from": "form",
"form_codes": {
"돌쌓기(찰)": "FP-13-04-05",
"돌쌓기(메)": "FP-13-04-02"
},
"class_note": "기슭막이는 **형태가 돌쌓기면 돌쌓기 식**(`_UnitQuantity_Revetment`) — 공종도 돌쌓기 찰·메와 같음(실무 정본 탭 「돌기슭막이(H=2.0m, 찰쌓기, 기초유)」). 그 밖 형태(콘크리트·돌망태·통나무·바자)는 전개식이 없어 사유로 막힘. 관 유입·유출부 기슭막이와 독립 기슭막이가 이 줄로 섬(A1, 2026-09-14).",
"secondary_axes": [
"stone_kind"
],
"billing_component": "돌쌓기",
"variant_axis": "back_len_cm"
},
{
"type_id": "ford_pavement",
"work_item_code": "FP-12-06",
@@ -414,7 +429,7 @@
"diameter_option_key": "pipe_diameter_mm",
"variant_axis": "pipe_diameter_mm",
"facility_rule": "⚠ `pipe_points.json` 은 **계곡 통과 시설 전부의 정본**이다(배관·BOX암거·물넘이·세월교·독립 기슭막이). `facility` 가 `pipe` 인 점만 배관이다 — `pipe_diameter_mm` 유무로 가르면 **관경 미지정 관을 놓친다**(2026-09-08 랩탑 창).",
"revetment_note": "⚠ 유출·유입부 기슭막이는 **관 옵션(`inlet_revet_*`·`outlet_revet_*`)이 정본**이다. 레지스트리 `revetment` 타입이 `managed_by: pipe_points` 라 구조물 목록에서 빠졌으므로 **구조물 쪽으로 또 세지 않는다**(2026-08-28 이관).",
"revetment_note": "⚠ 유출·유입부 기슭막이는 **관 옵션(`inlet_revet_*`·`outlet_revet_*`)이 정본**이다. 레지스트리 `revetment` 타입이 `managed_by: pipe_points` 라 구조물 목록에서 빠졌다(2026-08-28 이관). ⭐ **관 줄에는 안 넣고 원단위 전개가 한 번 셈** — B08 이 관 지점을 읽어 「배수관 · 유입부/유출부 기슭막이」 줄로 세움(`Engine_Pipe.facility_structures`, A1 2026-09-14). 관 부설(품셈 12-11 m당: 관·기초콘크리트·거푸집)에 기슭막이 몫이 없어 겹치지 않음. 그전에는 **어느 쪽도 안 세고 있었음**.",
"not_ready": {
"흄관 밑수 두 벌": "`FP-12-11-02` 는 밑수가 「1 m」와 「1 개소」 두 벌이다(표가 둘). B09 가 `#갈래` 로 두 표를 각각 세우므로 B08 은 `variant_value` 로 어느 쪽인지 보내면 된다.",
"터파기·되메우기": "⚠ 관 부설과 터파기·되메우기가 각각 오면 **같은 굴착을 두 번 셀 수 있다**(B09 ㉡ 가드). 관 줄에는 지금 터파기를 붙이지 않는다."
@@ -0,0 +1,86 @@
"""계곡 통과 시설(`pipe_points.json`)이 원단위 전개·인계에 선다 (A1, 2026-09-14).
⚠ 앞서 전개가 `structures.json` 만 읽어 관 유입·유출부 기슭막이·집수정·독립 기슭막이가
원단위·내역에 한 줄도 안 섰음. 관 줄(품셈 12-11 m당)에는 기슭막이 몫이 없어 **한 번만** 셈.
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402
from B08_Quantity import B08_Quantity_Router_Material as material # noqa: E402
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff # noqa: E402
from B08_Quantity.B08_Quantity_Engine_Pipe import build_rows, facility_structures # noqa: E402
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table # noqa: E402
NAMES = {"pipe": "배수관", "revetment": "기슭막이", "ford_bridge": "세월교"}
def _table(points: list[dict]) -> dict:
return build_table(facility_structures(points), NAMES)
def test_관_하나에_기슭막이_둘이_기본값_사유와_함께_서고_관_줄에는_없다() -> None:
point = {"chainage_m": 100.0, "options": {"pipe_diameter_mm": 1000}}
table = _table([point])
names = [s["name"] for s in table["structures"]]
assert names == ["배수관 · 유입부 기슭막이", "배수관 · 유출부 기슭막이"]
inlet, outlet = table["structures"]
assert inlet["options"]["form"] == "돌쌓기(찰)" and outlet["options"]["form"] == "돌쌓기(메)"
assert all(s["components"] and s["length_m"] == 10 for s in table["structures"])
assert "등록부 기본값으로 섰음" in inlet["notes"][0]
# 관 줄은 관 연장만 — 기슭막이가 거기 섞이지 않음(두 번 안 셈).
pipe = build_rows([point], [{"chainage_m": 100.0, "design": {"pipe_length_m": 8}}])
assert [row["quantity"] for row in pipe["rows"]] == [8.0]
work = {row["name"]: row for row in build_handoff(unit_quantity_table=table)["work_items"]}
assert work["배수관 · 유입부 기슭막이"]["work_item_code"] == "FP-13-04-05"
assert work["배수관 · 유출부 기슭막이"]["work_item_code"] == "FP-13-04-02"
assert (
work["배수관 · 유입부 기슭막이"]["in_bill"]
and work["배수관 · 유입부 기슭막이"]["unit"] == ""
)
def test_유입구가_집수정이면_집수정_줄이_사유로_서고_유입_기슭막이는_없다() -> None:
table = _table([{"chainage_m": 5.0, "options": {"inlet_type": "집수정"}}])
basin, outlet = table["structures"]
assert basin["type_id"] == "pipe_inlet_basin" and not basin["components"] and basin["notes"]
assert outlet["name"] == "배수관 · 유출부 기슭막이"
def test_유입구가_기슭막이인데_집수정_형식이_남아_있으면_집수정은_안_세고_알린다() -> None:
options = {"inlet_type": "기슭막이", "inlet_basin_form": "□형(기본형)"}
inlet = _table([{"chainage_m": 5.0, "options": options}])["structures"][0]
assert inlet["type_id"] == "revetment" and "집수정은 안 셈" in inlet["notes"][0]
def test_독립_기슭막이_한쪽이면_두_칸이_같을_때만_세고_다르면_사유() -> None:
same = {"facility": "revetment", "chainage_m": 50.0, "options": {"side": ""}}
rows = _table([same])["structures"]
assert [r["name"] for r in rows] == ["기슭막이 · 좌 벽"] and rows[0]["components"]
differ = {**same, "options": {"side": "", "inlet_revet_height_m": 3.0}}
row = _table([differ])["structures"][0]
assert not row["components"] and "서버가 못 가림" in row["notes"][-1]
def test_산출식_없는_세월교는_사유로_서고_터파기_빠짐_줄을_안_만든다() -> None:
table = _table([{"facility": "ford_bridge", "chainage_m": 30.0, "options": {}}])
assert "산출식이 아직 없습니다" in table["structures"][0]["notes"][0]
rows = build_handoff(unit_quantity_table=table)["work_items"]
assert not any("터파기가 안 선 구조물" in str(row.get("spec_detail")) for row in rows)
def test_구조물_목록의_관_지점_종류_옛_저장분은_안_셈(monkeypatch, tmp_path) -> None:
old = StructureInstance(
structure_id="old", type_id="revetment", placement="point", chainage_m=10.0
)
monkeypatch.setattr(material, "load_structures", lambda root: (1, [old]))
targets, _names, skipped = material._collect_structures(str(tmp_path))
assert targets == [] and any("관 지점 정본이 주인" in note for note in skipped)