Files
Aislo/resources/tester/test_349_0u_save_rebase.py
T
eomsangdonandClaude Opus 5.5 2140719c98 fix(저장): 349-0u 덧 노선을 저장한 [저장] — 세션 관 초안이 서버 따라감을 덮지 않음
- 노선 저장 전 · 뒤 정본 관을 받아 초안을 얹음(rebasePipeDraft) — 옛 자리 그대로인 관 = 서버가 옮긴 자리 · 옵션 고침은 그대로 · 사용자가 옮긴 · 새로 놓은 관 그대로 · 지운 관 없음 · 서버가 새로 놓은 교차 관 붙음
- 관 초안이 없거나 노선 저장이 없으면 전과 같음
- 따라감 미리보기 — 좌표 없는 초안 관 중 안 옮긴 것은 정본 좌표로(서버 노선 저장과 같은 측점)
- 시험 test_349_0u_save_rebase(실제 design_save · 서버 따라감 = project_pipe_points) · test_349_0u_follow_draft_xy 서버 대조 · design_store_save 가짜 보탬

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
2026-10-16 18:12:46 +09:00

123 lines
5.3 KiB
Python

"""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)