"""286 물넘이포장 종횡단 설계(요구 180-2). 못박는 것 - 286-1 물넘이포장도 기준 측점 하나 — TS `pipeStationMarks` · `plantedChainages` ↔ 서버 `resolve_extra_stations` 같은 값 · 라벨 「물넘이포장」 · B05 · B06 거름 걷음. - 286-2 물넘이 자리 = 측구 없음(서버 정본 · 화면 짝 · 거울 = `test_b06_cross_design_mirror`) · 노면 = 경사각도로 표고 낮은 쪽(설계선 양 끝 표고 · 같으면 단면유형). - 286-3 도수로/돌붙임 = 다른 구조물과 같은 칸 + 맨 아래 「길이 (m)」(`ford_chute_{n}_length_m`) · 비면 원지반까지 · 있으면 사면 따라 그 길이 · 서버 · TS 같은 값 · 집계 · B08 수량에 안 실림. 진짜 TS 는 `helper_286_1_ford_pavement.cjs` 가 Node 로 돌림. 화면은 사용자 확인. """ from __future__ import annotations import json import subprocess import sys from pathlib import Path import pytest HERE = Path(__file__).resolve().parent ROOT = HERE.parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B05_Profile.B05_Profile_Engine_Sections import resolve_extra_stations # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402 from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402 from B06_Section.B06_Section_Router_Design import ford_pavement_ranges, paved_at # noqa: E402 from B06_Section.B06_Section_Engine_Culvert import _chute # noqa: E402 from B08_Quantity.B08_Quantity_Router_Summary import _chutes # noqa: E402 from common_util.common_util_drainage_pipes import parse_pipe_points # noqa: E402 from M02_MasterTemplete.M02_Table_Fill_Spec import container_rows # noqa: E402 PIPES = [ {"chainage_m": 95.0, "source": "user"}, {"chainage_m": 230.0, "source": "user", "facility": "ford_pavement"}, {"chainage_m": 300.0, "source": "user", "facility": "ford_bridge"}, ] pytestmark = pytest.mark.skipif( not (ROOT / "config" / "node_modules" / "typescript").is_dir(), reason="프론트엔드 의존성 미설치", ) def _read(*parts: str) -> str: return (ROOT.joinpath(*parts)).read_text(encoding="utf-8") @pytest.fixture(scope="module") def ts(tmp_path_factory) -> dict: types = [ { "type_id": item.type_id, "group": item.group, "placement": item.placement, "managed_by": item.managed_by, } for item in structure_type_map().values() ] source = tmp_path_factory.mktemp("s286") / "in.json" source.write_text( json.dumps({"pipes": PIPES, "structures": [], "types": types}), encoding="utf-8" ) proc = subprocess.run( ["node", str(HERE / "helper_286_1_ford_pavement.cjs"), str(source)], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def test_물넘이포장_측점_TS_파이썬_같은_값(ts, tmp_path): assert ts["fordMarks"] == [{"chainage_m": 230, "role": "anchor"}] server = resolve_extra_stations(tmp_path, parse_pipe_points(PIPES)) assert [mark for mark, _label in server] == ts["planted"] == [95, 230, 300] assert dict(server)[230.0] == "물넘이포장" def test_B05_B06_물넘이_거름_걷음(): page = _read("B05_Profile", "B05_Profile_UI_Page_Structures.ts") assert "withoutFordPavement" not in page assert "FACILITY_NAMES.ford_pavement" not in page view = _read("B06_Section", "B06_Section_UI_Section_View_Profile.ts") assert '"ford_pavement"' not in view drafts = _read("B06_Section", "B06_Section_Section_Drafts.ts") assert 'if (facility === "ford_pavement") return null;' not in drafts def _ground(slope: float) -> list[dict]: return [ {"offset_m": o / 2, "elevation_m": round(100.0 + slope * o / 2, 4), "valid": True} for o in range(-24, 25) ] def test_물넘이_자리_측구_없음(): kwargs = {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"} plain = compute_cross_design(_ground(0.35), 100.4, **kwargs) ford = compute_cross_design(_ground(0.35), 100.4, tilted=True, **kwargs) assert plain["ditch_enabled"] is True and "tilted" not in plain assert ford["ditch_enabled"] is False and ford["tilted"] is True assert ford["ditch_area_m2"] == 0 # 사용자가 측구를 켜도 물넘이 자리는 안 섬 forced = compute_cross_design(_ground(0.35), 100.4, tilted=True, ditch_choice=True, **kwargs) assert forced["ditch_enabled"] is False def test_물넘이_범위_서버_판정(tmp_path): edits = tmp_path / "B04_PreProcess" / "drainage" / "edits" edits.mkdir(parents=True) (edits / "pipe_points.json").write_text( json.dumps({"points": [{**PIPES[1], "options": {"ford_width_m": 8}}]}), encoding="utf-8" ) ranges = ford_pavement_ranges(tmp_path) assert ranges == [(226.0, 234.0)] assert paved_at(230.0, ranges) and paved_at(226.0, ranges) and not paved_at(240.0, ranges) def test_노면은_표고_낮은_쪽으로(): # 287-4 — 설계 노면 = 경사각도(%)로 지대 낮은 쪽 한 경사 · 중심 = 계획고(콘크리트 윗면) kwargs = {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"} for slope, high in ((0.35, "left"), (-0.35, "right")): got = compute_cross_design(_ground(slope), 100.4, tilted=True, tilt_pct=5.0, **kwargs) left, right = (got["road_edges"][k]["elevation_m"] for k in ("left", "right")) assert (left > right) == (high == "left") assert got["design_elevation_m"] == 100.4 and got["cross_slope_pct"] == 5.0 assert got["tilt_pct"] == 5.0 def test_저장_뒤_서버가_물넘이_측구를_맞춤(): router = _read("B06_Section", "B06_Section_Router_Design.py") assert "stored_tilt(design) == tilt" in router refresh = _read("B06_Section", "B06_Section_Cross_Refresh.ts") assert "tilted: Boolean(section.ford_pavement || section.underdrain)," in refresh CHUTE = {"ford_chute_1_structure": "돌붙임", "ford_chute_1_thickness": 30} def test_도수로_길이_서버_TS_같은_값(ts): assert ( _chute(CHUTE, "ford_chute") == ts["chuteNoLength"] == { "structure": "돌붙임", "thickness_m": 0.3, } ) long = {**CHUTE, "ford_chute_1_length_m": 3} assert _chute(long, "ford_chute") == ts["chuteLength"] assert ts["chuteLength"]["length_m"] == 3 def test_도수로_길이_없음_원지반까지_있음_그_길이(ts): # 유출 = 좌(우가 표고 높음) · 노견(2.5, 100) → 원지반(10, 95) assert ts["lineNoLength"] == [ {"offset": 2.5, "elevation": 100}, {"offset": 10, "elevation": 95}, ] start, end = ts["lineLength"] assert start == {"offset": 2.5, "elevation": 100} assert (end["offset"] - 2.5) ** 2 + (100 - end["elevation"]) ** 2 == pytest.approx(9.0) assert end["offset"] == pytest.approx(2.5 + 3 * 7.5 / 9.0139, abs=1e-3) assert ts["lineTooLong"] == ts["lineNoLength"] # 사면보다 길면 원지반에서 멈춤 def test_컨테이너_같은_칸_맨_아래_길이(): spec = _read("A00_Common", "design", "design_structure_ford_pavement.ts") assert ( 'items: [...CHUTE_ITEMS, { id: "length_m", label: "길이 (m)", kind: "number", step: 0.1 }],' in spec ) def test_길이는_집계_수량에_안_실림(): options = {**CHUTE, "ford_chute_1_bond": "찰붙임", "ford_chute_1_area": 12} item = {"type_id": "ford_pavement", "options": options, "복사본": "pipe@50.0"} plain = container_rows(item, {"gfordpavechute1": "SS1"}) long = container_rows( {**item, "options": {**options, "ford_chute_1_length_m": 7}}, {"gfordpavechute1": "SS1"} ) # 집계 열이 읽는 칸(length_m · 쪽별 revet_length_m)은 그대로 — 길이는 안 읽히는 이름으로만 for key in ("length_m", "inlet_revet_length_m", "outlet_revet_length_m", "area_m"): assert plain[0]["options"].get(key) == long[0]["options"].get(key) class Sheet: def __init__(self) -> None: self.rows: list[tuple] = [] def add(self, at, key, sort, group, label, value, combos) -> None: self.rows.append((key, label, value)) sheet = Sheet() _chutes(sheet, 50.0, {**options, "ford_chute_1_length_m": 7}, {"gfordpavechute1": "SS1"}, {}) assert sheet.rows == [("chute|찰붙임", "㎡", 12.0)] # 돌붙임 수량 = 면적 · 길이 안 탐