- POST /design/save 에 route(노선 초안) · carry(옛 노선 세션 칸) 칸 · 서버 = 노선 저장 → Node facility.rebase(rebasePipeDraft · rebaseStructureDraft) → 6-A 차례(새 노선) → Node stations.carry(routeCarryPatches) → 횡단 저장 - 되돌림 범위 = 노선 행 · 자식 표 · 단계 · 배수 폴더 · 노선 파일 · 지표면 확정(설계 지형 바꿈) - 손 상단측은 안 넘김(요구 274) · 노선 바뀐 [확정] = [저장](새 노선 본 뒤 다시) - 브라우저 = 노선 초안 + 사용자 칸만 한 본문 · 옛 차례 길(saveRouteDraft 뒤 flush · carryAfterRouteSave · keepDraftsOnNewRoute) · 부름 0 된 flush 창구 여섯 · saveCachedCrossPatches 걷음 - 등록부 R9 stations.carry · F10 facility.rebase · 옛 차례 단언 시험 한 본문 꼴로 · test_472_6_route_save(옛 길 = 새 길 · 실패 되돌림) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
4.3 KiB
TypeScript
111 lines
4.3 KiB
TypeScript
/* =============================================================================
|
|
* A00_Common/design/design_save_api.ts
|
|
* [저장] · [확정] 한 본문(PLAN 472-6 6-A) — 차례 조각 꼴 · 서버 한 창구 `POST /design/save`.
|
|
*
|
|
* 차례마다 조각 하나 = 본문(없으면 null · 서버가 그 차례를 건너뜀) + 저장 뒤 정리(초안 비움 · 화면 맞춤).
|
|
* 서버는 전부 저장하거나 아무것도 안 바꿈 — 실패면 차례 이름 · 까닭(`restored` 거짓 = 복구 확인 필요).
|
|
* 노선 바뀐 [저장](6-B) = 덧 칸 `route`(노선 초안) · `carry`(옛 노선 세션 칸) — 서버가 맨 앞에서 노선 저장 · 다시 얹기.
|
|
* ========================================================================== */
|
|
|
|
import { API_ANALYSIS_TIMEOUT_MS, API_BASE_URL } from "@config/config_frontend";
|
|
|
|
/** 서버 본문 칸 차례 그대로(`B06_Section_Router_DesignSave`). */
|
|
export const SAVE_PART_KEYS = [
|
|
"settings",
|
|
"pipes",
|
|
"structures",
|
|
"plusStations",
|
|
"alignment",
|
|
"culvertOptions",
|
|
"uphill",
|
|
"cross",
|
|
] as const;
|
|
|
|
export type SavePartKey = (typeof SAVE_PART_KEYS)[number];
|
|
|
|
/** 차례 조각 — `after` 는 서버가 다 저장한 뒤에만(그 차례 응답을 받음). */
|
|
export interface SavePart {
|
|
body: unknown | null;
|
|
after?: (response: Record<string, unknown> | undefined) => Promise<void> | void;
|
|
}
|
|
|
|
/** 노선 저장 체인 시간 한도 — 옛 `/route/replan` 요청 한도와 같음(`B05_Profile_Api_Replan`). */
|
|
const ROUTE_SAVE_TIMEOUT_MS = 15 * 60 * 1000;
|
|
|
|
const BODY_KEY: Record<SavePartKey, string> = {
|
|
settings: "settings",
|
|
pipes: "pipes",
|
|
structures: "structures",
|
|
plusStations: "plus_stations",
|
|
alignment: "alignment",
|
|
culvertOptions: "culvert_options",
|
|
uphill: "uphill",
|
|
cross: "cross",
|
|
};
|
|
|
|
/** 서버 차례 이름 — 본문 칸 차례 + 노선(6-B · 맨 앞). */
|
|
export type SaveStepKey = SavePartKey | "route";
|
|
|
|
export interface DesignSaveResult {
|
|
waited: boolean;
|
|
/** 차례별 응답 · `route` = 노선 저장 결과 · `carry` = 넘김(`outside` = 노선 밖으로 버린 고침 수). */
|
|
steps: Partial<Record<SaveStepKey | "carry", Record<string, unknown>>>;
|
|
}
|
|
|
|
/** 실패 — 차례 이름(`step`) · 되돌림 됨(`restored`). */
|
|
export class DesignSaveError extends Error {
|
|
constructor(
|
|
message: string,
|
|
readonly step: SaveStepKey | null,
|
|
readonly restored: boolean,
|
|
) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
/** 조각 본문을 모아 한 번에 보냄 · `extra` = 노선 바뀐 [저장] 덧 칸(`route` · `carry`). */
|
|
export async function postDesignSave(
|
|
projectId: string,
|
|
routeId: number,
|
|
confirm: boolean,
|
|
parts: Partial<Record<SavePartKey, SavePart | null>>,
|
|
extra: { route?: unknown; carry?: unknown } = {},
|
|
): Promise<DesignSaveResult> {
|
|
const body: Record<string, unknown> = { route_id: routeId, confirm, ...extra };
|
|
for (const key of SAVE_PART_KEYS) {
|
|
const part = parts[key];
|
|
if (part && part.body !== null && part.body !== undefined) body[BODY_KEY[key]] = part.body;
|
|
}
|
|
const controller = new AbortController();
|
|
// [확정]은 낡은 배수유역을 다시 셈 — 옛 차례들의 시간 한도 합 만큼 · 노선 저장이 끼면 그 한도(15 분)를 더함
|
|
const limit = API_ANALYSIS_TIMEOUT_MS * 3 + (extra.route ? ROUTE_SAVE_TIMEOUT_MS : 0);
|
|
const timer = window.setTimeout(() => controller.abort(), limit);
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/design/save`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
signal: controller.signal,
|
|
});
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok)
|
|
throw new DesignSaveError(
|
|
payload?.message ?? `HTTP ${response.status}`,
|
|
(payload?.step as SaveStepKey) ?? null,
|
|
payload?.restored !== false,
|
|
);
|
|
return { waited: payload?.waited === true, steps: payload?.steps ?? {} };
|
|
} catch (error) {
|
|
if (error instanceof DOMException && error.name === "AbortError")
|
|
throw new DesignSaveError(
|
|
"저장 응답을 시간 안에 못 받았습니다 — 새로고침해 저장됐는지 확인해 주세요.",
|
|
null,
|
|
false,
|
|
);
|
|
throw error;
|
|
} finally {
|
|
window.clearTimeout(timer);
|
|
}
|
|
}
|