"""213-2 — 유령 계획고 편집 근본 막음. ① 관을 옮기면 편집 키가 새 자리로 따라감 — 화면 `followedStation` (B05_Profile_UI_Profile_Alignment.ts · B05 applyIrregularStations) · B06 movePipeTo 는 resetStationEdit(옛, 새). ② [저장] 때 서버가 측점 · 관 · 구조물 자리 어디에도 없는 편집 키를 버리고 자리 목록을 돌려줌 (`drop_unplaced_edits` · `_apply_alignment_edits(marks=...)`). 자료 = 고정 사본(fixtures/b05_192_1_moved_pipe_longitudinal.json · 226fc805 종단 460~840m). """ from __future__ import annotations import json import subprocess import sys from pathlib import Path import numpy as np import pytest PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from B05_Profile.B05_Profile_Engine_Grade_Alignment import drop_unplaced_edits # noqa: E402 from B05_Profile.B05_Profile_Router import _apply_alignment_edits # noqa: E402 TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" FIXTURE = PROJECT_ROOT / "resources/tester/fixtures/b05_192_1_moved_pipe_longitudinal.json" REL = "B06_Section/longitudinal/longitudinal.json" GHOST = {"station_offsets": {"769.280": 0.506058, "769.380": 0.051428}, "curve_radii": {}} def test_drop_keeps_station_and_marks_only(): edits = { "station_offsets": {"20.000": 1.0, "263.206": 0.5, "263.310": 0.9, "400.000": 0.2}, "curve_radii": {"263.206": 300.0, "500.000": 80.0}, } kept, dropped = drop_unplaced_edits(edits, np.array([0.0, 20.0, 263.21, 263.31])) # 263.206 = 측점 263.21 의 0.006m 안(반올림) · 263.31 = 관 자리 → 남음 assert kept["station_offsets"] == {"20.000": 1.0, "263.206": 0.5, "263.310": 0.9} assert kept["curve_radii"] == {"263.206": 300.0} assert dropped == [400.0, 500.0] def _project(tmp_path: Path, station_m: float) -> Path: """고정 사본 — 측점 769.28(옮긴 관, 측점 안 다시 뜸) 또는 769.38(다시 뜸 · 옛 꼭짓점 없음).""" data = json.loads(FIXTURE.read_text(encoding="utf-8")) if station_m != 769.28: for station in data["stations"]: if abs(station["chainage_m"] - 769.28) < 1e-6: station.update(chainage_m=station_m, station_id="station_000000769380") base = data["profile_alignment"]["base_pvi"] data["profile_alignment"]["base_pvi"] = [ node for node in base if abs(node["chainage_m"] - 769.28) > 1e-6 ] target = tmp_path / REL target.parent.mkdir(parents=True) target.write_text(json.dumps(data), encoding="utf-8") return target def test_save_drops_ghost_after_station_regenerated(tmp_path): """226fc805 꼴 — 측점 769.38 로 다시 섰는데 세션 초안이 옛 키 769.280 을 다시 보냄.""" path = _project(tmp_path, 769.38) alignment, summary, dropped = _apply_alignment_edits(tmp_path, REL, GHOST, [769.38]) assert dropped == [769.28] assert alignment["edits"]["station_offsets"] == {"769.380": 0.051428} near = [p["chainage_m"] for p in alignment["pvi"] if 760 < p["chainage_m"] < 780] assert near == [769.38] assert summary["max_grade_pct"] < 100 saved = json.loads(path.read_text(encoding="utf-8"))["profile_alignment"] assert "769.280" not in saved["edits"]["station_offsets"] def test_save_keeps_moved_pipe_edit_before_regeneration(tmp_path): """관만 옮기고 측점은 옛 자리(769.28) — 옛 측점 편집 · 새 관 자리 편집 둘 다 지킴.""" _project(tmp_path, 769.28) alignment, _, dropped = _apply_alignment_edits(tmp_path, REL, GHOST, [769.38]) assert dropped == [] assert set(alignment["edits"]["station_offsets"]) == {"769.280", "769.380"} def test_save_without_marks_drops_nothing(tmp_path): """노선이 안 맞아 관 · 구조물 자리를 못 읽으면 버리지 않음(잃는 쪽보다 남는 쪽).""" _project(tmp_path, 769.38) _, _, dropped = _apply_alignment_edits(tmp_path, REL, GHOST, None) assert dropped == [] _RUNNER = """ import { readFileSync, writeFileSync } from "node:fs"; import { followedStation } from "./B05_Profile_UI_Profile_Alignment.js"; const cases = JSON.parse(readFileSync(process.argv[2], "utf8")); const out = cases.map((c) => followedStation(c.station, c.gone, c.born)?.chainage_m ?? null); writeFileSync(process.argv[3], JSON.stringify(out)); """ ST = lambda sid, m: {"id": sid, "chainage_m": m} # noqa: E731 CASES = [ # 옮긴 관 하나(키 한 번 +0.1) → 따라감 ( { "station": ST("pipe-263.21", 263.21), "gone": [ST("pipe-263.21", 263.21)], "born": [ST("pipe-263.31", 263.31)], }, 263.31, ), # 반올림(263.206 → 263.21) → 따라감 ( { "station": ST("pipe-263.21", 263.206), "gone": [ST("pipe-263.21", 263.206)], "born": [ST("pipe-263.21", 263.21)], }, 263.21, ), # A군 구조물 = 같은 id 로 ( { "station": ST("s1", 100.0), "gone": [ST("s1", 100.0), ST("pipe-5.00", 5.0)], "born": [ST("pipe-9.00", 9.0), ST("s1", 130.0)], }, 130.0, ), # 둘이 한꺼번에 바뀜(지움 + 새로 놓음 둘) → 안 따라감(지움) ( { "station": ST("pipe-5.00", 5.0), "gone": [ST("pipe-5.00", 5.0), ST("pipe-7.00", 7.0)], "born": [ST("pipe-50.00", 50.0), ST("pipe-70.00", 70.0)], }, None, ), # 지우기만 → 지움 ({"station": ST("pipe-5.00", 5.0), "gone": [ST("pipe-5.00", 5.0)], "born": []}, None), ] def test_followed_station_ts(tmp_path): if not TSC.is_file(): pytest.skip("프론트엔드 의존성 미설치") out = tmp_path / "js" subprocess.run( # noqa: S603 — 고정 실행 파일 [ "node", str(TSC), str(PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Profile_Alignment.ts"), "--outDir", str(out), "--module", "esnext", "--target", "es2022", "--moduleResolution", "bundler", "--ignoreConfig", ], cwd=str(PROJECT_ROOT), check=True, capture_output=True, ) (out / "runner.mjs").write_text(_RUNNER, encoding="utf-8") src, dst = tmp_path / "in.json", tmp_path / "out.json" src.write_text(json.dumps([case for case, _ in CASES]), encoding="utf-8") subprocess.run( # noqa: S603 ["node", str(out / "runner.mjs"), str(src), str(dst)], check=True, capture_output=True ) assert json.loads(dst.read_text(encoding="utf-8")) == [want for _, want in CASES] def test_move_sites_follow_edits(): """옮김 자리 둘이 편집을 따라 옮김 — B06 movePipeTo · B05 측점 갱신(문자열 짝 확인).""" b06 = (PROJECT_ROOT / "B06_Section/B06_Section_UI_Page_Structures_Panel.ts").read_text( encoding="utf-8" ) b05 = (PROJECT_ROOT / "B05_Profile/B05_Profile_UI_Page_Structures.ts").read_text( encoding="utf-8" ) assert "deps.shared.profile?.resetStationEdit(hit.chainage_m, to)" in b06 assert "followedStation(station, gone, born)" in b05