Files
Aislo/resources/tester/test_b08_facility_structures.py
eomsangdonandClaude Opus 5 a314ac12fd feat(b05·b06·b08): BOX암거 수량식 — 등록부 두께·연장 칸 + m당 × 연장 전개식 · 세월교·날개벽은 근거 없음 사유로 닫음(12장 D · 브레인 판정 ①~⑥)
- 등록부 box_culvert: 연장(기본값 없음) · 측벽 0.3 · 상·저판 0.25 · 헌치 0.2 · 버림 0.1 — 제안값 = 산림과임업기술(임도) 5장 수량산출 예제 · KDS 44 90 00 300㎜ 병기(판 0.25 는 못 미침을 나란히)
- B06 BOX 세트(파이썬·TS 짝)가 두께 칸을 읽음 — 종전 「세월교 값 승계」(벽 0.2·판 0.3) 걷음 · 그림 따라감
- B08 전개식 `_UnitQuantity_Box`: 콘크리트 · 버림 · 유로폼(외벽·내벽·상판 밑·귀면) · 버림 옆 합판 · 동바리 · 비계(시종점 두 면은 개소당 한 번) · 철근은 울진 2×2 관측(D13 179·D16 157kg/m)만, 나머지 크기는 「크기별 식 없음」
- 울진 기번3 「암거수량집계표(2×2)」 m당과 맞음: 레미콘 2.84 · 버림 0.28 · 유로폼 11.131 · 합판 0.2 · 동바리 3.92 · 비계 벽체 5.2
- 치수 칸을 안 적은 BOX 는 제안값으로 서되 미확정(금액 밖) · 연장이 비면 안 섬
- 묶음 조각 12-01-01·12-38·12-04(6회)·12-03·12-20·12-19·12-25 · 동바리 대상 표시 · 세월교 「수량 근거 없음」

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-15 02:36:25 +09:00

132 lines
7.3 KiB
Python

"""계곡 통과 시설(`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]
# 높이는 횡단도와 같은 관경 기준 최소 높이(Ø1000 → 2.0) — 그림 둘·표가 한 값(브레인 판정)
assert inlet["height_m"] == outlet["height_m"] == 2.0 and "관경 기준" 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["배수관 · 유입부 기슭막이"]["unit"] == "㎡"
def test_기본값으로_선_벽은_줄만_서고_금액_합에는_안_든다() -> None:
"""브레인 판정(2026-09-14) — 줄은 서야 채울 자리가 보이고, 금액은 실제 값이 있을 때만."""
point = {"chainage_m": 100.0, "options": {"pipe_diameter_mm": 1000}}
table = _table([point])
assert all(s["unconfirmed"] and s["components"] for s in table["structures"])
assert table["totals"] == [] # 원단위 합(자재·채집석 밑수)에도 안 듦
handoff = build_handoff(unit_quantity_table=table)
rows = handoff["work_items"]
walls = [r for r in rows if "기슭막이" in r["name"]]
assert len(walls) == 2 and all(
not r["in_bill"] and r["blocked_kind"] == "unconfirmed" for r in walls
)
assert walls[0]["quantity"] > 0 # 수량은 보임
# 터파기·되메우기·기초잡석·버림 타설이 금액 줄로 번지지 않음
assert not [r for r in rows if r["in_bill"] and r["quantity"] > 0]
from B09_Estimation.B09_Estimation_BillOfQuantities import bill_summary, build_bill
bill = build_bill(handoff)
placed = [r for r in bill.rows if not r.is_group and "기슭막이" in r.name]
assert len(placed) == 2 and all(r.unconfirmed == 1 and r.amount_krw is None for r in placed)
summary = bill_summary(bill)
assert summary["unpriced_count"] == 2 and summary["body_total_krw"] == "0"
# 치수를 다 적으면 미확정이 풀림
filled = {
f"{side}_revet_{key}": value
for side in ("inlet", "outlet")
for key, value in (("form", "돌쌓기(메)"), ("height_m", 1.5), ("length_m", 6))
}
confirmed = _table([{**point, "options": {**point["options"], **filled}}])
assert not any(s["unconfirmed"] for s in confirmed["structures"])
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": "좌", "height_m": 2.0}}
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:
"""2026-09-14 브레인 판정 — 높이 = 계획홍수위 + 0.5~0.7m(사방기술교본 2-나:141 · 3-가:181)."""
point = {"facility": "revetment", "chainage_m": 50.0, "options": {"side": "좌"}}
(row,) = _table([point])["structures"]
assert "높이를 안 적음" in row["unconfirmed"] and "2-나:141" in row["unconfirmed"]
assert any("사방(계류) 교본 기준" in note and "3-가:151" in note for note in row["notes"])
with_height = {**point, "options": {"side": "좌", "height_m": 2.0, "form": "돌쌓기(메)"}}
(row,) = _table([with_height])["structures"]
assert "높이를 안 적음" not in row["unconfirmed"]
def test_산출식_없는_세월교는_사유로_서고_터파기_빠짐_줄을_안_만든다() -> None:
table = _table([{"facility": "ford_bridge", "chainage_m": 30.0, "options": {}}])
# 2026-09-15 브레인 판정 ⑥ — 「아직 없음」이 아니라 「근거 없음」으로 닫음.
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)