- ① 관 옮김 = 편집 키도 새 자리로: B06 movePipeTo 가 resetStationEdit(옛, 새) · B05 측점 갱신이 사라진 것 하나 · 새것 하나(또는 같은 id · 0.006 m 안)를 짝으로 옮김(followedStation) — 세션 초안도 새 키라 옛 키가 다시 안 나감 - ② [저장](PUT profile-alignment)에서 서버가 측점 · 관 · 구조물 자리 어디에도 없는 편집 키를 버림(drop_unplaced_edits · 저장 차례가 관 · 구조물 뒤라 최신) · 응답 dropped_edit_chainages · 화면 알림 「계획고 편집 N곳을 지웠습니다」 · 자리 못 읽으면 안 버림 - ③ test_192_1 = 고정 사본(fixtures/b05_192_1_moved_pipe_longitudinal.json · 226fc805 460~840 m) · 새 시험 test_213_2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
184 lines
7.2 KiB
Python
184 lines
7.2 KiB
Python
"""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
|