- [등록] · [길찾기] 뒤 길 선 · 표지 → 서버 헤드리스 Edge(없으면 chrome)로 카카오 지도 1200×800 PNG → auto/<경로 id>.png - 페이지 주소 = KAKAO_CAPTURE_ORIGIN(기본 http://localhost:5173) + /__route_capture page.route 채움 - 사용자 그림 우선 · 자동 그림으로 되돌리기 · 실패 = 까닭 한 줄 · 동시 요청 잠금 · 좌표 저장 없음 - [지도 캡처] · [화면 캡처] · getDisplayMedia · 244-10 캡처 안내 없앰(지도 SDK 도메인 안내는 그대로) - 새 시험 test_244_11 · 244_4 · 244_9 · 244_10 기대값 고침 · requirements playwright Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
163 lines
6.5 KiB
TypeScript
163 lines
6.5 KiB
TypeScript
/* =============================================================================
|
|
* B08_Quantity_UI_Milestone_KakaoMap.ts
|
|
* 거리이정표 경로 지도(PLAN 244-9 · 사용자 138번) — 카카오 지도 JS 위 길 선 · 표지(보기만).
|
|
*
|
|
* SDK = dapi.kakao.com/v2/maps/sdk.js(autoload=false) · 키 = 서버 GET …/milestone/map-key
|
|
* (.env JS 키 · 공개용 · 도메인 등록으로 막힘) · 키 없음 · 못 뜸 = 안내 한 줄 + 사진 올리기
|
|
* 길 선 = 마지막 [길찾기] 응답 경로 좌표(`routePaths` · 메모리만 · 저장 안 함) · 표지 = 거래처 · 기준점 ·
|
|
* 시점(`mapMarks`) · 경로 전체 맞춤
|
|
* 경로 그림 = 서버 자동 캡처(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 키 · 도메인 등록 필요 — 지도 대신 [그림 올리기]로 넣으세요";
|
|
const SDK_FAIL = "KakaoSdkFail";
|
|
const SDK = "https://dapi.kakao.com/v2/maps/sdk.js";
|
|
const LINE_COLOR = "#E60046"; // 테마 무관 또렷한 길 선
|
|
|
|
type LatLng = object;
|
|
interface KMap {
|
|
setBounds(bounds: object): void;
|
|
relayout(): void;
|
|
}
|
|
interface KOverlay {
|
|
setMap(map: KMap | null): void;
|
|
}
|
|
interface KakaoMaps {
|
|
load(done: () => void): void;
|
|
LatLng: new (lat: number, lng: number) => LatLng;
|
|
LatLngBounds: new () => { extend(p: LatLng): void };
|
|
Map: new (box: HTMLElement, o: { center: LatLng; level: number }) => KMap;
|
|
Polyline: new (o: {
|
|
path: LatLng[];
|
|
strokeWeight: number;
|
|
strokeColor: string;
|
|
strokeOpacity: number;
|
|
}) => KOverlay;
|
|
Marker: new (o: { position: LatLng; title: string }) => KOverlay;
|
|
CustomOverlay: new (o: { position: LatLng; content: HTMLElement; yAnchor: number }) => KOverlay;
|
|
}
|
|
|
|
let sdk: Promise<KakaoMaps> | null = null;
|
|
|
|
/** SDK 한 번 싣기 — 키 없음 · 못 받음 = NO_MAP 으로 거절(다음 부름 때 다시) */
|
|
export function loadKakaoMaps(projectId: string): Promise<KakaoMaps> {
|
|
sdk ??= (async () => {
|
|
const { key } = await request<{ key: string | null }>(
|
|
`/projects/${projectId}/quantity/milestone/map-key`,
|
|
);
|
|
if (!key) throw new Error(NO_MAP);
|
|
const w = window as unknown as { kakao?: { maps: KakaoMaps } };
|
|
if (!w.kakao?.maps) {
|
|
await new Promise<void>((done, fail) => {
|
|
const script = document.createElement("script");
|
|
script.src = `${SDK}?appkey=${encodeURIComponent(key)}&autoload=false`;
|
|
script.onload = () => done();
|
|
script.onerror = () =>
|
|
fail(
|
|
Object.assign(
|
|
new Error(
|
|
`카카오 JavaScript 키 도메인에 지금 주소(${location.origin}) 등록 필요 — 지도 대신 [그림 올리기]로 넣으세요`,
|
|
),
|
|
{ name: SDK_FAIL },
|
|
),
|
|
);
|
|
document.head.append(script);
|
|
});
|
|
}
|
|
const maps = w.kakao?.maps;
|
|
if (!maps) throw new Error(NO_MAP);
|
|
await new Promise<void>((done) => maps.load(done));
|
|
return maps;
|
|
})();
|
|
sdk.catch(() => (sdk = null));
|
|
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 };
|
|
}
|