/* ============================================================================= * A00_Common/design/design_save.ts * B05 · B06 **저장 한 줄기**(계획서 24-1) — [저장] · [확정]이 어느 보기에서 눌려도 같은 차례. * * 관 → 구조물 → 계획고 → 암거 옵션 → 오르막(상단측) → 횡단 * * 차례는 여기 한 곳에만 둔다. 보기마다 다른 것은 **화면이 쥔 값으로 한 번 더 맞추는 일** * (B05 배수유역도 목록 · 구조물 목록 새로 받기 · 종단 패널 다시 그리기)과 횡단 저장 본문뿐이라, * 그 몫만 `steps` · `cross` 로 받는다. 서버 라우트는 그대로다. * * ⚠ 차례를 바꾸면 결과가 갈린다 — 관 목록은 스냅샷이라 구간값(암거 옵션)보다 먼저, 상단측은 * 횡단 patch 보다 먼저 나가야 사용자 수정이 맨 위에 얹힌다(2026-09-06 · 09-12). * 영구저장은 여기서만 한다(CLAUDE.md 5장 — 자동저장 없음). * ========================================================================== */ import { hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements"; import { flushPendingPipes } from "../../B05_Profile/B05_Profile_Api_Pipes_Draft"; import { flushPendingStructures } from "../../B05_Profile/B05_Profile_Api_Structures"; import { flushUphillOverrides, saveProfileAlignment, } from "../../B05_Profile/B05_Profile_Api_Fetch"; import { markAlignmentSaved, pendingAlignment, } from "../../B05_Profile/B05_Profile_UI_Profile_Edit"; import { flushCulvertOptions } from "../../B06_Section/B06_Section_Api_Culvert_Options"; import { stateKey } from "../b_page_state"; /** 저장 차례 — 이 순서 그대로 돈다. */ export const DESIGN_SAVE_ORDER = [ "pipes", "structures", "alignment", "culvertOptions", "uphill", "cross", ] as const; export type DesignSaveStep = (typeof DESIGN_SAVE_ORDER)[number]; export interface DesignSaveContext { projectId: string; routeId: () => number | null; /** 저장 앞 준비 — 면적을 모으기 전 전 측점 재계산(B06). */ prepare?: () => Promise; /** * 차례마다 **기본 창구 대신** 도는 일. 없으면 기본 창구(세션 초안 → 정본)가 돈다. * `pipes` 만 예외 — 기본(세션 초안)이 먼저 나가고 그 뒤에 돈다(화면 목록으로 한 번 더 맞춤). */ steps?: Partial, () => Promise>>; /** 횡단 — 보기마다 본문이 다르다. `confirm` 이면 단계를 닫는 길로. */ cross: (confirm: boolean) => Promise; /** 모두 나간 뒤 — 캐시 비우기 · 화면 다시 읽기 · 알림 · 다음 단계. */ done?: (confirm: boolean) => Promise | void; /** 멈춘 저장을 알린다. */ failed: (error: unknown, confirm: boolean) => void; } /** 실패해도 저장을 멈추지 않는 창구 — 알리고 넘어간다(초안은 세션에 남아 다시 나갈 수 있다). */ function reportOnly(label: string): (error: unknown) => void { return (error) => { const detail = error instanceof Error ? ` ${error.message}` : ""; showToast(`${label} 저장에 실패했습니다.${detail}`, "error"); }; } /** 계획고 초안(가게 하나)을 종단 정본에 쓴다. 없으면 아무 일도 하지 않는다. */ async function flushAlignmentDraft(projectId: string, routeId: number): Promise { const draft = pendingAlignment(routeId); if (!draft) return; await saveProfileAlignment(projectId, routeId, draft); markAlignmentSaved(routeId); } /** 실패해도 다음 차례로 넘어가는 창구와 그 이름(보기가 준 일에도 같이 씌운다). */ const TOLERANT: Partial> = { pipes: "배수관", structures: "구조물", alignment: "계획선", }; /** 차례마다 기본 창구. 암거 옵션 · 상단측은 조용히 넘어간다(다음 [저장]에서 다시 나감). */ function defaultStep( step: Exclude, projectId: string, routeId: number, ): () => Promise { switch (step) { case "pipes": return () => flushPendingPipes(projectId); case "structures": return () => flushPendingStructures(projectId); case "alignment": return () => flushAlignmentDraft(projectId, routeId); case "culvertOptions": return () => flushCulvertOptions(projectId, stateKey("culvertopt", projectId, routeId)).catch( () => undefined, ); case "uphill": return () => flushUphillOverrides(projectId).catch(() => undefined); } } /** * [저장] · [확정] — 세션에 쌓인 조작을 정해진 차례로 정본에 보낸다. * 노선이 없으면 아무 일도 하지 않고 false. 끝까지 나가면 true. */ export async function saveDesign( ctx: DesignSaveContext, options: { confirm?: boolean } = {}, ): Promise { const confirm = options.confirm ?? false; const routeId = ctx.routeId(); if (routeId === null) return false; showLoadingOverlay(); try { await ctx.prepare?.(); for (const step of DESIGN_SAVE_ORDER) { if (step === "cross") { await ctx.cross(confirm); continue; } const own = ctx.steps?.[step]; const base = defaultStep(step, ctx.projectId, routeId); const run = step === "pipes" ? async () => { await base(); await own?.(); } : (own ?? base); const label = TOLERANT[step]; await (label ? run().catch(reportOnly(label)) : run()); } await ctx.done?.(confirm); return true; } catch (error) { ctx.failed(error, confirm); return false; } finally { hideLoadingOverlay(); } }