/* ============================================================================= * B06_Section_Api_Culvert_Options.ts * B06 조정창에서 만진 **배수관 세트 구간값**(기슭막이·집수정 길이·전/후)을 * 원천인 `pipe_points.json`에 되돌려 쓴다(2026-08-24 사용자). * * 값을 B06 정본(design)에 따로 담지 않는 이유: 같은 숫자가 B05 배수관 카드에도 * 있어서 두 곳이 갈리면 어느 쪽이 참인지 알 수 없다. 원천은 하나여야 한다. * * 저장은 **묶어서 늦게** 한다 — +/- 한 번마다 PUT하면 세부유역 재계산이 매번 * 돌아간다. 화면은 캐시를 고쳐 즉시 반영하고, 저장은 마지막 조작 뒤 한 번만 간다. * ========================================================================== */ import { fetchDetailPipePoints, saveDetailPipePoints, } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; /** 마지막 조작 뒤 이만큼 조용하면 저장한다(ms). */ const SAVE_DELAY_MS = 800; /** 측점 하나에 얹을 옵션 조각 — 키는 `pipe_points` 옵션 키 그대로. * 값은 수치가 대부분이나 날개벽 설치("있음"/"없음")처럼 문자열도 있다. */ export type CulvertOptionPatch = Record; export interface CulvertOptionWriter { /** 측점 옵션을 예약한다. 같은 측점의 앞선 예약과는 합쳐진다. */ queue: (chainageM: number, patch: CulvertOptionPatch) => void; /** 예약분을 즉시 내보낸다(확정·임시저장 직전에 쓴다). */ flush: () => Promise; } /** * 저장기 하나를 만든다. `projectId`가 없으면 아무것도 하지 않는다(조회 전용 상태). * `onError`는 저장 실패를 화면에 알리는 쪽이 구현한다. */ export function createCulvertOptionWriter( projectId: () => string | null, onError?: (message: string) => void, ): CulvertOptionWriter { /** 측점(누가거리 문자열) → 아직 안 보낸 옵션 조각. */ const pending = new Map(); let timer: ReturnType | null = null; let inFlight: Promise = Promise.resolve(); const keyOf = (chainageM: number): string => chainageM.toFixed(2); async function send(): Promise { const id = projectId(); if (!id || pending.size === 0) { pending.clear(); return; } const batch = new Map(pending); pending.clear(); try { const current = await fetchDetailPipePoints(id); let touched = false; const points: DetailPipeInput[] = current.pipe_points.map((point) => { // 좌표는 서버가 다시 계산하므로 싣지 않는다(DetailPipeInput 규약). const { lonlat: _lonlat, ...input } = point; const patch = batch.get(keyOf(point.chainage_m)); if (!patch) return input; touched = true; return { ...input, options: { ...(input.options ?? {}), ...patch } }; }); if (!touched) return; await saveDetailPipePoints(id, points); } catch (error) { // 실패한 조각은 되살려 다음 기회에 다시 시도한다 — 조용히 잃으면 화면과 // 저장분이 갈린다. batch.forEach((patch, key) => { pending.set(key, { ...patch, ...(pending.get(key) ?? {}) }); }); onError?.(error instanceof Error ? error.message : "배수관 구간값 저장 실패"); } } return { queue(chainageM, patch) { const key = keyOf(chainageM); pending.set(key, { ...(pending.get(key) ?? {}), ...patch }); if (timer) clearTimeout(timer); timer = setTimeout(() => { timer = null; inFlight = inFlight.then(send); }, SAVE_DELAY_MS); }, async flush() { if (timer) { clearTimeout(timer); timer = null; } inFlight = inFlight.then(send); await inFlight; }, }; }