feat(B01): 임시 보관함 UI를 그룹+파일 리스트 컨테이너로 개편

프로젝트/사용자 관리 컨테이너와 같은 형태로 통일한다. 등록 폼을 화면에 상시
노출하지 않고, 우측 상단 [+] 모달로 받아 그룹(임시 프로젝트명) 아래 파일 표를
그린다.

프론트엔드
- B01_Dashboard_UI_TempModal.ts 신설: 등록/추가 모달(임시 프로젝트명 + 파일 선택,
  고른 파일을 모달 안 표로 표시, 하단 [취소][확인] — 기존 대시보드 모달과 동일 규격)
- B01_Dashboard_UI_TempUpload.ts: 섹션 전체(제목·[+]·목록)를 반환하도록 변경.
  그룹 카드 + 파일 표(종류/파일명/크기/상태/작업), 그룹별 [파일 추가],
  파일별 [삭제], 진행률은 해당 파일 행 안에서 표시
- 청크마다 목록을 다시 조회하지 않고 막대 DOM만 갱신하도록 정리
- 대시보드 배치: 프로젝트 컨테이너 바로 아래(역할 3분기 모두)

백엔드
- DELETE /api/temp-uploads/{batch_id}/files/{file_type}: DB 행과 임시 저장소
  실제 파일을 함께 삭제. 필수 파일이 빠지면 상태를 uploading으로 되돌리고
  만료일은 최초 완료 시점 기준을 유지

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 13:23:58 +09:00
co-authored by Claude Fable 5
parent 17834d8189
commit f60ecc255f
9 changed files with 601 additions and 192 deletions
+56 -1
View File
@@ -36,7 +36,9 @@ from B03_FileInput.B03_FileInput_Repository_Temp import (
create_temp_batch,
create_temp_upload_session,
delete_temp_batch,
delete_temp_batch_file,
get_temp_batch,
get_temp_batch_file,
get_temp_batch_file_types,
get_temp_upload_session,
is_batch_required_complete,
@@ -44,6 +46,7 @@ from B03_FileInput.B03_FileInput_Repository_Temp import (
list_temp_batch_sessions,
list_temp_batches,
mark_temp_batch_completed,
mark_temp_batch_incomplete,
mark_temp_batch_linked,
upsert_temp_batch_file,
)
@@ -102,11 +105,14 @@ async def _batch_root(connection: Any, *, batch_id: str, user_id: int) -> Path:
async def _refresh_batch_status(connection: Any, *, batch_id: str) -> bool:
"""필수 파일이 다 찼으면 완료로 올린다. 완료 여부를 돌려준다."""
"""필수 파일 충족 여부에 맞춰 상태를 맞춘다. 완료 여부를 돌려준다."""
file_types = await get_temp_batch_file_types(connection, batch_id=batch_id)
complete = is_batch_required_complete(file_types)
if complete:
await mark_temp_batch_completed(connection, batch_id=batch_id)
else:
# 파일을 지워 필수 조건이 깨진 경우 — 프로젝트 연결 대상에서 빠져야 한다.
await mark_temp_batch_incomplete(connection, batch_id=batch_id)
return complete
@@ -235,6 +241,55 @@ async def remove_batch(
)
@router.delete("/{batch_id}/files/{file_type}")
async def remove_batch_file(
batch_id: str,
file_type: str,
session: dict[str, Any] = Depends(verify_session),
) -> JSONResponse:
"""묶음 안 파일 1건을 지운다 — DB 행과 임시 저장소 실제 파일을 함께 지운다."""
user_id = int(session["user_id"])
normalized = file_type.lower().lstrip(".")
pool = get_db_pool()
try:
async with pool.acquire() as connection:
batch = await get_temp_batch(connection, batch_id=batch_id, user_id=user_id)
if str(batch["status"]) == "linked":
return JSONResponse(
status_code=400,
content={
"status": "error",
"message": "프로젝트에 연결된 보관함은 수정할 수 없습니다.",
},
)
item = await get_temp_batch_file(connection, batch_id=batch_id, file_type=normalized)
await delete_temp_batch_file(connection, batch_id=batch_id, file_type=normalized)
required_complete = await _refresh_batch_status(connection, batch_id=batch_id)
await connection.commit()
batch_root = Path(resolve_temp_batch_path(user_id, batch_id, create=False))
stored = batch_root / str(item["relative_path"])
stored.unlink(missing_ok=True)
return JSONResponse(
content={
"status": "success",
"batch_id": batch_id,
"file_type": normalized,
"required_complete": required_complete,
}
)
except LookupError as exc:
return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)})
except OSError as exc:
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
except Exception:
logger.exception("보관함 파일 삭제 실패: batch_id=%s type=%s", batch_id, file_type)
return JSONResponse(
status_code=500,
content={"status": "error", "message": "파일 삭제 중 오류가 발생했습니다."},
)
@router.post("/{batch_id}/files", response_model=TempFileUploadResponse)
async def upload_batch_files(
batch_id: str,