"""272-1 측점 한 벌 — 화면(TS)과 서버(파이썬)가 같은 측점 목록을 냄(요구 167 · 측점 겹침 근본). - 심는 자리: TS `plantedChainages` ↔ 서버 `resolve_extra_stations`(관 · 독립 기슭막이 시작 · 기준 · 끝 · A군 · C군 구간형 시작 · 기준 · 끝 · 물넘이포장 기준(286-1) · 다른 군 · 점형 C/D 는 안 심음). - 같은 m = 측점 하나 · 구조물이 위 · 떠나면 일반 측점: TS `oneStationList` ↔ 서버 `_merge_irregular_into_longitudinal`. 진짜 TS 는 `helper_272_1_station_list.cjs` 가 Node 로 돌림. """ from __future__ import annotations import json import math 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_Router_Confirm import _merge_irregular_into_longitudinal # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402 from common_util.common_util_drainage_pipes import parse_pipe_points # noqa: E402 PIPES = [ {"chainage_m": 95.0, "source": "user"}, {"chainage_m": 160.0, "source": "user", "facility": "box_culvert"}, {"chainage_m": 230.0, "source": "user", "facility": "ford_pavement"}, {"chainage_m": 300.0, "source": "user", "facility": "ford_bridge"}, { "chainage_m": 400.0, "source": "user", "facility": "revetment", "start_m": 395.0, "end_m": 410.0, }, ] STRUCTURES = [ # 노출형 = 구조물 표 횡단도 none(322-1 ★1) — 측점 안 심음 {"type_id": "cross_drain_exposed", "placement": "point", "chainage_m": 120.0}, { "type_id": "retaining_wall", "placement": "interval", "chainage_m": 510.0, "start_m": 500.0, "end_m": 530.0, }, { # 옹벽 끝 530 과 같은 자리에서 시작 — 같은 자리는 측점 하나(322-3 측점 계획 · 이름 = 앞선 옹벽) "type_id": "berm", "placement": "interval", "chainage_m": 545.0, "start_m": 530.0, "end_m": 560.0, }, {"type_id": "erosion_check", "placement": "point", "chainage_m": 600.0}, # D군 점형 { "type_id": "pavement_concrete", "placement": "interval", "chainage_m": 650.0, "start_m": 650.0, "end_m": 700.0, }, {"type_id": "evac_road", "placement": "point", "chainage_m": 720.0}, ] EXPECTED = [95, 160, 230, 300, 395, 400, 410, 500, 510, 530, 545, 560] # 서버 종단 측점(맞추기 전) — 격자 0~100 · 옛 관 측점 95.38 · 구조물이 떠난 격자 자리 40(`grid`) · # 옛 판 「regular + 글」 60 · 새 목록 = 격자 위 100(구조물이 위) · 새 자리 35 SERVER = [ {"chainage_m": 0.0, "kind": "bp"}, {"chainage_m": 20.0, "kind": "regular"}, {"chainage_m": 40.0, "kind": "irregular", "grid": True, "structure": "옹벽"}, {"chainage_m": 60.0, "kind": "regular", "structure": "옛 글"}, {"chainage_m": 80.0, "kind": "regular"}, {"chainage_m": 95.38, "kind": "irregular", "structure": "파형강관 D800"}, {"chainage_m": 100.0, "kind": "regular"}, ] FRESH = [ {"chainage_m": 35.0, "kind": "irregular", "structure": "파형강관 D800"}, {"chainage_m": 100.0, "kind": "irregular", "grid": True, "structure": "BOX암거"}, ] pytestmark = pytest.mark.skipif( not (ROOT / "config" / "node_modules" / "typescript").is_dir(), reason="프론트엔드 의존성 미설치", ) def _types() -> list[dict]: return [ { "type_id": item.type_id, "group": item.group, "placement": item.placement, "managed_by": item.managed_by, } for item in structure_type_map().values() ] FIXTURE = ROOT / "resources/tester/fixtures/b05_192_1_moved_pipe_longitudinal.json" def _snapped(data: dict) -> list[dict]: """옛 소수 구조물 측점을 정수 m 로 옮긴 새 목록(272-4 자동 맞춤 뒤).""" return [ {**s, "chainage_m": float(math.floor(s["chainage_m"] + 0.5))} for s in data["stations"] if s.get("kind") == "irregular" ] @pytest.fixture(scope="module") def ts(tmp_path_factory) -> dict: source = tmp_path_factory.mktemp("s272") / "in.json" fixture = json.loads(FIXTURE.read_text(encoding="utf-8")) payload = { "pipes": PIPES, "structures": STRUCTURES, "types": _types(), "server": SERVER, "fresh": FRESH, "nodes": fixture["profile_alignment"]["base_pvi"], "gone": [s["chainage_m"] for s in fixture["stations"] if s.get("kind") == "irregular"], "born": [s["chainage_m"] for s in _snapped(fixture)], } source.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8") proc = subprocess.run( # noqa: S603 — 고정 실행 파일 ["node", str(HERE / "helper_272_1_station_list.cjs"), str(source)], capture_output=True, text=True, encoding="utf-8", cwd=str(ROOT), check=False, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def test_planted_mirror(tmp_path: Path, ts) -> None: path = tmp_path / "B05_Profile/route/structures.json" path.parent.mkdir(parents=True) path.write_text( json.dumps({"revision": 1, "structures": STRUCTURES}, ensure_ascii=False), encoding="utf-8", ) server = [mark for mark, _label in resolve_extra_stations(tmp_path, parse_pipe_points(PIPES))] assert sorted(server) == EXPECTED assert ts["planted"] == EXPECTED def test_one_list_mirror(tmp_path: Path, ts) -> None: rel = "B06_Section/longitudinal/longitudinal.json" path = tmp_path / rel path.parent.mkdir(parents=True) path.write_text(json.dumps({"stations": SERVER}, ensure_ascii=False), encoding="utf-8") _merge_irregular_into_longitudinal(tmp_path, rel, FRESH) merged = json.loads(path.read_text(encoding="utf-8"))["stations"] assert [s["chainage_m"] for s in merged] == [0, 20, 35, 40, 60, 80, 100] assert [s["chainage_m"] for s in ts["list"]] == [s["chainage_m"] for s in merged] # 일반 측점은 글을 떼고 일반으로 · 같은 m 는 구조물 측점 하나(새 목록 것) for side in (merged, ts["list"]): by = {s["chainage_m"]: s for s in side} assert by[40]["kind"] == "regular" and "structure" not in by[40] and "grid" not in by[40] assert "structure" not in by[60] assert by[100]["structure"] == "BOX암거" assert 95.38 not in by def test_moved_nodes_mirror(tmp_path: Path, ts) -> None: """옛 소수 → 정수 m 맞춤 뒤 꼭짓점 — 화면(저장 전 그림)과 서버([저장] 뒤)가 같음.""" rel = "B06_Section/longitudinal/longitudinal.json" path = tmp_path / rel path.parent.mkdir(parents=True) fixture = json.loads(FIXTURE.read_text(encoding="utf-8")) path.write_text(json.dumps(fixture, ensure_ascii=False), encoding="utf-8") _merge_irregular_into_longitudinal(tmp_path, rel, _snapped(fixture)) server = json.loads(path.read_text(encoding="utf-8"))["profile_alignment"]["base_pvi"] assert [(n["chainage_m"], n["elevation_m"]) for n in ts["nodes"]] == [ (n["chainage_m"], n["elevation_m"]) for n in server ]