|
|
|
@@ -0,0 +1,185 @@
|
|
|
|
|
"""244-4 B08 거리이정표 고침(사용자 130번) — [+] 모달(항목 · 업체 이름 · 목적지 + 주소 검색 재활용) ·
|
|
|
|
|
방위 = 좌표 자동 · 시점 모달(등록 주소 · VWorld 주소 · 지도) · 다이어그램 팬 · 줌 공용 ·
|
|
|
|
|
경로 하위 탭 = 그림만(다음 지도 길찾기 새 창 · 화면 캡처) · 상자 누름 = 항목 정보 모달.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import re
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
if str(ROOT) not in sys.path:
|
|
|
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
|
|
|
|
|
|
from B01_Dashboard import B01_Dashboard_Map as vmap # noqa: E402
|
|
|
|
|
from B08_Quantity import B08_Quantity_Router_Milestone as ms # noqa: E402
|
|
|
|
|
|
|
|
|
|
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read(rel):
|
|
|
|
|
return (ROOT / rel).read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _node(expr):
|
|
|
|
|
src = _read("B08_Quantity/B08_Quantity_Milestone_Layout.ts")
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
["node", "--experimental-strip-types", "--no-warnings",
|
|
|
|
|
"--input-type=module-typescript", "-e",
|
|
|
|
|
IMPORTS.sub("", src) + f"\nconsole.log(JSON.stringify({expr}));"],
|
|
|
|
|
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
|
|
|
|
) # fmt: skip
|
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
|
|
|
return json.loads(result.stdout)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _route(rid, lon=None, lat=None, via=True):
|
|
|
|
|
return {"id": rid, "name": rid, "address": "", "via_base": via, "dir": None, "lon": lon,
|
|
|
|
|
"lat": lat, "legs": [], "rows": [{"item": "", "dest": "start", "note": ""}]} # fmt: skip
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _center(lay, bid):
|
|
|
|
|
box = next(b for b in lay["boxes"] if b["id"] == bid)
|
|
|
|
|
return box["x"] + box["w"] / 2, box["y"] + box["h"] / 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_방위_좌표_자동():
|
|
|
|
|
site = {"start_lonlat": [127.8, 35.7], "mid_lonlat": None}
|
|
|
|
|
base = {"enabled": True, "name": "기준", "address": "", "dir": None, "legs": []}
|
|
|
|
|
data = {
|
|
|
|
|
"site": {"mid_station_m": None, "mid_road": "비포장"},
|
|
|
|
|
"base": base,
|
|
|
|
|
# 기준점 좌표 없음 → 시점 좌표 기준 · 업체 = 시점 남쪽 · 기준점 안 거침 업체 = 동쪽
|
|
|
|
|
"routes": [_route("s", 127.8, 35.6), _route("e", 127.9, 35.7, via=False)],
|
|
|
|
|
}
|
|
|
|
|
lay = _node(f"layoutMilestone({json.dumps(data)}, {json.dumps(site)}, 0)")
|
|
|
|
|
bx, by = _center(lay, "base")
|
|
|
|
|
sx, sy = _center(lay, "s")
|
|
|
|
|
assert abs(sx - bx) < 1 and sy > by + 200 # 남 = 아래
|
|
|
|
|
cx, cy = _center(lay, "site")
|
|
|
|
|
ex, ey = _center(lay, "e")
|
|
|
|
|
assert ex > cx + 200 and abs(ey - cy) < 1
|
|
|
|
|
# 기준점 좌표 있음 → 기준점 기준(업체가 기준점 북쪽)
|
|
|
|
|
data["base"] = {**base, "lon": 127.7, "lat": 35.7}
|
|
|
|
|
data["routes"] = [_route("n", 127.7, 35.8)]
|
|
|
|
|
lay = _node(f"layoutMilestone({json.dumps(data)}, {json.dumps(site)}, 0)")
|
|
|
|
|
bx, by = _center(lay, "base")
|
|
|
|
|
nx, ny = _center(lay, "n")
|
|
|
|
|
assert bx < cx - 200 # 기준점 = 시점 서쪽(좌표)
|
|
|
|
|
assert abs(nx - bx) < 1 and ny < by - 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_다음_지도_길찾기_주소():
|
|
|
|
|
pts = [{"name": "거창군청", "lon": 127.909, "lat": 35.686},
|
|
|
|
|
{"name": "병곡리, 968/31", "lon": 127.75, "lat": 35.78},
|
|
|
|
|
{"name": "임도 시점", "lon": 127.74, "lat": 35.79}] # fmt: skip
|
|
|
|
|
url = _node(f"kakaoRouteUrl({json.dumps(pts, ensure_ascii=False)})")
|
|
|
|
|
# 공식 꼴 /link/by/car/이름,위도,경도/… · 이름의 쉼표 · 빗금은 빈칸
|
|
|
|
|
assert url == (
|
|
|
|
|
"https://map.kakao.com/link/by/car/%EA%B1%B0%EC%B0%BD%EA%B5%B0%EC%B2%AD,35.686,127.909/"
|
|
|
|
|
"%EB%B3%91%EA%B3%A1%EB%A6%AC%20%20968%2031,35.78,127.75/"
|
|
|
|
|
"%EC%9E%84%EB%8F%84%20%EC%8B%9C%EC%A0%90,35.79,127.74"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_VWorld_좌표_주소(monkeypatch):
|
|
|
|
|
body = {"response": {"status": "OK", "result": [
|
|
|
|
|
{"type": "parcel", "text": "경남 거창군 북상면 병곡리 968-31"},
|
|
|
|
|
{"type": "road", "text": "경남 거창군 북상면 덕유월성로 1"}]}} # fmt: skip
|
|
|
|
|
seen = []
|
|
|
|
|
|
|
|
|
|
def fetch(url, timeout=5):
|
|
|
|
|
seen.append(url)
|
|
|
|
|
return json.dumps(body).encode()
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(vmap, "_fetch", fetch)
|
|
|
|
|
assert vmap._reverse_sync(127.75, 35.78) == "경남 거창군 북상면 덕유월성로 1"
|
|
|
|
|
assert "request=getAddress" in seen[0] and "point=127.75%2C35.78" in seen[0]
|
|
|
|
|
body["response"]["result"] = body["response"]["result"][:1]
|
|
|
|
|
assert vmap._reverse_sync(127.75, 35.78) == "경남 거창군 북상면 병곡리 968-31"
|
|
|
|
|
monkeypatch.setattr(vmap, "_fetch", lambda url, timeout=5: b"not json")
|
|
|
|
|
assert vmap._reverse_sync(127.75, 35.78) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_시점_모달_주소_길(tmp_path, monkeypatch):
|
|
|
|
|
from fastapi import FastAPI
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
|
|
|
|
async def root(_pid):
|
|
|
|
|
return tmp_path
|
|
|
|
|
|
|
|
|
|
async def project(_pid):
|
|
|
|
|
return {"region": "경남 거창군 북상면"}
|
|
|
|
|
|
|
|
|
|
async def where(lon, lat):
|
|
|
|
|
return f"주소 {lon:.3f}"
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(ms, "_project_root", root)
|
|
|
|
|
monkeypatch.setattr(ms, "get_project_by_id", project)
|
|
|
|
|
monkeypatch.setattr(ms, "reverse_geocode", where)
|
|
|
|
|
app = FastAPI()
|
|
|
|
|
app.include_router(ms.router)
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
url = f"/api/projects/{uuid4()}/quantity/milestone/site-address"
|
|
|
|
|
empty = client.get(url).json()
|
|
|
|
|
assert empty["region"] == "경남 거창군 북상면" and empty["start_address"] is None
|
|
|
|
|
path = tmp_path / "B05_Profile" / "route" / "planned_route.csv"
|
|
|
|
|
path.parent.mkdir(parents=True)
|
|
|
|
|
path.write_text("sequence,x,y\n0,208000,367000\n1,209000,367000", encoding="utf-8")
|
|
|
|
|
got = client.get(url).json()
|
|
|
|
|
lon, lat = got["start_lonlat"]
|
|
|
|
|
assert 124 < lon < 132 and 33 < lat < 39
|
|
|
|
|
assert got["start_address"] == f"주소 {lon:.3f}" and got["mid_address"].startswith("주소 ")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_화면_모달_팬줌_그림탭():
|
|
|
|
|
modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts")
|
|
|
|
|
# ① [+] 모달 — 항목 · 업체 이름 · 목적지 + 주소 검색(B01 재활용) · 방위 고르기 없음 · [등록]
|
|
|
|
|
add = modal[
|
|
|
|
|
modal.index("export function openAddItem") : modal.index("export function openRouteInfo")
|
|
|
|
|
]
|
|
|
|
|
for text in ('label: "항목"', 'label: "업체 이름"', 'label: "목적지"', 'label: "등록"',
|
|
|
|
|
"buildAddressField(null)", "placeOf(address,"): # fmt: skip
|
|
|
|
|
assert text in add
|
|
|
|
|
assert "방위" not in add.replace("방위는 이 자리로 자동", "") and "dirOptions" not in modal
|
|
|
|
|
assert 'from "../B01_Dashboard/B01_Dashboard_UI_AddressField"' in modal
|
|
|
|
|
assert 'from "../B01_Dashboard/B01_Dashboard_UI_MapPreview"' in modal
|
|
|
|
|
field = _read("B01_Dashboard/B01_Dashboard_UI_AddressField.ts")
|
|
|
|
|
assert "point: () => picked," in field and "searchAddress(text)" in field
|
|
|
|
|
# ② 시점 모달 — 등록 주소 · VWorld 주소 · 지도
|
|
|
|
|
assert "/quantity/milestone/site-address" in modal and "map.showPoint(" in modal
|
|
|
|
|
assert "프로젝트 등록 주소" in modal and "시점 브이월드 주소" in modal
|
|
|
|
|
# ⑤ 업체 상자 = 항목 정보(구간 km · 도로 구분 · 고침 · 삭제)
|
|
|
|
|
info = modal[
|
|
|
|
|
modal.index("export function openRouteInfo") : modal.index("export function openBaseInfo")
|
|
|
|
|
]
|
|
|
|
|
for text in ('label: "고침"', 'label: "삭제"', 'legsEditor(route.legs, "2차포장")',
|
|
|
|
|
"rowsEditor(route.rows)", "putMilestone(projectId, fresh)"): # fmt: skip
|
|
|
|
|
assert text in info
|
|
|
|
|
diagram = _read("B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts")
|
|
|
|
|
assert "route: (id) => openRouteInfo(projectId, id)," in diagram
|
|
|
|
|
assert "start: () => openSiteInfo(projectId)," in diagram
|
|
|
|
|
# ③ 팬 · 줌 = 공용
|
|
|
|
|
assert 'from "../A00_Common/b_svg_zoom_pan"' in diagram and "attachSvgZoomPan({" in diagram
|
|
|
|
|
assert "wheelZoom: true" in diagram and "initial: zoomState" in diagram
|
|
|
|
|
# ④ 경로 하위 탭 = 그림만 · 다음 지도 새 창 · 화면 캡처 → 그림 올리기 길
|
|
|
|
|
route = _read("B08_Quantity/B08_Quantity_UI_Milestone_Route.ts")
|
|
|
|
|
assert (
|
|
|
|
|
"createDataTable" not in route and "구간" not in route.split("export function routeView")[1]
|
|
|
|
|
)
|
|
|
|
|
assert "window.open(kakaoRouteUrl(points)" in route
|
|
|
|
|
assert "getDisplayMedia({ video: true, audio: false })" in route
|
|
|
|
|
assert (
|
|
|
|
|
"captureWindow().then(put," in route and "uploadImage(projectId, routeId, picked)" in route
|
|
|
|
|
)
|
|
|
|
|
assert 'label: "그림 올리기"' in route
|
|
|
|
|
page = _read("B08_Quantity/B08_Quantity_UI_Milestone.ts")
|
|
|
|
|
assert 'label: "+ 추가"' not in page and 'label: "삭제"' not in page
|
|
|
|
|
for rel in ("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts",
|
|
|
|
|
"B08_Quantity/B08_Quantity_UI_Milestone_Route.ts",
|
|
|
|
|
"B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts",
|
|
|
|
|
"B08_Quantity/B08_Quantity_Router_Milestone.py"): # fmt: skip
|
|
|
|
|
assert len(_read(rel).splitlines()) <= 700
|