Files
Aislo/B08_Quantity/B08_Quantity_UI_Hydro_Sheet.ts
T
eomsangdonandClaude Opus 5.5 14c3f01f11 fix(B08): 임의 · 실무 값 빈칸 — 관경 · 련 미입력 · 1본 프로젝트 값만 · 거창 계수 비율 미정 · 수심비 빈칸(143-2)
- 집계표 — 관경 · 련 빈칸 = 「미입력」 열(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
2026-10-09 14:56:57 +09:00

563 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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}`;
},
};
}