/* ============================================================================= * B05_Profile/logic/B05_Profile_Drainage_Suggest.ts * 배수유역 제안(347-3) — 관경 · 시설 종류 · 관 자리 후보를 모으고 [적용] 때 관 초안에 한 번에 얹음. * 순수 함수만(화면 = `B05_Profile_UI_Drainage_Suggest.ts`). * * - 관경 = 유역 추천이 배관이고 추천 관경이 지금 관경과 다를 때(「Ø→Ø 배관 제안」). * - 시설 = 유역 추천이 BOX암거 · 세월교이고 지금 관이 그 종류가 아닐 때. * - 관 자리 = 서버 `suggest_pipe_positions`(간격 보충 규칙 · 기존 관에서 먼 곳). * - 자동 바꿈 없음 — 모으기만 하고 바꾸는 것은 `applySuggestions`([적용])뿐. * ========================================================================== */ /** 관 자리가 같은 유역으로 볼 거리(m) — 화면 `PIPE_MATCH_M` 과 같은 값. */ const SAME_PIPE_M = 0.5; interface PipeLike { chainage_m: number; source?: string; facility?: string; options?: Record; } interface BasinLike { chainage_m: number; recommended_facility?: string; recommended_diameter_mm?: number | null; } export type Suggestion = | { kind: "diameter"; chainageM: number; from: number | null; to: number } | { kind: "facility"; chainageM: number; from: string; to: string } | { kind: "position"; chainageM: number }; const near = (a: number, b: number): boolean => Math.abs(a - b) < SAME_PIPE_M; /** 응답(유역 · 관 자리 후보) + 지금 관 목록 → 제안 목록(관 자리 차례). */ export function collectSuggestions( pipes: ReadonlyArray, basins: ReadonlyArray, candidates: ReadonlyArray<{ chainage_m: number }>, ): Suggestion[] { const found: Suggestion[] = []; basins.forEach((basin) => { const pipe = pipes.find((item) => near(item.chainage_m, basin.chainage_m)); if (!pipe) return; const facility = pipe.facility ?? "pipe"; const want = basin.recommended_facility ?? "pipe"; if (want !== "pipe" && facility !== want) { found.push({ kind: "facility", chainageM: pipe.chainage_m, from: facility, to: want }); return; } const to = basin.recommended_diameter_mm; const raw = pipe.options?.pipe_diameter_mm; const from = raw === undefined || raw === "" ? null : Number(raw); if (want === "pipe" && facility === "pipe" && to != null && from !== to) found.push({ kind: "diameter", chainageM: pipe.chainage_m, from, to }); }); candidates .filter((item) => !pipes.some((pipe) => near(pipe.chainage_m, item.chainage_m))) .forEach((item) => found.push({ kind: "position", chainageM: item.chainage_m })); return found.sort((a, b) => a.chainageM - b.chainageM); } /** [적용] — 제안을 관 초안에 한 번에(관 더함 · 관경 · 시설 바꿈). 입력 목록은 안 고침. */ export function applySuggestions

( pipes: ReadonlyArray

, suggestions: ReadonlyArray, ): P[] { const next = pipes.map( (pipe) => ({ ...pipe, options: pipe.options && { ...pipe.options } }) as P, ); suggestions.forEach((item) => { if (item.kind === "position") { if (!next.some((pipe) => near(pipe.chainage_m, item.chainageM))) next.push({ chainage_m: item.chainageM, source: "spacing" } as P); return; } const pipe = next.find((candidate) => near(candidate.chainage_m, item.chainageM)); if (!pipe) return; if (item.kind === "facility") pipe.facility = item.to; else pipe.options = { ...(pipe.options ?? {}), pipe_diameter_mm: item.to }; }); return next.sort((a, b) => a.chainage_m - b.chainage_m); }