Files
Aislo/B05_Profile/B05_Profile_UI_Profile_MinCover.ts
T
eomsangdonandClaude Fable 5 eb716ffbb1 fix(B05): 종단 편집 최소고 가드 개편 - 구간 쉬프트 차단 + 시설별 산식
2026-08-23 사용자 보고: 횡단배수 최소고를 어기고 구간 쉬프트가 되는 경우
있음(가드가 측점 ▼에만, 관경 고정 산식).

- 가드 개편(blocksMinCover): 편집 후보로 정렬을 계산해 시설별 최소고
  (minCoverPoints 원천 - 배수관 관경+토피, BOX암거 구체높이+토피, 세월교
  +물넘이, 물넘이포장 제외) 위반이 새로 생기거나 커지면 차단. 측점 ▲▼와
  구간 ⇧⇩ 모두 적용, 이웃 틸트가 종단곡선을 거쳐 배관 계획고를 내리는
  경로까지 커버. 이미 위반이면 악화만 차단(복구 편집 허용). 기존
  blocksPipeLowering(측점 ▼ 전용·관경 1000 고정)은 제거.
- shiftSegment 시딩 수정: 초기 오프셋을 곡선 샘플 계획고(PVI z - 중앙종거)
  대신 변화점 실제 z로 계산 - 첫 이동에서 변화점이 내려앉아 상승 후 하강
  복귀가 비대칭이던 화면 실측 버그 해소.
- MinCover 낡은 주석 정정(구현은 세월교 포함, 물넘이포장만 제외).

검증: tsc 통과 · pytest 182 passed(가드·시딩 신규 8건) · 공용 브라우저
실조작 - 배수관 앵커 구간 ⬇ 즉시 차단(+1.5), BOX암거 구간 차단(+2.5),
⬆/⬇ 대칭 복귀, 검증 상태 전부 원복.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 16:27:41 +09:00

130 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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<string, string | number>;
}
/** 최소 토피(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}`;
}