/* ============================================================================= * B06_Section_UI_Page_Design_Sync.ts * 측점 설계 선택 반영·재계산 창구. `_UI_Page` 700줄 제한 대응으로 떼어낸 것이며 * 동작은 그대로다(2026-09-13 분리). * - `handleDesignChange` : 버튼 선택 즉시 반영 + 세션 초안 기록 + 재계산 * - `recomputeIfRock` : 암 경계·암 절토 경사 변경 후 암 측점만 재계산 * - `reconcileStaleDesigns`: 횡단 다시 셈을 계산 묶음에 부탁(cross 단계 한 곳 · 387-4) * - `syncBermSpans` : 구조물 목록의 소단을 세션 사본으로 펴기 * - `applyPanelToAll` : 표준 횡단면 설정 [전체 반영] * 고친 뒤 계산 묶음 cross 입력도 다시(`pipelineCross` · 노선 초안 동안 카드 = 묶음 새 노선 상세 · 365-2). * ========================================================================== */ import { writeBermDepth, writeCrossDesignChoice, writeShoulderExtend, } from "./B06_Section_Cross_Design_Session"; import { bermSpansFromStructures, readBermSpans, writeBermSpans, } from "./B06_Section_UI_Page_Persist"; import { showToast } from "@ui/ui_template_elements"; import { L } from "./B06_Section_UI_Page_Common"; import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; import type { CrossDesignChange, SectionViewController } from "./B06_Section_UI_Section_View"; import type { StructureInstance } from "../B05_Profile/B05_Profile_Api_Structures"; import { setCrossDetailHost } from "./B06_Section_UI_Cross_Detail"; import { useStructureIds } from "./B06_Section_Station_Keys"; import { pipelineCross, reconcilePagePipeline } from "../A00_Common/design/design_pipeline_page"; import { forceCross } from "../A00_Common/design/design_pipeline_cross"; export interface DesignSyncContext { projectId: string | null; routeId: () => number | null; detail: () => SectionDetailResponse | null; /** 뷰는 이 모듈보다 **뒤에** 만들어지므로 그때 채워지는 참조를 통해 부른다. */ view: () => SectionViewController; } export interface DesignSyncController { handleDesignChange: (chainageM: number, change: CrossDesignChange) => Promise; recomputeIfRock: (chainageM: number) => void; reconcileStaleDesigns: (options?: { force?: boolean }) => Promise; syncBermSpans: (structures: ReadonlyArray) => void; applyPanelToAll: () => Promise; } export function createDesignSync(ctx: DesignSyncContext): DesignSyncController { const { projectId } = ctx; // 횡단 [상세] 모달(138-4)이 쓰는 프로젝트 · 노선 · 상세 · 칸 크기 그림(173-4) if (projectId) setCrossDetailHost({ projectId, routeId: ctx.routeId, detail: ctx.detail, chart: (stationId, widthPx, heightPx) => ctx.view().detailChart(stationId, widthPx, heightPx), }); /** * 측점 설계 버튼 변경 처리: (1) 선택을 즉시 로컬 반영해 해당 카드만 리프레시(버튼 즉시 반응), * (2) 서버에서 단면적을 계산·저장하고 최신 요청이면 그 카드만 다시 갱신한다. 전체 재렌더 없음. * 설정 패널 편집값을 요청에 실어 요청값 → DB 저장 옵션 → config 기본값 우선순위를 지킨다. */ async function handleDesignChange(chainageM: number, change: CrossDesignChange): Promise { const sectionDetail = ctx.detail(); const currentRouteId = ctx.routeId(); if (!projectId || currentRouteId === null || !sectionDetail) return; const target = sectionDetail.cross_sections.find( (section) => Math.abs(section.chainage_m - chainageM) < 0.01, ); if (!target) return; // 설계선 쉬프트(139-6) — 그 쪽 노견 확장만 세션에 · 캐시에 바로 · 브라우저 재계산(같은 창구) const shift = change.shoulder_extend; if (shift) { writeShoulderExtend(projectId, currentRouteId, chainageM, shift.side, shift.valueM); if (target.design) target.design[`shoulder_extend_${shift.side}_m`] = shift.valueM; await reconcileStaleDesigns({ force: true }); return; } // 소단 높이(194-1) — 그 쪽 깊이만 세션에 · 캐시에 바로 · 브라우저 재계산(같은 창구) const berm = change.berm_depth; if (berm) { writeBermDepth(projectId, currentRouteId, chainageM, berm.side, berm.valueM); if (target.design) target.design[`berm_depth_${berm.side}_m`] = berm.valueM; await reconcileStaleDesigns({ force: true }); return; } // (1) 즉시 로컬 반영: 선택 버튼만 갱신(숫자·설계선은 기존값 유지) → 해당 카드만 교체. if (target.design) { target.design = { ...target.design, ground_type: change.ground_type, section_mode: change.section_mode, ditch_side: change.ditch_side ?? target.design.ditch_side, ditch_type: change.ditch_type, paved: change.paved, two_stage_slope: change.two_stage_slope, ditch_choice: change.ditch_choice, }; ctx.view().refreshCard(chainageM); } // (2) 선택은 **세션 초안**으로 남긴다 — 화면을 오가거나 새로고침해도 남고, // [저장]·[확정] 때 한 번에 정본으로 나간다(2026-09-06 사용자 확정: 캐시가 저절로 // 영구저장소로 새면 안 된다). 예전에는 여기서 서버가 계산하고 바로 저장했다. writeCrossDesignChoice(projectId, currentRouteId, chainageM, { ground_type: change.ground_type, section_mode: change.section_mode, ditch_side: change.ditch_side ?? null, ditch_type: change.ditch_type, paved: change.paved, two_stage_slope: change.two_stage_slope, ditch_choice: change.ditch_choice, }); // (3) 계산은 브라우저 안에서 — B05·B06 이 같이 쓰는 창구 하나로 돌린다. await reconcileStaleDesigns({ force: true }); } /** 암 경계 · 암 경사 변경 뒤 다시 셈(암 측점 2단계 무릎 · 단면적) — 지금 값을 「사용자 선택」으로 * 박지 않음(자동 판정이 굳으면 노선이 바뀌어도 재판정 안 됨 · 346-2). 암 경계는 세션에서 읽음. */ function recomputeIfRock(_chainageM: number): void { void reconcileStaleDesigns({ force: true }); } /** * 횡단 다시 셈 = 계산 묶음 cross 단계 **한 곳**(387-4) — 여기서는 묶음에 부탁하고 그 차례가 끝날 때까지 * 기다림. 입력이 같으면 안 셈 · `force` = 입력 칸 밖 세션 고침(노견 · 소단 · 단면 유형 · 조정창)이라 한 번 더. * 바뀐 카드는 묶음 알림(`onCrossDesignsUpdated`)이 다시 그림. */ async function reconcileStaleDesigns(options?: { force?: boolean }): Promise { if (!projectId) return; if (options?.force) forceCross(); pipelineCross(projectId); await reconcilePagePipeline(projectId); } /** * 구조물 목록의 **소단**(C군 사면안정)을 세션 사본으로 편다 — 재계산이 측점마다 읽는 값이다. * * 사용자는 「구조물 배치」에서 놓고(2026-09-07 확정), 계산은 그 사본만 본다. 달라졌을 때만 * 다시 계산한다 — 목록은 화면을 열 때도 오므로 매번 돌리면 진입이 느려진다. */ function syncBermSpans(structures: ReadonlyArray): void { const currentRouteId = ctx.routeId(); if (!projectId || currentRouteId === null) return; useStructureIds(projectId, structures); // 구조물 측점 열쇠 = 구조물 id(344-3) const next = bermSpansFromStructures(structures); if (JSON.stringify(next) === JSON.stringify(readBermSpans(projectId, currentRouteId))) return; writeBermSpans(projectId, currentRouteId, next); void reconcileStaleDesigns({ force: true }); } /** 패널 [전체 반영](N-2-1 · 346-2): 표준값으로 전 측점을 **한 번** 다시 셈 — 측점마다 돌면 * 66 × 66 = 4.1 s 였음. 표준값은 다시 셈이 `effectiveStandardCross` 로 읽고, 측점별 선택은 * 세션에 있는 것만 씀(지금 값을 「사용자 선택」으로 박지 않음 · 사용자 확정 10-16). */ let applyingAll = false; async function applyPanelToAll(): Promise { const sectionDetail = ctx.detail(); if (applyingAll || !sectionDetail || !projectId || ctx.routeId() === null) return; if (!sectionDetail.cross_sections.some((section) => section.design)) return; // 화면을 잠그지 않는다(CLAUDE.md 5장) — 도는 동안 다시 누르는 것만 막는다. applyingAll = true; try { await reconcileStaleDesigns({ force: true }); showToast(L("B06_Std_ApplyAll_Success"), "success"); } finally { applyingAll = false; } } return { handleDesignChange, recomputeIfRock, reconcileStaleDesigns, syncBermSpans, applyPanelToAll, }; }