/* ============================================================================= * B05_Profile_UI_Page_Latest.ts * 종단 보기의 **최신 노선 · 설정값** 창구 — 세션 캐시 우선 읽기 · 뒤에서 신선도 확인 · 좌측 폼 되살리기 * 값. 종단 보기 본체(`design_view_profile`)가 700줄을 넘어 여기로 뗐다(계획서 24-3b). * ========================================================================== */ import { fetchLatestRoute, readRouteLatestCache, writeRouteLatestCache, type RouteLatestResponse, } from "./B05_Profile_Api_Fetch"; import { panelValuesOfDraft, readPageSettings } from "./B05_Profile_Api_PageSettings"; import type { RoutePanelValues } from "./B05_Profile_UI_Panel"; import { SPEED_CHOICES, gradeLimitOf } from "./B05_Profile_UI_Panel_Fields"; import type { DesignProfile } from "../A00_Common/design/design_profile"; /** * 확정 이력이 있으면 매 진입마다 DB(latest) 조회 대신 브라우저 세션 캐시를 우선 사용해 * 응답속도를 높인다. 캐시 미스면 latest를 조회해 적재하고, solve·확정 성공 시 신선한 값으로 * 갱신한다(세션 = 탭 단위, 탭 종료 시 소멸). */ export function createLatestSource(projectId: string) { const read = (): RouteLatestResponse | null => readRouteLatestCache(projectId); const write = (value: RouteLatestResponse): void => writeRouteLatestCache(projectId, value); /** 캐시 우선 latest 로드. forceFresh=true(solve/확정 직후)는 항상 DB를 읽고 캐시를 갱신한다. */ async function load(forceFresh = false): Promise { if (!forceFresh) { const cached = read(); if (cached) return cached; } const fresh = await fetchLatestRoute(projectId); write(fresh); return fresh; } /** 이 응답이 "같은 자료"인지 가리는 서명 — 노선 번호·지표면 모델·지표면 설정. */ const signature = (value: RouteLatestResponse): string => [ value.route?.id ?? "", value.route?.surface_model_id ?? "", value.surface_params?.source_filter, value.surface_params?.method, String(value.surface_params?.smooth), ].join("|"); /** * 캐시로 먼저 그린 뒤 **뒤에서** 신선도를 확인한다(2026-09-06 캐시·세션 일원화). * 서명(노선번호·지표면)이 달라졌을 때만 `onChanged` 로 다시 그린다. */ async function verify( shown: RouteLatestResponse, onChanged: (fresh: RouteLatestResponse) => Promise, ): Promise { try { const fresh = await fetchLatestRoute(projectId); write(fresh); if (signature(fresh) === signature(shown)) return; await onChanged(fresh); } catch { /* 확인 실패는 조용히 넘긴다 — 화면은 캐시로 이미 서 있다. */ } } return { read, write, load, verify }; } /** 저장된 노선 설정값 → 좌측 폼 되살리기 값 — 세션 페이지 설정 초안이 있으면 그 위에(448). * 시 · 종점 계획고 조정은 종단 편집이 비추므로 여기서 안 채움(옛 params 값은 자동 선형에 이미 듦). */ export function panelValuesOf(next: RouteLatestResponse): Partial { const options = next.route_params?.options ?? {}; const draft = readPageSettings(next.project_id); return { contourInterval: next.surface_params?.contour_interval_m, // 없음(옛 · 시험 노선) = 패널 값 그대로 gradeClass: options.grade_class as RoutePanelValues["gradeClass"] | undefined, stationInterval: next.route_params?.station_interval_m ?? undefined, crossSampleInterval: next.route_params?.cross_sample_interval_m ?? undefined, longSampleInterval: next.route_params?.long_sample_interval_m ?? undefined, terrainType: options.terrain_type as RoutePanelValues["terrainType"] | undefined, designSpeed: options.design_speed_kph as RoutePanelValues["designSpeed"] | undefined, maxGradePct: next.route_params?.max_grade_pct ?? undefined, minVerticalRadius: next.route_params?.min_vertical_radius_m ?? undefined, minTangentLength: next.route_params?.min_tangent_length_m ?? undefined, enforcePipeClearance: (next.route_params?.enforce_pipe_clearance as boolean | undefined) ?? undefined, stationDisplay: next.route_params?.station_display ?? undefined, ...(draft ? panelValuesOfDraft(draft) : {}), }; } /** 폼의 페이지 설정 칸만 — 초안 ?? 저장 노선(빈 기준값 = 빈 칸 · 강제 = 해제) · ↶ ↷ 뒤 폼에 다시 얹음(N6 · 472-5). */ export function pageSettingsPanelValues(projectId: string): Partial | null { const latest = readRouteLatestCache(projectId); const draft = readPageSettings(projectId); if (!latest) return draft ? panelValuesOfDraft(draft) : null; const all = panelValuesOf(latest); return { designSpeed: all.designSpeed, terrainType: all.terrainType, maxGradePct: all.maxGradePct ?? null, minVerticalRadius: all.minVerticalRadius ?? null, minTangentLength: all.minTangentLength ?? null, enforcePipeClearance: all.enforcePipeClearance === true, stationDisplay: all.stationDisplay ?? { station: 0, cumulative: 0 }, }; } /** 페이지 설정 한 칸(463 · 요구 271-5) — 종단도 기울기 상한 · 시작 측점 = 세션 초안(448) ?? 저장 노선 * 설정(빈 칸 = 좌측 패널 기본). 어느 보기로 서도 종단도를 그린 뒤 얹음 · B05 패널은 세션에 쓰고 고친 즉시 얹음. */ export function applyPageSettings( profile: Pick, projectId: string, latest: RouteLatestResponse | null = readRouteLatestCache(projectId), ): void { const draft = readPageSettings(projectId); const values = latest ? panelValuesOf(latest) : draft ? panelValuesOfDraft(draft) : null; if (!values) return; // 모름 — 종단도 그대로(서버 경고 기준) const allowed = SPEED_CHOICES[values.gradeClass ?? "trunk"] ?? [20]; const speed = allowed.includes(values.designSpeed ?? 20) ? (values.designSpeed ?? 20) : allowed[0]; profile.setGradeLimit( gradeLimitOf(speed, values.terrainType ?? "special", values.maxGradePct ?? null), ); profile.setStationDisplay(values.stationDisplay ?? { station: 0, cumulative: 0 }); }