/* ============================================================================= * B05_Profile_UI_Profile_MinCover.ts * 횡단배수 시설이 요구하는 **측점별 최소 계획고**를 산출한다. * * 왜(2026-08-23 사용자 지시): 배수관·BOX암거는 관(또는 구체) 위에 최소 토피가 * 있어야 성립한다. 계획선이 그 아래로 내려오면 3D 형상도, 실제 시공도 성립하지 * 않는다. 그래서 시설 제원으로 최소 계획고를 되짚어 종단에 표시하고, 계획선이 * 그 아래로 내려오면 경고한다. * * 산식(사용자 확정 예시 그대로): * 배수관 Ø1000 → 지반고 + 1.0(관경) + 0.5(토피) = +1.5 * BOX암거 2.0×2.0 → 지반고 + 2.0(구체 높이) + 0.5(토피) = +2.5 * 세월교 Ø1000 → 지반고 + 1.0(관경) + 0.5(토피) + 0.5(물넘이 몫) = +2.0 * 물넘이포장 → 도로에 그대로 앉히는 시설이라 요구 여유 없음 * 토피 0.5m는 B06 배수관 엔진의 `MIN_PIPE_COVER_M`과 같은 값이다(교차 확인). * * **정본 산식은 백엔드** `common_util_drainage_pipes.facility_clearance_m`이다 — * 계획선 자동 생성이 그 값으로 변화점을 들어 올린다. 여기 있는 것은 편집 중 즉시 * 경고하기 위한 같은 산식의 화면 사본이며, 상수 일치는 테스트로 잠가 둔다. * ========================================================================== */ import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; /** 최소고 판정에 필요한 것만 받는다 — 관 목록의 출처(정본/화면)에 매이지 않는다. */ export interface MinCoverPipe { chainage_m: number; facility?: PipeFacility; options?: Record; } /** 최소 토피(m) — B06 `MIN_PIPE_COVER_M`과 같은 값. */ export const MIN_COVER_M = 0.5; /** 시설 제원이 비었을 때 쓰는 기본값 — 화면 기본 선택과 맞춘다. */ const DEFAULT_PIPE_DIAMETER_MM = 1000; const DEFAULT_BOX_HEIGHT_M = 2; /** 세월교 추가 여유(m) — 배관 다발 위에 물넘이가 얹힌다(2026-08-23 사용자 확정). */ const FORD_BRIDGE_EXTRA_M = 0.5; export interface MinCoverPoint { chainage_m: number; /** 시설이 요구하는 지반고 대비 최소 여유(m) — 관경/구체높이 + 토피. */ clearance_m: number; /** 표시용 시설 이름(경고 문구에 쓴다). */ label: string; } function numberOf(value: unknown, fallback: number): number { const parsed = typeof value === "number" ? value : Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } /** * 관 지점 목록에서 최소 여유가 필요한 측점을 뽑는다. * 물넘이포장만 제외 — 도로 위에 그대로 앉는 시설이라 요구 여유가 없다. */ export function minCoverPoints(pipes: MinCoverPipe[]): MinCoverPoint[] { const result: MinCoverPoint[] = []; for (const pipe of pipes) { const facility = pipe.facility ?? "pipe"; const options = pipe.options ?? {}; if (facility === "box_culvert") { const heightM = numberOf(options.body_height_m, DEFAULT_BOX_HEIGHT_M); result.push({ chainage_m: pipe.chainage_m, clearance_m: heightM + MIN_COVER_M, label: `BOX암거 H${heightM.toFixed(1)}`, }); } else if (facility === "ford_pavement") { // 물넘이포장은 도로 위에 그대로 만든다 — 들어 올릴 이유가 없다. continue; } else { // 세월교는 배관 다발이라 배수관과 같은 산식 + 물넘이 몫 0.5m. const diameterM = numberOf(options.pipe_diameter_mm, DEFAULT_PIPE_DIAMETER_MM) / 1000; const extra = facility === "ford_bridge" ? FORD_BRIDGE_EXTRA_M : 0; result.push({ chainage_m: pipe.chainage_m, clearance_m: diameterM + MIN_COVER_M + extra, label: `${facility === "ford_bridge" ? "세월교" : "배수관"} Ø${Math.round(diameterM * 1000)}`, }); } } return result.sort((a, b) => a.chainage_m - b.chainage_m); } export interface MinCoverViolation extends MinCoverPoint { /** 요구 최소 계획고(m). */ required_m: number; /** 현재 계획고(m). */ planned_m: number; /** 모자란 높이(m, 양수). */ shortfall_m: number; } /** * 계획선이 최소 계획고에 못 미치는 측점을 찾는다. * groundAt/planAt은 chainage로 지반고·계획고를 돌려주는 보간기다. */ export function findMinCoverViolations( points: MinCoverPoint[], groundAt: (chainageM: number) => number | null, planAt: (chainageM: number) => number | null, toleranceM = 0.001, ): MinCoverViolation[] { const violations: MinCoverViolation[] = []; for (const point of points) { const ground = groundAt(point.chainage_m); const planned = planAt(point.chainage_m); if (ground === null || planned === null) continue; const required = ground + point.clearance_m; if (planned < required - toleranceM) { violations.push({ ...point, required_m: required, planned_m: planned, shortfall_m: required - planned, }); } } return violations; } /** 경고 문구 — 가장 크게 모자란 곳부터 알려 준다. */ export function minCoverWarningText(violations: MinCoverViolation[]): string | null { if (!violations.length) return null; const worst = [...violations].sort((a, b) => b.shortfall_m - a.shortfall_m)[0]; const more = violations.length > 1 ? ` 외 ${violations.length - 1}곳` : ""; return `${worst.label} 최소고 ${worst.shortfall_m.toFixed(2)}m 부족${more}`; }