Files
Aislo/resources/tester/test_286_1_ford_pavement_design.py
T
eomsangdonandClaude Opus 5.5 209383ed17 feat(b05,b06,b07): 도수로/돌붙임 띠 = 낮은 쪽 끝단에서 · 원지반 닿으면 끝 · 길이 = 그 길이 · 모든 시설 같은 규칙과 길이 칸 (294-4)
- 공용 chuteRun(Wall_Hatch) — 길이 비움 = 원지반 닿으면 끝 · 넣음 = 사면 따라 그 길이 · 사면보다 길면 원지반 따라 이어 그 길이 · 끝단이 이미 원지반이면 안 그림
- 물넘이 — 띠 시작 = 늘인 노면 끝(294-3) · 아니면 유출 쪽 노견 끝 · 원지반 닿으면 끝(양측 절토 = 띠 없음)
- 배관 · 박스 · 세월교 — 띠 계산이 원지반(layout.ground)을 받아 같은 규칙 · 3D 띠도 같음 · 코리도 해시에 길이
- 명세 CHUTE_ITEMS 맨 아래 「길이 (m)」(chute_add_{n}_length_m · 그림 전용) · 물넘이는 공용 칸 그대로
- 면적 자동 = 물넘이 · 세월교만 월류 폭 × 길이(배관 · 박스 = 폭 정의 없어 손 입력 그대로 · 브레인 정함)
- 길이는 집계 열 · B08 수량(면적만)에 안 실림 · 마스터 정의 이름 표는 안 건드림(이름 표 대조 시험에서 이 칸만 밖)
- 시험 test_294_3 · 286_1 · 52 · 65 · 152_3 기대값

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
2026-10-12 21:58:16 +09:00

202 lines
8.4 KiB
Python

"""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," in spec # 공용 칸 맨 아래 길이(294-4 · CHUTE_ITEMS 로 옮김)
)
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)] # 돌붙임 수량 = 면적 · 길이 안 탐