"""배수관 물량 — 정본 셋을 잇는 자리의 짝 시험 (2026-09-08). ⚠ 여기서 겨누는 것 넷 ① `facility` 가 있는 점(세월교 등)을 **관으로 세지 않는가** ② 연장이 없으면 **0 으로 때우지 않고 사유와 함께 막히는가** ③ 관종으로 공종이 갈리는가 · 기본값으로 선 것을 **알리는가** ④ ⚠ 좁게 — 측점 맞추기가 **옆 측점 길이를 물어 오지 않는가** """ from __future__ import annotations import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B08_Quantity.B08_Quantity_Engine_Pipe import build_rows # noqa: E402 MAPPING = json.loads( ( ROOT / "resources" / "data_work_item_mapping" / "work_item_mapping_2026-01-01.json" ).read_text(encoding="utf-8") )["pipe"] def 관(chainage: float, **options: object) -> dict: return {"chainage_m": chainage, "facility": "pipe", "options": options} def 길이(chainage: float, length: float) -> dict: return {"chainage_m": chainage, "design": {"pipe_length_m": length}} def test_관이_아닌_시설은_안_센다() -> None: """⚠ `pipe_points.json` 은 계곡 통과 시설 **전부**의 정본이다 — 세월교가 섞여 있다.""" points = [ 관(85.05, pipe_diameter_mm=1000), {"chainage_m": 173.09, "facility": "ford_bridge", "options": {}}, {"chainage_m": 352.14, "facility": "ford_bridge", "options": {}}, 관(258.12, pipe_diameter_mm=800), ] out = build_rows(points, [], MAPPING) assert out["pipe_count"] == 2, "세월교를 관으로 셌다" def test_관경이_없어도_관은_관이다() -> None: """⚠ `pipe_diameter_mm` 유무로 가르면 **관경 미지정 관을 놓친다**.""" out = build_rows([관(100.0)], [], MAPPING) assert out["pipe_count"] == 1 assert out["rows"][0]["variant_value"] is None def test_연장이_없으면_0_으로_때우지_않는다() -> None: out = build_rows([관(85.05, pipe_diameter_mm=1000)], [], MAPPING) row = out["rows"][0] assert row["in_bill"] is False assert row["blocked_kind"] == "input_missing" assert "횡단설계에서 [저장]" in row["blocked_reason"] def test_연장이_오면_값이_선다() -> None: out = build_rows([관(85.05, pipe_diameter_mm=1000)], [길이(85.05, 9.0)], MAPPING) row = out["rows"][0] assert (row["in_bill"], row["quantity"], row["unit"]) == (True, 9.0, "m") assert out["length_total_m"] == 9.0 def test_관종으로_공종이_갈린다() -> None: 보기 = {"파형강관": "FP-12-11-03", "흄관": "FP-12-11-02", "VR관": "FP-12-11-01"} for kind, code in 보기.items(): out = build_rows([관(85.0, pipe_kind=kind, pipe_diameter_mm=800)], [길이(85.0, 9.0)], MAPPING) assert out["rows"][0]["work_item_code"] == code, kind def test_관종을_안_정하면_기본값으로_서되_알린다() -> None: """기본 「파형강관」은 2026-08-17 사용자 확정값이라 근거가 있다 — 다만 **조용히 쓰지 않는다**.""" out = build_rows([관(85.0, pipe_diameter_mm=800)], [길이(85.0, 9.0)], MAPPING) row = out["rows"][0] assert row["work_item_code"] == "FP-12-11-03" assert row["kind_from_default"] is True assert any("기본값" in note for note in out["notes"]) def test_모르는_관종은_공종을_못_고른다() -> None: out = build_rows([관(85.0, pipe_kind="철근콘크리트관")], [길이(85.0, 9.0)], MAPPING) row = out["rows"][0] assert row["work_item_code"] is None assert row["in_bill"] is False assert "아는 관종이 아니라" in row["blocked_reason"] def test_측점이_소수점에서_어긋나도_찾는다() -> None: out = build_rows([관(85.05, pipe_diameter_mm=800)], [길이(85.0, 9.0)], MAPPING) assert out["rows"][0]["quantity"] == 9.0 def test_옆_측점_길이를_물어_오지_않는다() -> None: """⚠ 좁게 — 넓히면 다른 측점 값이 조용히 실린다.""" out = build_rows([관(85.0, pipe_diameter_mm=800)], [길이(120.0, 9.0)], MAPPING) row = out["rows"][0] assert row["quantity"] == 0.0 and row["in_bill"] is False def test_실제_자료로_세월교가_걸러진다() -> None: """정본 대조 — `5601e828` 은 11점 중 관 9 · 세월교 2 임(2026-09-08 랩탑 창 확인).""" path = ( ROOT / "storage" / "1" / "3" / "5601e828-feea-487a-9b25-415e5199f2f5" / "B04_PreProcess" / "drainage" / "edits" / "pipe_points.json" ) if not path.is_file(): return points = json.loads(path.read_text(encoding="utf-8"))["points"] assert build_rows(points, [], MAPPING)["pipe_count"] == 9 # ── 「[저장]하면 풀림」과 「눌러도 안 풀림」을 갈라 말하기 (2026-09-08 실측) ──── # 실측: `5601e828` 관 9개 중 3개(439.55·620.43·720.37)가 **횡단 행 자체가 없는** 자리였다. # 거기에 「[저장]을 누르면 섭니다」를 띄우면 눌러 보고 안 되어 헤맨다. def test_횡단이_있는데_길이만_없으면_저장하라고_말한다() -> None: out = build_rows( [관(85.0, pipe_diameter_mm=800)], [], MAPPING, section_chainages=[85.0], ) assert "[저장]을 한 번 누르면" in out["rows"][0]["blocked_reason"] def test_횡단_자체가_없으면_다르게_말한다() -> None: out = build_rows( [관(620.43, pipe_diameter_mm=1500)], [], MAPPING, section_chainages=[85.0, 258.12], ) 사유 = out["rows"][0]["blocked_reason"] assert "횡단 자체가 없습니다" in 사유 assert "[저장]으로는 안 풀립니다" in 사유 def test_측점_목록을_안_주면_종전대로_말한다() -> None: """⚠ 좁게 — 목록이 없다고 「횡단 없음」으로 단정하면 거짓말이 된다.""" out = build_rows([관(620.43, pipe_diameter_mm=1500)], [], MAPPING) assert "[저장]을 한 번 누르면" in out["rows"][0]["blocked_reason"] def test_0_5m_안의_측점은_그_관의_횡단이다() -> None: """⚠ **뒤집힌 시험이다** — 종전에는 「옆 측점이 있다고 횡단이 있는 것이 아니다」였다. 2026-09-09 랩탑 메인 실측이 그 전제를 깼다 — 측점은 **정수 미터 격자로 스냅**되므로 관 439.55 의 횡단은 **측점 440.0** 이고, 그 측점은 구조물 이름표까지 달고 있다. 좁게 보면 「그 측점의 횡단 자체가 없습니다」라는 **거짓 사유**가 뜬다. 스냅 최대 어긋남이 0.5m 라 길이 찾기와 허용오차가 같아졌다(`e1fed5f6` — 배수관 넷 복구). """ out = build_rows( [관(439.55, pipe_diameter_mm=800)], [], MAPPING, section_chainages=[440.0], ) assert "[저장]을 한 번 누르면" in out["rows"][0]["blocked_reason"]