feat(B03): 새 자료로 갈기 전에 「지우고 진행할까」 확인 한 단계 신설
업로드 하나가 그 프로젝트의 설계 산출물·초기값 스냅숏을 되돌릴 수 없게 지운다. 창 넷이 한 프로젝트를 볼 수 있어 말없이 지우면 남의 작업이 사라진다. - `describe_existing_outputs`: 지워질 것을 사람 말로 낸다(전처리·노선·횡단·도면· 수량·원가·초기값). 빈 폴더는 세지 않는다. - 지울 것이 있는데 확인이 없으면 409 `confirm_required` — 무엇이 지워지는지 함께 준다. - 세 갈래(직행·청크·보관함 연결) 전부에 `confirm_replace` 를 붙임. - ⚠ 기본값은 확인 켬(True) — 자동 절차(체인·스크립트)는 물음에 안 걸린다. 사람이 올리는 라우터만 False 로 불러 확인을 받는다. - 화면: 409 를 받으면 「되돌릴 수 없습니다」와 지워질 목록을 보이고 한 번 묻는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,9 +51,33 @@ export interface UploadStatusResponse {
|
||||
completed_chunk_indexes: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 「지우고 진행할까?」를 물어야 하는 상태(409). 서버가 무엇이 지워지는지 함께 준다.
|
||||
* 되돌릴 수 없는 삭제라 사람이 한 번 보고 정한다(2026-09-08 사용자 지시).
|
||||
*/
|
||||
export class ReplaceOutputsConfirmRequired extends Error {
|
||||
readonly targets: string[];
|
||||
|
||||
constructor(message: string, targets: string[]) {
|
||||
super(message);
|
||||
this.name = "ReplaceOutputsConfirmRequired";
|
||||
this.targets = targets;
|
||||
}
|
||||
}
|
||||
|
||||
async function readJsonOrThrow<T>(response: Response): Promise<T> {
|
||||
const payload = (await response.json()) as T & { message?: string };
|
||||
const payload = (await response.json()) as T & {
|
||||
message?: string;
|
||||
confirm?: string;
|
||||
targets?: string[];
|
||||
};
|
||||
if (!response.ok) {
|
||||
if (response.status === 409 && payload.confirm === "replace_outputs") {
|
||||
throw new ReplaceOutputsConfirmRequired(
|
||||
payload.message ?? "이미 만들어 둔 결과가 지워집니다.",
|
||||
payload.targets ?? [],
|
||||
);
|
||||
}
|
||||
throw new Error(payload.message ?? `HTTP ${response.status}`);
|
||||
}
|
||||
return payload;
|
||||
@@ -62,9 +86,11 @@ async function readJsonOrThrow<T>(response: Response): Promise<T> {
|
||||
export async function uploadProjectFiles(
|
||||
projectId: string,
|
||||
files: readonly File[],
|
||||
confirmReplace = false,
|
||||
): Promise<FileUploadResponse> {
|
||||
const formData = new FormData();
|
||||
for (const file of files) formData.append("files", file, file.name);
|
||||
if (confirmReplace) formData.append("confirm_replace", "true");
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = window.setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
||||
@@ -88,6 +114,7 @@ export async function createUploadSession(
|
||||
fingerprint?: string | null,
|
||||
completeUpload = false,
|
||||
lasFree = false,
|
||||
confirmReplace = false,
|
||||
): Promise<ChunkSessionCreateResponse> {
|
||||
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/upload-sessions`, {
|
||||
method: "POST",
|
||||
@@ -100,6 +127,7 @@ export async function createUploadSession(
|
||||
fingerprint: fingerprint ?? null,
|
||||
complete_upload: completeUpload,
|
||||
las_free: lasFree,
|
||||
confirm_replace: confirmReplace,
|
||||
}),
|
||||
});
|
||||
return await readJsonOrThrow<ChunkSessionCreateResponse>(response);
|
||||
@@ -131,6 +159,7 @@ export async function finalizeUploadSession(
|
||||
completeUpload: boolean,
|
||||
fingerprint?: string | null,
|
||||
lasFree = false,
|
||||
confirmReplace = false,
|
||||
): Promise<FileUploadResponse> {
|
||||
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/finalize`, {
|
||||
method: "POST",
|
||||
@@ -142,6 +171,7 @@ export async function finalizeUploadSession(
|
||||
complete_upload: completeUpload,
|
||||
fingerprint: fingerprint ?? null,
|
||||
las_free: lasFree,
|
||||
confirm_replace: confirmReplace,
|
||||
}),
|
||||
});
|
||||
return await readJsonOrThrow<FileUploadResponse>(response);
|
||||
|
||||
Reference in New Issue
Block a user