/* ============================================================================= * B05_Profile_UI_Drainage_Propose.ts * 배수유역 칸 [자동 제안](510) — 서버가 후보를 셈(`post_pipe_proposals` · 세류선 교차 + 간격 보충 · * 있는 관 · 지운 관 상태) · 화면은 목록 + 배수유역도 점선 원 · 고른 것만 관 넣기 길(`addPipe`)로. * * - 있는 관(라벨 짝 · 교차 5 m 안 관) = 목록에서 뺌(머리 글에 수만) · 지운 관 = 「지운 관」 표시 · * 고르면 다시 넣음(서버가 지움 표지 걷음). * - 아무것도 미리 안 고름 · [넣기] = 고른 것 · 세류선 교차 = 라벨 실음(노선 바뀐 [저장] 따라감 · 507-2). * - 노선 · 관이 바뀌면(배수유역 낡음) 목록 닫음 — 옛 노선 후보가 새 노선에 안 들어가게. * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; export interface PipeProposal { chainage_m: number; kind: "stream" | "spacing"; state: "new" | "held" | "dismissed"; stream_label?: DetailPipeInput["stream_label"]; stream_name?: string | null; } interface ProposalResponse { proposals: PipeProposal[]; /** 간격 보충까지 셈(분석이 지금 노선) — 거짓 = 세류선 교차만. */ spacing: boolean; message?: string; } /** 후보 한 줄 글. */ export function proposalText(item: PipeProposal): string { const at = `측점 ${Math.round(item.chainage_m)} m`; const what = item.kind === "stream" ? `세류선 교차${item.stream_name ? `(${item.stream_name})` : ""}` : "간격 보충(흐름이 모이는 자리)"; return `${at} — ${what}${item.state === "dismissed" ? " · 지운 관" : ""}`; } export interface DrainagePropose { root: HTMLElement; /** 서버에 후보를 물어 목록을 엶. */ open(): Promise; close(): void; /** 목록에 오른 후보 누가거리 — 배수유역도 점선 원. */ candidates(): number[]; } export function createDrainagePropose(deps: { projectId: () => string | null; pipeRequest: () => DetailPipeInput[]; /** 고른 후보를 관 넣기 길로(패널 `addPipe` 와 같은 길). */ addPipes: (picks: PipeProposal[]) => void; redraw: () => void; }): DrainagePropose { const root = document.createElement("div"); root.className = "b05-drainage__suggest b05-drainage__proposals"; root.hidden = true; const head = document.createElement("div"); head.className = "b05-drainage__suggest-head"; const title = document.createElement("span"); const add = document.createElement("button"); add.type = "button"; add.className = "b05-drainage__analyze"; add.textContent = "넣기"; add.title = "고른 후보만 관으로 넣습니다 — 초안이라 [저장] 때 남습니다."; const shut = document.createElement("button"); shut.type = "button"; shut.className = "b05-drainage__analyze"; shut.textContent = "닫기"; head.append(title, add, shut); const list = document.createElement("ul"); list.className = "b05-drainage__suggest-list b05-drainage__proposals-list"; root.append(head, list); let shown: PipeProposal[] = []; const picked = new Set(); let sequence = 0; const close = (): void => { sequence += 1; root.hidden = true; shown = []; picked.clear(); list.replaceChildren(); deps.redraw(); }; const render = (response: ProposalResponse): void => { shown = response.proposals.filter((item) => item.state !== "held"); const held = response.proposals.length - shown.length; title.textContent = `자동 제안 ${shown.length}건` + (held ? ` · 있는 관 ${held} 뺌` : "") + (response.spacing ? "" : " · 배수유역 낡음 — 세류선 교차만"); list.replaceChildren( ...shown.map((item, index) => { const row = document.createElement("li"); const label = document.createElement("label"); const box = document.createElement("input"); box.type = "checkbox"; box.addEventListener("change", () => { if (box.checked) picked.add(index); else picked.delete(index); add.disabled = picked.size === 0; }); label.append(box, ` ${proposalText(item)}`); row.append(label); return row; }), ); add.disabled = true; root.hidden = false; deps.redraw(); }; add.addEventListener("click", () => { const picks = shown.filter((_, index) => picked.has(index)); close(); if (picks.length) deps.addPipes(picks); }); shut.addEventListener("click", close); return { root, async open() { const projectId = deps.projectId(); if (!projectId) return; const sent = ++sequence; title.textContent = "자동 제안 — 후보 셈 중"; root.hidden = false; list.replaceChildren(); add.disabled = true; try { const response = await fetch( `${API_BASE_URL}/projects/${projectId}/drainage/pipe-proposals`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points: deps.pipeRequest() }), }, ); const payload = (await response.json()) as ProposalResponse; if (sent !== sequence) return; if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`); render(payload); } catch (error) { if (sent !== sequence) return; title.textContent = `자동 제안 실패 — ${error instanceof Error ? error.message : error}`; } }, close, candidates: () => (root.hidden ? [] : shown.map((item) => item.chainage_m)), }; }