/* ============================================================================= * B06_Section_UI_Missing_Stations.ts * 「측점 없는 관 N개」 알림 + [측점 만들기] 단추 (계획서 3-14 ㉯). * * 무엇이 문제였나 — 측점을 만드는 자리는 **B05 노선 [확정] 한 곳뿐**이라, 관을 나중에 * 놓거나 옮기면 그 측점이 안 생긴다. 그 관은 횡단도에도 안 서고 **수량·금액에서 통째로 * 빠지는데 아무 말도 안 나온다**(실측: 배수관 넷이 B09 에서 막혀 있었다). * * ⚠ 자동으로 만들지 않는다 — 사용자가 누를 때만 돈다(비용이 큰 지표 샘플링이다). * 대신 **왜 값이 없는지**가 화면에 남는다. * ⚠ 지표 샘플링 조건이 저장에 없으면 **단추를 잠그고 사유를 보인다** — 조건을 지어내면 * 그 측점만 다른 지표에서 뽑혀 옆 측점과 지반고가 어긋난다. * [저장] 뒤 다시 봄(428) — 관 · 구조물 초안이 정본으로 나가 비면 저장이 끝난 뒤 한 번 더 묻고 줄을 갈음 * (서버가 관 PUT 끝에 측점을 다시 세움 · 옛 알림이 남지 않게). * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { createButton, showToast } from "@ui/ui_template_elements"; import { attachHoverInfo } from "@ui/ui_template_hover_info"; import { useDesignStore } from "../A00_Common/design/design_store"; import { savingDesign } from "../A00_Common/design/design_save"; interface MissingStation { chainage_m: number; label: string; } interface MissingResponse { missing?: MissingStation[]; can_create?: boolean; reason?: string; created?: number; message?: string; } async function call(projectId: string, method: "GET" | "POST"): Promise { const response = await fetch(`${API_BASE_URL}/projects/${projectId}/section/missing-stations`, { method, credentials: "include", headers: { "Content-Type": "application/json" }, }); const payload = (await response.json()) as MissingResponse; if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`); return payload; } /** * 빠진 측점이 있으면 알림 줄을 `host` 맨 앞에 얹는다. 없으면 아무것도 하지 않는다. * `onCreated` 는 측점이 실제로 생긴 뒤에만 불린다(화면을 다시 읽는 자리). */ export async function mountMissingStationNotice( host: HTMLElement, projectId: string, onCreated: () => void | Promise, ): Promise { watchSaves(host, projectId, onCreated); let data: MissingResponse; try { data = await call(projectId, "GET"); } catch { return; // 점검이 안 되는 것으로 화면을 막지 않는다 — 이 줄은 덤이다. } host.querySelector(":scope > .b06-missing-stations")?.remove(); // 다시 봄 = 줄 하나 const missing = data.missing ?? []; if (!missing.length) return; const box = document.createElement("div"); box.className = "b06-missing-stations"; const text = document.createElement("p"); text.className = "b06-missing-stations__text"; const where = missing .slice(0, 6) .map((item) => `${item.chainage_m.toFixed(2)}m ${item.label}`) .join(" · "); text.textContent = `측점이 없는 구조물 ${missing.length}개 — ${where}` + (missing.length > 6 ? ` 외 ${missing.length - 6}개` : ""); // 긴 까닭 글 = 호버(196-1) attachHoverInfo(text, "이 자리는 횡단도에도 안 서고 수량에서도 빠집니다."); box.append(text); if (data.can_create) { const button = createButton({ label: "측점 만들기", variant: "filled" }); button.addEventListener("click", async () => { button.disabled = true; button.textContent = "만드는 중…"; try { const result = await call(projectId, "POST"); showToast(`측점 ${result.created ?? 0}개를 만들었습니다.`, "success"); box.remove(); await onCreated(); } catch (error) { const detail = error instanceof Error ? ` ${error.message}` : ""; showToast(`측점을 만들지 못했습니다.${detail}`, "error"); button.disabled = false; button.textContent = "측점 만들기"; } }); box.append(button); } else if (data.reason) { const reason = document.createElement("p"); reason.className = "b06-missing-stations__reason"; reason.textContent = data.reason; box.append(reason); } host.prepend(box); } const watched = new WeakSet(); /** 관 · 구조물 초안이 비면(저장 · 되돌림) 저장이 끝난 뒤 다시 물음 — 자리(`host`)가 떠나면 끊음. */ function watchSaves( host: HTMLElement, projectId: string, onCreated: () => void | Promise, ): void { if (watched.has(host)) return; watched.add(host); const store = useDesignStore(projectId, null); let timer = 0; // ponytail: 저장 끝 신호가 없어 0.3 s 마다 봄 — 저장 끝 알림 창구가 생기면 그리로 const recheck = (): void => { window.clearTimeout(timer); if (!host.isConnected) return stops.forEach((stop) => stop()); if (savingDesign(projectId)) timer = window.setTimeout(recheck, 300); else void mountMissingStationNotice(host, projectId, onCreated); }; const stops = (["pipes", "structures"] as const).map((key) => store.subscribe(key, (value) => value === null && recheck()), ); }