/* ============================================================================= * B06_Section_UI_Page_Station_Controls.ts * 측점 단위 표시 제어 두 가지 — **개별 표시 반폭**(2026-08-06)과 **기슭막이 X 자리** * (2026-08-21). 둘 다 세션에만 담고 카드 재렌더로 반영한다. 페이지 본체 * (`_UI_Page.ts`)에서 700줄 제한으로 분리했다. * ========================================================================== */ import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; import type { RevetKey } from "./B06_Section_UI_Cross_Culvert"; import type { RevetOffsetControl, StationWidthControl } from "./B06_Section_UI_Cross_View"; /** 제어가 페이지에서 가져다 쓰는 값들 — 클로저 대신 함수로 받아 결합을 끊는다. */ export interface StationControlDeps { sessionKey: (kind: "crossw" | "revetx") => string | null; refreshCard: (chainageM: number) => void; detail: () => SectionDetailResponse | null; crossHalfWidth: () => number | undefined; sampledHalfWidth: () => number; } /** 반폭·기슭막이 제어 묶음. `load`는 경로가 바뀔 때 세션 값을 다시 읽는다. */ export interface StationControls { stationWidth: StationWidthControl; revetOffset: RevetOffsetControl; widths: Map; load: () => void; /** 전체 반영 — 개별 반폭을 전역값으로 덮는다(없으면 비운다). */ applyGlobalWidth: (requested: number | undefined, chainages: number[]) => void; } export function createStationControls(deps: StationControlDeps): StationControls { /* ── 측점 개별 표시 반폭(2026-08-06 사용자 지시) ────────────────────── * 카드 하단 ◀/▶/↺으로 1m씩 조절. 세션에 보관했다가 종/횡단 확정·임시저장 때 * cross_patches(design.display_half_width_m)로 영구 저장돼 재접근 시 유지된다. * 값 우선순위: 세션 → 저장값(design) → 없음(전역 반폭). */ const stationWidths = new Map(); const widthKey = (chainageM: number): string => chainageM.toFixed(2); const widthSessionKey = (): string | null => deps.sessionKey("crossw"); function loadStationWidths(): void { stationWidths.clear(); const key = widthSessionKey(); if (!key) return; try { const raw = window.sessionStorage.getItem(key); if (!raw) return; const parsed = JSON.parse(raw) as Record; Object.entries(parsed).forEach(([chainage, width]) => { if (Number.isFinite(width) && width > 0) stationWidths.set(chainage, width); }); } catch { /* 손상된 세션 값은 무시 — 저장값·전역 반폭으로 재시작. */ } } function persistStationWidths(): void { const key = widthSessionKey(); if (!key) return; try { window.sessionStorage.setItem(key, JSON.stringify(Object.fromEntries(stationWidths))); } catch { /* 세션 저장 실패는 무시(용량 초과 등) — 값은 메모리에 유지된다. */ } } /** 개별 반폭 하한 2m·상한은 보유 샘플 폭 — 표시용이라 샘플 밖은 의미가 없다. */ const clampStationWidth = (value: number): number => Math.min(Math.max(value, 2), Math.max(deps.sampledHalfWidth(), 2)); const stationWidthControl: StationWidthControl = { widthFor: (section) => { const session = stationWidths.get(widthKey(section.chainage_m)); if (session !== undefined) return session; const stored = section.design?.display_half_width_m; return typeof stored === "number" && stored > 0 ? stored : undefined; }, adjust: (chainageM, deltaM) => { const key = widthKey(chainageM); const section = deps .detail() ?.cross_sections.find((entry) => Math.abs(entry.chainage_m - chainageM) < 0.01); const stored = section?.design?.display_half_width_m; const current = stationWidths.get(key) ?? (typeof stored === "number" && stored > 0 ? stored : undefined) ?? deps.crossHalfWidth() ?? deps.sampledHalfWidth(); stationWidths.set(key, clampStationWidth(Math.round(current + deltaM))); persistStationWidths(); deps.refreshCard(chainageM); }, reset: (chainageM) => { // 초기화 = 전역 반폭 복귀. 저장값(design)도 무시해야 하므로 세션에 전역값을 명시한다. const globalWidth = deps.crossHalfWidth(); if (globalWidth === undefined) stationWidths.delete(widthKey(chainageM)); else stationWidths.set(widthKey(chainageM), clampStationWidth(globalWidth)); persistStationWidths(); deps.refreshCard(chainageM); }, }; /* ── 기슭막이 X 자리(2026-08-21 사용자 ①) ──────────────────────────── * 벽을 눌러 고르고 카드 하단 ◀/▶로 0.1m씩 민다. 값은 세션에만 담는다 — 자동 자리가 * 지형·계획고를 따라 다시 풀리므로, 손으로 민 값은 그 세션의 표시 조정으로 본다. * 키는 `누가거리:역할`. `select`는 다시 그리지 않고 값만 담는다(줌·팬 보존). */ const revetShifts = new Map(); const revetSelected = new Map(); const revetKey = (chainageM: number, role: RevetKey): string => `${chainageM.toFixed(2)}:${role}`; const revetSessionKey = (): string | null => deps.sessionKey("revetx"); function loadRevetShifts(): void { revetShifts.clear(); revetSelected.clear(); const key = revetSessionKey(); if (!key) return; try { const raw = window.sessionStorage.getItem(key); if (!raw) return; const parsed = JSON.parse(raw) as Record; Object.entries(parsed).forEach(([entry, shift]) => { if (Number.isFinite(shift)) revetShifts.set(entry, shift); }); } catch { /* 손상된 세션 값은 무시 — 자동 자리로 재시작. */ } } function persistRevetShifts(): void { const key = revetSessionKey(); if (!key) return; try { window.sessionStorage.setItem(key, JSON.stringify(Object.fromEntries(revetShifts))); } catch { /* 세션 저장 실패는 무시 — 값은 메모리에 유지된다. */ } } /** 손으로 미는 범위 한계(m) — 조정 단위가 관 길이 1m이라 ±10m(=관 10m분)까지 둔다. */ const clampRevetShift = (value: number): number => Math.min(Math.max(Math.round(value * 10) / 10, -10), 10); const revetOffsetControl: RevetOffsetControl = { shiftFor: (section, role) => revetShifts.get(revetKey(section.chainage_m, role)) ?? 0, selectedFor: (section) => revetSelected.get(section.chainage_m.toFixed(2)) ?? null, select: (chainageM, key) => { if (key) revetSelected.set(chainageM.toFixed(2), key); else revetSelected.delete(chainageM.toFixed(2)); }, adjust: (chainageM, role, deltaM) => { const key = revetKey(chainageM, role); revetShifts.set(key, clampRevetShift((revetShifts.get(key) ?? 0) + deltaM)); persistRevetShifts(); deps.refreshCard(chainageM); }, reset: (chainageM, role) => { revetShifts.delete(revetKey(chainageM, role)); persistRevetShifts(); deps.refreshCard(chainageM); }, }; return { stationWidth: stationWidthControl, revetOffset: revetOffsetControl, widths: stationWidths, load: () => { loadStationWidths(); loadRevetShifts(); }, applyGlobalWidth: (requested, chainages) => { stationWidths.clear(); if (requested !== undefined) { for (const chainageM of chainages) { stationWidths.set(widthKey(chainageM), clampStationWidth(requested)); } } persistStationWidths(); }, }; }