/* ============================================================================= * B06_Section_UI_Page_Structures_Spec.ts * 횡단 보기 「구조물 배치」 폼의 **기본값 채움** — 조정창 · 도면이 쓰는 스펙 값 중 조작 · 저장분만 * 폼 옵션에 싣는다(2026-09-14 브레인 판정 ④ 「기본값을 몰래 확정으로 안 바꿈」). * `_Page_Structures_Panel` 이 700줄을 넘어 여기로 뗐다(계획서 24-3b). * ========================================================================== */ import { FORD_BRIDGE_DEFAULT_WIDTH_M } from "@config/config_frontend"; import type { StructureType } from "../B05_Profile/B05_Profile_Api_Structures"; import { PIPE_MATCH_M } from "../A00_Common/design/design_selection"; import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; type Merged = Record; /** 그 종류의 등록부 기본값 `{키: 기본값}` — 스펙 값이 기본값에서 왔는지 가르는 데 씀. */ function registryDefaults(types: StructureType[], typeId: string): Record { const options = types.find((type) => type.type_id === typeId)?.options ?? []; return Object.fromEntries(options.map((option) => [option.key, option.default])); } /** * 스펙 값 한 칸을 옵션에 싣는 규칙 — **저장돼 있던 칸이거나, 조작해 기본값과 달라진 칸만.** * 기본값에서 온 값을 실으면 페이지를 열기만 해도 임시 저장·[저장]에 기본값이 확정으로 굳음 * (2026-09-14 브레인 판정 ④ 「기본값을 몰래 확정으로 바꾸지 않는다」). 폼은 빈 칸에 제안을 보임. */ function keepSpec( merged: Merged, stored: Merged | undefined, key: string, spec: string | number | null | undefined, fromDefault: unknown, ): void { if (spec === null || spec === undefined) return; if (stored?.[key] !== undefined || String(spec) !== String(fromDefault ?? "")) merged[key] = spec; } type WingSpec = { installed: boolean; height_m: number | null; length_m: number | null; angle_deg: number | null; }; function keepWings( merged: Merged, stored: Merged | undefined, defaults: Record, wings: ReadonlyArray, ): void { for (const [wing, prefix] of wings) { // 백엔드 `_wing_spec` 과 같은 채움 — 설치는 「없음」이 아니면 있음 · 치수는 기본 0/0/45. const install = String(defaults[prefix] ?? "") === "없음" ? "없음" : "있음"; keepSpec(merged, stored, prefix, wing.installed ? "있음" : "없음", install); keepSpec( merged, stored, `${prefix}_height_m`, wing.height_m, defaults[`${prefix}_height_m`] ?? 0, ); keepSpec( merged, stored, `${prefix}_length_m`, wing.length_m, defaults[`${prefix}_length_m`] ?? 0, ); keepSpec( merged, stored, `${prefix}_angle_deg`, wing.angle_deg, defaults[`${prefix}_angle_deg`] ?? 45, ); } } /** * 세월교 폼 옵션 — **지금 그려진 스펙**(`section.ford`) 중 조작·저장분만 싣는다. 조정창 조작이 * 스펙을 제자리에서 고치므로 그 값은 폼에 보여야 함(2026-08-30 사용자: 수량(련) 칸이 비고 조정창은 * 1련) — 다만 1련이 **기본값에서 온 값이면** 제안으로만(판정 ④). */ function withFordSpec( options: Merged | undefined, ford: NonNullable, defaults: Record, ): Merged { const merged: Merged = { ...(options ?? {}) }; keepSpec(merged, options, "pipe_kind", ford.pipe_kind, defaults.pipe_kind); const diameter = Math.round(ford.diameter_m * 1000); keepSpec(merged, options, "pipe_diameter_mm", diameter, defaults.pipe_diameter_mm ?? 1000); const count = Number(defaults.pipe_count ?? 0); keepSpec( merged, options, "pipe_count", ford.pipe_count, count > 0 ? Math.max(Math.trunc(count), 1) : 1, ); keepSpec( merged, options, "ford_width_m", ford.span_m, defaults.ford_width_m ?? FORD_BRIDGE_DEFAULT_WIDTH_M, ); keepWings(merged, options, defaults, [ [ford.wing_in, "wing_in"], [ford.wing_out, "wing_out"], ]); return merged; } /** BOX암거 폼 옵션 — 세월교와 같은 규칙(조작·저장분만). 구체 길이·표고는 조정 채널이라 여기 안 옴. */ function withBoxSpec( options: Merged | undefined, box: NonNullable, defaults: Record, ): Merged { const merged: Merged = { ...(options ?? {}) }; keepSpec(merged, options, "body_width_m", box.inner_width_m, defaults.body_width_m ?? 2); keepSpec(merged, options, "body_height_m", box.inner_height_m, defaults.body_height_m ?? 2); keepWings(merged, options, defaults, [ [box.wing_in, "wing_in"], [box.wing_out, "wing_out"], ]); return merged; } /** * 관 지점 옵션에 **조정창에서 조작한 값**만 싣는다 — 조정창과 폼이 어긋나지 않게(2026-08-29 사용자). * ⚠ 스펙의 기본값(관종·관경·구조·벽 길이·전/후·집수정 길이)은 **안 실음** — 페이지를 열기만 해도 * 임시 저장에 기본값이 확정으로 굳던 자리(2026-09-14 브레인 판정 ④). 폼 빈 칸은 제안을 보임. */ export function withSpecDefaults( options: Merged | undefined, detail: SectionDetailResponse | null, chainageM: number, types: StructureType[], wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null, wallForm?: (chainageM: number, role: "inlet" | "outlet") => string | null, ): Merged | undefined { const owner = detail?.cross_sections.find( (section) => Math.abs(section.chainage_m - chainageM) < PIPE_MATCH_M, ); // 세월교·BOX는 폼과 조정창이 **같은 캐시**를 봄 — 조작한 값은 저장된 옵션보다 앞섬(2026-08-30). if (owner?.ford) return withFordSpec(options, owner.ford, registryDefaults(types, "ford_bridge")); if (owner?.box) return withBoxSpec(options, owner.box, registryDefaults(types, "box_culvert")); const culvert = owner?.culvert; if (!culvert) return options; const merged: Merged = { ...(options ?? {}) }; for (const prefix of ["inlet", "outlet"] as const) { // 형태·높이는 **조작한 값**이 이긴다 — 조정값이 저장된 옵션보다 앞서기 때문(2026-08-30 사용자: // 배관 유출구 기슭막이 형태가 폼과 다르게 그려졌다). 조작이 없으면 부르는 쪽이 null 을 줌. const drawnForm = wallForm?.(chainageM, prefix); if (drawnForm) merged[`${prefix}_revet_form`] = drawnForm; const drawnHeight = wallHeight?.(chainageM, prefix); if (drawnHeight != null) merged[`${prefix}_revet_height_m`] = Number(drawnHeight.toFixed(1)); } return merged; }