Files
Aislo/B08_Quantity/B08_Quantity_UI_Hydro.ts
T
eomsangdonandClaude Opus 5.5 a1f4949672 feat(B08): 수리집수면적표 박스암거 · 세월교 지점 = 빈 장(계산 방법 정하는 중 · PLAN 73-1)
- 관 지점 시설 속성(box_culvert · ford_bridge)으로 보조 탭 줄에 「측점 종류」 표시 · 그 탭 = 공용 카드 빈 장(측점 · 종류 · 안내)
- 관 지점 계산 장은 그대로 · 물넘이포장 · 기슭막이는 지금처럼 관 계산 장
- [저장]에서 빈 장 지점은 빠짐

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
2026-10-07 07:54:19 +09:00

228 lines
8.2 KiB
TypeScript

/* =============================================================================
* B08_Quantity_UI_Hydro.ts
* 4탭 수리집수면적표 — 배수지점 측점마다 보조 탭 · 한 장 = 홍수량 계산서(PLAN 62-2 · 69-4~6).
*
* 측점 목록만 저장된 관 지점(pipe_points)에서 · 입력은 이 탭 자기 값(B05/B06 값과 안 이음)
* 박스암거 · 세월교 지점 = 빈 장(계산 방법 정하는 중 · 73-1) · [저장]에서 빠짐
* 처음 값 = 표준 값 · 측점 고유 값은 빈칸 · 지역 · R24 = 공사지 자료(서버)
* 조작 = 캐시(sessionStorage) · [저장] = B08 자기 파일(PUT …/quantity/hydro) · 뒤 캐시 비움
* 계산 = 서버 단독(POST …/quantity/hydro/calc · 최신 응답만 채택)
* ========================================================================== */
import "./B08_Quantity_UI_Hydro.css";
import { API_BASE_URL } from "@config/config_frontend";
import {
createButton,
createCard,
hideLoadingOverlay,
showLoadingOverlay,
showToast,
} from "@ui/ui_template_elements";
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
import { stationLabel } from "../common_util/common_util_station";
import { stationInterval } from "./B08_Quantity_UI_Summary";
import {
buildHydroSheet,
type HydroInputs,
type HydroOptions,
type HydroResult,
} from "./B08_Quantity_UI_Hydro_Sheet";
interface HydroStation {
chainage_m: number;
facility: string;
/** 박스암거 · 세월교 등 계산 방법 정하는 중인 시설 이름(관이면 "") */
kind: string;
/** true = 관 계산 대신 빈 장(73-1) */
pending: boolean;
}
type Drafts = Record<string, Partial<HydroInputs>>;
type HydroPayload = { stations: HydroStation[]; saved: Drafts } & HydroOptions;
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(`${API_BASE_URL}${path}`, {
credentials: "include",
headers: { "Content-Type": "application/json" },
...init,
});
const body = (await response.json().catch(() => null)) as { detail?: unknown } | null;
if (!response.ok) {
const detail = body?.detail;
throw new Error(typeof detail === "string" ? detail : `HTTP ${response.status}`);
}
return body as T;
}
function note(text: string): HTMLElement {
const p = document.createElement("p");
p.className = "ui-general-block__empty";
p.textContent = text;
return p;
}
const keyOf = (station: HydroStation): string => station.chainage_m.toFixed(3);
/** 박스암거 · 세월교 지점 빈 장(73-1) — 계산 방법이 정해지면 이 자리에 계산서가 들어감. */
function pendingSheet(stationText: string, kind: string): HTMLElement {
const row = (label: string, value: string): HTMLElement => {
const line = document.createElement("div");
line.className = "b08-hydro__row";
const name = document.createElement("span");
name.className = "b08-hydro__label";
name.textContent = label;
const text = document.createElement("span");
text.className = "b08-hydro__value";
text.textContent = value;
line.append(name, text);
return line;
};
const card = createCard({
title: `홍수량 계산 — ${kind}`,
body: [
row("측점", stationText),
row("종류", kind),
note("계산 방법 정하는 중 — 관과 다른 계산(사각 단면 · 월류)을 정한 뒤 이 장에 넣음."),
],
});
card.classList.add("b08-hydro__card");
const sheet = document.createElement("div");
sheet.className = "b08-hydro__sheet b08-hydro__sheet--pending";
sheet.dataset.b08HydroPending = kind;
const body = document.createElement("div");
body.className = "b08-hydro__body"; // 관 장과 같은 격자 — 카드 폭이 같음
body.append(card);
sheet.append(body);
return sheet;
}
export async function renderHydro(host: HTMLElement, projectId: string): Promise<void> {
host.replaceChildren(note("홍수량 계산서를 불러오는 중…"));
let sheet: HydroPayload;
let interval = 20;
try {
[sheet, interval] = await Promise.all([
request<HydroPayload>(`/projects/${projectId}/quantity/hydro`),
stationInterval(projectId).catch(() => 20),
]);
} catch (error) {
host.replaceChildren(note(`홍수량 계산서를 불러오지 못했습니다 — ${String(error)}`));
return;
}
if (!sheet.stations.length) {
host.replaceChildren(
note("저장된 배수지점(관 지점)이 없습니다 — 배수유역 · 종단에서 관을 저장하세요."),
);
return;
}
let drafts: Drafts = readState<Drafts>("b08-hydro", projectId) ?? {};
const inputsOf = (station: HydroStation): HydroInputs =>
({
...sheet.defaults,
...sheet.saved[keyOf(station)],
...drafts[keyOf(station)],
}) as HydroInputs;
const tabs = document.createElement("div");
tabs.className = "b08-quantity__tabs b08-hydro__tabs";
tabs.setAttribute("role", "tablist");
const save = createButton({
label: "저장",
variant: "filled",
onClick: () => void saveAll(),
});
save.dataset.b08HydroSave = "";
const toolbar = document.createElement("div");
toolbar.className = "b08-hydro__toolbar";
toolbar.append(tabs, save);
const page = document.createElement("div");
host.replaceChildren(toolbar, page);
let seq = 0;
let openIndex = 0;
const open = (index: number): void => {
openIndex = index;
const station = sheet.stations[index];
tabs.replaceChildren(
...sheet.stations.map((s, i) => {
const label = stationLabel(s.chainage_m, interval);
const button = createButton({
label: s.pending ? `${label} ${s.kind}` : label,
variant: i === index ? "filled" : "ghost",
size: "sm",
onClick: () => i !== index && open(i),
});
button.setAttribute("role", "tab");
button.setAttribute("aria-selected", String(i === index));
button.dataset.b08HydroStation = String(s.chainage_m);
button.title = s.kind || s.facility;
return button;
}),
);
if (station.pending) {
seq++; // 앞 장의 늦은 계산 응답을 버림
page.replaceChildren(pendingSheet(stationLabel(station.chainage_m, interval), station.kind));
return;
}
const draw = (inputs: HydroInputs): void => {
const view = buildHydroSheet(
stationLabel(station.chainage_m, interval),
inputs,
sheet,
(next) => {
drafts[keyOf(station)] = next;
writeState("b08-hydro", drafts, projectId);
// 단면 가정 · 관 종류는 그림 · 식이 바뀜 — 장을 다시 그림 · 그 밖은 값만
if (next.section !== inputs.section || next.pipe_kind !== inputs.pipe_kind) draw(next);
else void calc(next, view.show);
},
);
page.replaceChildren(view.root);
void calc(inputs, view.show);
};
draw(inputsOf(station));
};
const calc = async (
inputs: HydroInputs,
show: (r: HydroResult | null, message?: string) => void,
): Promise<void> => {
const mine = ++seq;
try {
const result = await request<HydroResult>(`/projects/${projectId}/quantity/hydro/calc`, {
method: "POST",
body: JSON.stringify(inputs),
});
if (mine === seq) show(result);
} catch (error) {
if (mine === seq) show(null, `계산하지 못했습니다 — ${String(error)}`);
}
};
/** [저장] — 측점 전부의 지금 입력을 B08 자기 파일로 · 성공하면 캐시를 비우고 저장본으로 다시 엶 */
const saveAll = async (): Promise<void> => {
const stations = Object.fromEntries(
sheet.stations.filter((s) => !s.pending).map((s) => [keyOf(s), inputsOf(s)]),
);
showLoadingOverlay();
try {
const body = await request<{ saved: Drafts }>(`/projects/${projectId}/quantity/hydro`, {
method: "PUT",
body: JSON.stringify({ stations }),
});
sheet.saved = body.saved;
drafts = {};
clearState("b08-hydro", projectId);
showToast("수리집수면적표를 저장했습니다.", "success");
open(openIndex);
} catch (error) {
showToast(`저장하지 못했습니다 — ${String(error)}`, "error");
} finally {
hideLoadingOverlay();
}
};
open(0);
}