/* ============================================================================= * B06_Section_Api_Culvert_Options.ts * B06 조정창에서 만진 **배수관 세트 구간값**(기슭막이·집수정 길이·전/후)을 * 원천인 `pipe_points.json`에 되돌려 쓴다(2026-08-24 사용자). * * 값을 B06 정본(design)에 따로 담지 않는 이유: 같은 숫자가 B05 배수관 카드에도 * 있어서 두 곳이 갈리면 어느 쪽이 참인지 알 수 없다. 원천은 하나여야 한다. * * 조작분은 **세션에만** 쌓고 영구저장은 [저장]·[확정]에서만 한다(CLAUDE.md 5장 * 조작·데이터 흐름 정책, 2026-08-29 사용자 확정). 종전 800ms 디바운스 자동 PUT은 * 폐지했다 — 저장을 누르지 않았는데 영구저장소가 바뀌면 [초기화]의 기준선이 흐려진다. * 세션에 담는 이유는 B06에서 만진 값을 B05 [저장]으로도 내보내야 하기 때문이다. * ========================================================================== */ import { fetchDetailPipePoints, saveDetailPipePoints, } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; /** 측점 하나에 얹을 옵션 조각 — 키는 `pipe_points` 옵션 키 그대로. * 값은 수치가 대부분이나 날개벽 설치("있음"/"없음")처럼 문자열도 있다. */ export type CulvertOptionPatch = Record; /** 세션에 쌓인 조각 — 측점(누가거리 2자리 문자열) → 옵션 조각. */ type PendingMap = Record; export interface CulvertOptionWriter { /** 측점 옵션을 세션에 예약한다. 같은 측점의 앞선 예약과는 합쳐진다. */ queue: (chainageM: number, patch: CulvertOptionPatch) => void; /** 기준 측점 이동을 예약한다(2026-08-29 사용자: B06에서도 옮길 수 있어야 한다). * 세부 배수유역 재분할은 저장 시 서버가 관 목록을 다시 받아 처리한다. */ queueMove: (fromChainageM: number, toChainageM: number) => void; /** 예약분을 즉시 내보낸다([저장]·[확정]에서만 부른다). */ flush: () => Promise; } const keyOf = (chainageM: number): string => chainageM.toFixed(2); function readPending(sessionKey: string | null): PendingMap { if (!sessionKey) return {}; try { const raw = window.sessionStorage.getItem(sessionKey); return raw ? (JSON.parse(raw) as PendingMap) : {}; } catch { return {}; } } function writePending(sessionKey: string | null, pending: PendingMap): void { if (!sessionKey) return; try { if (Object.keys(pending).length === 0) window.sessionStorage.removeItem(sessionKey); else window.sessionStorage.setItem(sessionKey, JSON.stringify(pending)); } catch { /* 세션 저장 실패는 무시 — 값은 화면 캐시에 남아 있다. */ } } /** * 세션에 쌓인 구간값을 `pipe_points.json`에 한 번에 쓴다. 예약분이 없으면 아무것도 * 하지 않는다. 성공하면 세션을 비우고, 실패하면 **그대로 두어** 다음 [저장]에서 * 다시 시도한다 — 조용히 잃으면 화면과 저장분이 갈린다. */ /** 예약된 이동 — 옛 측점 키 → 새 누가거리(m). */ type PendingMoves = Record; function readMoves(sessionKey: string | null): PendingMoves { if (!sessionKey) return {}; try { const raw = window.sessionStorage.getItem(sessionKey); return raw ? (JSON.parse(raw) as PendingMoves) : {}; } catch { return {}; } } export async function flushCulvertOptions( projectId: string | null, sessionKey: string | null, moveKey: string | null = null, ): Promise { const pending = readPending(sessionKey); const moves = readMoves(moveKey); if (!projectId || (Object.keys(pending).length === 0 && Object.keys(moves).length === 0)) return; const current = await fetchDetailPipePoints(projectId); let touched = false; const points: DetailPipeInput[] = current.pipe_points.map((point) => { // 좌표는 서버가 다시 계산하므로 싣지 않는다(DetailPipeInput 규약). const { lonlat: _lonlat, ...input } = point; const key = keyOf(point.chainage_m); const patch = pending[key]; const movedTo = moves[key]; if (!patch && movedTo === undefined) return input; touched = true; const next: DetailPipeInput = patch ? { ...input, options: { ...(input.options ?? {}), ...patch } } : { ...input }; // 기준 측점 이동 — 관 위치가 바뀌면 서버가 세부 배수유역을 다시 나눠 응답한다. if (movedTo !== undefined) next.chainage_m = movedTo; return next; }); if (touched) await saveDetailPipePoints(projectId, points); writePending(sessionKey, {}); writePending(moveKey, {}); } /** * 저장기 하나를 만든다. `projectId`·`sessionKey`가 없으면 예약만 메모리에 남는다 * (조회 전용 상태). `onError`는 [저장] 시점 실패를 화면에 알리는 쪽이 구현한다. */ export function createCulvertOptionWriter( projectId: () => string | null, sessionKey: () => string | null, onError?: (message: string) => void, /** 이동 예약 세션 키 — 없으면 이동은 메모리에도 남지 않는다. */ moveKey: () => string | null = () => null, ): CulvertOptionWriter { return { queue(chainageM, patch) { const key = sessionKey(); const pending = readPending(key); pending[keyOf(chainageM)] = { ...(pending[keyOf(chainageM)] ?? {}), ...patch }; writePending(key, pending); }, queueMove(fromChainageM, toChainageM) { const key = moveKey(); if (!key) return; const moves = readMoves(key); // 이미 옮긴 관을 또 옮기면 **원래 자리** 기준으로 최종 위치만 남긴다. const origin = Object.keys(moves).find((entry) => Math.abs(moves[entry] - fromChainageM) < 0.005) ?? keyOf(fromChainageM); moves[origin] = toChainageM; window.sessionStorage.setItem(key, JSON.stringify(moves)); }, async flush() { try { await flushCulvertOptions(projectId(), sessionKey(), moveKey()); } catch (error) { onError?.(error instanceof Error ? error.message : "배수관 구간값 저장 실패"); throw error; } }, }; }