"""255-2 거리이정표 경로 탭 — 시점 끌기 = 길찾기용 덮개(site.start_pin · 노선 그대로) · 표지 옮기면 그 자리에 닿는 구간(나가는 · 들어오는) 모두 다시 길찾기 · 열 때 사슬 구간 모두 선 · 실패 안내 · 끌기는 그림 캡처 안 부름.""" import json import re import subprocess import sys from pathlib import Path import pytest from pydantic import ValidationError ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B08_Quantity import B08_Quantity_Router_Milestone as ms # noqa: E402 IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) STUBS = """ let cache = null; const sent = []; const API_BASE_URL = ""; const showToast = () => undefined; const readState = () => cache; const writeState = (_key, d) => { cache = d; }; const clearState = () => { cache = null; }; const request = async (url, opts) => { if (url.endsWith("/quantity/milestone")) return { data: DATA, site: { length_m: 1000, mid_default_m: 500, start_lonlat: [127, 35], mid_lonlat: null }, images: {}, auto_images: {} }; sent.push(JSON.parse(opts.body)); return { km: 1.5, reason: null, path: [] }; }; """ def _read(rel): return (ROOT / rel).read_text(encoding="utf-8") def _place(pid, to, lon, lat, **more): return {"id": pid, "name": pid, "address": "", "dir": None, "lon": lon, "lat": lat, "legs": [], "to": to, **more} # fmt: skip DATA = { "site": {"mid_station_m": None, "mid_road": "비포장"}, "bases": [_place("b1", "site", 127.5, 35.5), _place("b2", "b1", 127.6, 35.6)], "routes": [ _place("r1", "site", 127.2, 35.2, rows=[]), _place("r2", "b1", 127.3, 35.3, rows=[]), _place("r3", "b2", 127.4, 35.4, rows=[]), _place("r4", "b1", 127.1, 35.1, rows=[], linked=False), ], } def _run(script): src = "".join( IMPORTS.sub("", _read(f"B08_Quantity/B08_Quantity_{n}.ts")) for n in ("Milestone_Calc", "Milestone_Layout", "UI_Milestone_State") ) code = f"const DATA = {json.dumps(DATA)};\n{STUBS}\n{src}\n{script}" result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript"], input=code, capture_output=True, text=True, encoding="utf-8", timeout=60, ) # fmt: skip assert result.returncode == 0, result.stderr return json.loads(result.stdout) def test_시점_덮개_길찾기(): got = _run(""" await loadMilestone("p"); await findSegmentKm("p", "r1", false); const d = liveMilestone("p"); d.site.start_pin = { lon: 128.25, lat: 36.5 }; putMilestone("p", d); await findSegmentKm("p", "r1", false); await findSegmentKm("p", "b1", false); console.log(JSON.stringify({ sent, start: startPoint(liveMilestone("p")) })); """) dest = [s["destination"] for s in got["sent"]] assert dest[0] == {"lon": 127, "lat": 35} # 덮개 없음 = 노선 첫 점 assert dest[1] == dest[2] == {"lon": 128.25, "lat": 36.5} # 거래처 · 기준점 → 시점 = 덮개 assert got["start"] == [128.25, 36.5] def test_닿는_구간_모두(): got = _run(""" console.log(JSON.stringify(["start", "b1", "b2", "r1", "r3"].map((k) => touchingSegments(DATA, k)))); """) start, b1, b2, r1, r3 = got assert start == ["b1", "r1"] # 시점에 들어오는 구간 assert b1 == ["b1", "b2", "r2"] # 나가는 구간 + 들어오는 경로들(안 이은 r4 빠짐) assert b2 == ["b2", "r3"] assert r1 == ["r1"] and r3 == ["r3"] def test_서버_모델_시점_덮개(): got = ms.Milestone.model_validate({"site": {"start_pin": {"lon": 127.5, "lat": 35.5}}}) assert got.model_dump()["site"]["start_pin"] == {"lon": 127.5, "lat": 35.5} assert ms.Milestone().model_dump()["site"]["start_pin"] is None with pytest.raises(ValidationError): ms.Milestone.model_validate({"site": {"start_pin": {"lon": 500, "lat": 35}}}) def test_경로_탭_끌기_다시_길찾기(): route = _read("B08_Quantity/B08_Quantity_UI_Milestone_Route.ts") assert "movable: true" in route and 'movable: key !== "start"' not in route assert "fresh.site.start_pin = { lon, lat };" in route assert "await refind(touchingSegments(fresh, key));" in route assert "findSegmentKm(projectId, id, false)" in route # 끌기 · 열기 = 그림 캡처 없음 assert "[routeId, ...chain].filter((id) => !segmentPath(id).length)" in route # 열 때 모든 구간 assert "status.hidden = !bad.length;" in route # 실패 안내 calc = _read("B08_Quantity/B08_Quantity_Milestone_Calc.ts") assert "start_pin?: { lon: number; lat: number } | null;" in calc