feat(B05): 배수 추천 구조물·관경 자동 설정 + 물넘이·세월교 개략 단면
지시 2 — 배수유역도가 유효직경 다음에 추천을 보여 주고, 초기 전처리 계산이 그 값으로 관 지점을 설정한다: - recommend_structure(): 유효직경 임계로 구조물·관경을 고른다. D≤1500 배관(규격 스냅 800·1000·1200·1500), 1500<D≤2000 BOX암거 후보, D>2000 세월교 검토. 판정 근거는 유량뿐 — 계곡 횡단경사·하천 차수는 지형 계산이 필요해 뺐고 화면이 "현장 확인"으로 안내한다(2026-08-17 사용자 확정) - WatershedBasin에 recommended_facility·recommended_diameter_mm·required_area_m2 추가, 응답 payload와 DetailBasin 타입까지 배선 - _apply_recommendations(): 자동 배치 관의 **빈칸에만** 추천을 채운다. 사용자가 놓은 관(source=user)·이미 바꾼 시설·이미 있는 옵션 키는 건드리지 않는다. carry_facility_attributes 뒤에 얹어 승계값이 덮이지 않게 했다 - 유역 목록 표기: `Ø1234mm → 추천 Ø1500 배관` / `→ BOX암거 후보` - 추천 관경 목록은 레지스트리 pipe_diameter_mm 선택지와 같아야 한다 — 폼에서 못 고르는 값을 추천하지 않도록 테스트로 묶었다 지시 1 — 물넘이포장·세월교는 지정할 옵션이 거의 없으니 계산값이라도 보여 준다: - 레지스트리에 ford_width_m(월류 폭) 신설. 기본값 없음 — 지식DB에 폭 근거가 없어 프로그램이 지어내지 않는다 - fordSection(): 설계유량과 폭으로 필요 수심을 되짚는다. 광폭 근사로 출발해 실제 동수반경(R=A/P)으로 수렴시킨다 — 근사값을 그대로 쓰면 통수능이 늘 설계유량에 못 미쳐 판정이 무의미해진다. n=0.017·S=10%는 실무 수리계산서 역산값 (울진1 물넘이·관 시트가 둘 다 S=0.10에서 검산 일치) - 폼에 월류 폭 한 칸 + 읽기전용 요약(설계유량·필요 수심·필요 단면·유속). 설계유량은 유역 payload에서 폼까지 배선(onPipesChanged → Page → setFacility) ⚠ 실무 시트의 수심 0.27m는 Qd 역산값이 아니라 가정 수심이다(B=20·h=0.27의 통수능 41.2㎥/s ≫ 그 시트 Qd 2.54㎥/s). 실무는 단면을 가정해 통수능을 검증하고, 프로그램은 필요 최소 수심을 낸다 — 화면 문구도 "필요 수심"으로 적었다. 검증: pytest tmp/tests 124 passed·7 skipped(신규 17건), npm run typecheck 무오류, npm run build 성공. 물넘이 산식은 프론트 전용이라 수치 손검증으로 대신했다 — Qd=2.54·B=20에서 필요 수심 0.0503m·유속 2.526m/s, 통수능이 2.5400㎥/s로 Qd와 일치. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -480,6 +480,12 @@ export interface DetailBasin {
|
||||
design_flow_m3s?: number | null;
|
||||
/** 유효직경이 관 최대 규격 초과 — 세월교·물넘이·교량 검토 대상(임도설치규정 제12조). */
|
||||
bridge_required?: boolean;
|
||||
/** 필요 통수단면적(㎡) = 설계유량 / 유속. 물넘이·세월교 개략 단면의 출발값. */
|
||||
required_area_m2?: number | null;
|
||||
/** 유량 근거 추천 구조물(2026-08-17 사용자 확정) — pipe/box_culvert/ford_bridge. */
|
||||
recommended_facility?: PipeFacility;
|
||||
/** 추천 관경(㎜) — 배관일 때만. 레지스트리 선택지로 스냅한 값이다. */
|
||||
recommended_diameter_mm?: number | null;
|
||||
}
|
||||
|
||||
export interface DetailBasinResponse {
|
||||
|
||||
@@ -26,6 +26,7 @@ from B04_PreProcess.B04_PreProcess_Engine_Watershed_Export import drainage_dir
|
||||
from common_util.common_util_drainage_context import DrainageContext, load_drainage_context
|
||||
from common_util.common_util_drainage_detail import DrainageDetail, build_detail
|
||||
from common_util.common_util_drainage_pipes import (
|
||||
PIPE_FACILITY_PIPE,
|
||||
PIPE_SOURCE_USER,
|
||||
PipePoint,
|
||||
carry_facility_attributes,
|
||||
@@ -64,7 +65,10 @@ def _build(
|
||||
rebuilt = [PipePoint(chainage_m=pipe.chainage_m, source=pipe.reason) for pipe in detail.pipes]
|
||||
# 계산기는 chainage만 다뤄 시설 종류·구간·옵션이 사라진다 — 원본에서 되붙인다
|
||||
# (2026-08-17 계곡 통과 시설: 배관/BOX암거/물넘이/세월교).
|
||||
return detail, carry_facility_attributes(rebuilt, points or [])
|
||||
carried = carry_facility_attributes(rebuilt, points or [])
|
||||
# 되붙인 뒤에 추천을 얹는다 — 순서가 뒤바뀌면 승계된 사용자 값이 추천에 덮인다.
|
||||
_apply_recommendations(detail, carried)
|
||||
return detail, carried
|
||||
|
||||
|
||||
def _retag(pipes: list[StructureCandidate], points: list[PipePoint]) -> None:
|
||||
@@ -78,6 +82,39 @@ def _retag(pipes: list[StructureCandidate], points: list[PipePoint]) -> None:
|
||||
pipe.reason = source or PIPE_SOURCE_USER
|
||||
|
||||
|
||||
def _apply_recommendations(detail: DrainageDetail, points: list[PipePoint]) -> None:
|
||||
"""유역별 추천(시설 종류·관경)을 관 지점 옵션의 **빈칸에만** 채운다.
|
||||
|
||||
"초기 전처리 계산에서 해당값으로 설정"(2026-08-17 사용자 지시) — 자동 배치된 관이
|
||||
유역 유량에 맞는 규격을 처음부터 갖고 있어야 폼을 열지 않아도 하류가 값을 받는다.
|
||||
|
||||
덮어쓰지 않는 두 경우: ① 사용자가 직접 놓거나 옮긴 관(`source == "user"`) ②
|
||||
그 옵션 키가 이미 있는 관. 설계자가 고른 값을 재계산이 되돌리면 안 되기 때문이다.
|
||||
"""
|
||||
by_chainage = {round(basin.chainage_m, 2): basin for basin in detail.basins}
|
||||
for point in points:
|
||||
if point.source == PIPE_SOURCE_USER:
|
||||
continue
|
||||
basin = by_chainage.get(round(point.chainage_m, 2))
|
||||
if basin is None:
|
||||
continue
|
||||
options = dict(point.options or {})
|
||||
# 시설 종류는 아직 손대지 않은 기본 배관일 때만 추천으로 바꾼다.
|
||||
if (
|
||||
point.facility == PIPE_FACILITY_PIPE
|
||||
and basin.recommended_facility != PIPE_FACILITY_PIPE
|
||||
):
|
||||
point.facility = basin.recommended_facility
|
||||
if (
|
||||
point.facility == PIPE_FACILITY_PIPE
|
||||
and basin.recommended_diameter_mm is not None
|
||||
and "pipe_diameter_mm" not in options
|
||||
):
|
||||
options["pipe_diameter_mm"] = basin.recommended_diameter_mm
|
||||
if options:
|
||||
point.options = options
|
||||
|
||||
|
||||
def _payload(
|
||||
project_id: UUID,
|
||||
context: DrainageContext,
|
||||
@@ -136,6 +173,11 @@ def _payload(
|
||||
"design_flow_m3s": basin.design_flow_m3s,
|
||||
# 유효직경이 관 최대 규격 초과 — 세월교·물넘이·교량 검토 대상(임도설치규정 제12조).
|
||||
"bridge_required": basin.bridge_required,
|
||||
# 필요 통수단면적(㎡) — 물넘이·세월교 개략 단면의 출발값.
|
||||
"required_area_m2": basin.required_area_m2,
|
||||
# 유량 근거 추천(2026-08-17 사용자 확정) — 배관이면 규격 스냅 관경이 붙는다.
|
||||
"recommended_facility": basin.recommended_facility,
|
||||
"recommended_diameter_mm": basin.recommended_diameter_mm,
|
||||
}
|
||||
for basin in detail.basins
|
||||
],
|
||||
|
||||
@@ -276,7 +276,17 @@
|
||||
"abbr": "물넘이"
|
||||
},
|
||||
"drawing_views": ["plan", "profile", "cross_section", "detail", "quantity"],
|
||||
"options": []
|
||||
"options": [
|
||||
{
|
||||
"key": "ford_width_m",
|
||||
"label": "월류 폭",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type_id": "ford_bridge",
|
||||
@@ -314,6 +324,15 @@
|
||||
"unit": "련",
|
||||
"default": null,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "ford_width_m",
|
||||
"label": "월류 폭",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
BOX_SIZE_PRESETS,
|
||||
createRevetmentFields,
|
||||
createWingFields,
|
||||
fordSection,
|
||||
grid,
|
||||
group,
|
||||
INLET_REVET_KEYS,
|
||||
@@ -141,6 +142,8 @@ export interface FacilityOptionsForm {
|
||||
setFacility: (
|
||||
facility: PipeFacility | "revetment" | null,
|
||||
options?: Record<string, string | number>,
|
||||
/** 담당 유역의 설계유량(㎥/s) — 물넘이·세월교 개략 단면의 입력. 없으면 안내만. */
|
||||
designFlow?: number | null,
|
||||
) => void;
|
||||
/** 현재 필드 값을 시설 옵션으로 읽는다(빈 값 생략). */
|
||||
readOptions: () => Record<string, string | number>;
|
||||
@@ -256,6 +259,33 @@ export function createFacilityOptionsForm(
|
||||
const fordCount = numberInput("1", "1", "련");
|
||||
const fordRow = grid(labeled("수량 (련)", fordCount));
|
||||
|
||||
// ── 물넘이·세월교 개략 단면 — 지정할 옵션이 거의 없으니 계산값이라도 보여 준다
|
||||
// (2026-08-17 사용자 지시). 폭만 받고 설계유량으로 수심·통수능을 되짚는다.
|
||||
const fordWidth = numberInput("0.1");
|
||||
const fordWidthRow = grid(labeled("월류 폭 (m)", fordWidth));
|
||||
const fordSummary = document.createElement("p");
|
||||
fordSummary.className = "b05-drainage__facility-note";
|
||||
/** 담당 유역 설계유량(㎥/s) — 개략 단면의 입력. 유역이 없으면 null. */
|
||||
let designFlowM3s: number | null = null;
|
||||
|
||||
function syncFordSummary(): void {
|
||||
if (designFlowM3s === null) {
|
||||
fordSummary.textContent =
|
||||
"담당 유역의 설계유량이 아직 없습니다 — [유역 분석] 후 개략 단면이 나옵니다.";
|
||||
return;
|
||||
}
|
||||
const section = fordSection(designFlowM3s, Number.parseFloat(fordWidth.value));
|
||||
const head = `설계유량 ${designFlowM3s.toFixed(3)} ㎥/s`;
|
||||
if (!section) {
|
||||
fordSummary.textContent = `${head} · 월류 폭을 넣으면 필요 수심·단면을 계산합니다.`;
|
||||
return;
|
||||
}
|
||||
// 필요 최소 수심이다 — 실제 설계 수심은 여기에 여유를 더해 정한다.
|
||||
fordSummary.textContent =
|
||||
`${head} · 필요 수심 ${section.depthM.toFixed(2)} m · ` +
|
||||
`필요 단면 ${section.areaM2.toFixed(2)} ㎡ · 유속 ${section.velocityMs.toFixed(2)} m/s`;
|
||||
}
|
||||
|
||||
root.append(
|
||||
pipeRow,
|
||||
inletGroup.root,
|
||||
@@ -264,7 +294,9 @@ export function createFacilityOptionsForm(
|
||||
boxWrap,
|
||||
wingInFields.root,
|
||||
wingOutFields.root,
|
||||
fordWidthRow,
|
||||
fordRow,
|
||||
fordSummary,
|
||||
);
|
||||
|
||||
let current: PipeFacility | "revetment" | null = null;
|
||||
@@ -283,7 +315,11 @@ export function createFacilityOptionsForm(
|
||||
boxWrap.hidden = !isBox;
|
||||
wingInFields.root.hidden = !isBox;
|
||||
wingOutFields.root.hidden = !isBox;
|
||||
const isFord = current === "ford_pavement" || current === "ford_bridge";
|
||||
fordRow.hidden = current !== "ford_bridge";
|
||||
fordWidthRow.hidden = !isFord;
|
||||
fordSummary.hidden = !isFord;
|
||||
if (isFord) syncFordSummary();
|
||||
if (isBox) {
|
||||
syncBoxSize();
|
||||
wingInFields.syncVisibility();
|
||||
@@ -321,12 +357,14 @@ export function createFacilityOptionsForm(
|
||||
...wingInFields.inputs,
|
||||
...wingOutFields.inputs,
|
||||
fordCount,
|
||||
fordWidth,
|
||||
].forEach((input) => input.addEventListener("change", emit));
|
||||
|
||||
return {
|
||||
root,
|
||||
setFacility(facility, options = {}) {
|
||||
setFacility(facility, options = {}, designFlow = null) {
|
||||
current = facility;
|
||||
designFlowM3s = designFlow ?? null;
|
||||
if (facility === null) {
|
||||
syncVisibility();
|
||||
return;
|
||||
@@ -365,6 +403,7 @@ export function createFacilityOptionsForm(
|
||||
wingInFields.write(options);
|
||||
wingOutFields.write(options);
|
||||
fordCount.value = isFord ? text("pipe_count") : "";
|
||||
fordWidth.value = text("ford_width_m");
|
||||
syncVisibility();
|
||||
},
|
||||
readOptions() {
|
||||
@@ -389,11 +428,14 @@ export function createFacilityOptionsForm(
|
||||
putNumber(options, "body_height_m", boxHeight.value);
|
||||
wingInFields.read(options);
|
||||
wingOutFields.read(options);
|
||||
} else if (current === "ford_pavement") {
|
||||
putNumber(options, "ford_width_m", fordWidth.value);
|
||||
} else if (current === "ford_bridge") {
|
||||
options.pipe_kind = pipeMaterial.value;
|
||||
options.pipe_diameter_mm = Number(pipeDiameter.value);
|
||||
const count = Number.parseInt(fordCount.value, 10);
|
||||
if (Number.isFinite(count) && count > 0) options.pipe_count = count;
|
||||
putNumber(options, "ford_width_m", fordWidth.value);
|
||||
}
|
||||
return options;
|
||||
},
|
||||
|
||||
@@ -8,6 +8,50 @@
|
||||
* 유입·유출·독립 기슭막이(D4)가 같은 조각을 키만 바꿔 쓰는 구조도 그대로다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { FORD_MANNING_N, FORD_SLOPE } from "@config/config_frontend";
|
||||
|
||||
/* ── 물넘이·세월교 개략 단면 ────────────────────────────────────────────── */
|
||||
|
||||
/** 월류 단면 한 벌 — 설계유량을 흘리는 데 **필요한** 수심·단면적과 그때의 유속. */
|
||||
export interface FordSection {
|
||||
depthM: number;
|
||||
areaM2: number;
|
||||
velocityMs: number;
|
||||
}
|
||||
|
||||
/** 물넘이(노면 개수로)의 개략 단면 — 설계유량과 월류 폭으로 필요 수심을 되짚는다.
|
||||
*
|
||||
* 광폭 직사각형(B ≫ h)으로 보면 Manning을 h에 대해 바로 풀 수 있다:
|
||||
* Qd = B·h·(1/n)·h^(2/3)·√S → h = (Qd·n / (B·√S))^(3/5)
|
||||
* 다만 실제 동수반경은 R = A/P (P = B + 2h)로 h보다 작아, 광폭 근사값을 그대로 쓰면
|
||||
* 통수능이 설계유량에 조금 못 미친다. 그래서 그 값을 출발점 삼아 실제 R로 몇 번
|
||||
* 되짚어 **B·h·V(h) = Qd**가 되는 h로 수렴시킨다.
|
||||
*
|
||||
* n·S는 실무 수리계산서 역산값이다(`config_frontend.FORD_MANNING_N`·`FORD_SLOPE`,
|
||||
* 2026-08-17 확인). 폭 B는 지식DB에 수치 근거가 없어 사용자가 넣는다 — 프로그램이
|
||||
* 지어내지 않는다. 유량이나 폭이 없으면 null(표시할 값 없음)이다.
|
||||
*
|
||||
* ⚠ 여기서 내는 수심은 **필요 최소 수심**이다. 실무 수리계산서가 적는 수심은 설계자가
|
||||
* 가정한 값이고(울진1: B=20m·h=0.27m → 통수능 41.2㎥/s ≫ Qd 2.54㎥/s), 그 시트는
|
||||
* 가정 단면의 통수능이 설계유량을 넘는지 검증하는 방식이다. 둘은 의미가 다르다. */
|
||||
export function fordSection(designFlowM3s: number | null, widthM: number): FordSection | null {
|
||||
if (!designFlowM3s || designFlowM3s <= 0 || !Number.isFinite(widthM) || widthM <= 0) return null;
|
||||
const slopeRoot = Math.sqrt(FORD_SLOPE);
|
||||
const velocityAt = (depth: number): number =>
|
||||
(1 / FORD_MANNING_N) * Math.pow((widthM * depth) / (widthM + 2 * depth), 2 / 3) * slopeRoot;
|
||||
// 광폭 근사 출발값 — 실제 R보다 크게 잡히므로 아래 반복이 조금씩 키워 수렴한다.
|
||||
let depth = Math.pow((designFlowM3s * FORD_MANNING_N) / (widthM * slopeRoot), 3 / 5);
|
||||
for (let index = 0; index < 20; index += 1) {
|
||||
const next = designFlowM3s / (widthM * velocityAt(depth));
|
||||
if (Math.abs(next - depth) < 1e-6) {
|
||||
depth = next;
|
||||
break;
|
||||
}
|
||||
depth = next;
|
||||
}
|
||||
return { depthM: depth, areaM2: widthM * depth, velocityMs: velocityAt(depth) };
|
||||
}
|
||||
|
||||
/* ── 폼 조각 ────────────────────────────────────────────────────────────── */
|
||||
|
||||
export function labeled(text: string, input: HTMLElement): HTMLLabelElement {
|
||||
|
||||
@@ -102,6 +102,8 @@ export interface DrainagePanelCallbacks {
|
||||
pipes: Array<{
|
||||
chainage_m: number;
|
||||
effective_diameter_mm: number | null;
|
||||
/** 담당 유역의 설계유량(㎥/s, 100년빈도·2.0배). 물넘이·세월교 개략 단면의 입력이다. */
|
||||
design_flow_m3s?: number | null;
|
||||
facility: PipeFacility;
|
||||
start_m?: number;
|
||||
end_m?: number;
|
||||
@@ -400,6 +402,8 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
chainage_m: chainage,
|
||||
effective_diameter_mm:
|
||||
basin && !basin.bridge_required ? (basin.pipe_diameter_mm ?? null) : null,
|
||||
// 설계유량은 세월교 검토 유역에도 붙인다 — 물넘이·세월교 단면이 이 값에서 나온다.
|
||||
design_flow_m3s: basin?.design_flow_m3s ?? null,
|
||||
facility: attributes?.facility ?? "pipe",
|
||||
start_m: attributes?.start_m,
|
||||
end_m: attributes?.end_m,
|
||||
|
||||
@@ -111,6 +111,14 @@ export function addLayerToggle(
|
||||
}
|
||||
|
||||
/** 유역 제원 목록을 다시 그린다. 항목을 누르면 `onPick`으로 번호를 돌려준다. */
|
||||
/** 유역 추천을 한 조각 문구로 — 배관이면 규격 스냅 관경, BOX암거면 후보 표기.
|
||||
* 추천이 아직 없는(강우량표 전) 유역은 계산 유효직경만 남긴다. */
|
||||
function recommendationOf(basin: DetailBasin): string {
|
||||
if (basin.recommended_facility === "box_culvert") return L("B05_Drainage_Basin_RecBox");
|
||||
if (basin.recommended_diameter_mm == null) return L("B05_Drainage_Basin_Undecided");
|
||||
return L("B05_Drainage_Basin_RecPipe").replace("{d}", String(basin.recommended_diameter_mm));
|
||||
}
|
||||
|
||||
export function renderBasinRows(
|
||||
container: HTMLElement,
|
||||
basins: ReadonlyArray<DetailBasin>,
|
||||
@@ -131,6 +139,8 @@ export function renderBasinRows(
|
||||
metrics.className = "b05-drainage__basin-metrics";
|
||||
// 유효직경은 강우량표(rainfall_table.json)가 생기기 전까지 null → "미정" 표기.
|
||||
// 관 최대 규격 초과 계류 유역은 관이 아니라 세월교 대상 — "세월교 검토"로 표기한다.
|
||||
// 그 아래 유역은 계산 유효직경 뒤에 추천(규격 스냅 관경 또는 BOX암거)을 덧붙인다
|
||||
// (2026-08-17 사용자 지시 — 추천 근거는 유량뿐, 지형 조건은 툴팁으로 안내).
|
||||
const pipe = basin.bridge_required
|
||||
? L("B05_Drainage_Basin_Bridge").replace(
|
||||
"{d}",
|
||||
@@ -138,7 +148,7 @@ export function renderBasinRows(
|
||||
)
|
||||
: basin.pipe_diameter_mm === null
|
||||
? L("B05_Drainage_Basin_Undecided")
|
||||
: `Ø${Math.round(basin.pipe_diameter_mm)}mm`;
|
||||
: `Ø${Math.round(basin.pipe_diameter_mm)}mm → ${recommendationOf(basin)}`;
|
||||
metrics.textContent = L("B05_Drainage_Basin_Metrics")
|
||||
.replace("{area}", formatArea(basin.area_m2))
|
||||
.replace("{relief}", basin.relief_m.toFixed(1))
|
||||
@@ -154,6 +164,8 @@ export function renderBasinRows(
|
||||
.replace("{i}", String(basin.intensity_mm_hr ?? "-"))
|
||||
.replace("{q}", String(basin.design_flow_m3s));
|
||||
}
|
||||
// 추천 근거의 한계를 같은 툴팁에 밝힌다 — 유량만 보고 고른 값이다.
|
||||
row.title += "\n" + L("B05_Drainage_Basin_RecNote");
|
||||
row.append(badge, metrics);
|
||||
row.addEventListener("click", () => onPick(basin.index));
|
||||
container.append(row);
|
||||
|
||||
@@ -98,6 +98,7 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
||||
start_m: pipe.start_m,
|
||||
end_m: pipe.end_m,
|
||||
source: pipe.source,
|
||||
design_flow_m3s: pipe.design_flow_m3s,
|
||||
options: pipe.options,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -91,6 +91,8 @@ export interface RouteProfilePanelCallbacks {
|
||||
pipes: Array<{
|
||||
chainage_m: number;
|
||||
effective_diameter_mm: number | null;
|
||||
/** 담당 유역의 설계유량(㎥/s) — 물넘이·세월교 개략 단면의 입력. */
|
||||
design_flow_m3s?: number | null;
|
||||
facility: PipeFacility;
|
||||
start_m?: number;
|
||||
end_m?: number;
|
||||
|
||||
@@ -60,6 +60,8 @@ export interface PipeFacilityItem {
|
||||
end_m?: number;
|
||||
/** 자동 배치 출처(stream/spacing/user) — 배관 유형(계곡부형/보완형) 제안 근거. */
|
||||
source?: PipeSource;
|
||||
/** 담당 유역의 설계유량(㎥/s) — 물넘이·세월교 개략 단면 계산의 입력. */
|
||||
design_flow_m3s?: number | null;
|
||||
/** 부속 옵션(유형·집수정·기슭막이·돌붙임·날개벽·세월교 관 등). */
|
||||
options?: Record<string, string | number>;
|
||||
}
|
||||
@@ -246,8 +248,11 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
}
|
||||
|
||||
/** 종류에 맞춰 부속 옵션 서브폼을 켠다(계곡 통과 시설·독립 기슭막이, 값은 인자로). */
|
||||
function syncFacilityForm(options: Record<string, string | number> = {}): void {
|
||||
facilityOptions.setFacility(facilityFormKind(currentType()), options);
|
||||
function syncFacilityForm(
|
||||
options: Record<string, string | number> = {},
|
||||
designFlow: number | null = null,
|
||||
): void {
|
||||
facilityOptions.setFacility(facilityFormKind(currentType()), options, designFlow);
|
||||
}
|
||||
|
||||
/** 타입의 옵션 스키마대로 입력 칸을 다시 그린다 — 상세(detail) 옵션은 B06/B07
|
||||
@@ -385,7 +390,7 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
startFields.write(null, step);
|
||||
endFields.write(null, step);
|
||||
memoField.value = "";
|
||||
syncFacilityForm(pipe.options ?? {});
|
||||
syncFacilityForm(pipe.options ?? {}, pipe.design_flow_m3s ?? null);
|
||||
[anchorFields, startFields, endFields].forEach((fields) => fields.clearInvalid());
|
||||
syncPlacementFields();
|
||||
syncButtons();
|
||||
|
||||
@@ -36,6 +36,11 @@ from B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze import find_inflow_h
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Export import STAGES
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Flow import largest_ring, polygonize_labels
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Grid import GridSpec
|
||||
from common_util.common_util_drainage_pipes import (
|
||||
PIPE_FACILITY_BOX,
|
||||
PIPE_FACILITY_FORD_BRIDGE,
|
||||
PIPE_FACILITY_PIPE,
|
||||
)
|
||||
from common_util.common_util_route_geometry import (
|
||||
RouteVertex,
|
||||
StructureCandidate,
|
||||
@@ -44,11 +49,13 @@ from common_util.common_util_route_geometry import (
|
||||
)
|
||||
from common_util.common_util_wamis_rainfall import idf_intensity
|
||||
from config.config_system import (
|
||||
DRAINAGE_BOX_THRESHOLD_MM,
|
||||
DRAINAGE_BRIDGE_THRESHOLD_MM,
|
||||
DRAINAGE_DESIGN_FLOW_FACTOR,
|
||||
DRAINAGE_DITCH_SAMPLE_M,
|
||||
DRAINAGE_FLOW_AREA_RATIO,
|
||||
DRAINAGE_MANNING_N,
|
||||
DRAINAGE_RECOMMEND_DIAMETERS_MM,
|
||||
DRAINAGE_PIPE_MAX_SPACING_M,
|
||||
DRAINAGE_PIPE_MIN_SPACING_M,
|
||||
DRAINAGE_PIPE_SLOPE_DEG,
|
||||
@@ -57,6 +64,7 @@ from config.config_system import (
|
||||
DRAINAGE_TC_MIN_MINUTES,
|
||||
DRAINAGE_VELOCITY_MAX_MS,
|
||||
DRAINAGE_VELOCITY_MIN_MS,
|
||||
PIPE_DEFAULT_DIAMETER_MM,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -108,6 +116,12 @@ class WatershedBasin:
|
||||
# 유효직경이 관 최대 규격을 넘는 계류 유역 — 관이 아니라 세월교·물넘이·교량 대상
|
||||
# (임도설치규정 제12조). 화면은 "세월교 검토"로 표기하고 관경 자동 지정을 하지 않는다.
|
||||
bridge_required: bool = False
|
||||
# 필요 통수단면적(㎡) = 설계유량 / 유속. 물넘이·세월교 개략 단면의 출발값이다.
|
||||
required_area_m2: float | None = None
|
||||
# 유효직경으로 고른 추천 구조물·관경 (2026-08-17 사용자 확정: 유량 근거만).
|
||||
# 배관이면 레지스트리 선택지로 스냅한 관경이 붙고, BOX암거·세월교면 None이다.
|
||||
recommended_facility: str = "pipe"
|
||||
recommended_diameter_mm: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -519,6 +533,7 @@ def assemble_basins(
|
||||
relief = max(0.0, highest - outlet_z)
|
||||
flow_length = float(routing.path_length[member].max())
|
||||
sizing = size_pipe(area, relief, flow_length, idf)
|
||||
facility, recommended = recommend_structure(sizing["diameter_mm"] if sizing else None)
|
||||
basins.append(
|
||||
WatershedBasin(
|
||||
index=len(basins) + 1,
|
||||
@@ -536,6 +551,9 @@ def assemble_basins(
|
||||
bridge_required=bool(
|
||||
sizing and sizing["diameter_mm"] > DRAINAGE_BRIDGE_THRESHOLD_MM
|
||||
),
|
||||
required_area_m2=sizing["required_area_m2"] if sizing else None,
|
||||
recommended_facility=facility,
|
||||
recommended_diameter_mm=recommended,
|
||||
)
|
||||
)
|
||||
return basins
|
||||
@@ -617,9 +635,38 @@ def size_pipe(
|
||||
"intensity_mm_hr": round(intensity, 1),
|
||||
"design_flow_m3s": round(design_flow, 4),
|
||||
"diameter_mm": round(diameter * 1000.0, 1),
|
||||
# 물넘이·세월교는 관이 아니라 개수로라 직경 대신 이 단면적에서 출발한다.
|
||||
"required_area_m2": round(required_area, 4),
|
||||
}
|
||||
|
||||
|
||||
def recommend_structure(diameter_mm: float | None) -> tuple[str, int | None]:
|
||||
"""유효직경으로 추천 구조물과 관경을 고른다 (2026-08-17 사용자 확정: 유량 근거만).
|
||||
|
||||
지식DB 선정 트리(개거_세월시설 §5·횡단배수관_암거 §5)에서 유량 조건만 취했다 —
|
||||
계곡 횡단경사·하천 차수는 지형 계산이 필요해 화면이 "현장 확인"으로 안내한다.
|
||||
|
||||
D ≤ 1,500㎜ 배관 (레지스트리 선택지로 스냅, 하한 800㎜)
|
||||
1,500 < D ≤ 2,000 BOX암거 후보
|
||||
D > 2,000㎜ 세월교·물넘이 검토 (관 최대 규격 초과)
|
||||
|
||||
유효직경을 못 구한 유역(강우량표 없음)은 배관·관경 미정으로 둔다.
|
||||
"""
|
||||
if diameter_mm is None:
|
||||
return PIPE_FACILITY_PIPE, None
|
||||
if diameter_mm > DRAINAGE_BRIDGE_THRESHOLD_MM:
|
||||
return PIPE_FACILITY_FORD_BRIDGE, None
|
||||
if diameter_mm > DRAINAGE_BOX_THRESHOLD_MM:
|
||||
return PIPE_FACILITY_BOX, None
|
||||
# 별표2 (나) 예외 하한 800㎜ — 계산값이 더 작아도 그 아래로는 내리지 않는다.
|
||||
need = max(float(PIPE_DEFAULT_DIAMETER_MM), diameter_mm)
|
||||
size = next(
|
||||
(value for value in DRAINAGE_RECOMMEND_DIAMETERS_MM if value >= need),
|
||||
DRAINAGE_RECOMMEND_DIAMETERS_MM[-1],
|
||||
)
|
||||
return PIPE_FACILITY_PIPE, int(size)
|
||||
|
||||
|
||||
def estimate_pipe_diameter_mm(
|
||||
area_m2: float,
|
||||
relief_m: float,
|
||||
|
||||
@@ -147,6 +147,13 @@ export const PIPE_DEFAULT_TYPE = "파형강관";
|
||||
/** 자동 지정 기본 관경 — 별표2 (나) 예외 하한 800mm. 유효직경이 더 크면 바로 위 규격. */
|
||||
export const PIPE_DEFAULT_DIAMETER_MM = 800;
|
||||
|
||||
/** 물넘이 개수로 조도계수 — KDS 표 2.7-1 콘크리트 수로 "보통"값이자 실무 물넘이 관측치.
|
||||
* 백엔드 `config_system.FORD_MANNING_N`과 같은 값이어야 한다. */
|
||||
export const FORD_MANNING_N = 0.017;
|
||||
/** 물넘이 수리계산 경사 — 실무 수리계산서 역산값 10%(2026-08-17 확인).
|
||||
* 백엔드 `config_system.FORD_SLOPE`와 같은 값이어야 한다. */
|
||||
export const FORD_SLOPE = 0.1;
|
||||
|
||||
export const ESCAPE_ROUTE_WIDTHS_M = [1.5, 2.0, 2.5, 3.0] as const;
|
||||
export const ESCAPE_ROUTE_DEFAULT_WIDTH_M = 2.0;
|
||||
export const STRUCTURE_ETC_DEFAULT_NAME = "기타 구조물";
|
||||
|
||||
@@ -385,6 +385,22 @@ DRAINAGE_FLOW_AREA_RATIO = float(os.getenv("DRAINAGE_FLOW_AREA_RATIO", "0.7"))
|
||||
# 세월교·물넘이·교량 대상이다 — 「임도설치 및 관리 등에 관한 규정」 제12조: 계류 횡단
|
||||
# 구간은 배수구 막힘 우려가 없는 물넘이 포장(세월교) 또는 교량으로 설계(2026-08-05 조사).
|
||||
DRAINAGE_BRIDGE_THRESHOLD_MM = float(os.getenv("DRAINAGE_BRIDGE_THRESHOLD_MM", "2000"))
|
||||
# BOX암거 전환 문턱(mm). 교본 "수리계산 Ø1,500㎜ 이상 유역·협곡·횡단경사 40% 이내"에서
|
||||
# 유량 조건만 취한 값이다 — 횡단경사·협곡 판정은 지형 계산이 필요해 화면이 "현장 확인"으로
|
||||
# 안내한다 (2026-08-17 사용자 확정: 추천은 유량 근거만).
|
||||
DRAINAGE_BOX_THRESHOLD_MM = float(os.getenv("DRAINAGE_BOX_THRESHOLD_MM", "1500"))
|
||||
# 추천 관경 규격(mm) — B05 레지스트리 `pipe_diameter_mm` 선택지와 **같아야 한다**.
|
||||
# 폼에서 고를 수 없는 값을 추천하면 사용자가 그대로 확정하지 못한다.
|
||||
DRAINAGE_RECOMMEND_DIAMETERS_MM = (800, 1000, 1200, 1500)
|
||||
|
||||
# ── 물넘이포장·세월교 개략 단면 (2026-08-17 실무문서 역산 확인) ──
|
||||
# 물넘이는 노면 개수로다. 조도계수 0.017은 KDS 표 2.7-1 콘크리트 수로 "보통"값이자
|
||||
# 실무 물넘이 관측치와 일치하고, 경사 10%는 실무 수리계산서 역산값이다 — 울진1공구
|
||||
# 물넘이 시트(B=20m·h=0.27m·n=0.017 → V=7.636 m/s)와 같은 공사지 관 시트(Ø1000·240°
|
||||
# 유효단면·n=0.025 → V=5.694 m/s)가 둘 다 S=0.10에서 검산이 맞는다
|
||||
# (original/실무문서/_숨김탭분석.md §6-1·§6-2).
|
||||
FORD_MANNING_N = float(os.getenv("FORD_MANNING_N", "0.017"))
|
||||
FORD_SLOPE = float(os.getenv("FORD_SLOPE", "0.10"))
|
||||
|
||||
# ── B05 구조물 옵션 (드롭다운 목록·기본값, 2026-08-05 사용자 확정) ──
|
||||
# 프론트는 이 값을 /api 설정 응답 또는 빌드타임 복사로 받아 쓴다. 수정은 여기서만 한다.
|
||||
|
||||
@@ -91,6 +91,15 @@ export const ui_locales_b2 = {
|
||||
],
|
||||
/* {chainage}=측점 누가거리(m) */
|
||||
B05_Drainage_Basin_Chainage: ["측점 누가거리 {chainage}m", "Station chainage {chainage}m"],
|
||||
/* {d}=규격 스냅 관경(mm). 유효직경 이상인 가장 작은 레지스트리 선택지 */
|
||||
B05_Drainage_Basin_RecPipe: ["추천 Ø{d} 배관", "Rec. Ø{d} pipe"],
|
||||
/* 유효직경 Ø1,500 초과 — 교본 BOX암거 전환 유량 조건 */
|
||||
B05_Drainage_Basin_RecBox: ["BOX암거 후보", "Box culvert candidate"],
|
||||
/* 추천은 유량 근거만 — 횡단경사·차수 조건은 화면이 판정하지 않는다 */
|
||||
B05_Drainage_Basin_RecNote: [
|
||||
"추천은 유량(유효직경) 근거만 반영합니다 — 계곡 횡단경사·하천 차수는 현장 확인.",
|
||||
"Recommendation uses discharge only — check valley cross slope and stream order on site.",
|
||||
],
|
||||
|
||||
/* --- B05_Profile 경로 설계 --- */
|
||||
B05_Route_Title: ["종단설계", "Profile Design"],
|
||||
|
||||
Reference in New Issue
Block a user