Files
Aislo/resources/tester/test_110_b08_bed_slope.py
T
eomsangdonandClaude Opus 5.5 70665bba12 feat(B08): 수리 판정 하상경사 ← B06 관 경사 · 박스 원지반 경사 연결값(110-1)
- B06 정본 키 둘(pipe_slope_pct · box_ground_slope_pct) — 관 길이와 같은 길(STRUCTURE_ROW_KEYS ↔ _AREA_KEYS ↔ CrossSectionPatch) · 주인 측점에만
- B08 수리 표 linked 에 slope_denominator = 100 / 경사 · 0 % 이하 · 짝 없음은 빈칸 · 세월교 · 물넘이는 장 기본값 그대로
- 수리 표 원천 표 「B06」 · 96 「하상경사 안 이음」 주석 고침 · 시험 test_110 새로 · 짝 시험 키 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
2026-10-08 16:14:56 +09:00

132 lines
5.7 KiB
Python

"""110 — B08 수리 판정 하상경사 ← B06 횡단 경사.
관 = 관 바닥 두 끝 경사(`CulvertLayout.pipe.slopePct`) · 박스 = 박스 두 끝 원지반 경사(본체는 늘 수평) ·
관 길이와 같은 길(`STRUCTURE_ROW_KEYS` → 서버 Node → `_AREA_KEYS` → 정본) · B08 은 연결값(linked)으로
실음(저장된 측점 null 이 덮지 않게) · 0 % 이하는 빈칸(손 입력) · 세월교 · 물넘이는 장 기본값 그대로.
"""
from __future__ import annotations
import copy
import json
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B06_Section.B06_Section_Schema import CrossSectionPatch # noqa: E402
from B08_Quantity.B08_Quantity_Engine_Hydro import compute # noqa: E402
from B08_Quantity.B08_Quantity_Hydro_Defaults import ( # noqa: E402
DEFAULTS,
SHEET_DEFAULTS,
station_rows,
)
from common_util.common_util_drainage_pipes import PipePoint # noqa: E402
FIXTURES = Path(__file__).parent / "fixtures"
NODE_MODULES = ROOT / "config" / "node_modules"
needs_node = pytest.mark.skipif(not NODE_MODULES.is_dir(), reason="프론트엔드 의존성 미설치")
BASINS = [
{"kind": "detail_basin", "chainage_m": c, "area_m2": 10717.0, "flow_length_m": 163.9,
"relief_m": 120.0, "design_flow_m3s": 1.0, "tc_minutes": 5.0}
for c in (85.05, 173.09, 300.0, 400.0, 500.0)
] # fmt: skip
POINTS = [
PipePoint(chainage_m=85.052, options={"pipe_diameter_mm": "1000"}),
PipePoint(chainage_m=173.09, facility="ford_bridge", options={"ford_width_m": 10}),
PipePoint(chainage_m=300.004, facility="box_culvert",
options={"body_width_m": 2.0, "body_height_m": 1.5}),
PipePoint(chainage_m=400.0, options={"pipe_diameter_mm": 800}),
PipePoint(chainage_m=500.0, options={"pipe_diameter_mm": 800}),
] # fmt: skip
DESIGNS = [
# 관 자리 85.052 ↔ 횡단 85.0(스냅 · 0.5 m 안)
{"chainage_m": 85.0, "design": {"pipe_slope_pct": 5.0, "pipe_length_m": 9}},
# 세월교에 경사 키가 있어도 안 이음(장 기본값)
{"chainage_m": 173.09, "design": {"pipe_slope_pct": 4.0, "box_ground_slope_pct": 4.0}},
{"chainage_m": 300.0, "design": {"box_ground_slope_pct": 2.0}},
{"chainage_m": 400.0, "design": {"pipe_slope_pct": 0.0}}, # 수평 관 → 빈칸
{"chainage_m": 501.0, "design": {"pipe_slope_pct": 3.0}}, # 1 m 떨어짐 → 짝 없음
]
def _rows() -> dict[float, dict]:
return {r["chainage_m"]: r for r in station_rows(POINTS, BASINS, DESIGNS)}
def test_관_경사가_하상경사_칸으로() -> None:
assert _rows()[85.052]["linked"]["slope_denominator"] == 20.0 # 5 % → 1/20
def test_박스는_원지반_경사() -> None:
assert _rows()[300.004]["linked"]["slope_denominator"] == 50.0 # 2 % → 1/50
def test_0_퍼센트_이하와_짝_없음은_빈칸() -> None:
rows = _rows()
assert "slope_denominator" not in rows[400.0]["linked"]
assert "slope_denominator" not in rows[500.0]["linked"]
assert DEFAULTS["slope_denominator"] is None # 손 입력 자리
def test_세월교는_장_기본값_그대로() -> None:
assert "slope_denominator" not in _rows()[173.09]["linked"]
assert SHEET_DEFAULTS["ford"]["slope_denominator"] > 0
def test_연결값으로_판정이_나옴() -> None:
row = _rows()[85.052]
out = compute({**DEFAULTS, **row["linked"], "r24_mm": 231.56, "sheet": "pipe"})
assert out["ok"] in (True, False)
assert out["slope_bed"] == pytest.approx(0.05)
def test_저장된_측점_null_이_연결값을_덮지_않음() -> None:
"""화면은 저장본 · 초안의 null 을 연결값 칸에서 건너뜀 — 연결값으로 실어야 저장된 측점도 바뀜."""
ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Hydro.ts").read_text(encoding="utf-8")
assert "v != null || !(k in linkedOf(station))" in ui
assert "...station.linked," in ui
def test_패치가_받는다() -> None:
patch = CrossSectionPatch(chainage_m=1, pipe_slope_pct=0.0, box_ground_slope_pct=-3.5)
assert (patch.pipe_slope_pct, patch.box_ground_slope_pct) == (0.0, -3.5)
def _server_rows(sections: list[dict]) -> dict[float, dict]:
from B06_Section.B06_Section_Server_Calc_Prebuild import _NPM_SCRIPT, BUNDLE
from common_util.common_util_node_bundle import run_bundle_json
output = run_bundle_json(BUNDLE, _NPM_SCRIPT, {"detail": {"cross_sections": sections}})
assert output, "서버 Node 번들 실행 실패"
return {round(r["chainage_m"], 3): r for r in output["areas"]}
@needs_node
def test_서버가_관_경사를_관_길이와_같이_냄() -> None:
sections = json.loads((FIXTURES / "b07_cross_overlay_sections.json").read_text("utf-8"))
row = _server_rows(sections["cross_sections"])[85.052]
assert row["pipe_length_m"] > 0
# 관 바닥 두 끝 경사 — 유입이 유출보다 낮게 그려지지 않음(역경사는 수평으로 클램프)
assert row["pipe_slope_pct"] >= 0
@needs_node
def test_서버가_박스_원지반_경사를_주인_측점에만() -> None:
data = json.loads((FIXTURES / "b06_73_4_body_guard_sections.json").read_text("utf-8"))
sections = data["cross_sections"]
box_at = next(s for s in sections if s.get("box"))
# 옆 측점에 같은 박스가 걸쳐도(폭 안 연동) 주인 측점에만 실림
neighbour = next(s for s in sections if s["chainage_m"] == 340)
neighbour["box"] = copy.deepcopy(box_at["box"])
rows = _server_rows(sections)
assert isinstance(rows[352.14]["box_ground_slope_pct"], float)
assert "box_ground_slope_pct" not in rows[340]
# 세월교 · 박스 흙막이는 관이 아님 — 관 경사 안 실음
assert all("pipe_slope_pct" not in r for r in rows.values())