- 측점 보조 탭 = 저장된 pipe_points 측점만 읽음 · 입력은 이 탭 자기 기본값(B05/B06 · 배수유역 값과 안 이음) - 1-1 합리식(Qd = Q × 할증) · 1-2 Kirpich 도달시간(최소 5분) · 1-3 rt = (R24/24)·(24/T)^0.557 - 2 단면검토 — 관 단면 그림 · 단면적 · 윤변 · 경심 · 경사 · n · Manning 유속 · Q = A·V · OK/NG 점선 상자 · 유출율 - 선택지(할증 2.0/1.5/1.2 · 만관/240°/유효 70 % · 관 종류별 n) · 기본값 한 파일(B08_Quantity_Hydro_Defaults.py) - 계산 = 서버 단독(B08_Quantity_Engine_Hydro · /quantity/hydro · /quantity/hydro/calc · 최신 응답만) · 입력은 캐시만 · 어떤 파일에도 안 씀 - 흰 종이 계산서(입력 빨강 · 계산 파랑 · 주요 입력 노랑) · 쪽 표시 · 시험 test_62_2_b08_hydro_engine.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
350 lines
14 KiB
TypeScript
350 lines
14 KiB
TypeScript
/* =============================================================================
|
||
* B08_Quantity_UI_Hydro_Sheet.ts
|
||
* 홍수량 계산서 한 장(PLAN 62-2) — 견본(울진 2공구 수리계산서) 모양.
|
||
*
|
||
* 왼쪽 「1. 홍수량 계산」 1-1 합리식 · 1-2 도달시간(Kirpich) · 1-3 강우강도
|
||
* 오른쪽 「2. 단면검토」 관 단면 그림 · 단면적 · 윤변 · 경심 · 경사 · n · 유속 · 유출량 · 판정
|
||
* 입력값 빨강 · 계산값 파랑 · 주요 입력 노랑 바탕 · 흰 종이(어두움 테마에서도 같음).
|
||
*
|
||
* 입력 칸은 한 번 만들고 계산 결과만 제자리에 바꿈 — 고치는 중 칸 포커스를 잃지 않게.
|
||
* ========================================================================== */
|
||
|
||
export interface HydroInputs {
|
||
factor: number;
|
||
runoff: number;
|
||
area_km2: number;
|
||
length_m: number;
|
||
height_m: number;
|
||
tc_min_minutes: number;
|
||
r24_mm: number;
|
||
region: string;
|
||
return_period: number;
|
||
pipe_kind: string;
|
||
n: number | null;
|
||
diameter_mm: number;
|
||
slope_denominator: number;
|
||
section: "full" | "arc240" | "eff70";
|
||
}
|
||
|
||
export interface HydroResult {
|
||
area_ha: number;
|
||
slope_ground: number;
|
||
tc_raw_hr: number;
|
||
tc_hr: number;
|
||
tc_floored: boolean;
|
||
intensity: number;
|
||
flow: number;
|
||
design_flow: number;
|
||
n: number;
|
||
section_area: number;
|
||
perimeter: number;
|
||
radius: number;
|
||
slope_bed: number;
|
||
velocity: number;
|
||
capacity: number;
|
||
ok: boolean;
|
||
ratio_pct: number | null;
|
||
}
|
||
|
||
export interface HydroOptions {
|
||
defaults: HydroInputs;
|
||
factors: number[];
|
||
sections: Record<HydroInputs["section"], string>;
|
||
pipe_kinds: Record<string, number>;
|
||
}
|
||
|
||
export interface HydroSheet {
|
||
root: HTMLElement;
|
||
/** 계산값 칸을 바꿈 — null 이면 비움 + 까닭 한 줄 */
|
||
show(result: HydroResult | null, message?: string): void;
|
||
}
|
||
|
||
type Key = keyof HydroInputs;
|
||
|
||
const fmt = (value: number | null | undefined, digits: number): string =>
|
||
value == null || !Number.isFinite(value) ? "-" : value.toFixed(digits);
|
||
|
||
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;
|
||
}
|
||
|
||
/** 관 단면 그림 — 단면 가정대로 물 찬 자리를 칠함. */
|
||
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;
|
||
}
|
||
|
||
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,
|
||
page: { index: number; total: number },
|
||
inputs: HydroInputs,
|
||
options: HydroOptions,
|
||
onChange: (next: HydroInputs) => void,
|
||
): HydroSheet {
|
||
const out = new Map<string, HTMLElement>();
|
||
const outCell = (id: string, className = ""): HTMLElement => {
|
||
const cell = node("span", `b08-hydro__out ${className}`.trim());
|
||
out.set(id, cell);
|
||
return cell;
|
||
};
|
||
let current = inputs;
|
||
const commit = (key: Key, value: unknown): void => {
|
||
current = { ...current, [key]: value } as HydroInputs;
|
||
onChange(current);
|
||
};
|
||
const numberInput = (key: Key, step = "any"): HTMLInputElement => {
|
||
const input = node("input", "b08-hydro__in");
|
||
input.type = "number";
|
||
input.step = step;
|
||
input.dataset.b08Hydro = key;
|
||
const value = inputs[key];
|
||
input.value = value == null ? "" : String(value);
|
||
input.addEventListener("change", () => {
|
||
// n 은 비우면 관 종류 값 · 최소 도달시간은 0 까지 · 그 밖은 0 보다 커야 함 — 아니면 되돌림
|
||
const parsed = input.value === "" ? null : Number(input.value);
|
||
const valid =
|
||
parsed === null
|
||
? key === "n"
|
||
: Number.isFinite(parsed) && (key === "tc_min_minutes" ? parsed >= 0 : parsed > 0);
|
||
if (valid) commit(key, parsed);
|
||
else input.value = current[key] == null ? "" : String(current[key]);
|
||
});
|
||
return input;
|
||
};
|
||
const textInput = (key: Key): HTMLInputElement => {
|
||
const input = node("input", "b08-hydro__in b08-hydro__in--text");
|
||
input.value = String(inputs[key] ?? "");
|
||
input.dataset.b08Hydro = key;
|
||
input.addEventListener("change", () => commit(key, input.value));
|
||
return input;
|
||
};
|
||
const select = (key: Key, choices: Array<[string, string]>): HTMLSelectElement => {
|
||
const el = node("select", "b08-hydro__in b08-hydro__select");
|
||
el.dataset.b08Hydro = key;
|
||
for (const [value, label] of choices) el.append(new Option(label, value));
|
||
el.value = String(inputs[key]);
|
||
el.addEventListener("change", () =>
|
||
commit(key, key === "factor" ? Number(el.value) : el.value),
|
||
);
|
||
return el;
|
||
};
|
||
/** 표 줄 — 이름 · 값 칸(입력 또는 계산) · 덧칸 */
|
||
const row = (label: string, value: HTMLElement, extra?: HTMLElement, key = false) => {
|
||
const line = node("div", `b08-hydro__row${key ? " is-key" : ""}`);
|
||
line.append(node("span", "b08-hydro__label", label), value, extra ?? node("span"));
|
||
return line;
|
||
};
|
||
/** 오른쪽 줄 — 이름 · 식(또는 입력) · 값 칸 */
|
||
const rowF = (label: string, middle: HTMLElement | null, value: HTMLElement, key = false) => {
|
||
const line = node("div", `b08-hydro__row b08-hydro__row--f${key ? " is-key" : ""}`);
|
||
line.append(node("span", "b08-hydro__label", label), middle ?? node("span"), value);
|
||
return line;
|
||
};
|
||
|
||
// ── 왼쪽 1. 홍수량 계산 ──
|
||
const left = node("section", "b08-hydro__col");
|
||
const station = node("div", "b08-hydro__station");
|
||
station.append("측점 : ", node("mark", "", stationText));
|
||
const factor = select(
|
||
"factor",
|
||
options.factors.map((f) => [String(f), f.toFixed(1)]),
|
||
);
|
||
const qdLabel = node("span", "b08-hydro__label");
|
||
qdLabel.append("Qd : 홍수량 × ", factor);
|
||
const qdRow = node("div", "b08-hydro__row");
|
||
qdRow.append(qdLabel, outCell("design_flow"));
|
||
left.append(
|
||
node("h3", "b08-hydro__h1", "1. 홍 수 량 계 산"),
|
||
station,
|
||
node("h4", "b08-hydro__h2", "1-1. Rational Method 공식"),
|
||
formula("Q = 1/3.6 × f × rt × A"),
|
||
qdRow,
|
||
row("Q : 홍수량 ㎥/sec", outCell("flow")),
|
||
row("f : 유출계수", numberInput("runoff")),
|
||
row("rt : 평균시간 강우량 mm/hr", outCell("intensity")),
|
||
row("A : 유역면적 ㎢", numberInput("area_km2"), outCell("area_ha", "b08-hydro__ha"), true),
|
||
node("h4", "b08-hydro__h2", "1-2. 홍수도달시간"),
|
||
node("p", "b08-hydro__note", "산지부 (Kirpich 공식)"),
|
||
formula("T = 0.0663 × L^0.77^ × s^-0.385^ (L : ㎞ · T : hr)"),
|
||
row("T : 홍수도달시간 (hr)", outCell("tc_hr"), outCell("tc_note", "b08-hydro__tcnote")),
|
||
row("L : 산정에서 하천시점까지 거리 (m)", numberInput("length_m"), undefined, true),
|
||
row("s : 지표면경사 (H/L)", outCell("slope_ground")),
|
||
row("H : 산정에서 하천시점까지 표고차 (m)", numberInput("height_m"), undefined, true),
|
||
row("최소 도달시간 (분)", numberInput("tc_min_minutes")),
|
||
node("h4", "b08-hydro__h2", "1-3. 강우강도 rt의 결정"),
|
||
);
|
||
const rain = node("div", "b08-hydro__rain");
|
||
const r24 = node("div", "b08-hydro__row is-key");
|
||
r24.append(formula("R_24_ : 확률강우량 (mm/day)", "b08-hydro__label"), numberInput("r24_mm"));
|
||
const place = node("div", "b08-hydro__place");
|
||
place.append(
|
||
node("span", "b08-hydro__place-head", "지 역"),
|
||
node("span", "b08-hydro__place-head", "재현기간"),
|
||
textInput("region"),
|
||
numberInput("return_period"),
|
||
);
|
||
const rtRow = node("div", "b08-hydro__row");
|
||
rtRow.append(formula("rt = (R_24_/24) × (24/T)^0.557^"), outCell("intensity2"));
|
||
rain.append(rtRow, r24, place);
|
||
left.append(
|
||
rain,
|
||
node("p", "b08-hydro__note", "(확률강우량 — 기본값 · 지역 관측소 값으로 고침)"),
|
||
);
|
||
|
||
// ── 오른쪽 2. 단면검토 ──
|
||
const right = node("section", "b08-hydro__col");
|
||
const box = node("div", "b08-hydro__box");
|
||
const kinds = Object.entries(options.pipe_kinds).map(
|
||
([kind, n]) => [kind, `${kind} (n ${n})`] as [string, string],
|
||
);
|
||
const kindLine = node("div", "b08-hydro__kind");
|
||
kindLine.append(
|
||
"- ",
|
||
select("pipe_kind", kinds),
|
||
" · ",
|
||
select("section", Object.entries(options.sections) as Array<[string, string]>),
|
||
);
|
||
const total = node("div", "b08-hydro__row b08-hydro__row--wide");
|
||
total.append(node("span", "b08-hydro__label", "1. 총 홍수량 Qd (㎥/s)"), outCell("design_flow2"));
|
||
const texts = SECTION_TEXT[inputs.section];
|
||
const dims = node("div", "b08-hydro__dims");
|
||
const nInput = numberInput("n");
|
||
nInput.placeholder = String(options.pipe_kinds[inputs.pipe_kind] ?? "");
|
||
dims.append(
|
||
rowF("단 면 : D (mm)", null, numberInput("diameter_mm"), true),
|
||
rowF("단면적 :", formula(texts.area), outCell("section_area")),
|
||
rowF("윤 변 :", formula(texts.perimeter), outCell("perimeter")),
|
||
rowF("경 심 :", formula(texts.radius), outCell("radius")),
|
||
rowF("하상경사 : S = 1 /", numberInput("slope_denominator"), outCell("slope_pct")),
|
||
rowF("n", node("span", "b08-hydro__hint", "비우면 관 종류 값"), nInput),
|
||
);
|
||
const figure = node("div", "b08-hydro__figure");
|
||
figure.append(pipeFigure(inputs.section), dims);
|
||
const verdict = node("div", "b08-hydro__verdict");
|
||
out.set("verdict", verdict);
|
||
box.append(
|
||
kindLine,
|
||
total,
|
||
node("div", "b08-hydro__h2", "2. 유 출 량"),
|
||
figure,
|
||
rowF("- 유 속 :", formula("V = 1/n × R^2/3^ × S^1/2^ (m/sec)"), outCell("velocity")),
|
||
rowF("- 유출량 :", formula("Q = A × V (㎥/sec)"), outCell("capacity")),
|
||
verdict,
|
||
rowF("- 유출율 (%)", formula("Q / Qd × 100"), outCell("ratio_pct")),
|
||
);
|
||
right.append(node("h3", "b08-hydro__h1", "2. 단면검토"), box);
|
||
|
||
const status = node("p", "b08-hydro__status");
|
||
const paper = node("article", "b08-hydro__paper");
|
||
paper.dataset.b08HydroSheet = stationText;
|
||
const body = node("div", "b08-hydro__body");
|
||
body.append(left, right);
|
||
paper.append(
|
||
body,
|
||
status,
|
||
node("footer", "b08-hydro__page", `— ${page.index} / ${page.total} 쪽 —`),
|
||
);
|
||
|
||
const set = (id: string, text: string): void => {
|
||
const cell = out.get(id);
|
||
if (cell) cell.textContent = text;
|
||
};
|
||
return {
|
||
root: paper,
|
||
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("intensity2", fmt(r?.intensity, 2));
|
||
set("area_ha", r ? `${fmt(r.area_ha, 1)} ha` : "-");
|
||
set("tc_hr", fmt(r?.tc_hr, 3));
|
||
set(
|
||
"tc_note",
|
||
r?.tc_floored
|
||
? `Kirpich ${fmt(r.tc_raw_hr * 60, 1)}분 → 최소 ${current.tc_min_minutes}분`
|
||
: "",
|
||
);
|
||
set("slope_ground", fmt(r?.slope_ground, 3));
|
||
set("section_area", r ? `${fmt(r.section_area, 3)} ㎡` : "-");
|
||
set("perimeter", r ? `${fmt(r.perimeter, 3)} m` : "-");
|
||
set("radius", fmt(r?.radius, 3));
|
||
set("slope_pct", r ? `${fmt(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));
|
||
verdict.classList.toggle("is-ng", !!r && !r.ok);
|
||
verdict.textContent = r
|
||
? `${fmt(r.capacity, 3)}㎥/s ${r.ok ? ">" : "<"} Qd = ${fmt(r.design_flow, 2)}㎥/s - ${r.ok ? "O K" : "N G"} -`
|
||
: "-";
|
||
},
|
||
};
|
||
}
|