From eaadf2778282ac884f7d6ac3e809f4e2351f0a47 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 11 Oct 2026 00:27:46 +0900 Subject: [PATCH] =?UTF-8?q?refactor(b08):=20=EA=B1=B0=EB=A6=AC=EC=9D=B4?= =?UTF-8?q?=EC=A0=95=ED=91=9C=20=EA=B1=B0=EB=9E=98=EC=B2=98=20=ED=83=AD=20?= =?UTF-8?q?=3D=20=EA=B7=B8=EB=A6=BC=EB=A7=8C=20=E2=80=94=20=EC=83=88=20?= =?UTF-8?q?=EC=B0=BD=20=EA=B8=B8=EC=B0=BE=EA=B8=B0=20=C2=B7=20=ED=83=AD=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=B9=B8=20=EC=97=86=EC=95=B0=20(250-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY --- B08_Quantity/B08_Quantity_Milestone_Layout.ts | 7 -- .../B08_Quantity_UI_Milestone_KakaoMap.ts | 89 +------------------ .../B08_Quantity_UI_Milestone_Route.ts | 47 +--------- .../test_244_10_b08_milestone_tailscale.py | 7 +- .../tester/test_244_4_b08_milestone_modal.py | 15 +--- .../test_244_9_b08_milestone_route_map.py | 12 ++- 6 files changed, 24 insertions(+), 153 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Milestone_Layout.ts b/B08_Quantity/B08_Quantity_Milestone_Layout.ts index b6b33a7c1..d3f7016da 100644 --- a/B08_Quantity/B08_Quantity_Milestone_Layout.ts +++ b/B08_Quantity/B08_Quantity_Milestone_Layout.ts @@ -530,13 +530,6 @@ export function layoutMilestone( }; } -/** 다음(카카오) 지도 자동차 길찾기 새 창 주소(공식 `/link/by/car/이름,위도,경도/…` · 경유 ≤ 5) */ -export function kakaoRouteUrl(points: Array<{ name: string; lon: number; lat: number }>): string { - const part = (p: { name: string; lon: number; lat: number }): string => - `${encodeURIComponent(p.name.replace(/[,/]/g, " ").trim() || "지점")},${p.lat},${p.lon}`; - return `https://map.kakao.com/link/by/car/${points.map(part).join("/")}`; -} - /** 선 글 한 줄 → 그 구간 — from = 거래처 id(거래처 구간) · "base"(공통 구간) · "start"(시점 → 중점) */ export function legAt(data: MilestoneData, from: string, i: number, midKm: number): Leg | null { if (from === "start") return { road: data.site.mid_road, km: Math.round(midKm * 1000) / 1000 }; diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts b/B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts index e30eb7972..6a4980ccf 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts @@ -1,19 +1,17 @@ /* ============================================================================= * B08_Quantity_UI_Milestone_KakaoMap.ts - * 거리이정표 경로 지도(PLAN 244-9 · 사용자 138번) — 카카오 지도 JS 위 길 선 · 표지(보기만). + * 거리이정표 카카오 지도 칸(PLAN 244-9 · 사용자 138번) — 등록 모달(소재지 고르기) · 연결선 모달(길 선 · 표지 끌기) + * 한 벌(`editMap`) · 경로 하위 탭 지도 칸은 없앰(156 · 탭 = 그림만). * * SDK = dapi.kakao.com/v2/maps/sdk.js(autoload=false) · 키 = 서버 GET …/milestone/map-key * (.env JS 키 · 공개용 · 도메인 등록으로 막힘) · 키 없음 · 못 뜸 = 안내 한 줄 + 사진 올리기 - * 길 선 = 마지막 [길찾기] 응답 경로 좌표(`routePaths` · 메모리만 · 저장 안 함) · 표지 = 거래처 · 기준점 · - * 시점(`mapMarks`) · 경로 전체 맞춤 + * 길 선 = 마지막 [길찾기] 응답 경로 좌표(메모리만 · 저장 안 함) · 처음 한 번만 맞춤(153 · 그 뒤 보기 그대로) * 경로 그림 = 서버 자동 캡처(244-11 · `…_State` autoRouteImage) — 브라우저 캡처 없음(접속 주소 무관) * (244-10) SDK 못 받음(도메인 미등록 401) = 지금 주소(location.origin) 등록 안내 한 줄 * ========================================================================== */ import { el } from "@ui/ui_template_elements"; -import { mapMarks } from "./B08_Quantity_Milestone_Layout"; import { request } from "./B08_Quantity_UI_Hydro"; -import { liveMilestone, routePaths, siteInfo } from "./B08_Quantity_UI_Milestone_State"; export const NO_MAP = "카카오 JavaScript 키 · 도메인 등록 필요 — 지도 대신 [그림 올리기]로 넣으세요"; @@ -93,87 +91,6 @@ export function loadKakaoMaps(projectId: string): Promise { return sdk; } -export interface RouteMapView { - root: HTMLElement; - /** 지금 값 · 마지막 [길찾기] 길 선으로 다시 그림 */ - draw: () => void; -} - -/** 경로 지도 칸 — 거래처 모달 · 경로 하위 탭 같은 한 벌 */ -export function routeMapView(projectId: string, routeId: string): RouteMapView { - const box = el("div", { className: "b08-milestone__map" }); - const hint = el("p", { className: "b08-options__hint" }); - hint.dataset.b08MilestoneMapHint = ""; - let map: KMap | null = null; - let drawn: KOverlay[] = []; - let shown = ""; // 같은 표지 · 길 선이면 다시 안 그림(사용자가 옮긴 지도 자리 그대로) - - const draw = (): void => { - const marks = mapMarks(liveMilestone(projectId), routeId, siteInfo()?.start_lonlat ?? null); - const lines = routePaths(routeId); - const sig = JSON.stringify([marks, lines]); - if (map && sig === shown) return; - loadKakaoMaps(projectId).then( - (maps) => - requestAnimationFrame(() => { - const points = [ - ...marks.map((m) => new maps.LatLng(m.lat, m.lon)), - ...lines.flat().map(([lat, lon]) => new maps.LatLng(lat, lon)), - ]; - if (!points.length) { - hint.textContent = "거래처 주소 · 노선 시점이 있어야 지도에 놓임"; - return; - } - box.hidden = false; - map ??= new maps.Map(box, { center: points[0], level: 7 }); - map.relayout(); - drawn.forEach((o) => o.setMap(null)); - drawn = [ - ...lines.map( - (line) => - new maps.Polyline({ - path: line.map(([lat, lon]) => new maps.LatLng(lat, lon)), - strokeWeight: 6, - strokeColor: LINE_COLOR, - strokeOpacity: 0.9, - }), - ), - ...marks.flatMap((m) => { - const at = new maps.LatLng(m.lat, m.lon); - return [ - new maps.Marker({ position: at, title: m.name }), - new maps.CustomOverlay({ - position: at, - content: el("span", { className: "b08-milestone__map-label", text: m.name }), - yAnchor: 2.6, - }), - ]; - }), - ]; - drawn.forEach((o) => o.setMap(map)); - const bounds = new maps.LatLngBounds(); - points.forEach((p) => bounds.extend(p)); - map.setBounds(bounds); - shown = sig; - hint.textContent = lines.length - ? "길 선 = 카카오 길찾기 · 경로 그림은 서버가 자동으로 만듦" - : "[길찾기]를 누르면 길 선 · 경로 그림이 생김"; - }), - (error: unknown) => { - box.hidden = true; // 키 없음 · SDK 못 받음 — 안내 한 줄(사진 올리기는 그대로) - hint.textContent = (error as Error)?.name === SDK_FAIL ? (error as Error).message : NO_MAP; - }, - ); - }; - - const root = el("div", { - className: "b08-milestone__map-wrap", - children: [hint, box], - }); - draw(); - return { root, draw }; -} - /** 고르는 지도 표지(244-13) — key = 끌어 옮길 때 돌려받는 이름 */ export interface MapPin { key: string; diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_Route.ts b/B08_Quantity/B08_Quantity_UI_Milestone_Route.ts index d61fee88b..655e4724b 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_Route.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_Route.ts @@ -5,9 +5,7 @@ * 그림 = [등록] · [길찾기] 때 서버 자동 캡처(244-11 · 카카오 지도 + 길 선 + 표지) · [그림 올리기] = 사용자 * 그림 우선 · [자동 그림으로 되돌리기] = 사용자 그림만 지움 · [그림 지우기] = 둘 다 · 즉시 서버 * 자동 그림 실패 = 까닭 한 줄(이 탭 안내 줄) · 사진 올리기 - * [다음 지도에서 길찾기] = 새 창 map.kakao.com 자동차 길찾기(출발 = 거래처 좌표 · 경유 = 기준점 좌표(거침) · - * 도착 = 목적지 좌표) — 보기만 - * 카카오 지도(244-9) = `…_UI_Milestone_KakaoMap` — 길 선 · 표지(보기만) · JS 키 없음 = 안내 한 줄 + * 길찾기 · 카카오 지도는 연결선 모달 몫(156 · 이 탭은 그림만 · 새 창 길찾기 · 지도 칸 없앰) * ========================================================================== */ import { @@ -17,7 +15,6 @@ import { } from "@ui/ui_template_context_menu"; import { createButton, el, showToast } from "@ui/ui_template_elements"; import type { Dest, MilestoneData, MilestoneRoute } from "./B08_Quantity_Milestone_Calc"; -import { kakaoRouteUrl } from "./B08_Quantity_Milestone_Layout"; import { autoImageNote, canRevertImage, @@ -26,10 +23,8 @@ import { liveMilestone, putMilestone, routeImageKey, - siteInfo, uploadImage, } from "./B08_Quantity_UI_Milestone_State"; -import { routeMapView } from "./B08_Quantity_UI_Milestone_KakaoMap"; let menu: MapContextMenu | null = null; let lastCell: Element | null = null; @@ -70,26 +65,10 @@ export interface MilestoneView { refresh: () => void; } -/** 새 창 길찾기 지점 — 거래처 → (기준점 거침 · 좌표 있으면) → 목적지(첫 표 줄 · 시점 · 중점) */ -export function routePoints( - d: MilestoneData, - r: MilestoneRoute, -): Array<{ name: string; lon: number; lat: number }> | null { - const site = siteInfo(); - const dest = r.rows[0]?.dest ?? "start"; - const to = dest === "mid" ? site?.mid_lonlat : site?.start_lonlat; - if (r.lon == null || r.lat == null || !to) return null; - const points = [{ name: r.name || "거래처", lon: r.lon, lat: r.lat }]; - if (d.base.enabled && r.via_base && d.base.lon != null && d.base.lat != null) - points.push({ name: d.base.name || "기준점", lon: d.base.lon, lat: d.base.lat }); - points.push({ name: `임도 ${DEST_TEXT[dest]}`, lon: to[0], lat: to[1] }); - return points; -} - export function routeView(projectId: string, routeId: string): MilestoneView { const title = el("h4"); const HINT = - "경로 그림 = [길찾기] 때 서버가 지도에 길 선을 그려 자동으로 만듦 · [그림 올리기] = 내 그림 우선"; + "경로 그림 = 연결선 길찾기 때 서버가 카카오 지도에 길 선을 그려 자동으로 만듦 · [그림 올리기] = 내 그림 우선"; const hint = el("p", { className: "b08-options__hint", text: HINT }); hint.dataset.b08MilestoneRouteHint = ""; @@ -124,23 +103,6 @@ export function routeView(projectId: string, routeId: string): MilestoneView { if (picked) void put(picked); }); - const kakao = createButton({ - label: "다음 지도에서 길찾기", - size: "sm", - variant: "filled", - onClick: () => { - const d = liveMilestone(projectId); - const r = d.routes.find((x) => x.id === routeId); - const points = r ? routePoints(d, r) : null; - if (!points) - return showToast( - "거래처 주소(다이어그램 상자 → 항목 정보)와 노선 시점을 먼저 정하세요.", - "info", - ); - window.open(kakaoRouteUrl(points), "_blank", "noopener"); - }, - }); - kakao.dataset.b08MilestoneKakao = ""; const upload = createButton({ label: "그림 올리기", size: "sm", @@ -167,7 +129,6 @@ export function routeView(projectId: string, routeId: string): MilestoneView { ), }); - const mapView = routeMapView(projectId, routeId); const root = el("section", { className: "b08-milestone__route ui-card", attrs: { "data-b08-milestone-route": routeId }, @@ -175,10 +136,9 @@ export function routeView(projectId: string, routeId: string): MilestoneView { title, el("div", { className: "b08-milestone__bar", - children: [kakao, upload, revert, removeImage, file], + children: [upload, revert, removeImage, file], }), hint, - mapView.root, noImage, img, ], @@ -193,7 +153,6 @@ export function routeView(projectId: string, routeId: string): MilestoneView { loadImage(); revert.hidden = !canRevertImage(routeId); hint.textContent = autoImageNote(routeId) ?? HINT; - mapView.draw(); }; refresh(); return { root, refresh }; diff --git a/resources/tester/test_244_10_b08_milestone_tailscale.py b/resources/tester/test_244_10_b08_milestone_tailscale.py index b1d8968e3..bed1745c6 100644 --- a/resources/tester/test_244_10_b08_milestone_tailscale.py +++ b/resources/tester/test_244_10_b08_milestone_tailscale.py @@ -38,7 +38,8 @@ const routePaths = () => [[[35.68, 127.9], [35.64, 127.83]]]; FAKE_KAKAO = """ class Fake { setMap() {} relayout() {} setBounds() {} extend() {} } (globalThis as any).kakao = { maps: { load: (f: () => void) => f(), LatLng: Fake, - LatLngBounds: Fake, Map: Fake, Polyline: Fake, Marker: Fake, CustomOverlay: Fake } }; + LatLngBounds: Fake, Map: Fake, Polyline: Fake, Marker: Fake, CustomOverlay: Fake, + event: { addListener: () => {} } } }; """ @@ -48,7 +49,9 @@ def _view(setup): + setup + IMPORTS.sub("", _read(KMAP)) + """ -const view = routeMapView("p1", "r1"); +// 156 경로 탭 지도 칸 없앰 — 같은 SDK 싣기 · 안내 길 = 연결선 모달 지도 칸(editMap) +const view = editMap("p1", { onMove: () => {}, hint: "길 선 = 카카오 길찾기 · 경로 그림은 서버가 자동으로 만듦" }); +view.set([{ key: "r1", name: "거창군청", lon: 127.9, lat: 35.68, movable: true }], routePaths()); await new Promise((r) => setTimeout(r, 30)); const [hint, box] = view.root.children; console.log(JSON.stringify({ injected, hint: hint.textContent, boxHidden: box.hidden, diff --git a/resources/tester/test_244_4_b08_milestone_modal.py b/resources/tester/test_244_4_b08_milestone_modal.py index 5f8db74f8..44cb13b38 100644 --- a/resources/tester/test_244_4_b08_milestone_modal.py +++ b/resources/tester/test_244_4_b08_milestone_modal.py @@ -76,16 +76,10 @@ def test_방위_좌표_자동(): 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" - ) + # 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): @@ -174,7 +168,6 @@ def test_화면_모달_팬줌_그림탭(): assert ( "createDataTable" not in route and "구간" not in route.split("export function routeView")[1] ) - assert "window.open(kakaoRouteUrl(points)" in route assert "getDisplayMedia" not in route and "uploadImage(projectId, routeId, picked)" in route assert 'label: "그림 올리기"' in route page = _read("B08_Quantity/B08_Quantity_UI_Milestone.ts") diff --git a/resources/tester/test_244_9_b08_milestone_route_map.py b/resources/tester/test_244_9_b08_milestone_route_map.py index 7bd1fe72e..fc797d644 100644 --- a/resources/tester/test_244_9_b08_milestone_route_map.py +++ b/resources/tester/test_244_9_b08_milestone_route_map.py @@ -159,7 +159,11 @@ const PATHS = {json.dumps(paths)}; const asked: string[] = []; const request = async (url: string) => (asked.push(url), {{ key: KEY }}); {extra} -const view = routeMapView("p1", "r1"); +// 156 경로 탭 지도 칸 없앰 — 같은 SDK 싣기 · 안내 길 = 연결선 모달 지도 칸(editMap) +const view = editMap("p1", {{ onMove: () => {{}}, hint: "길 선 = 카카오 길찾기 · 경로 그림은 서버가 자동으로 만듦" }}); +view.set([{{ key: "r1", name: "거창군청", lon: 127.9, lat: 35.68, movable: true }}, + {{ key: "base", name: "병곡리", lon: 127.85, lat: 35.66, movable: true }}, + {{ key: "start", name: "시점", lon: 127.83, lat: 35.64, movable: false }}], PATHS); await new Promise((r) => setTimeout(r, 30)); const [hint, box] = view.root.children; console.log(JSON.stringify({{ asked, injected, hint: hint.textContent, boxHidden: box.hidden, @@ -193,6 +197,7 @@ const on = (kind: string, o: any) => ({ setMap: (m: any) => m && LOG.overlays.pu Polyline: function (o: any) { return on("line", o); }, Marker: function (o: any) { return on("marker", o); }, CustomOverlay: function (o: any) { return on("label", o); }, + event: { addListener: () => {} }, } }; """ @@ -218,8 +223,9 @@ def test_화면_자리_캡처_키_서버만(): leg = _read("B08_Quantity/B08_Quantity_UI_Milestone_LegModal.ts") # 244-13 = 연결선 모달 지도 assert "mapHead, map.root" in leg and "map.set(pins()" in leg route = _read("B08_Quantity/B08_Quantity_UI_Milestone_Route.ts") - assert "routeMapView(projectId, routeId)" in route - assert "mapView.draw();" in route + # 156 경로 탭 = 그림만(새 창 길찾기 · 지도 칸 없앰 · 길찾기 · 지도 = 연결선 모달) + assert "routeMapView" not in route and "다음 지도에서 길찾기" not in route + assert "routeMapView" not in kmap state = _read("B08_Quantity/B08_Quantity_UI_Milestone_State.ts") assert "paths.set(from, res.path!);" in state # 메모리만(캐시 · 서버 안 감) for rel in Path(ROOT / "B08_Quantity").glob("*.ts"): # 키 이름은 서버만