- [등록] · [길찾기] 뒤 길 선 · 표지 → 서버 헤드리스 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
201 lines
7.4 KiB
TypeScript
201 lines
7.4 KiB
TypeScript
/* =============================================================================
|
|
* B08_Quantity_UI_Milestone_Route.ts
|
|
* 거리이정표 경로 하위 탭(PLAN 244-1 · 244-4) — 길찾기 지도 결과 그림 하나만(구간 · 표 줄은 항목 정보 모달).
|
|
*
|
|
* 그림 = [등록] · [길찾기] 때 서버 자동 캡처(244-11 · 카카오 지도 + 길 선 + 표지) · [그림 올리기] = 사용자
|
|
* 그림 우선 · [자동 그림으로 되돌리기] = 사용자 그림만 지움 · [그림 지우기] = 둘 다 · 즉시 서버
|
|
* 자동 그림 실패 = 까닭 한 줄(이 탭 안내 줄) · 사진 올리기
|
|
* [다음 지도에서 길찾기] = 새 창 map.kakao.com 자동차 길찾기(출발 = 거래처 좌표 · 경유 = 기준점 좌표(거침) ·
|
|
* 도착 = 목적지 좌표) — 보기만
|
|
* 카카오 지도(244-9) = `…_UI_Milestone_KakaoMap` — 길 선 · 표지(보기만) · JS 키 없음 = 안내 한 줄
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
createMapContextMenu,
|
|
type MapContextMenu,
|
|
type MapContextMenuItem,
|
|
} 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,
|
|
deleteImage,
|
|
imageBlobUrl,
|
|
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;
|
|
|
|
/** 표 칸 누름 자리 기억 — 「▾」 메뉴를 그 칸 아래에 엶 */
|
|
export function trackCells(root: HTMLElement): void {
|
|
root.addEventListener("click", (e) => (lastCell = (e.target as Element).closest("td")), true);
|
|
}
|
|
|
|
export function pickAtCell(items: MapContextMenuItem[]): void {
|
|
const box = lastCell?.getBoundingClientRect();
|
|
if (!box) return;
|
|
menu ??= createMapContextMenu("b08-milestone");
|
|
if (!menu.element.isConnected) {
|
|
menu.element.style.position = "fixed";
|
|
document.body.append(menu.element);
|
|
}
|
|
menu.open(box.left, box.bottom, items);
|
|
}
|
|
|
|
export const DEST_TEXT: Record<Dest, string> = { start: "시점", mid: "중점" };
|
|
|
|
/** 그 경로를 고쳐 캐시에 */
|
|
export function editRoute(
|
|
projectId: string,
|
|
routeId: string,
|
|
fn: (data: MilestoneData, route: MilestoneRoute) => void,
|
|
): void {
|
|
const data = liveMilestone(projectId);
|
|
const route = data.routes.find((r) => r.id === routeId);
|
|
if (!route) return;
|
|
fn(data, route);
|
|
putMilestone(projectId, data);
|
|
}
|
|
|
|
export interface MilestoneView {
|
|
root: HTMLElement;
|
|
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 = "";
|
|
|
|
// 그림 — 경로 하나 = 그림 하나
|
|
const img = el("img", { className: "b08-milestone__image", attrs: { alt: "경로 그림" } });
|
|
const noImage = el("p", { className: "ui-general-block__empty", text: "길찾기 그림 없음" });
|
|
let shown: string | null | undefined;
|
|
const loadImage = (force = false): void => {
|
|
const name = routeImageKey(routeId);
|
|
if (!force && name === shown) return;
|
|
shown = name;
|
|
img.hidden = !name;
|
|
noImage.hidden = !!name;
|
|
if (img.src) URL.revokeObjectURL(img.src);
|
|
img.removeAttribute("src");
|
|
if (name)
|
|
imageBlobUrl(projectId, routeId).then(
|
|
(url) => (img.src = url),
|
|
(error: unknown) => showToast(`그림을 못 읽었습니다 — ${String(error)}`, "error"),
|
|
);
|
|
};
|
|
const put = (picked: File): Promise<void> =>
|
|
uploadImage(projectId, routeId, picked).then(
|
|
() => loadImage(true),
|
|
(error: unknown) => showToast(`그림을 올리지 못했습니다 — ${String(error)}`, "error"),
|
|
);
|
|
const file = el("input", { attrs: { type: "file", accept: "image/*" } });
|
|
file.hidden = true;
|
|
file.addEventListener("change", () => {
|
|
const picked = file.files?.[0];
|
|
file.value = "";
|
|
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",
|
|
variant: "ghost",
|
|
onClick: () => file.click(),
|
|
});
|
|
const revert = createButton({
|
|
label: "자동 그림으로 되돌리기",
|
|
size: "sm",
|
|
variant: "ghost",
|
|
onClick: () =>
|
|
void deleteImage(projectId, routeId, true).catch((error: unknown) =>
|
|
showToast(`자동 그림으로 되돌리지 못했습니다 — ${String(error)}`, "error"),
|
|
),
|
|
});
|
|
revert.dataset.b08MilestoneRevert = "";
|
|
const removeImage = createButton({
|
|
label: "그림 지우기",
|
|
size: "sm",
|
|
variant: "ghost",
|
|
onClick: () =>
|
|
void deleteImage(projectId, routeId).catch((error: unknown) =>
|
|
showToast(`그림을 지우지 못했습니다 — ${String(error)}`, "error"),
|
|
),
|
|
});
|
|
|
|
const mapView = routeMapView(projectId, routeId);
|
|
const root = el("section", {
|
|
className: "b08-milestone__route ui-card",
|
|
attrs: { "data-b08-milestone-route": routeId },
|
|
children: [
|
|
title,
|
|
el("div", {
|
|
className: "b08-milestone__bar",
|
|
children: [kakao, upload, revert, removeImage, file],
|
|
}),
|
|
hint,
|
|
mapView.root,
|
|
noImage,
|
|
img,
|
|
],
|
|
});
|
|
|
|
const refresh = (): void => {
|
|
const d = liveMilestone(projectId);
|
|
const r = d.routes.find((x) => x.id === routeId);
|
|
if (!r) return;
|
|
const dest = DEST_TEXT[r.rows[0]?.dest ?? "start"];
|
|
title.textContent = `${r.name || "거래처"} → ${dest}${r.address ? ` · ${r.address}` : ""}`;
|
|
loadImage();
|
|
revert.hidden = !canRevertImage(routeId);
|
|
hint.textContent = autoImageNote(routeId) ?? HINT;
|
|
mapView.draw();
|
|
};
|
|
refresh();
|
|
return { root, refresh };
|
|
}
|