- 집계표 — 관경 · 련 빈칸 = 「미입력」 열(or 1000 · or 1 지움) · 관경 빈칸 흙막이 빈 높이 미입력 · 1본 = 프로젝트 층 변수만(시스템 층 폴백 지움) - 토공집계표 — 성토면다짐 · 초류종자살포 · 지장목제거 = 빈칸 + 「비율 미정」 · 구조물 쪽 늘 빈 줄 8 뺌(나머지 7 유지) - 수리 박스 — 수심비 처음 값 빈칸 · 빈칸이면 수심 · 유출량 멈춤 · 칸 옆 알림 - 좌측 패널 1본 읽기 전 빈칸 · 시험 test_69 · 98 · 122 · 73_11 고침 + 새 test_143_2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
563 lines
23 KiB
TypeScript
563 lines
23 KiB
TypeScript
/* =============================================================================
|
||
* B08_Quantity_UI_Hydro_Sheet.ts
|
||
* 홍수량 계산서 한 장(PLAN 62-2 · 69-4) — 우리 양식(공용 카드 · 입력 · 드롭다운 · 테마 색).
|
||
*
|
||
* 왼쪽 카드 「1. 홍수량 계산」 1-1 합리식 · 1-2 도달시간(Kirpich) · 1-3 강우강도
|
||
* 오른쪽 카드 「2. 단면 검토」 관 단면 그림 · 단면적 · 윤변 · 경심 · 경사 · n · 유속 · 유출량 · 판정
|
||
* 장(sheet) 갈래 — 박스 = 사각 내공 B · H · 수심비(73-11) · 세월교 · 물넘이 = 월류 폭 → 월류 높이(73-12)
|
||
* 테두리는 입력 · 선택 칸만(공용 ui-input · ui-select) · 계산 칸은 글자만
|
||
* 굵게는 중요 결과(Qd · 유출량 · 판정 · 유출율)만 · 글자색은 판정 따라 O K 초록 · N G 붉음(132-1)
|
||
* · 빈칸에 기대는 계산 칸은 「-」.
|
||
* 연결값과 같은 칸은 이름 옆 작은 원천 표 — 「B05」(풍선 「B05 분석」) · 하상경사는 「B06」
|
||
* (풍선 「B06 횡단 경사」 · 110) · Qd · T 옆에 B05 값 참고(96-2).
|
||
*
|
||
* 입력 칸은 한 번 만들고 계산 결과만 제자리에 바꿈 — 고치는 중 칸 포커스를 잃지 않게.
|
||
* ========================================================================== */
|
||
|
||
import { createCard, createInputField, createSelectField } from "@ui/ui_template_elements";
|
||
|
||
export interface HydroInputs {
|
||
factor: number;
|
||
runoff: number;
|
||
area_km2: number | null;
|
||
length_m: number | null;
|
||
height_m: number | null;
|
||
tc_min_minutes: number;
|
||
r24_mm: number | null;
|
||
region: string;
|
||
return_period: number;
|
||
pipe_kind: string;
|
||
n: number | null;
|
||
diameter_mm: number | null;
|
||
slope_denominator: number | null;
|
||
section: "full" | "arc240" | "eff70";
|
||
/** 장 — 측점 시설이 정함(박스 · 세월교 · 물넘이 · 관) */
|
||
sheet: "pipe" | "box" | "ford";
|
||
box_width_m?: number | null;
|
||
box_height_m?: number | null;
|
||
depth_ratio?: number | null;
|
||
overflow_width_m?: number | null;
|
||
}
|
||
|
||
export interface HydroResult {
|
||
area_ha: number | null;
|
||
r24_mm: number | null;
|
||
slope_ground: number | null;
|
||
tc_raw_hr: number | null;
|
||
tc_hr: number | null;
|
||
tc_floored: boolean;
|
||
intensity: number | null;
|
||
flow: number | null;
|
||
design_flow: number | null;
|
||
n: number;
|
||
/** 박스 수심 h · 세월교 · 물넘이 월류 높이 */
|
||
depth: number | null;
|
||
section_area: number | null;
|
||
perimeter: number | null;
|
||
radius: number | null;
|
||
slope_bed: number | null;
|
||
velocity: number | null;
|
||
capacity: number | null;
|
||
ok: boolean | null;
|
||
ratio_pct: number | null;
|
||
}
|
||
|
||
/** 공사지 — 지역(전처리 행정구역 · 프로젝트 등록) · 관측소 · 재현기간별 24시간 확률강우량 */
|
||
export interface HydroSite {
|
||
region: { name: string; full: string; source: string };
|
||
station: string;
|
||
periods: number[];
|
||
r24: Record<string, number>;
|
||
}
|
||
|
||
export interface HydroOptions {
|
||
defaults: HydroInputs;
|
||
factors: number[];
|
||
sections: Record<HydroInputs["section"], string>;
|
||
pipe_kinds: Record<string, number>;
|
||
/** 장별 처음 값(박스 n · 수심비 · 세월교 · 물넘이 n · 월류 폭 · 경사) */
|
||
sheet_defaults: Record<"box" | "ford", Partial<HydroInputs>>;
|
||
site: HydroSite;
|
||
}
|
||
|
||
/** 연결(96 · 110) — 칸 값(B05 배수유역 · 하상경사는 B06 횡단) · 비교용 설계유량 · 도달시간(분) */
|
||
export interface HydroLink {
|
||
values: Partial<Record<keyof HydroInputs, number>>;
|
||
b05: { design_flow_m3s: number | null; tc_minutes: number | null } | null;
|
||
}
|
||
|
||
export interface HydroSheet {
|
||
root: HTMLElement;
|
||
/** 계산값 칸을 바꿈 — null 이면 비움 + 까닭 한 줄 */
|
||
show(result: HydroResult | null, message?: string): void;
|
||
}
|
||
|
||
type Key = keyof HydroInputs;
|
||
|
||
/** 비워 둘 수 있는 칸 — 측점 고유 값 · n(관 종류 값) · R24(공사지 자료가 없을 때 입력) */
|
||
const NULLABLE = new Set<Key>([
|
||
"area_km2",
|
||
"length_m",
|
||
"height_m",
|
||
"diameter_mm",
|
||
"slope_denominator",
|
||
"n",
|
||
"r24_mm",
|
||
"box_width_m",
|
||
"box_height_m",
|
||
"overflow_width_m",
|
||
]);
|
||
|
||
const DASH = "-";
|
||
const fmt = (value: number | null | undefined, digits: number, unit = ""): string =>
|
||
value == null || !Number.isFinite(value) ? DASH : `${value.toFixed(digits)}${unit}`;
|
||
|
||
/** 입력 칸이 놓인 줄의 이름 칸 */
|
||
function sheetLabel(left: HTMLElement, right: HTMLElement, key: string): HTMLElement | null {
|
||
const input = [left, right]
|
||
.map((card) => card.querySelector(`input[data-b08-hydro="${key}"]`))
|
||
.find(Boolean);
|
||
return input?.closest(".b08-hydro__row")?.querySelector(".b08-hydro__label") ?? null;
|
||
}
|
||
|
||
function node<K extends keyof HTMLElementTagNameMap>(
|
||
tag: K,
|
||
className = "",
|
||
text = "",
|
||
): HTMLElementTagNameMap[K] {
|
||
const el = document.createElement(tag);
|
||
if (className) el.className = className;
|
||
if (text) el.textContent = text;
|
||
return el;
|
||
}
|
||
|
||
/** 식 글 — `^…^` 사이는 위첨자 · `_…_` 사이는 아래첨자. */
|
||
function formula(text: string, className = "b08-hydro__formula"): HTMLElement {
|
||
const box = node("div", className);
|
||
for (const part of text.split(/(\^[^^]+\^|_[^_]+_)/)) {
|
||
if (part.startsWith("^") && part.endsWith("^")) box.append(node("sup", "", part.slice(1, -1)));
|
||
else if (part.startsWith("_") && part.endsWith("_"))
|
||
box.append(node("sub", "", part.slice(1, -1)));
|
||
else if (part) box.append(document.createTextNode(part));
|
||
}
|
||
return box;
|
||
}
|
||
|
||
/** 관 단면 그림 — 단면 가정대로 물 찬 자리를 칠함(색은 테마 토큰 · CSS). */
|
||
function pipeFigure(section: HydroInputs["section"]): SVGSVGElement {
|
||
const ns = "http://www.w3.org/2000/svg";
|
||
const svg = document.createElementNS(ns, "svg");
|
||
svg.setAttribute("viewBox", "0 0 200 170");
|
||
svg.setAttribute("class", "b08-hydro__pipe");
|
||
const [cx, cy, r] = [75, 85, 70];
|
||
const add = (tag: string, attrs: Record<string, string | number>, text = ""): void => {
|
||
const el = document.createElementNS(ns, tag);
|
||
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v));
|
||
if (text) el.textContent = text;
|
||
svg.append(el);
|
||
};
|
||
// 물 찬 자리 — 만관 = 원 전체 · 240° = 위 120° 활꼴 뺌(수심 0.75D) · 70 % = 수심 0.626D
|
||
const level = section === "full" ? -r : section === "arc240" ? -0.5 * r : -0.252 * r;
|
||
if (section === "full") add("circle", { cx, cy, r, class: "b08-hydro__water" });
|
||
else {
|
||
const half = Math.sqrt(r * r - level * level);
|
||
const [x1, x2, y] = [cx - half, cx + half, cy + level];
|
||
add("path", { d: `M${x1} ${y} A${r} ${r} 0 1 0 ${x2} ${y} Z`, class: "b08-hydro__water" });
|
||
if (section === "arc240") {
|
||
add("path", { d: `M${x1} ${y} L${cx} ${cy} L${x2} ${y}`, class: "b08-hydro__line" });
|
||
add("text", { x: cx, y: y + 18, "text-anchor": "middle" }, "120°");
|
||
}
|
||
}
|
||
add("circle", { cx, cy, r, class: "b08-hydro__wall" });
|
||
add("circle", { cx, cy, r: r + 5, class: "b08-hydro__wall" });
|
||
// 지름 치수선
|
||
const right = cx + r + 14;
|
||
add("path", { d: `M${right} ${cy - r} L${right} ${cy + r}`, class: "b08-hydro__line" });
|
||
const depth = section === "full" ? "D" : section === "arc240" ? "0.75D" : "0.63D";
|
||
add("text", { x: right + 4, y: cy + 4 }, depth);
|
||
return svg;
|
||
}
|
||
|
||
/** 사각 단면 그림 — 박스(닫힌 내공 · 수심 h) · 월류(열린 노면 · 월류 높이 h). */
|
||
function rectFigure(closed: boolean): SVGSVGElement {
|
||
const ns = "http://www.w3.org/2000/svg";
|
||
const svg = document.createElementNS(ns, "svg");
|
||
svg.setAttribute("viewBox", "0 0 200 170");
|
||
svg.setAttribute("class", "b08-hydro__pipe");
|
||
const add = (tag: string, attrs: Record<string, string | number>, text = ""): void => {
|
||
const el = document.createElementNS(ns, tag);
|
||
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v));
|
||
if (text) el.textContent = text;
|
||
svg.append(el);
|
||
};
|
||
const [x, w, bottom] = [20, 150, 140];
|
||
const top = closed ? 40 : 110; // 박스 = 내공 높이 H · 월류 = 얕은 물
|
||
const water = closed ? bottom - 0.8 * (bottom - top) : top;
|
||
add("rect", { x, y: water, width: w, height: bottom - water, class: "b08-hydro__water" });
|
||
add("path", {
|
||
d: closed
|
||
? `M${x} ${top} L${x + w} ${top} L${x + w} ${bottom} L${x} ${bottom} Z`
|
||
: `M${x} ${top - 40} L${x} ${bottom} L${x + w} ${bottom} L${x + w} ${top - 40}`,
|
||
class: "b08-hydro__wall",
|
||
});
|
||
add("text", { x: x + w / 2, y: bottom + 18, "text-anchor": "middle" }, "B");
|
||
const right = x + w + 8;
|
||
add("path", { d: `M${right} ${water} L${right} ${bottom}`, class: "b08-hydro__line" });
|
||
add("text", { x: right + 4, y: (water + bottom) / 2 + 4 }, "h");
|
||
if (closed) {
|
||
add("path", { d: `M${x - 8} ${top} L${x - 8} ${bottom}`, class: "b08-hydro__line" });
|
||
add("text", { x: x - 8, y: top - 6, "text-anchor": "middle" }, "H");
|
||
}
|
||
return svg;
|
||
}
|
||
|
||
const SECTION_TEXT: Record<
|
||
HydroInputs["section"],
|
||
{ area: string; perimeter: string; radius: string }
|
||
> = {
|
||
full: { area: "A = π D^2^ / 4 = 0.785 D^2^", perimeter: "P = π D", radius: "R = A / P = D / 4" },
|
||
arc240: {
|
||
area: "A = π D^2^/4 × 240/360 + (D/2)^2^ × sin60 × cos60 = 0.632 D^2^",
|
||
perimeter: "P = π D × 240/360",
|
||
radius: "R = A / P",
|
||
},
|
||
eff70: {
|
||
area: "A = 0.7 × π D^2^ / 4",
|
||
perimeter: "P = π D",
|
||
radius: "R = D / 4 (만관 경심)",
|
||
},
|
||
};
|
||
|
||
export function buildHydroSheet(
|
||
stationText: string,
|
||
inputs: HydroInputs,
|
||
options: HydroOptions,
|
||
onChange: (next: HydroInputs) => void,
|
||
/** 시설 이름(박스암거 · 세월교 · 물넘이포장) — 관이면 "" */
|
||
kind = "",
|
||
link: HydroLink = { values: {}, b05: null },
|
||
): HydroSheet {
|
||
const site = options.site;
|
||
/** 연결된 칸의 원천 표(「B05」 · 「B06」) — 값이 연결값과 같을 때만 보임 */
|
||
const marks = new Map<Key, HTMLElement>();
|
||
const markLinked = (key: Key): void => {
|
||
const mark = marks.get(key);
|
||
if (mark) mark.hidden = current[key] !== link.values[key];
|
||
};
|
||
const out = new Map<string, HTMLElement>();
|
||
const outCell = (id: string, key = false): HTMLElement => {
|
||
const cell = node("span", `b08-hydro__out${key ? " is-key" : ""}`, DASH);
|
||
out.set(id, cell);
|
||
return cell;
|
||
};
|
||
let current = inputs;
|
||
const commit = (key: Key, value: unknown): void => {
|
||
current = { ...current, [key]: value } as HydroInputs;
|
||
markLinked(key);
|
||
onChange(current);
|
||
};
|
||
const numberInput = (key: Key, placeholder = ""): HTMLInputElement => {
|
||
const { input } = createInputField({ type: "number", compact: true, placeholder });
|
||
input.classList.add("b08-hydro__in");
|
||
input.step = "any";
|
||
input.dataset.b08Hydro = key;
|
||
const value = inputs[key];
|
||
input.value = value == null ? "" : String(value);
|
||
const linked = link.values[key];
|
||
if (linked != null) {
|
||
// 원천 표 — 하상경사는 B06 횡단 경사(관 바닥 · 박스 원지반 두 끝 · 110) · 그 밖은 B05
|
||
const [source, title] =
|
||
key === "slope_denominator" ? ["B06", "B06 횡단 경사"] : ["B05", "B05 분석"];
|
||
input.title = `${title} ${linked}`;
|
||
const mark = node("span", "b08-hydro__hint", source);
|
||
mark.title = title;
|
||
mark.dataset.b08HydroLinked = key;
|
||
marks.set(key, mark);
|
||
}
|
||
input.addEventListener("change", () => {
|
||
// 빈칸은 NULLABLE 만 · 최소 도달시간은 0 까지 · 그 밖은 0 보다 커야 함 — 아니면 되돌림
|
||
const parsed = input.value === "" ? null : Number(input.value);
|
||
const valid =
|
||
parsed === null
|
||
? NULLABLE.has(key)
|
||
: Number.isFinite(parsed) &&
|
||
(key === "tc_min_minutes" ? parsed >= 0 : parsed > 0) &&
|
||
(key !== "depth_ratio" || parsed <= 1);
|
||
if (valid) commit(key, parsed);
|
||
else input.value = current[key] == null ? "" : String(current[key]);
|
||
});
|
||
return input;
|
||
};
|
||
const select = (
|
||
key: Key,
|
||
choices: Array<[string, string]>,
|
||
toValue: (raw: string) => unknown = (raw) => raw,
|
||
): HTMLSelectElement => {
|
||
const { select: el } = createSelectField({
|
||
compact: true,
|
||
options: choices.map(([value, text]) => ({ value, text })),
|
||
value: String(inputs[key]),
|
||
});
|
||
el.classList.add("b08-hydro__select");
|
||
el.dataset.b08Hydro = key;
|
||
el.addEventListener("change", () => commit(key, toValue(el.value)));
|
||
return el;
|
||
};
|
||
/** 표 줄 — 이름 · 값 칸(입력 또는 계산) · 덧칸 */
|
||
const row = (label: string | HTMLElement, value: HTMLElement, extra?: HTMLElement) => {
|
||
const line = node("div", "b08-hydro__row");
|
||
const name = typeof label === "string" ? node("span", "b08-hydro__label", label) : label;
|
||
line.append(name, value);
|
||
if (extra) line.append(extra);
|
||
return line;
|
||
};
|
||
/** 식 줄 — 이름 · 식 · 값 칸 */
|
||
const rowF = (label: string, middle: HTMLElement | null, value: HTMLElement) => {
|
||
const line = node("div", "b08-hydro__row b08-hydro__row--f");
|
||
line.append(node("span", "b08-hydro__label", label), middle ?? node("span"), value);
|
||
return line;
|
||
};
|
||
const sub = (text: string): HTMLElement => node("h4", "b08-hydro__h2", text);
|
||
const hint = (text: string): HTMLElement => node("span", "b08-hydro__hint", text);
|
||
/** B05 분석 값 참고(비교만) — 값이 없으면 칸 없음 */
|
||
const b05Hint = (
|
||
key: "design_flow_m3s" | "tc_minutes",
|
||
text: (value: number) => string,
|
||
): HTMLElement | undefined => {
|
||
const value = link.b05?.[key];
|
||
if (value == null) return undefined;
|
||
const el = hint(text(value));
|
||
el.title = "B05 분석";
|
||
el.dataset.b08HydroB05 = key;
|
||
return el;
|
||
};
|
||
|
||
// ── 왼쪽 1. 홍수량 계산 ──
|
||
const factor = select(
|
||
"factor",
|
||
options.factors.map((f) => [String(f), `× ${f.toFixed(1)}`]),
|
||
Number,
|
||
);
|
||
const r24Site = (period: number): number | undefined => site.r24[String(period)];
|
||
const hasRain = Object.keys(site.r24).length > 0;
|
||
const r24Value: HTMLElement = hasRain ? outCell("r24") : numberInput("r24_mm", "입력");
|
||
const periodSelect = select(
|
||
"return_period",
|
||
site.periods.map((p) => [String(p), `${p} 년`]),
|
||
Number,
|
||
);
|
||
periodSelect.addEventListener("change", () => {
|
||
// 공사지 자료 값은 바로 보임 — 계산은 서버가 같은 표로 다시 함
|
||
if (hasRain) out.get("r24")!.textContent = fmt(r24Site(current.return_period), 1);
|
||
});
|
||
const region: HTMLElement = site.region.name
|
||
? node("span", "b08-hydro__value", site.region.name)
|
||
: (() => {
|
||
const { input } = createInputField({ compact: true, placeholder: "지역 입력" });
|
||
input.classList.add("b08-hydro__in");
|
||
input.dataset.b08Hydro = "region";
|
||
input.value = inputs.region;
|
||
input.addEventListener("change", () => commit("region", input.value.trim()));
|
||
return input;
|
||
})();
|
||
if (site.region.name) region.dataset.b08Hydro = "region";
|
||
const regionNote = site.region.full
|
||
? `${site.region.full} · ${site.region.source}`
|
||
: "공사지 위치 자료 없음";
|
||
const rainNote = hasRain
|
||
? `${site.station ? `${site.station} 관측소 ` : ""}확률강우량(전처리 자료)`
|
||
: "강우 자료 없음 — 직접 입력";
|
||
|
||
const left = createCard({
|
||
title: "1. 홍수량 계산",
|
||
body: [
|
||
sub("1-1. 합리식 (Rational Method)"),
|
||
formula("Q = 1/3.6 × f × rt × A"),
|
||
row(
|
||
(() => {
|
||
const label = node("span", "b08-hydro__label b08-hydro__label--select");
|
||
label.append("Qd : 설계 홍수량 (㎥/s) = Q ", factor);
|
||
return label;
|
||
})(),
|
||
outCell("design_flow", true),
|
||
b05Hint("design_flow_m3s", (v) => `B05 ${fmt(v, 2)}`),
|
||
),
|
||
row("Q : 홍수량 (㎥/s)", outCell("flow")),
|
||
row("f : 유출계수", numberInput("runoff")),
|
||
row("rt : 평균 강우강도 (mm/hr)", outCell("intensity")),
|
||
row("A : 유역면적 (㎢)", numberInput("area_km2", "입력"), outCell("area_ha")),
|
||
sub("1-2. 홍수 도달시간 — 산지부 (Kirpich)"),
|
||
formula("T = 0.0663 × L^0.77^ × s^-0.385^ (L : ㎞ · T : hr)"),
|
||
row(
|
||
"T : 홍수 도달시간 (hr)",
|
||
outCell("tc_hr"),
|
||
(() => {
|
||
const note = node("span");
|
||
const b05 = b05Hint("tc_minutes", (v) => ` B05 ${fmt(v, 1)}분`);
|
||
note.append(outCell("tc_note"), ...(b05 ? [b05] : []));
|
||
return note;
|
||
})(),
|
||
),
|
||
row("L : 산정에서 하천시점까지 거리 (m)", numberInput("length_m", "입력")),
|
||
row("H : 산정에서 하천시점까지 표고차 (m)", numberInput("height_m", "입력")),
|
||
row("s : 지표면 경사 (H / L)", outCell("slope_ground")),
|
||
row("최소 도달시간 (분)", numberInput("tc_min_minutes")),
|
||
sub("1-3. 강우강도 rt"),
|
||
formula("rt = (R_24_ / 24) × (24 / T)^0.557^"),
|
||
row("지역", region, hint(regionNote)),
|
||
row("재현기간", periodSelect),
|
||
row(formula("R_24_ : 확률강우량 (mm/day)", "b08-hydro__label"), r24Value, hint(rainNote)),
|
||
],
|
||
});
|
||
left.classList.add("b08-hydro__card");
|
||
|
||
// ── 오른쪽 2. 단면 검토 ── 장마다 단면 칸이 다름 · 유속 · 유출량 · 판정은 같음
|
||
const sheetDefaults = inputs.sheet === "pipe" ? {} : options.sheet_defaults[inputs.sheet];
|
||
const nDefault = inputs.sheet === "pipe" ? options.pipe_kinds[inputs.pipe_kind] : sheetDefaults.n;
|
||
const nInput = numberInput("n", String(nDefault ?? ""));
|
||
const nHint = inputs.sheet === "pipe" ? "비우면 관 종류 값" : `기본 ${nDefault}`;
|
||
const slopeRow = rowF(
|
||
"하상경사 S = 1 /",
|
||
numberInput("slope_denominator", "입력"),
|
||
outCell("slope_pct"),
|
||
);
|
||
const dims = node("div", "b08-hydro__dims");
|
||
const figure = node("div", "b08-hydro__figure");
|
||
let head: HTMLElement[] = [];
|
||
let title = "2. 단면 검토";
|
||
if (inputs.sheet === "box") {
|
||
title = `2. 단면 검토 — ${kind}`;
|
||
dims.append(
|
||
rowF("내공 폭 B (m)", null, numberInput("box_width_m", "입력")),
|
||
rowF("내공 높이 H (m)", null, numberInput("box_height_m", "입력")),
|
||
rowF(
|
||
"수심비 h / H",
|
||
hint("빈칸이면 계산 멈춤 — 값 넣기"),
|
||
numberInput("depth_ratio", "입력"),
|
||
),
|
||
rowF("수심 h (m)", formula("h = 수심비 × H"), outCell("depth")),
|
||
rowF("단면적", formula("A = B × h"), outCell("section_area")),
|
||
rowF("윤변", formula("P = B + 2h"), outCell("perimeter")),
|
||
rowF("경심", formula("R = A / P"), outCell("radius")),
|
||
slopeRow,
|
||
rowF("조도계수 n", hint(nHint), nInput),
|
||
);
|
||
figure.append(rectFigure(true), dims);
|
||
} else if (inputs.sheet === "ford") {
|
||
title = `2. 월류 높이 — ${kind}`;
|
||
dims.append(
|
||
rowF(
|
||
"월류 폭 B (m)",
|
||
hint(`기본 ${sheetDefaults.overflow_width_m}`),
|
||
numberInput("overflow_width_m", "입력"),
|
||
),
|
||
slopeRow,
|
||
rowF("조도계수 n", hint(nHint), nInput),
|
||
rowF(
|
||
"월류 높이 h (m)",
|
||
formula("Q_d_ = B·h × 1/n × R^2/3^ × S^1/2^ 인 h"),
|
||
outCell("depth", true),
|
||
),
|
||
rowF("단면적", formula("A = B × h"), outCell("section_area")),
|
||
rowF("윤변", formula("P = B + 2h"), outCell("perimeter")),
|
||
rowF("경심", formula("R = A / P"), outCell("radius")),
|
||
);
|
||
figure.append(rectFigure(false), dims);
|
||
} else {
|
||
const kinds = Object.entries(options.pipe_kinds).map(
|
||
([pipeKind, n]) => [pipeKind, `${pipeKind} (n ${n})`] as [string, string],
|
||
);
|
||
const texts = SECTION_TEXT[inputs.section];
|
||
dims.append(
|
||
rowF("단면 D (mm)", null, numberInput("diameter_mm", "입력")),
|
||
rowF("단면적", formula(texts.area), outCell("section_area")),
|
||
rowF("윤변", formula(texts.perimeter), outCell("perimeter")),
|
||
rowF("경심", formula(texts.radius), outCell("radius")),
|
||
slopeRow,
|
||
rowF("조도계수 n", hint(nHint), nInput),
|
||
);
|
||
figure.append(pipeFigure(inputs.section), dims);
|
||
head = [
|
||
row("관 종류", select("pipe_kind", kinds)),
|
||
row(
|
||
"단면 가정",
|
||
select("section", Object.entries(options.sections) as Array<[string, string]>),
|
||
),
|
||
];
|
||
}
|
||
const verdict = node("div", "b08-hydro__verdict", DASH);
|
||
verdict.dataset.b08HydroVerdict = "";
|
||
const right = createCard({
|
||
title,
|
||
body: [
|
||
...head,
|
||
row("총 홍수량 Qd (㎥/s)", outCell("design_flow2", true)),
|
||
sub(inputs.sheet === "ford" ? "월류 높이 · 유출량" : "유출량"),
|
||
figure,
|
||
...(inputs.sheet === "ford"
|
||
? [hint("관 몫은 빼지 않음(월류만으로 Qd) · 여유 없음 · 0.01 m 올림")]
|
||
: []),
|
||
rowF("유속 (m/s)", formula("V = 1/n × R^2/3^ × S^1/2^"), outCell("velocity")),
|
||
rowF("유출량 (㎥/s)", formula("Q = A × V"), outCell("capacity", true)),
|
||
verdict,
|
||
rowF("유출율 (%)", formula("Q / Qd × 100"), outCell("ratio_pct", true)),
|
||
],
|
||
});
|
||
right.classList.add("b08-hydro__card");
|
||
|
||
// 「B05」 표는 그 칸 이름 옆에
|
||
for (const [key, mark] of marks) {
|
||
const label = sheetLabel(left, right, key);
|
||
label?.append(" ", mark);
|
||
markLinked(key);
|
||
}
|
||
|
||
const status = node("p", "b08-hydro__status");
|
||
const sheet = node("div", "b08-hydro__sheet");
|
||
sheet.dataset.b08HydroSheet = stationText;
|
||
const body = node("div", "b08-hydro__body");
|
||
body.append(left, right);
|
||
sheet.append(body, status);
|
||
|
||
const set = (id: string, text: string): void => {
|
||
const cell = out.get(id);
|
||
if (cell) cell.textContent = text;
|
||
};
|
||
return {
|
||
root: sheet,
|
||
show(result, message = "") {
|
||
status.textContent = message;
|
||
const r = result;
|
||
set("design_flow", fmt(r?.design_flow, 2));
|
||
set("design_flow2", fmt(r?.design_flow, 2));
|
||
set("flow", fmt(r?.flow, 2));
|
||
set("intensity", fmt(r?.intensity, 2));
|
||
set("area_ha", fmt(r?.area_ha, 1, " ha"));
|
||
set("r24", fmt(r?.r24_mm ?? r24Site(current.return_period), 1));
|
||
set("tc_hr", fmt(r?.tc_hr, 3));
|
||
set(
|
||
"tc_note",
|
||
r?.tc_floored && r.tc_raw_hr != null
|
||
? `Kirpich ${fmt(r.tc_raw_hr * 60, 1)}분 → 최소 ${current.tc_min_minutes}분`
|
||
: "",
|
||
);
|
||
set("slope_ground", fmt(r?.slope_ground, 3));
|
||
set("depth", fmt(r?.depth, 2));
|
||
set("section_area", fmt(r?.section_area, 3, " ㎡"));
|
||
set("perimeter", fmt(r?.perimeter, 3, " m"));
|
||
set("radius", fmt(r?.radius, 3));
|
||
set("slope_pct", fmt(r?.slope_bed == null ? null : r.slope_bed * 100, 2, " %"));
|
||
set("velocity", fmt(r?.velocity, 3));
|
||
set("capacity", fmt(r?.capacity, 3));
|
||
set("ratio_pct", fmt(r?.ratio_pct, 0));
|
||
const judged = r != null && r.ok != null;
|
||
verdict.classList.toggle("is-key", judged);
|
||
sheet.classList.toggle("is-ok", judged && r.ok === true);
|
||
sheet.classList.toggle("is-ng", judged && !r.ok);
|
||
verdict.textContent = judged
|
||
? `${fmt(r.capacity, 3)} ㎥/s ${r.ok ? "≥" : "<"} Qd ${fmt(r.design_flow, 2)} ㎥/s — ${r.ok ? "O K" : "N G"}`
|
||
: `판정 ${DASH}`;
|
||
},
|
||
};
|
||
}
|