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:
2026-09-08 18:14:54 +09:00
co-authored by Claude Opus 5
parent d57c0f71ce
commit 09e1c983ae
7 changed files with 181 additions and 16 deletions
+46 -14
View File
@@ -15,6 +15,7 @@ import {
checkWF1AnalysisStatus,
createUploadSession,
finalizeUploadSession,
ReplaceOutputsConfirmRequired,
uploadFileChunk,
type UploadedFileResult,
} from "./B03_FileInput_Api_Fetch";
@@ -77,6 +78,31 @@ export function confirmReplaceUpload(slotLabel: string, fileName: string): Promi
* 파일 1건을 청크로 올린다. 중단된 세션이 있으면 그 지점부터 이어 올린다.
* 진행 상황은 `onProgress`로만 알린다 — 갱신 주기는 config 값으로 제한한다.
*/
/**
* 「이미 만들어 둔 결과가 지워집니다」를 사람에게 한 번 묻는다.
* 되돌릴 수 없는 삭제라 확인 없이는 진행하지 않는다(2026-09-08 사용자 지시).
* 자동 절차는 이 자리를 지나지 않는다 — 서버가 확인을 켜고 부른다.
*/
async function askReplaceOutputs<T>(run: (confirmReplace: boolean) => Promise<T>): Promise<T> {
try {
return await run(false);
} catch (error) {
if (!(error instanceof ReplaceOutputsConfirmRequired)) throw error;
const detail = error.targets.length
? `
지워지는 것: ${error.targets.join(" · ")}`
: "";
const agreed = window.confirm(
`${error.message}${detail}
되돌릴 수 없습니다. 계속할까요?`,
);
if (!agreed) throw new Error("업로드를 취소했습니다 — 기존 결과는 그대로 있습니다.");
return await run(true);
}
}
export async function uploadOneFile(
projectId: string,
state: FileSlotState,
@@ -97,13 +123,16 @@ export async function uploadOneFile(
const fingerprint = state.uploadSessionId ? null : await fileFingerprint(file);
let session = state.uploadSessionId;
if (!session) {
const created = await createUploadSession(
projectId,
file,
chunkSizeBytes,
fingerprint,
completeUpload,
lasFree,
const created = await askReplaceOutputs((confirmReplace) =>
createUploadSession(
projectId,
file,
chunkSizeBytes,
fingerprint,
completeUpload,
lasFree,
confirmReplace,
),
);
if (created.already_uploaded) {
state.progressBytes = file.size;
@@ -155,13 +184,16 @@ export async function uploadOneFile(
}
}
const response = await finalizeUploadSession(
projectId,
session,
totalChunks,
completeUpload,
fingerprint,
lasFree,
const response = await askReplaceOutputs((confirmReplace) =>
finalizeUploadSession(
projectId,
session,
totalChunks,
completeUpload,
fingerprint,
lasFree,
confirmReplace,
),
);
localStorage.removeItem(storageKey);
saveB03UploadedFile(projectId, {