Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
160 lines
5.6 KiB
TypeScript
160 lines
5.6 KiB
TypeScript
/* =============================================================================
|
|
* B08_Quantity_UI_Milestone_Route.ts
|
|
* 거리이정표 경로 하위 탭(PLAN 244-1 · 244-4) — 길찾기 지도 결과 그림 하나만(구간 · 표 줄은 항목 정보 모달).
|
|
*
|
|
* 그림 = [등록] · [길찾기] 때 서버 자동 캡처(244-11 · 카카오 지도 + 길 선 + 표지) · [그림 올리기] = 사용자
|
|
* 그림 우선 · [자동 그림으로 되돌리기] = 사용자 그림만 지움 · [그림 지우기] = 둘 다 · 즉시 서버
|
|
* 자동 그림 실패 = 까닭 한 줄(이 탭 안내 줄) · 사진 올리기
|
|
* 길찾기 · 카카오 지도는 연결선 모달 몫(156 · 이 탭은 그림만 · 새 창 길찾기 · 지도 칸 없앰)
|
|
* ========================================================================== */
|
|
|
|
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 {
|
|
autoImageNote,
|
|
canRevertImage,
|
|
deleteImage,
|
|
imageBlobUrl,
|
|
liveMilestone,
|
|
putMilestone,
|
|
routeImageKey,
|
|
uploadImage,
|
|
} from "./B08_Quantity_UI_Milestone_State";
|
|
|
|
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 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 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 root = el("section", {
|
|
className: "b08-milestone__route ui-card",
|
|
attrs: { "data-b08-milestone-route": routeId },
|
|
children: [
|
|
title,
|
|
el("div", {
|
|
className: "b08-milestone__bar",
|
|
children: [upload, revert, removeImage, file],
|
|
}),
|
|
hint,
|
|
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;
|
|
};
|
|
refresh();
|
|
return { root, refresh };
|
|
}
|