- B06 [>] · 단계 판 B07~B09 = moveStage 한 단계(설계 확정) · progress = GET /design/save/progress 1 s 물음 · B03 · B04 로 = moveStage 잠금 길(떠남 확인은 잠금 풀린 뒤) - 낡음 확인 창 · 화면 side.update() 걷음 — 배수유역은 서버 [확정] 관 차례가 한 번만 셈 · 진행 이름 「배수유역 다시 셈」(basins) - 서버 _run_steps step() 진행을 메모리(B06_Section_Router_DesignSave_Progress) · 차례 목록 = N · 끝에 비움 - 노선 바뀐 채 [>] = 물음(잠금판 위 · z 1050 > 900) → 다시 세운 페이지 둘째 확정을 저장 줄 밖에서 기다림 → 잠금판이 다시 마운트를 넘어 이어져 이동 · 120 s 넘으면 머묾 - saveSharedDesign 이 참 · 거짓을 돌려줌 · 확정 이동은 부른 쪽(moveStage) - 낡은 시험 고침 — 512_2(확인 창 → 잠금판 한 단계 · 서버 basins) · 298_3 · 272 · b06_save_channels · b06_save_recompute(글 약속 새 길) - helper_ts_stub_loader .css = 빈 모듈 — 520-3 ui_template_progress.css import 로 451 · 384_4 · 416 등 helper 시험이 깨짐(브레인 승인) - 새 시험 test_520_4_save_progress(n/N · 배수유역 자리) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
4.7 KiB
TypeScript
125 lines
4.7 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);
|
|
}
|
|
}
|
|
|
|
/** 서버 저장 진행(520-4) — 차례 이름 · n/N · 지난 초 · 안 돌면 active 거짓 · 물음 실패 = null. */
|
|
export async function fetchDesignSaveProgress(projectId: string): Promise<{
|
|
active: boolean;
|
|
step?: string;
|
|
index?: number;
|
|
total?: number;
|
|
elapsed_s?: number;
|
|
} | null> {
|
|
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/design/save/progress`, {
|
|
credentials: "include",
|
|
});
|
|
return response.ok ? response.json() : null;
|
|
}
|