Files
Aislo/B05_Profile/B05_Profile_UI_Drainage_Propose.ts
T
eomsangdonandClaude Opus 5.5 1abac6377f feat(510-1 자동 관 제안 = B05 배수유역 후보 고름): [자동 제안] = 서버 후보(지금 노선 × 세류선 교차 5 m 솎기 · 정수 m · 라벨 + 분석이 지금 노선이면 간격 보충) · 상태 있음(라벨 짝 · 교차 5 m 안 관) · 지운 관(지움 표지 짝) · 새 · 배수유역 칸 목록 + 지도 점선 원 · 고른 것만 관 넣기 길(addPipe · 세류선 교차 = 라벨 실음) · 옛 두 방식 창(전체 자동 · 지정 포함 추가) 걷음
- 서버 POST drainage/pipe-proposals(아무것도 안 씀) · propose_pipes(common_util_stream_crossing) · claim_crossings = {교차점: 라벨} 짝
- 라벨 왕복 — 계산 carry 가 화면 라벨 실음 · 관 속성 보관소가 응답 · 초안 · 요청에 라벨 실어 나름 · 옵션 고침에도 남김
- 지운 관을 다시 넣으면 지움 표지 걷음(keep_labels)
- 계산 등록부 D2 runner = propose_pipes · 시험 test_510 · 349 낡은 두 방식 기대 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 12:36:48 +09:00

156 lines
5.8 KiB
TypeScript

/* =============================================================================
* 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<void>;
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<number>();
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)),
};
}