- ① 관 옮김 = 편집 키도 새 자리로: B06 movePipeTo 가 resetStationEdit(옛, 새) · B05 측점 갱신이 사라진 것 하나 · 새것 하나(또는 같은 id · 0.006 m 안)를 짝으로 옮김(followedStation) — 세션 초안도 새 키라 옛 키가 다시 안 나감 - ② [저장](PUT profile-alignment)에서 서버가 측점 · 관 · 구조물 자리 어디에도 없는 편집 키를 버림(drop_unplaced_edits · 저장 차례가 관 · 구조물 뒤라 최신) · 응답 dropped_edit_chainages · 화면 알림 「계획고 편집 N곳을 지웠습니다」 · 자리 못 읽으면 안 버림 - ③ test_192_1 = 고정 사본(fixtures/b05_192_1_moved_pipe_longitudinal.json · 226fc805 460~840 m) · 새 시험 test_213_2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
162 lines
6.4 KiB
TypeScript
162 lines
6.4 KiB
TypeScript
/* =============================================================================
|
|
* 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 { clearState, 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<void>;
|
|
/**
|
|
* 차례마다 **기본 창구 대신** 도는 일. 없으면 기본 창구(세션 초안 → 정본)가 돈다.
|
|
* `pipes` 만 예외 — 기본(세션 초안)이 먼저 나가고 그 뒤에 돈다(화면 목록으로 한 번 더 맞춤).
|
|
*/
|
|
steps?: Partial<Record<Exclude<DesignSaveStep, "cross">, () => Promise<void>>>;
|
|
/** 횡단 — 보기마다 본문이 다르다. `confirm` 이면 단계를 닫는 길로. */
|
|
cross: (confirm: boolean) => Promise<void>;
|
|
/** 모두 나간 뒤 — 캐시 비우기 · 화면 다시 읽기 · 알림 · 다음 단계. */
|
|
done?: (confirm: boolean) => Promise<void> | 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");
|
|
};
|
|
}
|
|
|
|
/** 서버가 버린 계획고 편집 자리(측점 · 관 · 구조물이 없는 자리 · 213-2)를 알린다. */
|
|
export function noticeDroppedEdits(saved: { dropped_edit_chainages?: number[] }): void {
|
|
const dropped = saved.dropped_edit_chainages ?? [];
|
|
if (!dropped.length) return;
|
|
const where = dropped
|
|
.slice(0, 5)
|
|
.map((value) => `${value.toFixed(2)}m`)
|
|
.join(" · ");
|
|
showToast(
|
|
`계획고 편집 ${dropped.length}곳을 지웠습니다 — 측점 · 관 · 구조물이 없는 자리: ${where}` +
|
|
(dropped.length > 5 ? ` 외 ${dropped.length - 5}곳` : ""),
|
|
"info",
|
|
);
|
|
}
|
|
|
|
/** 계획고 초안(가게 하나)을 종단 정본에 쓴다. 없으면 아무 일도 하지 않는다. */
|
|
async function flushAlignmentDraft(projectId: string, routeId: number): Promise<void> {
|
|
const draft = pendingAlignment(routeId);
|
|
if (!draft) return;
|
|
noticeDroppedEdits(await saveProfileAlignment(projectId, routeId, draft));
|
|
markAlignmentSaved(routeId);
|
|
}
|
|
|
|
/** 실패해도 다음 차례로 넘어가는 창구와 그 이름(보기가 준 일에도 같이 씌운다). */
|
|
const TOLERANT: Partial<Record<DesignSaveStep, string>> = {
|
|
pipes: "배수관",
|
|
structures: "구조물",
|
|
alignment: "계획선",
|
|
};
|
|
|
|
/** 차례마다 기본 창구. 암거 옵션 · 상단측은 조용히 넘어간다(다음 [저장]에서 다시 나감). */
|
|
function defaultStep(
|
|
step: Exclude<DesignSaveStep, "cross">,
|
|
projectId: string,
|
|
routeId: number,
|
|
): () => Promise<void> {
|
|
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<boolean> {
|
|
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());
|
|
}
|
|
clearState("structure-type-cache", ctx.projectId); // 종류별 캐시는 저장 뒤 비움(54-32)
|
|
await ctx.done?.(confirm);
|
|
return true;
|
|
} catch (error) {
|
|
ctx.failed(error, confirm);
|
|
return false;
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
}
|