feat(B08): 수리집수면적표 박스암거 사각 단면 장 · 세월교 · 물넘이 월류 높이 장(73-11 · 73-12)
- 박스 = 내공 B · H · 수심비(기본 0.8) · n(기본 0.013) · A = B·h · P = B + 2h · Manning · Qd 판정 - 세월교 · 물넘이포장 = 월류 폭(기본 10 m) · 경사(1/10) · n(0.017) → 월류 높이 자동(ford_depth) - ford_depth = fordHeightM 파이썬 짝(두 파일 머리 · 거울 시험 test_73_11) · 관 몫 안 뺌 · 여유 없음 - [저장]에 새 칸 · 빈 장 없앰 · 물넘이포장은 관 장에서 옮김 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
* 설계유량이 없으면 null = 손 입력.
|
||||
* 짝: 자동 배치 새 세월교 빈 높이 = 서버 `B04_PreProcess_Router_Basins.ford_height_m`(같은 풀이 · 65-4) ·
|
||||
* 거울 시험 test_65_ford_defaults.
|
||||
* 짝: B08 수리집수면적표 세월교 · 물넘이 월류 높이 = 서버 `B08_Quantity_Engine_Hydro.ford_depth`
|
||||
* (같은 풀이 · n · S 를 받음) · 거울 시험 test_73_11_b08_hydro_box_ford.
|
||||
* ========================================================================== */
|
||||
|
||||
import { fordSection } from "../../B05_Profile/B05_Profile_UI_Drainage_Facility_Fields";
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
1-2 도달시간 Kirpich T = 0.0663·L[㎞]^0.77·s^-0.385 [hr] · s = H/L · 최소 도달시간 적용
|
||||
1-3 강우강도 rt = (R24/24)·(24/T)^0.557 (Mononobe형)
|
||||
2 단면검토 V = (1/n)·R^(2/3)·S^(1/2) · Q = A·V · 판정 Q ≥ Qd · 유출율 = Q/Qd
|
||||
장(sheet) pipe = 관 단면 · box = 사각 h = 수심비 × H · A = B·h · P = B + 2h (73-11)
|
||||
ford = 세월교 · 물넘이 월류 높이 = 폭 · S · n 으로 Qd 를 흘리는 수심(73-12)
|
||||
|
||||
짝: `ford_depth` ↔ `A00_Common/design/design_structure_ford_calc.fordHeightM`
|
||||
(`fordSection` 같은 풀이)
|
||||
· 거울 시험 test_73_11_b08_hydro_box_ford · 관 몫은 빼지 않음 · 여유(0.9H) 안 넣음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -13,7 +19,7 @@ from __future__ import annotations
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from B08_Quantity.B08_Quantity_Hydro_Defaults import PIPE_KINDS
|
||||
from B08_Quantity.B08_Quantity_Hydro_Defaults import PIPE_KINDS, SHEET_DEFAULTS
|
||||
|
||||
MONONOBE_EXPONENT = 0.557
|
||||
EFFECTIVE_RATIO = 0.7
|
||||
@@ -36,6 +42,29 @@ def section_props(section: str, diameter_m: float) -> dict[str, float]:
|
||||
return {"area": full_area, "perimeter": math.pi * d, "radius": d / 4.0}
|
||||
|
||||
|
||||
def rect_props(width_m: float, depth_m: float) -> dict[str, float]:
|
||||
"""사각 단면(천장 빼고) — A = B·h · P = B + 2h · R = A/P."""
|
||||
area, perimeter = width_m * depth_m, width_m + 2.0 * depth_m
|
||||
return {"area": area, "perimeter": perimeter, "radius": area / perimeter}
|
||||
|
||||
|
||||
def ford_depth(flow_m3s: float, width_m: float, n: float, slope: float) -> float:
|
||||
"""월류 폭(m)으로 유량을 흘리는 수심(m) · 0.01 m 올림 — Manning 개수로 반복 풀이."""
|
||||
root = math.sqrt(slope)
|
||||
|
||||
def velocity(depth: float) -> float:
|
||||
return (1 / n) * ((width_m * depth) / (width_m + 2 * depth)) ** (2 / 3) * root
|
||||
|
||||
depth = ((flow_m3s * n) / (width_m * root)) ** (3 / 5)
|
||||
for _ in range(20):
|
||||
nxt = flow_m3s / (width_m * velocity(depth))
|
||||
if abs(nxt - depth) < 1e-6:
|
||||
depth = nxt
|
||||
break
|
||||
depth = nxt
|
||||
return math.ceil(depth * 100 - 1e-9) / 100
|
||||
|
||||
|
||||
def _num(inputs: dict[str, Any], key: str) -> float | None:
|
||||
value = inputs.get(key)
|
||||
return None if value is None or value == "" else float(value)
|
||||
@@ -59,10 +88,29 @@ def compute(inputs: dict[str, Any]) -> dict[str, Any]:
|
||||
flow = float(inputs["runoff"]) * intensity * area_km2 / 3.6
|
||||
design_flow = flow * float(inputs["factor"])
|
||||
|
||||
n = _num(inputs, "n") or PIPE_KINDS[inputs["pipe_kind"]]
|
||||
diameter_mm, slope_den = _num(inputs, "diameter_mm"), _num(inputs, "slope_denominator")
|
||||
props = section_props(inputs["section"], diameter_mm / 1000.0) if diameter_mm else {}
|
||||
sheet = inputs.get("sheet") or "pipe"
|
||||
if sheet == "pipe":
|
||||
n = _num(inputs, "n") or PIPE_KINDS[inputs["pipe_kind"]]
|
||||
else:
|
||||
n = _num(inputs, "n") or SHEET_DEFAULTS[sheet]["n"]
|
||||
slope_den = _num(inputs, "slope_denominator")
|
||||
slope_bed = 1.0 / slope_den if slope_den else None
|
||||
props: dict[str, float] = {}
|
||||
depth = None
|
||||
if sheet == "pipe":
|
||||
diameter_mm = _num(inputs, "diameter_mm")
|
||||
props = section_props(inputs["section"], diameter_mm / 1000.0) if diameter_mm else {}
|
||||
elif sheet == "box":
|
||||
width, height = _num(inputs, "box_width_m"), _num(inputs, "box_height_m")
|
||||
ratio = _num(inputs, "depth_ratio") or SHEET_DEFAULTS["box"]["depth_ratio"]
|
||||
if width and height:
|
||||
depth = ratio * height
|
||||
props = rect_props(width, depth)
|
||||
else:
|
||||
width = _num(inputs, "overflow_width_m")
|
||||
if width and slope_bed and design_flow:
|
||||
depth = ford_depth(design_flow, width, n, slope_bed)
|
||||
props = rect_props(width, depth)
|
||||
velocity = capacity = None
|
||||
if props and slope_bed:
|
||||
velocity = (1.0 / n) * props["radius"] ** (2.0 / 3.0) * math.sqrt(slope_bed)
|
||||
@@ -79,6 +127,7 @@ def compute(inputs: dict[str, Any]) -> dict[str, Any]:
|
||||
"flow": flow,
|
||||
"design_flow": design_flow,
|
||||
"n": n,
|
||||
"depth": depth,
|
||||
"section_area": props.get("area"),
|
||||
"perimeter": props.get("perimeter"),
|
||||
"radius": props.get("radius"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""B08 4탭 홍수량 계산서 — 기본값 · 선택지 한 곳 (PLAN 62-3).
|
||||
"""B08 4탭 홍수량 계산서 — 기본값 · 선택지 한 곳 (PLAN 62-3 · 73-11 · 73-12).
|
||||
|
||||
임의로 정한 값 — 사용자가 나중에 고침. B05/B06 · 배수유역 값과 잇지 않음(자기 기본값만).
|
||||
근거 후보: docs/raw/조사/2026-10-06_62-1_홍수량_방법.md (별표2 2.0배 · KDS 표 2.7-1 · 실무 견본).
|
||||
@@ -8,6 +8,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from config.config_system import FORD_MANNING_N, FORD_SLOPE
|
||||
|
||||
#: 할증(Qd = Q × 할증) — 2.0 별표2 법정 · 1.5 견본(울진 2공구) · 1.2 구기준
|
||||
FACTORS = (2.0, 1.5, 1.2)
|
||||
|
||||
@@ -37,21 +39,33 @@ DEFAULTS: dict[str, Any] = {
|
||||
}
|
||||
|
||||
|
||||
#: 계산 방법 정하는 중인 시설(73-1) — 관 계산 대신 빈 장 · 값은 관 지점 시설 속성(pipe_points)
|
||||
PENDING_FACILITIES = {"box_culvert": "박스암거", "ford_bridge": "세월교"}
|
||||
#: 시설 → 계산서 장(73-11 · 73-12) — 박스 = 사각 단면 · 세월교 · 물넘이 = 월류 높이 · 그 밖 = 관
|
||||
SHEETS = {
|
||||
"box_culvert": ("box", "박스암거"),
|
||||
"ford_bridge": ("ford", "세월교"),
|
||||
"ford_pavement": ("ford", "물넘이포장"),
|
||||
}
|
||||
|
||||
#: 장별 처음 값 — 박스 수심비 0.8H · n 0.013(울진 산출서 · 조사 69-7) · 내공 · 경사는 빈칸
|
||||
#: 세월교 · 물넘이 월류 폭 10 m(사용자 10-7) · n · 경사 = B05 세월교 높이 계산과 같은 값
|
||||
SHEET_DEFAULTS: dict[str, dict[str, Any]] = {
|
||||
"box": {"n": 0.013, "depth_ratio": 0.8, "box_width_m": None, "box_height_m": None},
|
||||
"ford": {
|
||||
"n": FORD_MANNING_N,
|
||||
"overflow_width_m": 10.0,
|
||||
"slope_denominator": round(1 / FORD_SLOPE, 6),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def station_rows(points: list[Any]) -> list[dict[str, Any]]:
|
||||
"""관 지점 → 보조 탭 줄(측점 차례) · 박스암거 · 세월교는 pending(빈 장)."""
|
||||
rows = [
|
||||
{
|
||||
"chainage_m": p.chainage_m,
|
||||
"facility": p.facility,
|
||||
"kind": PENDING_FACILITIES.get(p.facility, ""),
|
||||
"pending": p.facility in PENDING_FACILITIES,
|
||||
}
|
||||
for p in points
|
||||
]
|
||||
"""관 지점 → 보조 탭 줄(측점 차례) · 장 종류(pipe · box · ford)."""
|
||||
rows = []
|
||||
for p in points:
|
||||
sheet, kind = SHEETS.get(p.facility, ("pipe", ""))
|
||||
rows.append(
|
||||
{"chainage_m": p.chainage_m, "facility": p.facility, "kind": kind, "sheet": sheet}
|
||||
)
|
||||
return sorted(rows, key=lambda s: s["chainage_m"])
|
||||
|
||||
|
||||
@@ -62,4 +76,5 @@ def options() -> dict[str, Any]:
|
||||
"factors": list(FACTORS),
|
||||
"sections": SECTIONS,
|
||||
"pipe_kinds": PIPE_KINDS,
|
||||
"sheet_defaults": SHEET_DEFAULTS,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""B08 4탭 홍수량 계산서 길 (PLAN 62-2 · 62-3 · 69-5 · 69-6).
|
||||
"""B08 4탭 홍수량 계산서 길 (PLAN 62-2 · 62-3 · 69-5 · 69-6 · 73-11 · 73-12).
|
||||
|
||||
GET /api/projects/{id}/quantity/hydro 배수지점 측점(저장된 pipe_points) + 기본값 · 선택지
|
||||
+ 공사지(지역 · 재현기간별 R24) + 저장한 입력
|
||||
@@ -47,6 +47,12 @@ class HydroInputs(BaseModel):
|
||||
diameter_mm: float | None = Field(default=None, gt=0)
|
||||
slope_denominator: float | None = Field(default=None, gt=0)
|
||||
section: Literal["full", "arc240", "eff70"]
|
||||
# 장 — 박스(사각 단면 · 73-11) · 세월교 · 물넘이(월류 높이 · 73-12) · 그 밖 관
|
||||
sheet: Literal["pipe", "box", "ford"] = "pipe"
|
||||
box_width_m: float | None = Field(default=None, gt=0)
|
||||
box_height_m: float | None = Field(default=None, gt=0)
|
||||
depth_ratio: float | None = Field(default=None, gt=0, le=1)
|
||||
overflow_width_m: float | None = Field(default=None, gt=0)
|
||||
|
||||
|
||||
class HydroSave(BaseModel):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 4탭 수리집수면적표 — 배수지점 측점마다 보조 탭 · 한 장 = 홍수량 계산서(PLAN 62-2 · 69-4~6).
|
||||
*
|
||||
* 측점 목록만 저장된 관 지점(pipe_points)에서 · 입력은 이 탭 자기 값(B05/B06 값과 안 이음)
|
||||
* 박스암거 · 세월교 지점 = 빈 장(계산 방법 정하는 중 · 73-1) · [저장]에서 빠짐
|
||||
* 박스암거 = 사각 단면 장(73-11) · 세월교 · 물넘이포장 = 월류 높이 장(73-12) · 그 밖 = 관 장
|
||||
* 처음 값 = 표준 값 · 측점 고유 값은 빈칸 · 지역 · R24 = 공사지 자료(서버)
|
||||
* 조작 = 캐시(sessionStorage) · [저장] = B08 자기 파일(PUT …/quantity/hydro) · 뒤 캐시 비움
|
||||
* 계산 = 서버 단독(POST …/quantity/hydro/calc · 최신 응답만 채택)
|
||||
@@ -13,7 +13,6 @@ import "./B08_Quantity_UI_Hydro.css";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import {
|
||||
createButton,
|
||||
createCard,
|
||||
hideLoadingOverlay,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
@@ -31,10 +30,10 @@ import {
|
||||
interface HydroStation {
|
||||
chainage_m: number;
|
||||
facility: string;
|
||||
/** 박스암거 · 세월교 등 계산 방법 정하는 중인 시설 이름(관이면 "") */
|
||||
/** 박스암거 · 세월교 · 물넘이포장(관이면 "") */
|
||||
kind: string;
|
||||
/** true = 관 계산 대신 빈 장(73-1) */
|
||||
pending: boolean;
|
||||
/** 계산서 장 — 시설이 정함(서버 station_rows) */
|
||||
sheet: HydroInputs["sheet"];
|
||||
}
|
||||
|
||||
type Drafts = Record<string, Partial<HydroInputs>>;
|
||||
@@ -63,39 +62,6 @@ function note(text: string): HTMLElement {
|
||||
|
||||
const keyOf = (station: HydroStation): string => station.chainage_m.toFixed(3);
|
||||
|
||||
/** 박스암거 · 세월교 지점 빈 장(73-1) — 계산 방법이 정해지면 이 자리에 계산서가 들어감. */
|
||||
function pendingSheet(stationText: string, kind: string): HTMLElement {
|
||||
const row = (label: string, value: string): HTMLElement => {
|
||||
const line = document.createElement("div");
|
||||
line.className = "b08-hydro__row";
|
||||
const name = document.createElement("span");
|
||||
name.className = "b08-hydro__label";
|
||||
name.textContent = label;
|
||||
const text = document.createElement("span");
|
||||
text.className = "b08-hydro__value";
|
||||
text.textContent = value;
|
||||
line.append(name, text);
|
||||
return line;
|
||||
};
|
||||
const card = createCard({
|
||||
title: `홍수량 계산 — ${kind}`,
|
||||
body: [
|
||||
row("측점", stationText),
|
||||
row("종류", kind),
|
||||
note("계산 방법 정하는 중 — 관과 다른 계산(사각 단면 · 월류)을 정한 뒤 이 장에 넣음."),
|
||||
],
|
||||
});
|
||||
card.classList.add("b08-hydro__card");
|
||||
const sheet = document.createElement("div");
|
||||
sheet.className = "b08-hydro__sheet b08-hydro__sheet--pending";
|
||||
sheet.dataset.b08HydroPending = kind;
|
||||
const body = document.createElement("div");
|
||||
body.className = "b08-hydro__body"; // 관 장과 같은 격자 — 카드 폭이 같음
|
||||
body.append(card);
|
||||
sheet.append(body);
|
||||
return sheet;
|
||||
}
|
||||
|
||||
export async function renderHydro(host: HTMLElement, projectId: string): Promise<void> {
|
||||
host.replaceChildren(note("홍수량 계산서를 불러오는 중…"));
|
||||
let sheet: HydroPayload;
|
||||
@@ -120,8 +86,10 @@ export async function renderHydro(host: HTMLElement, projectId: string): Promise
|
||||
const inputsOf = (station: HydroStation): HydroInputs =>
|
||||
({
|
||||
...sheet.defaults,
|
||||
...(station.sheet === "pipe" ? {} : sheet.sheet_defaults[station.sheet]),
|
||||
...sheet.saved[keyOf(station)],
|
||||
...drafts[keyOf(station)],
|
||||
sheet: station.sheet,
|
||||
}) as HydroInputs;
|
||||
|
||||
const tabs = document.createElement("div");
|
||||
@@ -148,7 +116,7 @@ export async function renderHydro(host: HTMLElement, projectId: string): Promise
|
||||
...sheet.stations.map((s, i) => {
|
||||
const label = stationLabel(s.chainage_m, interval);
|
||||
const button = createButton({
|
||||
label: s.pending ? `${label} ${s.kind}` : label,
|
||||
label: s.kind ? `${label} ${s.kind}` : label,
|
||||
variant: i === index ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => i !== index && open(i),
|
||||
@@ -160,11 +128,6 @@ export async function renderHydro(host: HTMLElement, projectId: string): Promise
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
if (station.pending) {
|
||||
seq++; // 앞 장의 늦은 계산 응답을 버림
|
||||
page.replaceChildren(pendingSheet(stationLabel(station.chainage_m, interval), station.kind));
|
||||
return;
|
||||
}
|
||||
const draw = (inputs: HydroInputs): void => {
|
||||
const view = buildHydroSheet(
|
||||
stationLabel(station.chainage_m, interval),
|
||||
@@ -177,6 +140,7 @@ export async function renderHydro(host: HTMLElement, projectId: string): Promise
|
||||
if (next.section !== inputs.section || next.pipe_kind !== inputs.pipe_kind) draw(next);
|
||||
else void calc(next, view.show);
|
||||
},
|
||||
station.kind,
|
||||
);
|
||||
page.replaceChildren(view.root);
|
||||
void calc(inputs, view.show);
|
||||
@@ -202,9 +166,7 @@ export async function renderHydro(host: HTMLElement, projectId: string): Promise
|
||||
|
||||
/** [저장] — 측점 전부의 지금 입력을 B08 자기 파일로 · 성공하면 캐시를 비우고 저장본으로 다시 엶 */
|
||||
const saveAll = async (): Promise<void> => {
|
||||
const stations = Object.fromEntries(
|
||||
sheet.stations.filter((s) => !s.pending).map((s) => [keyOf(s), inputsOf(s)]),
|
||||
);
|
||||
const stations = Object.fromEntries(sheet.stations.map((s) => [keyOf(s), inputsOf(s)]));
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
const body = await request<{ saved: Drafts }>(`/projects/${projectId}/quantity/hydro`, {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*
|
||||
* 왼쪽 카드 「1. 홍수량 계산」 1-1 합리식 · 1-2 도달시간(Kirpich) · 1-3 강우강도
|
||||
* 오른쪽 카드 「2. 단면 검토」 관 단면 그림 · 단면적 · 윤변 · 경심 · 경사 · n · 유속 · 유출량 · 판정
|
||||
* 장(sheet) 갈래 — 박스 = 사각 내공 B · H · 수심비(73-11) · 세월교 · 물넘이 = 월류 폭 → 월류 높이(73-12)
|
||||
* 테두리는 입력 · 선택 칸만(공용 ui-input · ui-select) · 계산 칸은 글자만
|
||||
* 굵게 + 붉은 계열은 중요 결과(Qd · 유출량 · 판정 · 유출율)만 · 빈칸에 기대는 계산 칸은 「-」.
|
||||
*
|
||||
@@ -27,6 +28,12 @@ export interface HydroInputs {
|
||||
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 {
|
||||
@@ -40,6 +47,8 @@ export interface HydroResult {
|
||||
flow: number | null;
|
||||
design_flow: number | null;
|
||||
n: number;
|
||||
/** 박스 수심 h · 세월교 · 물넘이 월류 높이 */
|
||||
depth: number | null;
|
||||
section_area: number | null;
|
||||
perimeter: number | null;
|
||||
radius: number | null;
|
||||
@@ -63,6 +72,8 @@ export interface HydroOptions {
|
||||
factors: number[];
|
||||
sections: Record<HydroInputs["section"], string>;
|
||||
pipe_kinds: Record<string, number>;
|
||||
/** 장별 처음 값(박스 n · 수심비 · 세월교 · 물넘이 n · 월류 폭 · 경사) */
|
||||
sheet_defaults: Record<"box" | "ford", Partial<HydroInputs>>;
|
||||
site: HydroSite;
|
||||
}
|
||||
|
||||
@@ -83,6 +94,9 @@ const NULLABLE = new Set<Key>([
|
||||
"slope_denominator",
|
||||
"n",
|
||||
"r24_mm",
|
||||
"box_width_m",
|
||||
"box_height_m",
|
||||
"overflow_width_m",
|
||||
]);
|
||||
|
||||
const DASH = "-";
|
||||
@@ -147,6 +161,39 @@ function pipeFigure(section: HydroInputs["section"]): SVGSVGElement {
|
||||
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 }
|
||||
@@ -169,6 +216,8 @@ export function buildHydroSheet(
|
||||
inputs: HydroInputs,
|
||||
options: HydroOptions,
|
||||
onChange: (next: HydroInputs) => void,
|
||||
/** 시설 이름(박스암거 · 세월교 · 물넘이포장) — 관이면 "" */
|
||||
kind = "",
|
||||
): HydroSheet {
|
||||
const site = options.site;
|
||||
const out = new Map<string, HTMLElement>();
|
||||
@@ -195,7 +244,9 @@ export function buildHydroSheet(
|
||||
const valid =
|
||||
parsed === null
|
||||
? NULLABLE.has(key)
|
||||
: Number.isFinite(parsed) && (key === "tc_min_minutes" ? parsed >= 0 : parsed > 0);
|
||||
: 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]);
|
||||
});
|
||||
@@ -302,36 +353,88 @@ export function buildHydroSheet(
|
||||
});
|
||||
left.classList.add("b08-hydro__card");
|
||||
|
||||
// ── 오른쪽 2. 단면 검토 ──
|
||||
const kinds = Object.entries(options.pipe_kinds).map(
|
||||
([kind, n]) => [kind, `${kind} (n ${n})`] as [string, string],
|
||||
// ── 오른쪽 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 texts = SECTION_TEXT[inputs.section];
|
||||
const nInput = numberInput("n", String(options.pipe_kinds[inputs.pipe_kind] ?? ""));
|
||||
const dims = node("div", "b08-hydro__dims");
|
||||
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")),
|
||||
rowF("하상경사 S = 1 /", numberInput("slope_denominator", "입력"), outCell("slope_pct")),
|
||||
rowF("조도계수 n", hint("비우면 관 종류 값"), nInput),
|
||||
);
|
||||
const figure = node("div", "b08-hydro__figure");
|
||||
figure.append(pipeFigure(inputs.section), dims);
|
||||
const verdict = node("div", "b08-hydro__verdict", DASH);
|
||||
verdict.dataset.b08HydroVerdict = "";
|
||||
const right = createCard({
|
||||
title: "2. 단면 검토",
|
||||
body: [
|
||||
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(`기본 ${sheetDefaults.depth_ratio}`), 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("유출량"),
|
||||
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,
|
||||
@@ -370,6 +473,7 @@ export function buildHydroSheet(
|
||||
: "",
|
||||
);
|
||||
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));
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""B08 수리집수면적표 — 박스암거 사각 단면 장(73-11) · 세월교 · 물넘이 월류 높이 장(73-12).
|
||||
|
||||
못박는 것
|
||||
- 장 = 시설이 정함(박스 · 세월교 · 물넘이포장 · 관)
|
||||
- 박스 h = 수심비 × H · A = B·h · P = B + 2h · 울진 1공구 B 3.0 · H 1.5 → h 1.2 · A 3.6 · P 5.4
|
||||
- 박스 n · 수심비 · 세월교 n · 월류 폭 · 경사 기본값 · 고치면 계산이 따라감
|
||||
- 월류 높이 = TS `fordHeightM` 거울(helper_65_ford_box 표) · 폭 바꾸면 높이 바뀜
|
||||
- [저장] 모델이 새 칸을 받음
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_Hydro import compute, ford_depth # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Hydro_Defaults import ( # noqa: E402
|
||||
DEFAULTS,
|
||||
SHEET_DEFAULTS,
|
||||
station_rows,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Router_Hydro import HydroSave # noqa: E402
|
||||
from config.config_system import FORD_MANNING_N, FORD_SLOPE # noqa: E402
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
HYDRO = {"area_km2": 0.5, "length_m": 800.0, "height_m": 200.0, "r24_mm": 300.0}
|
||||
|
||||
|
||||
def _inputs(sheet: str, **extra) -> dict:
|
||||
return {**DEFAULTS, **SHEET_DEFAULTS[sheet], "sheet": sheet, **HYDRO, **extra}
|
||||
|
||||
|
||||
def test_장은_시설이_정함() -> None:
|
||||
class P:
|
||||
def __init__(self, c: float, f: str) -> None:
|
||||
self.chainage_m, self.facility = c, f
|
||||
|
||||
rows = station_rows(
|
||||
[P(3, "ford_pavement"), P(1, "pipe"), P(2, "box_culvert"), P(4, "ford_bridge")]
|
||||
)
|
||||
assert [(r["sheet"], r["kind"]) for r in rows] == [
|
||||
("pipe", ""),
|
||||
("box", "박스암거"),
|
||||
("ford", "물넘이포장"),
|
||||
("ford", "세월교"),
|
||||
]
|
||||
|
||||
|
||||
def test_박스_울진_산출서_단면() -> None:
|
||||
r = compute(_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50))
|
||||
assert SHEET_DEFAULTS["box"]["n"] == 0.013 and SHEET_DEFAULTS["box"]["depth_ratio"] == 0.8
|
||||
assert r["n"] == 0.013
|
||||
assert r["depth"] == pytest.approx(1.2)
|
||||
assert r["section_area"] == pytest.approx(3.6)
|
||||
assert r["perimeter"] == pytest.approx(5.4)
|
||||
assert r["radius"] == pytest.approx(3.6 / 5.4)
|
||||
# 실무 29.236 = 1/n 75 · R 0.67 반올림 — 같은 n 이면 R 반올림 차(0.3 %)만 남음
|
||||
r75 = compute(_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50, n=1 / 75))
|
||||
assert r75["capacity"] == pytest.approx(29.236, rel=0.004)
|
||||
assert 75 * 0.67 ** (2 / 3) * math.sqrt(1 / 50) * 3.6 == pytest.approx(29.236, abs=0.001)
|
||||
assert r["capacity"] > r75["capacity"] # n 고치면 따라감
|
||||
assert r["ok"] is (r["capacity"] >= r["design_flow"])
|
||||
deeper = compute(
|
||||
_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50, depth_ratio=0.9)
|
||||
)
|
||||
assert deeper["depth"] == pytest.approx(1.35)
|
||||
|
||||
|
||||
def test_박스_내공_경사_빈칸이면_대시() -> None:
|
||||
r = compute(_inputs("box"))
|
||||
assert r["design_flow"] is not None
|
||||
assert r["depth"] is None and r["capacity"] is None and r["ok"] is None
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts_table() -> list:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", os.path.join(HERE, "helper_65_ford_box.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)["table"]
|
||||
|
||||
|
||||
def test_월류_높이_TS_거울(ts_table) -> None:
|
||||
assert SHEET_DEFAULTS["ford"]["n"] == FORD_MANNING_N
|
||||
assert 1 / SHEET_DEFAULTS["ford"]["slope_denominator"] == pytest.approx(FORD_SLOPE)
|
||||
assert SHEET_DEFAULTS["ford"]["overflow_width_m"] == 10.0
|
||||
for q, b, h in ts_table:
|
||||
assert ford_depth(q, b, FORD_MANNING_N, FORD_SLOPE) == h, (q, b)
|
||||
|
||||
|
||||
def test_세월교_폭_10_높이_자동_폭_바꾸면_바뀜(ts_table) -> None:
|
||||
r = compute(_inputs("ford"))
|
||||
qd = r["design_flow"]
|
||||
assert r["depth"] == ford_depth(qd, 10.0, FORD_MANNING_N, FORD_SLOPE)
|
||||
assert r["section_area"] == pytest.approx(10.0 * r["depth"])
|
||||
assert r["capacity"] >= qd and r["ok"] is True
|
||||
narrow = compute(_inputs("ford", overflow_width_m=5.0))
|
||||
assert narrow["depth"] > r["depth"]
|
||||
rough = compute(_inputs("ford", n=0.025))
|
||||
assert rough["depth"] > r["depth"]
|
||||
assert compute(_inputs("ford", overflow_width_m=None))["depth"] is None
|
||||
|
||||
|
||||
def test_저장_모델이_새_칸을_받음() -> None:
|
||||
box = _inputs("box", box_width_m=3.0, box_height_m=1.5, depth_ratio=0.8)
|
||||
ford = _inputs("ford", overflow_width_m=12.0)
|
||||
saved = HydroSave(stations={"1.000": box, "2.000": ford}).model_dump()["stations"]
|
||||
assert saved["1.000"]["sheet"] == "box" and saved["1.000"]["box_height_m"] == 1.5
|
||||
assert saved["2.000"]["sheet"] == "ford" and saved["2.000"]["overflow_width_m"] == 12.0
|
||||
with pytest.raises(ValueError):
|
||||
HydroSave(stations={"1.000": {**box, "depth_ratio": 1.2}})
|
||||
@@ -1,4 +1,4 @@
|
||||
"""B08 수리집수면적표 — 박스암거 · 세월교 지점은 빈 장(계산 방법 정하는 중 · PLAN 73-1)."""
|
||||
"""B08 수리집수면적표 — 박스암거 · 세월교 지점은 따로 장(73-1 빈 장 → 73-11 · 73-12 계산 장)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -29,9 +29,9 @@ def test_박스_세월교만_빈_장_탭_수는_그_구조물_수() -> None:
|
||||
]
|
||||
rows = station_rows(points)
|
||||
assert [r["chainage_m"] for r in rows] == [85.0, 173.0, 300.0, 352.0, 400.0]
|
||||
pending = [r for r in rows if r["pending"]]
|
||||
assert len(pending) == 3 # 세월교 2 · 박스 1
|
||||
assert {r["kind"] for r in pending} == {"세월교", "박스암거"}
|
||||
# 관 · 물넘이포장은 지금 관 계산 장 그대로
|
||||
assert [r["facility"] for r in rows if not r["pending"]] == ["pipe", "ford_pavement"]
|
||||
assert all(r["kind"] == "" for r in rows if not r["pending"])
|
||||
# 73-11 · 73-12 뒤 — 빈 장 대신 박스 · 월류 장(물넘이포장도 월류 장)
|
||||
special = [r for r in rows if r["sheet"] != "pipe"]
|
||||
assert len(special) == 4 # 세월교 2 · 박스 1 · 물넘이 1
|
||||
assert {r["kind"] for r in special} == {"세월교", "박스암거", "물넘이포장"}
|
||||
assert [r["facility"] for r in rows if r["sheet"] == "pipe"] == ["pipe"]
|
||||
assert all(r["kind"] == "" for r in rows if r["sheet"] == "pipe")
|
||||
|
||||
Reference in New Issue
Block a user