Files
Aislo/A00_Common/design/design_save_api.ts
T
eomsangdonandClaude Opus 5.5 ed9e014db2 feat(520-4): B05 · B06 페이지 넘김 공용 moveStage — B06 [>] 잠금판 「설계 확정」 서버 저장 차례 n/N 이름 · 배수유역 다시 셈 한 번
- 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>
2026-10-25 08:19:22 +09:00

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;
}