diff --git a/B04_PreProcess/B04_PreProcess_Api_Fetch.ts b/B04_PreProcess/B04_PreProcess_Api_Fetch.ts index 6b722201..9f42f25f 100644 --- a/B04_PreProcess/B04_PreProcess_Api_Fetch.ts +++ b/B04_PreProcess/B04_PreProcess_Api_Fetch.ts @@ -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 { diff --git a/B04_PreProcess/B04_PreProcess_Router_Basins.py b/B04_PreProcess/B04_PreProcess_Router_Basins.py index b8cf8e73..c94522aa 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Basins.py +++ b/B04_PreProcess/B04_PreProcess_Router_Basins.py @@ -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 ], diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 9fcddff2..654a1ff6 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -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" } ] }, diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 2778649e..0fb5f934 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -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, + /** 담당 유역의 설계유량(㎥/s) — 물넘이·세월교 개략 단면의 입력. 없으면 안내만. */ + designFlow?: number | null, ) => void; /** 현재 필드 값을 시설 옵션으로 읽는다(빈 값 생략). */ readOptions: () => Record; @@ -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; }, diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts index 5bf2c0d8..a3ad68d6 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts @@ -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 { diff --git a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts index 86d2229b..bb9fb791 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts @@ -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, diff --git a/B05_Profile/B05_Profile_UI_Drainage_Parts.ts b/B05_Profile/B05_Profile_UI_Drainage_Parts.ts index d24e3290..94e308d0 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Parts.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Parts.ts @@ -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, @@ -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); diff --git a/B05_Profile/B05_Profile_UI_Page.ts b/B05_Profile/B05_Profile_UI_Page.ts index 53c62e38..601fd1bf 100644 --- a/B05_Profile/B05_Profile_UI_Page.ts +++ b/B05_Profile/B05_Profile_UI_Page.ts @@ -98,6 +98,7 @@ export async function renderB05Route(root: HTMLElement): Promise { start_m: pipe.start_m, end_m: pipe.end_m, source: pipe.source, + design_flow_m3s: pipe.design_flow_m3s, options: pipe.options, })), ); diff --git a/B05_Profile/B05_Profile_UI_Profile_Panel.ts b/B05_Profile/B05_Profile_UI_Profile_Panel.ts index 16f0e6ae..eb89a691 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Panel.ts @@ -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; diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index bc01d92c..14d384fc 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -60,6 +60,8 @@ export interface PipeFacilityItem { end_m?: number; /** 자동 배치 출처(stream/spacing/user) — 배관 유형(계곡부형/보완형) 제안 근거. */ source?: PipeSource; + /** 담당 유역의 설계유량(㎥/s) — 물넘이·세월교 개략 단면 계산의 입력. */ + design_flow_m3s?: number | null; /** 부속 옵션(유형·집수정·기슭막이·돌붙임·날개벽·세월교 관 등). */ options?: Record; } @@ -246,8 +248,11 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu } /** 종류에 맞춰 부속 옵션 서브폼을 켠다(계곡 통과 시설·독립 기슭막이, 값은 인자로). */ - function syncFacilityForm(options: Record = {}): void { - facilityOptions.setFacility(facilityFormKind(currentType()), options); + function syncFacilityForm( + options: Record = {}, + 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(); diff --git a/common_util/common_util_drainage_detail.py b/common_util/common_util_drainage_detail.py index e86852a8..33534c38 100644 --- a/common_util/common_util_drainage_detail.py +++ b/common_util/common_util_drainage_detail.py @@ -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, diff --git a/config/config_frontend.ts b/config/config_frontend.ts index 2ebddccf..699f83c1 100644 --- a/config/config_frontend.ts +++ b/config/config_frontend.ts @@ -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 = "기타 구조물"; diff --git a/config/config_system.py b/config/config_system.py index 0647bcea..c6fff498 100644 --- a/config/config_system.py +++ b/config/config_system.py @@ -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 설정 응답 또는 빌드타임 복사로 받아 쓴다. 수정은 여기서만 한다. diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index f4a46210..9a80a753 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -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"],