150 lines
5.5 KiB
TypeScript
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,
|
|
};
|
|
}
|