feat(B01,B03): 프로젝트 생성 전 임시 보관함 (temp upload)
라이다 원본은 업로드에 오래 걸려 프로젝트 정보 확정 전에 미리 올릴 수 있어야 한다.
계정에 묶인 임시 보관함을 만들고, 나중에 만든 프로젝트로 자료를 옮겨 쓴다.
저장·DB
- storage/tmp/{user_id}/{batch_id}/ 아래에 프로젝트 저장소와 동일한 구조를 써서
청크 저장·병합 엔진(resolve_upload_destination/merge_upload_chunks)을 그대로 재사용
- 010_temp_upload.sql: temp_upload_batches / temp_upload_files 신설,
upload_sessions.project_id NULL 허용 + temp_batch_id 추가(FK명 조회 후 재생성)
- config: TEMP_UPLOAD_DIR_NAME / TEMP_UPLOAD_RETENTION_DAYS(30) /
TEMP_UPLOAD_CLEANUP_INTERVAL_HOURS(6)
백엔드
- B03_FileInput_Router_Temp.py: 묶음 생성·목록·삭제, 일반/청크 업로드, finalize,
이어올리기 상태 조회, 프로젝트 연결(attach)
- attach: 파일 이동 후 input_files 등록, stage 0 완료, WF1·자동 설계 체인 트리거
- common_util_temp_cleanup.py: 완료 시각 기준 만료분 주기 삭제(서버 시작 시 1회 포함)
프론트엔드
- B01 대시보드 임시 보관함 섹션: 프로젝트 등록과 같은 폼 + 보관 목록.
진행률은 모달이 아니라 리스트 행에 표시, 새로고침 후 이어올리기 지원
- B03 업로드 컨테이너 내부 불러오기 버튼과 선택 모달.
완료된 묶음만 노출하고, 선택 후 업로드를 누르면 이동과 분석으로 이어짐
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,8 @@ import {
|
||||
type UploadedFileResult,
|
||||
} from "./B03_FileInput_Api_Fetch";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { attachTempBatch } from "../B01_Dashboard/B01_Dashboard_Api_Temp";
|
||||
import { createTempPicker } from "./B03_FileInput_UI_TempPicker";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import {
|
||||
fetchWorkflowState,
|
||||
@@ -55,6 +57,8 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
// 끝난 관리자 전용 점검 화면이라 일반 진행 경로가 아니다(2026-08-08 사용자 지시).
|
||||
const completionRoute = ROUTES.B05_PROFILE;
|
||||
const slots = initializeSlots();
|
||||
// 대시보드 임시 보관함에서 가져올 자료 선택기 — 선택되면 [업로드]가 이동을 수행한다.
|
||||
const tempPicker = createTempPicker(() => updateUploadButton());
|
||||
const cardMap = new Map<FileSlot, HTMLElement>();
|
||||
const resultList = document.createElement("ul");
|
||||
resultList.className = "b03-file__results";
|
||||
@@ -130,6 +134,12 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
|
||||
function updateUploadButton(): void {
|
||||
// 보관함 자료를 지정했으면 슬롯 검사와 무관하게 업로드(=이동)를 열어 준다.
|
||||
if (tempPicker.selected()) {
|
||||
uploadButton.disabled = false;
|
||||
pageError.textContent = "";
|
||||
return;
|
||||
}
|
||||
const validation = validateSlots();
|
||||
uploadButton.disabled = validation !== null;
|
||||
pageError.textContent = validation ?? "";
|
||||
@@ -577,7 +587,49 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 보관함 자료를 이 프로젝트로 옮기고 초기 분석까지 이어 간다.
|
||||
* 파일을 직접 고른 게 아니라 이미 서버에 있는 자료를 옮기는 것이라 청크 업로드를 타지
|
||||
* 않는다 — 이동이 끝나면 같은 분석 대기 흐름으로 합류한다.
|
||||
*/
|
||||
async function attachSelectedTempBatch(): Promise<void> {
|
||||
const batch = tempPicker.selected();
|
||||
if (!batch) return;
|
||||
activeProjectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
|
||||
if (!activeProjectId) {
|
||||
pageError.textContent = L("B03_File_Error_Project");
|
||||
return;
|
||||
}
|
||||
pageError.textContent = "";
|
||||
try {
|
||||
const result = await attachTempBatch(activeProjectId, batch.batch_id);
|
||||
tempPicker.clear();
|
||||
showToast(L("B03_Temp_Attach_Success"), "success");
|
||||
await applyUploadOverview();
|
||||
if (!result.analysis_started) {
|
||||
showToast(L("B03_Temp_Attach_NoAnalysis"), "warning");
|
||||
return;
|
||||
}
|
||||
showToast(L("B03_File_Analysis_InProgress"), "info");
|
||||
const analysisComplete = await pollWF1Analysis(activeProjectId);
|
||||
if (analysisComplete) {
|
||||
navigateTo(completionRoute);
|
||||
} else {
|
||||
showToast(L("B03_File_Analysis_StillRunning"), "warning");
|
||||
}
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : L("B03_Temp_Attach_Failed");
|
||||
pageError.textContent = `${L("B03_Temp_Attach_Failed")} ${detail}`;
|
||||
showToast(L("B03_Temp_Attach_Failed"), "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function startChunkedUpload(targetStates = selectedStates()): Promise<void> {
|
||||
// 보관함에서 불러온 자료가 지정돼 있으면 그것을 옮기는 것이 이 버튼의 동작이다.
|
||||
if (tempPicker.selected()) {
|
||||
await attachSelectedTempBatch();
|
||||
return;
|
||||
}
|
||||
activeProjectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
|
||||
const validation = validateSlots();
|
||||
if (validation) {
|
||||
@@ -667,6 +719,8 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
subtitle,
|
||||
overviewBanner,
|
||||
dropzone,
|
||||
// 대시보드 임시 보관함에서 자료를 끌어오는 자리 — 업로드 컨테이너 안에 둔다.
|
||||
tempPicker.root,
|
||||
resumeBanner,
|
||||
pageError,
|
||||
uploadButton,
|
||||
|
||||
Reference in New Issue
Block a user