Files
Aislo/B08_Quantity/B08_Quantity_Hydro_Table.ts
T
eomsangdonandClaude Opus 5.5 5e25f418cc refactor(b08): 탭 표 자료 읽기 · 표 짜기를 화면 없는 파일로 꺼냄 — 구조물 집계표 · 자재 · 토공 · 수리 입력 · 거리이정표 · 화면은 같은 함수를 씀 · 동작 그대로 (302-3)
- 새 파일 = B08_Quantity_Summary_Table · Material_Tables · Earthwork_Tables · Hydro_Table · Milestone_Table
- 화면 UI 파일 = 불러오기 · 그리기만 · 옛 B08 시험 = 옮긴 글 자리 따라 읽는 파일 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
2026-10-13 08:03:23 +09:00

189 lines
8.1 KiB
TypeScript

/* =============================================================================
* B08_Quantity_Hydro_Table.ts
* 수리집수면적표(PLAN 62-2 · 69-4) 자료 읽기 · 측점 입력 겹치기 · 인쇄 표 — 화면 없음(PLAN 302-3).
* 화면(`B08_Quantity_UI_Hydro`) · 인쇄 미리보기(`B09_Draft_Print_Quantity`)가 같은 입력 · 같은 서버 계산을 씀.
*
* 값 차례 = 기본 < 장 기본 < 연결 < 저장 < 초안(캐시 `b08-hydro`) · 연결값과 같은 칸은 null 로 저장(연동 유지)
* 계산 = 서버 단독(POST …/quantity/hydro/calc)
* `hydroTable` = 계산서 한 장(화면 두 카드 칸 차례 그대로)을 구분 · 항목 · 값 표로 — 인쇄 전용
* ========================================================================== */
import type { DataTableOptions } from "@ui/ui_template_data_table";
import { readState } from "../A00_Common/b_page_state";
import { request } from "./B08_Quantity_Material_Tables";
import { stationInterval } from "./B08_Quantity_Summary_Table";
import type {
HydroInputs,
HydroLink,
HydroOptions,
HydroResult,
} from "./B08_Quantity_UI_Hydro_Sheet";
export 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"];
}
export type Drafts = Record<string, Partial<HydroInputs>>;
export type HydroPayload = { stations: HydroStation[]; saved: Drafts } & HydroOptions;
export const keyOf = (station: HydroStation): string => station.chainage_m.toFixed(3);
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))),
);
/** 측점 입력 — 기본 < 장 기본 < 연결 < 저장 < 초안 */
export const hydroInputs = (
sheet: HydroPayload,
station: HydroStation,
drafts: Drafts,
): 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 값이 바뀌면 따라옴) · 다르면 손 값 */
export const unlinkedInputs = (station: HydroStation, inputs: HydroInputs): HydroInputs =>
Object.fromEntries(
Object.entries(inputs).map(([k, v]) => [k, linkedOf(station)[k] === v ? null : v]),
) as unknown as HydroInputs;
export async function loadHydro(
projectId: string,
): Promise<{ sheet: HydroPayload; interval: number; drafts: Drafts }> {
const [sheet, interval] = await Promise.all([
request<HydroPayload>(`/projects/${projectId}/quantity/hydro`),
stationInterval(projectId).catch(() => 20),
]);
return { sheet, interval, drafts: readState<Drafts>("b08-hydro", projectId) ?? {} };
}
export const hydroCalc = (projectId: string, inputs: HydroInputs): Promise<HydroResult> =>
request<HydroResult>(`/projects/${projectId}/quantity/hydro/calc`, {
method: "POST",
body: JSON.stringify(inputs),
});
const DASH = "-";
const fmt = (value: number | null | undefined, digits: number, unit = ""): string =>
value == null || !Number.isFinite(value) ? DASH : `${value.toFixed(digits)}${unit}`;
const raw = (value: number | string | null | undefined, unit = ""): string =>
value == null || value === "" ? DASH : `${value}${unit}`;
/** 계산서 한 장 → 표(구분 · 항목 · 값) — 칸 이름 · 자리수 = 화면 계산서(`…_UI_Hydro_Sheet`) */
export function hydroTable(
stationText: string,
kind: string,
inputs: HydroInputs,
r: HydroResult | null,
options: HydroOptions,
): DataTableOptions {
const site = options.site;
const flood = "1. 홍수량 계산";
const sheetDefaults = inputs.sheet === "pipe" ? {} : options.sheet_defaults[inputs.sheet];
const n =
inputs.n ?? (inputs.sheet === "pipe" ? options.pipe_kinds[inputs.pipe_kind] : sheetDefaults.n);
const slope = [
"하상경사 S = 1 /",
`${raw(inputs.slope_denominator)} (${fmt(r?.slope_bed == null ? null : r.slope_bed * 100, 2, " %")})`,
];
const section: Array<[string, string]> =
inputs.sheet === "box"
? [
["내공 폭 B (m)", raw(inputs.box_width_m)],
["내공 높이 H (m)", raw(inputs.box_height_m)],
["수심비 h / H", raw(inputs.depth_ratio)],
["수심 h (m)", fmt(r?.depth, 2)],
["단면적", fmt(r?.section_area, 3, " ㎡")],
["윤변", fmt(r?.perimeter, 3, " m")],
["경심", fmt(r?.radius, 3)],
slope as [string, string],
["조도계수 n", raw(n)],
]
: inputs.sheet === "ford"
? [
["월류 폭 B (m)", raw(inputs.overflow_width_m)],
slope as [string, string],
["조도계수 n", raw(n)],
["월류 높이 h (m)", fmt(r?.depth, 2)],
["단면적", fmt(r?.section_area, 3, " ㎡")],
["윤변", fmt(r?.perimeter, 3, " m")],
["경심", fmt(r?.radius, 3)],
]
: [
["관 종류", inputs.pipe_kind],
["단면 가정", options.sections[inputs.section] ?? inputs.section],
["단면 D (mm)", raw(inputs.diameter_mm)],
["단면적", fmt(r?.section_area, 3, " ㎡")],
["윤변", fmt(r?.perimeter, 3, " m")],
["경심", fmt(r?.radius, 3)],
slope as [string, string],
["조도계수 n", raw(n)],
];
const judged = r != null && r.ok != null;
const check =
inputs.sheet === "box"
? `2. 단면 검토 — ${kind}`
: inputs.sheet === "ford"
? `2. 월류 높이 — ${kind}`
: "2. 단면 검토";
const rows: Array<[string, string, string]> = [
[flood, `Qd : 설계 홍수량 (㎥/s) = Q × ${inputs.factor.toFixed(1)}`, fmt(r?.design_flow, 2)],
[flood, "Q : 홍수량 (㎥/s)", fmt(r?.flow, 2)],
[flood, "f : 유출계수", raw(inputs.runoff)],
[flood, "rt : 평균 강우강도 (mm/hr)", fmt(r?.intensity, 2)],
[flood, "A : 유역면적 (㎢)", `${raw(inputs.area_km2)} (${fmt(r?.area_ha, 1, " ha")})`],
[flood, "T : 홍수 도달시간 (hr)", fmt(r?.tc_hr, 3)],
[flood, "L : 산정에서 하천시점까지 거리 (m)", raw(inputs.length_m)],
[flood, "H : 산정에서 하천시점까지 표고차 (m)", raw(inputs.height_m)],
[flood, "s : 지표면 경사 (H / L)", fmt(r?.slope_ground, 3)],
[flood, "최소 도달시간 (분)", raw(inputs.tc_min_minutes)],
[flood, "지역", site.region.name || raw(inputs.region)],
[flood, "재현기간", `${inputs.return_period} 년`],
[
flood,
"R24 : 확률강우량 (mm/day)",
fmt(r?.r24_mm ?? site.r24[String(inputs.return_period)], 1),
],
[check, "총 홍수량 Qd (㎥/s)", fmt(r?.design_flow, 2)],
...section.map(([label, value]) => [check, label, value] as [string, string, string]),
[check, "유속 (m/s)", fmt(r?.velocity, 3)],
[check, "유출량 (㎥/s)", fmt(r?.capacity, 3)],
[
check,
"판정",
judged
? `${fmt(r.capacity, 3)} ㎥/s ${r.ok ? "≥" : "<"} Qd ${fmt(r.design_flow, 2)} ㎥/s — ${r.ok ? "O K" : "N G"}`
: DASH,
],
[check, "유출율 (%)", fmt(r?.ratio_pct, 0)],
];
return {
columns: [
{ key: "group", label: "구분", width: 120 },
{ key: "item", label: "항목", width: 260 },
{ key: "value", label: "값", width: 240 },
],
rows: rows.map(([group, item, value]) => ({ group, item, value })),
mergeSame: ["group"],
caption: `홍수량 계산서 ${stationText}${kind ? ` ${kind}` : ""}`,
};
}