- 노선 저장 전 · 뒤 정본 관을 받아 초안을 얹음(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
123 lines
5.3 KiB
Python
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)
|