- 구조물 › 배관 1본 길이 · 토공 › 연암 · 경암 % 입력(공용 compact 입력 · 접이 카드) - 조작 = 캐시 b08-options · [저장] = 프로젝트 층 M02 구조물집계표 변수 · project_settings rock_ratios_pct - 브라우저 짝 식 파일 B08_Quantity_Options_Calc.ts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
210 lines
8.1 KiB
TypeScript
210 lines
8.1 KiB
TypeScript
/* =============================================================================
|
|
* B08_Quantity_UI_Hydro.ts
|
|
* 4탭 수리집수면적표 — 배수지점 측점마다 보조 탭 · 한 장 = 홍수량 계산서(PLAN 62-2 · 69-4~6).
|
|
*
|
|
* 측점 목록은 저장된 관 지점(pipe_points) · B05 연결값(A · L · H · 관경 · 내공 · 월류 폭 · 96)이 빈칸을 채움
|
|
* 값 차례 = 기본 < 장 기본 < 연결 < 저장 < 초안 · 연결값과 같은 칸은 null 로 저장(연동 유지)
|
|
* 박스암거 = 사각 단면 장(73-11) · 세월교 · 물넘이포장 = 월류 높이 장(73-12) · 그 밖 = 관 장
|
|
* 처음 값 = 표준 값 · 측점 고유 값은 빈칸 · 지역 · 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,
|
|
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 HydroLink,
|
|
type HydroOptions,
|
|
type HydroResult,
|
|
} from "./B08_Quantity_UI_Hydro_Sheet";
|
|
|
|
interface HydroStation {
|
|
chainage_m: number;
|
|
facility: string;
|
|
/** 박스암거 · 세월교 · 물넘이포장(관이면 "") */
|
|
kind: string;
|
|
/** 계산서 장 — 시설이 정함(서버 station_rows) */
|
|
sheet: HydroInputs["sheet"];
|
|
/** B05 연결값(값이 있는 칸만) · 비교용 B05 Qd · T — 서버 station_rows */
|
|
linked: HydroLink["values"] | null;
|
|
b05: HydroLink["b05"];
|
|
}
|
|
|
|
type Drafts = Record<string, Partial<HydroInputs>>;
|
|
type HydroPayload = { stations: HydroStation[]; saved: Drafts } & HydroOptions;
|
|
|
|
export 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);
|
|
|
|
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 linkedOf = (station: HydroStation): Record<string, unknown> => station.linked ?? {};
|
|
/** 저장본 · 초안 한 겹 — 연결값이 있는 칸의 null 은 건너뜀(연결값을 덮지 않게) */
|
|
const layer = (station: HydroStation, values: Partial<HydroInputs> = {}) =>
|
|
Object.fromEntries(
|
|
Object.entries(values).filter(([k, v]) => v != null || !(k in linkedOf(station))),
|
|
);
|
|
const inputsOf = (station: HydroStation): HydroInputs =>
|
|
({
|
|
...sheet.defaults,
|
|
...(station.sheet === "pipe" ? {} : sheet.sheet_defaults[station.sheet]),
|
|
...station.linked,
|
|
// 초안은 측점 입력 통째 — 있으면 저장본 대신(초안의 null = 연결값으로 되돌림)
|
|
...layer(station, drafts[keyOf(station)] ?? sheet.saved[keyOf(station)]),
|
|
sheet: station.sheet,
|
|
}) as HydroInputs;
|
|
/** 연결값과 같은 칸은 null — 저장 · 초안이 연동을 유지(B05 값이 바뀌면 따라옴) · 다르면 손 값 */
|
|
const unlinked = (station: HydroStation, inputs: HydroInputs): HydroInputs =>
|
|
Object.fromEntries(
|
|
Object.entries(inputs).map(([k, v]) => [k, linkedOf(station)[k] === v ? null : v]),
|
|
) as unknown 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.kind ? `${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;
|
|
}),
|
|
);
|
|
const draw = (inputs: HydroInputs): void => {
|
|
const view = buildHydroSheet(
|
|
stationLabel(station.chainage_m, interval),
|
|
inputs,
|
|
sheet,
|
|
(next) => {
|
|
drafts[keyOf(station)] = unlinked(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);
|
|
},
|
|
station.kind,
|
|
{ values: station.linked ?? {}, b05: station.b05 },
|
|
);
|
|
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.map((s) => [keyOf(s), unlinked(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);
|
|
}
|