Files
Aislo/B05_Profile/B05_Profile_UI_Structures_Pavement.ts
eomsangdonandClaude Opus 5 80c2a265f9 feat(B05,B06): 물넘이 횡단 표현 + 포장 구간 수기 지정, 경사 자동 포장 폐지
2026-08-28 사용자 확정 스펙.

물넘이포장 — 노면을 판 자리로 그린다
- 서버가 _ford_pavement_set(월류 폭·월류 높이·바닥 경사)을 만들고 물넘이만
  span 연동을 켜 **범위 안 측점 전부**에 얹는다. 깊이가 없으면 None으로 두어
  화면이 그리지 않는다(수치를 지어내지 않는다).
- 횡단도: 기존 계획고 점선 + 물넘이 바닥 실선 + 진한 회색 빗금 포장.
  깊이는 노선 중심 기준, 바닥은 유입(상단측)이 높게 기운다. 경사를 비우면
  그 측점의 노면 횡단경사를 쓴다.
- 물넘이 폼에 "바닥 경사 유입→유출(%)" 칸을 추가하고, 월류 폭 기본값
  리터럴을 config_frontend 상수로 모아 서버 값과 짝지었다.

포장 — 사용자가 구간으로 지정한다
- 구조물 레지스트리 G군 pavement_concrete 를 되살려 기준측점 + 길이 + 전/후로
  받는다(기슭막이와 같은 폼). 길이 기본값은 0 = 미지정.
- pavement_ranges/paved_at 이 구간을 판정하고, enforce_pavement_ranges 가
  저장분이 비포장이어도 구간 안이면 포장으로 다시 계산한다(사용자 조작값 승계).
- **종단경사 자동 포장 적용을 없앴다** — paved=suggested 3곳 제거. 별표1-2 상한
  초과 경고(pavement_suggested 배지·근거 문구)는 그대로 남는다.
- 포장 구간이 물넘이를 통째로 품으면 모달로 알리고 앞/뒤로 나눠 저장하고,
  끝만 걸치면 값을 고치라고 안내하고 멈춘다.

700줄 제한: _compute_default_designs 를 Router_Design 으로 옮겼다(657/306줄).

검증: pytest 238 passed / 7 skipped(신규 9건). 공용 브라우저 실측 — 물넘이 임시
투입 시 240m 카드에만 파임 3요소가 그려지고(일반 포장 박스 0), 바닥이 계획고보다
0.4m 아래·노면 전폭 4.0m에 1.5% 기울기, 220·260은 비포장 유지. 겹침 규칙은
브라우저에서 모듈을 직접 불러 3분할·안내·취소를 확인했다. 실측용 임시 관 지점은
매번 원래 정본 4건으로 복구했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 12:16:38 +09:00

79 lines
3.4 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_Structures_Pavement.ts
* 포장 구간과 물넘이포장의 겹침 정리(2026-08-28 사용자 확정).
*
* 물넘이포장은 그 자체가 콘크리트 노면이라 포장 구간이 그 위를 덮을 수 없다.
* · 포장 구간이 물넘이를 **통째로 품으면** → 알리고 앞·뒤 두 구간으로 나눈다
* (물넘이가 가운데 한 구간 — 합쳐서 3구간).
* · **끝만 걸치면** → 어디까지 겹치는지 알리고 저장을 멈춘다. 사용자가 값을 고친다.
* ========================================================================== */
import { FORD_PAVEMENT_DEFAULT_WIDTH_M } from "@config/config_frontend";
import { showConfirmDialog } from "@ui/ui_template_elements";
import type { PipeFacilityItem } from "./B05_Profile_UI_Structures_Panel";
/** 누가거리 비교 허용오차(m) — 정본이 0.01m로 끊어 쓴다. */
const EPS = 0.005;
export interface Span {
start: number;
end: number;
}
/** 물넘이포장이 차지하는 누가거리 구간 — 기준 측점 ± 월류 폭/2. */
export function fordSpans(pipes: PipeFacilityItem[]): Span[] {
return pipes
.filter((pipe) => pipe.facility === "ford_pavement")
.map((pipe) => {
const raw = Number(pipe.options?.ford_width_m);
const width = Number.isFinite(raw) && raw > 0 ? raw : FORD_PAVEMENT_DEFAULT_WIDTH_M;
return { start: pipe.chainage_m - width / 2, end: pipe.chainage_m + width / 2 };
})
.sort((left, right) => left.start - right.start);
}
const format = (value: number): string => `${value.toFixed(2)}m`;
/**
* 포장 구간에서 물넘이 몫을 덜어낸다.
* 반환: 저장할 구간 목록. `null`이면 저장하지 않는다(사용자가 값을 고쳐야 한다).
*/
export async function splitPavementRange(
start: number,
end: number,
pipes: PipeFacilityItem[],
): Promise<Span[] | null> {
const overlaps = fordSpans(pipes).filter(
(ford) => ford.end > start + EPS && ford.start < end - EPS,
);
if (!overlaps.length) return [{ start, end }];
// 끝만 걸친 물넘이 — 자르면 사용자가 찍은 구간이 조용히 줄어든다. 값을 고치게 안내한다.
const partial = overlaps.find((ford) => ford.start <= start + EPS || ford.end >= end - EPS);
if (partial) {
await showConfirmDialog(
`지정한 포장 구간(${format(start)}~${format(end)})이 물넘이포장` +
`(${format(partial.start)}~${format(partial.end)})의 끝에 걸칩니다.\n` +
"물넘이는 그 자체가 포장이라 겹칠 수 없습니다 — 기준 측점·길이·전후 값을 고쳐 주세요.",
"확인",
);
return null;
}
const segments: Span[] = [];
let cursor = start;
for (const ford of overlaps) {
if (ford.start - cursor > EPS) segments.push({ start: cursor, end: ford.start });
cursor = ford.end;
}
if (end - cursor > EPS) segments.push({ start: cursor, end });
const inside = overlaps.map((ford) => `${format(ford.start)}~${format(ford.end)}`).join(", ");
const proceed = await showConfirmDialog(
`포장 구간 안에 물넘이포장(${inside})이 있습니다.\n` +
`물넘이는 그 자체가 포장이라 그 몫을 빼고 ${segments.length}개 구간으로 나눠 저장합니다.`,
"나눠 저장",
);
return proceed ? segments : null;
}