Files
Aislo/B05_Profile/B05_Profile_UI_Drainage_Propose.ts
T

150 lines
5.5 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_Drainage_Propose.ts
* 배수유역 칸 [자동 제안](510) — 서버가 후보를 셈(`post_pipe_proposals` · 세류선 교차 + 간격 보충 ·
* 있는 관 · 지운 관 상태) · 화면은 목록만(점선 원 걷음 · 326-6) · 고른 것만 관 넣기 길(`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<void>;
close(): void;
}
export function createDrainagePropose(deps: {
projectId: () => string | null;
pipeRequest: () => DetailPipeInput[];
/** 고른 후보를 관 넣기 길로(패널 `addPipe` 와 같은 길). */
addPipes: (picks: PipeProposal[]) => 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<number>();
let sequence = 0;
const close = (): void => {
sequence += 1;
root.hidden = true;
shown = [];
picked.clear();
list.replaceChildren();
};
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;
};
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,
};
}