"""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 from B08_Quantity.B08_Quantity_Milestone_Calc import migrate_milestone # 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): row = {"item": "", "dest": "start", "note": ""} return {"id": rid, "name": rid, "address": "", "via_base": via, "dir": None, "lon": lon, "lat": lat, "legs": [], "rows": [row]} # 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)], } # 254 옛 꼴 → bases · to(옛 기준점 = b1) lay = _node(f"layoutMilestone({json.dumps(migrate_milestone(data))}, {json.dumps(site)}, 0)") bx, by = _center(lay, "b1") sx, sy = _center(lay, "s") assert abs(sx - bx) < 1 and sy > by + 150 # 남 = 아래(254 빈 km = 최소 선 길이) 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)] # 254 옛 꼴 → bases · to(옛 기준점 = b1) lay = _node(f"layoutMilestone({json.dumps(migrate_milestone(data))}, {json.dumps(site)}, 0)") bx, by = _center(lay, "b1") nx, ny = _center(lay, "n") assert bx < cx - 200 # 기준점 = 시점 서쪽(좌표) assert abs(nx - bx) < 1 and ny < by - 150 def test_다음_지도_길찾기_주소(): # 156 경로 탭 새 창 길찾기 없앰(길찾기 = 연결선 모달 · 카카오 길찾기 API) — 주소 만들기도 없음 assert "kakaoRouteUrl" not in _read("B08_Quantity/B08_Quantity_Milestone_Layout.ts") route = _read("B08_Quantity/B08_Quantity_UI_Milestone_Route.ts") assert "window.open(" not in route and "다음 지도에서 길찾기" not in route 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") # ① [+] 모달 — 항목 · 거래처 이름 · 목적지 + 소재지(244-13 · 주소 찾기 = B01 후보 검색 재활용 · # 지도 = 다음 지도) · 방위 고르기 없음 · [등록] add = modal[ modal.index("export function openPlaceModal") : modal.index("export interface SiteAddress") ] for text in ('label: "항목"', 'label: "목적지"'): # 항목 · 목적지 줄 = rowField assert text in modal for text in ('label: "거래처 이름"', '"등록"', "placePicker(", "place.value()"): assert text in add assert "방위" not in add.replace("방위는 이 자리로 자동", "") and "dirOptions" not in modal place = _read("B08_Quantity/B08_Quantity_UI_Milestone_Place.ts") assert 'from "../B01_Dashboard/B01_Dashboard_Api_Fetch"' in place assert "searchAddress(text)" in place # ② 시점 모달 — 지도 = 공용 카카오 지도(255-1 · 주소 줄 지움) assert "/quantity/milestone/site-address" in modal and "editMap(projectId" in modal # ⑤ 거래처 상자 = 같은 등록 모달(고침 · 삭제 · 244-13) · 구간 km · 도로 구분 = 연결선 모달 for text in ('"수정"', 'label: "삭제"', "rowValues()", "putMilestone(projectId, fresh)"): assert text in add leg = _read("B08_Quantity/B08_Quantity_UI_Milestone_LegModal.ts") assert "legsEditor(legsOf(), kind)" in leg diagram = _read("B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts") assert "route: (id) => openPlaceModal(projectId, { add: false, id })," in diagram assert "start: () => openSiteInfo(projectId)," in diagram # ③ 팬 · 줌 = 공용 assert 'from "../A00_Common/b_svg_zoom_pan"' in diagram and "attachSvgZoomPan({" in diagram # 149 처음 배율 = 상자 범위 맞춤(사용자 줌 · 팬 뒤엔 그 배율) assert "wheelZoom: true" in diagram and "initial: userZoomed ? zoomState : fitBoxes(" in diagram # ④ 경로 하위 탭 = 그림만 · 다음 지도 새 창 · 그림 올리기 길(화면 캡처는 244-11 에서 서버 자동으로) 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 "getDisplayMedia" not in route and "uploadImage(projectId, routeId, picked)" in route assert 'label: "그림 변경"' in route # 157 그림 올리기 → 그림 변경 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 def test_선_글_바로_고침_모달과_한_벌(): from B08_Quantity.B08_Quantity_Milestone_Calc import compute_milestone data = { "site": {"mid_station_m": None, "mid_road": "비포장"}, "base": {"enabled": True, "name": "기준", "address": "", "dir": None, "legs": [{"road": "1차포장", "km": 0.6}]}, "routes": [{**_route("g"), "legs": [{"road": "2차포장", "km": None}]}], } # fmt: skip d = json.dumps(migrate_milestone(data)) # 254 옛 꼴 → bases · to(옛 기준점 = b1) got = _node( f"[legAt({d}, 'g', 0, 0.75), legAt({d}, 'start', 0, 0.75), legAt({d}, 'x', 0, 0)," f" setLeg(setLeg(setLeg({d}, 'g', 0, {{road: '1차포장', km: 23.8}})," f" 'b1', 0, {{km: null}}), 'start', 0, {{road: '2차포장', km: 1.2345}})]" ) assert got[0] == {"road": "2차포장", "km": None} assert got[1] == {"road": "비포장", "km": 0.75} and got[2] is None after = got[3] assert after["routes"][0]["legs"] == [{"road": "1차포장", "km": 23.8}] assert after["bases"][0]["legs"] == [{"road": "1차포장", "km": None}] assert after["site"] == {"mid_station_m": 1234.5, "mid_road": "2차포장"} # 표 셈 — 포장 종류 = 선 자리(152 · 기준점 거침 거래처 = 2차포장 · 저장 값 1차포장 이어도) (row,) = compute_milestone(after, 1500.0) assert (row["s2"], row["s1"], row["un"], row["total"]) == (23.8, 0, 0, 23.8) ui = _read("B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts") assert 'span.addEventListener("dblclick"' in ui and "e.stopPropagation();" in ui assert "editLegAt(projectId, span, ln.from, i, mk)" in ui assert "anchor.getBoundingClientRect()" in ui # 팬 · 줌 뒤 글 화면 자리 assert "putMilestone(projectId, setLeg(liveMilestone(projectId), from, i, patch))" in ui