"""PLAN 349-0u 덧 sub7 — 노선을 저장한 [저장]에서 세션 관 초안이 서버 따라감을 덮지 않음. · 초안 관이 노선 저장 전 정본 자리 그대로(따라감 전 값) = 서버가 옮긴 자리(옵션 고침은 얹음) · 사용자가 옮긴 · 새로 놓은 관 = 그대로 · 지운 관 = 없음 · 서버가 새로 놓은 교차 관 = 붙음 · 노선 저장 없음 = 초안 그대로 · 초안 없음 = 관 GET · PUT 없음 · 서버 따라감 = 실제 `project_pipe_points`(검증_sub7 관 · 노선 꼭짓점 하나 옮김) 짝: `helper_349_0u_save_rebase.cjs`(실제 `design_save.ts` 를 Node 로 · 서버 창구만 가짜). """ from __future__ import annotations import copy import csv import json import math import subprocess import sys import tempfile 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 common_util.common_util_drainage_pipes import ( # noqa: E402 parse_pipe_points, project_pipe_points, ) from common_util.common_util_route_geometry import RouteVertex # noqa: E402 pytestmark = pytest.mark.skipif( not (ROOT / "config" / "node_modules" / "typescript").is_dir(), reason="프론트엔드 의존성 미설치", ) PROJECT = ROOT / "storage/1/3/6d3b2812-6d5a-4168-a932-40e4ae303d5a" # 검증_sub7 PIPES = PROJECT / "B04_PreProcess/drainage/edits/pipe_points.json" LINE = PROJECT / "B05_Profile/route/planned_route.csv" def _vertices(line: list[list[float]]) -> list[RouteVertex]: walked, out = 0.0, [] for i, (x, y) in enumerate(line): walked += math.dist(line[i - 1], (x, y)) if i else 0.0 out.append(RouteVertex(x=x, y=y, z=0.0, chainage_m=walked)) return out @pytest.fixture(scope="module") def case() -> dict: if not (PIPES.is_file() and LINE.is_file()): pytest.skip("검증_sub7 없음") with LINE.open(encoding="utf-8") as handle: line = [[float(r["x"]), float(r["y"])] for r in csv.DictReader(handle)] raw = json.loads(PIPES.read_text(encoding="utf-8"))["points"] before = [{**pipe, "id": f"p{i}"} for i, pipe in enumerate(raw)] # 서버 노선 저장 — 앞쪽 꼭짓점을 서쪽으로 크게 옮긴 새 노선에 좌표 투영 + 새 교차 관 하나 moved = copy.deepcopy(line) moved[1] = [moved[1][0] - 60.0, moved[1][1]] points = project_pipe_points(parse_pipe_points(before), _vertices(moved)) after = [point.as_dict() for point in points] after.append({"chainage_m": 30.0, "source": "spacing", "id": "p-new", "x": 1.0, "y": 2.0}) # 세션 초안 = 정본 PUT 꼴(좌표 뺌 · 옛 노선 누가거리) — p1 옵션 고침 · p3 사용자가 7 m 옮김 · # p5 지움 · 새 관 하나(id 없음) draft = [{k: v for k, v in pipe.items() if k not in ("x", "y")} for pipe in before] draft[1]["options"] = {**draft[1].get("options", {}), "pipe_count": 3} draft[3]["chainage_m"] += 7.0 del draft[5] draft.append({"chainage_m": 333.0, "source": "user"}) job = {"before": before, "after": after, "draft": draft} with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "job.json" path.write_text(json.dumps(job), encoding="utf-8") proc = subprocess.run( # noqa: S603 — 고정 실행 파일 ["node", str(HERE / "helper_349_0u_save_rebase.cjs"), str(path)], capture_output=True, text=True, encoding="utf-8", timeout=120, ) assert proc.returncode == 0, proc.stderr return {**job, "out": json.loads(proc.stdout)} def test_노선_저장_뒤_관_차례가_서버_따라감_위에(case): r = case["out"]["rebased"] assert r["ok"] and r["calls"] == ["GET", "ROUTE", "GET", "PUT"], r["calls"] sent = {p.get("id", "user"): p for p in r["sent"][0]} after = {p["id"]: p for p in case["after"]} before = {p["id"]: p for p in case["before"]} assert set(sent) == ({p["id"] for p in case["before"]} - {"p5"}) | {"p-new", "user"} for pid in sent.keys() - {"p3", "user"}: assert sent[pid]["chainage_m"] == after[pid]["chainage_m"], pid # 서버 자리 moved = [p for p in before if p in after and after[p]["chainage_m"] != before[p]["chainage_m"]] assert moved # 실제로 서버가 옮긴 관이 있음 assert sent["p1"]["options"]["pipe_count"] == 3 # 사용자 옵션 고침은 얹음 assert sent["p3"]["chainage_m"] == before["p3"]["chainage_m"] + 7.0 # 사용자가 옮긴 자리 assert sent["user"]["chainage_m"] == 333.0 chain = [p["chainage_m"] for p in r["sent"][0]] assert chain == sorted(chain) assert r["left"] is None # 내보낸 뒤 초안 비움 def test_초안_없으면_관_안_건드림(case): r = case["out"]["noDraft"] assert r["ok"] and r["calls"] == ["ROUTE"], r["calls"] def test_노선_그대로면_초안_그대로(case): r = case["out"]["noRoute"] assert r["ok"] and r["calls"] == ["PUT"], r["calls"] assert r["sent"][0] == case["draft"] def test_안_옮긴_관은_정본_좌표(case): a, b, c = case["out"]["placed"] assert (a["x"], a["y"]) == (40.4, 3) # 같은 id · 같은 누가거리 = 정본 좌표(노선 밖이어도) assert (b["x"], b["y"]) == (60, 0) # 옮긴 관 = 노선 위 그 누가거리 assert (c["x"], c["y"]) == (70, 0)