"""287 물넘이포장 후속(요구 183-2 · 4 · 6 · 7 · 8 · 9). 못박는 것 - 301-1 도수로/돌붙임 면적 = 늘 손 입력(요구 192-2 · 287-1 자동 셈 걷음) — 길이 · 월류 폭을 바꾸거나 길이를 비워도 면적 그대로 · 길이 = 그림 전용(294-4). - 287-3 월류 폭 → 높이 자동 = 세월교 한 벌(`fordHeightM` · 폼 `overflow(…, "slope")`) · 유량 없으면 손 입력. - 287-5 물넘이포장 형상 누름 = 다른 시설과 같은 고름 창구(`toggleRevet("pipe")` → 폼 · 알약 · 측점선) · 판정면은 보라 손잡이 아래(요구 185 ①) · 끌기(3 px 넘게 움직임) = 고름 아님(②) · 노면공 포장층 (범위 표식)은 형상 고름 밖(③) · 목록 · 알약 고름이 형상 강조를 따라옴. - 287-2 · 287-4 콘크리트 윗면 = 설계 노면(서버 · TS 짝이 경사각도로 지대 낮은 쪽 한 경사 · 중심점이 그 위) · 포장층 = 노면공 포장 형상 노견까지(노면공 항목과 안 섞임) · 월류 높이 점선 = 노면 위 같은 각도 · 3D 노면을 파지 않음 · 설계 측구 없음. 진짜 TS 는 `helper_287_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)) 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() -> dict: proc = subprocess.run( ["node", str(HERE / "helper_287_ford_pavement.cjs")], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def test_도수로_면적은_손_입력_그대로(ts): # 길이 4 넣음 · 월류 폭 8 · 길이 비움 — 면적은 손 입력 17 그대로 assert ts["areaKeptPave"] == ["17", "17", "17"] assert ts["areaKeptFord"] == ["17", "17", "17"] def test_폼이_길이_면적을_잇지_않는다(): form = _read("B05_Profile", "B05_Profile_UI_Drainage_Facility.ts") assert "syncChuteArea" not in form assert "onChange: fordFlowBox.onChange," in form assert "onChange: paveFlowBox.onChange," in form calc = _read("A00_Common", "design", "design_structure_ford_calc.ts") assert "syncChuteArea" not in calc and "chuteAreaM2" not in calc def _pts(text: str) -> list[float]: return [float(v) for pair in text.split() for v in pair.split(",")] def test_포장층_노견까지_점선은_노면_위(ts): assert ts["drew"] is True # 포장층 하나 = 노견 끝(±3)까지 · 윗면 = 설계 노면 · 두께 0.2 assert len(ts["pavePoints"]) == 1 assert _pts(ts["pavePoints"][0]) == pytest.approx([3, 100.15, -3, 99.85, -3, 99.65, 3, 99.95]) # 월류 높이선 = 노면 + 0.4 · 같은 각도 · 우(성토) = 성토선 연장과 만난 자리 → 노면 끝(315-2) d = 0.4 / (2.65 / 4 - 0.3 / 6) meet = [-3 + d, 99.85 + 2.65 / 4 * d] assert _pts(ts["planPoints"][0]) == pytest.approx([3, 100.55, *meet, -3, 99.85]) assert ts["inflowLeft"] is True # 노면 높은 쪽 = 유입 · 도수로 띠 = 낮은 쪽 def test_3D_노면을_파지_않고_옛_파임_그림을_걷음(): station = _read("B05_Profile", "B05_Profile_UI_Corridor_Station.ts") assert "fordDeckElevationAt" not in station pave = _read("B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts") assert "hideFordPavedRoad" not in pave and "fordDeckElevationAt" not in pave view = _read("B06_Section", "B06_Section_UI_Cross_View.ts") assert "hideFordPavedRoad" not in view def test_월류_폭_넣으면_높이_자동_세월교_한_벌(ts): assert ts["paveSlopeItems"] == ["angle", "width", "height"] assert ts["heightByWidth"] and ts["heightByWidth"] > 0 assert ts["heightNoFlow"] is None form = _read("B05_Profile", "B05_Profile_UI_Drainage_Facility.ts") assert 'if (facility === "ford_pavement") paveFlowBox.fill();' in form def test_형상_판정면은_보라_손잡이_아래(ts): order = ts["shapeOrder"] # 그림 → 형상 판정면 → 보라 손잡이 판정 → 구조물 판정면(위가 이김) assert ( order.index("b06-chart__shape-hit") < order.index("b06-chart__design-hit") < order.index("b06-chart__culvert-revet-hit") ) def test_형상_누름은_고름_끌기는_고름_아님(ts): assert ts["picked"] == ["pipe-230.00"] # 누름 한 번만 고름 · 끌기는 안 셈 assert ts["clickStops"] is True and ts["dragStops"] is False # 끌기는 카드로 그대로 assert ts["pavedShapeIsPicked"] is False # 노면공 포장층은 형상 고름 밖(포장 고름 몫) def test_형상_강조와_표식(ts): assert ts["activeAfterSet"] is True # 물넘이 콘크리트는 「관」 고름(형상 누름) 몫 — 측점 길 강조(highlightShapes)가 끄지도 켜지도 않음(307) assert ts["activeAfterOther"] is True and ts["activeAfterSame"] is True assert ts["fordTag"] == ["pipe-230.00"] # 포장층 표식 = 관 지점 알약 id def test_보라_손잡이는_계획고만_형상_누름은_고름만(): view = _read("B06_Section", "B06_Section_UI_Cross_View.ts") # 형상 누름 = 시설 고름 창구만(설계 · 계획고를 안 건드림) · 보라 선 고름보다 먼저 꽂음(손잡이가 위) assert 'shapePickRoute(svg, (at) => toggleRevet("pipe", at))' in view assert view.index("attachShapePick(") < view.index("attachDesignLinePick({") pick = _read("B06_Section", "B06_Section_UI_Cross_Shape_Pick.ts") assert "onDesignChange" not in pick and "design_elevation" not in pick assert '"b06-chart__culvert-revet-hit"' in pick # 구조물 판정면 앞(아래)에 꽂을 기준만 assert 'hit.setAttribute("class", "b06-chart__shape-hit")' in pick # 보라 손잡이 판정 = 계획고 · 사면 이동 창구(설계선 고름 · 구조물 판정면 아래) design_pick = _read("B06_Section", "B06_Section_UI_Cross_Design_Pick.ts") assert 'el.classList.contains("b06-chart__culvert-revet-hit")' in design_pick # 목록 · 알약 · 폼 고름 → 형상 강조 section_view = _read("B06_Section", "B06_Section_UI_Section_View.ts") assert "highlightShapes(card, pickedMarkId);" in section_view selection = _read("B06_Section", "B06_Section_UI_Page_Structures_Selection.ts") assert "sectionAt.ford_pavement?.chainage_m" in selection def test_월류_폭_높이_화면_길_유량_있음(ts): got = ts["overflowFlow"] assert got["afterWidth8"] == "0.26" # 폭 8 → 필요 수심(유량 14.58 ㎥/s) assert got["afterClear"] == "0.26" # 높이 비움 → 다시 채움 assert got["afterLow"] == "0.26" # 필요 수심 아래 → 끌어올림 assert got["afterHigh"] == "0.9" # 올려 적은 값은 그대로 assert got["hint"] == ["필요 수심 0.26 이상"] and got["otherBoxHint"] == 0 assert got["emitted"] == 4 def test_월류_폭_높이_화면_길_유량_없음(ts): got = ts["overflowNoFlow"] assert got["afterWidth8"] == "0.05" and got["afterClear"] == "" # 손 입력 그대로 assert got["hint"] == ["설계유량 없음 — 수기 입력"] def test_세월교_물넘이_한_벌_안내_훅(): form = _read("B05_Profile", "B05_Profile_UI_Drainage_Facility.ts") flat = " ".join(form.split()) assert 'overflowSync( () => fordForm, "ford", () => fordFlow, emit, )' in flat assert 'overflowSync( () => paveForm, "slope", () => fordFlow, emit, )' in flat assert "...fordFlowBox.fields(c)" in form and "fields: paveFlowBox.fields," in form