feat(B03): 재접속 업로드 현황(10) + B05·B06 자동 설계 체인(9)

10. 재접속 현황·완료 표시·재업로드 경고:
- GET /projects/{id}/upload-overview 신설 — 완료 파일 목록(input_files 정본),
  중단 청크 세션(진행률), 필수 파일·WF1 분석 완료 여부
- 진입 시 서버 정본으로 슬롯 카드 표시(serverUploaded), localStorage는 보조로 강등
- 파일 재선택 전에도 중단 세션 이어올리기 안내 배너
- 전체 완료 배지 + 완료 슬롯 재업로드 시 교체 확인 모달(승인 시에만 진행)
- 필수 슬롯 검증: 서버 업로드분 있으면 충족 — 단일 파일 교체 업로드 허용

9. 자동 설계 체인 연장 (B03_FileInput_Service_Chain.py):
- WF1 자동 확정 후 같은 백그라운드 태스크에서 ① 계획노선 CSV 기반 B05 기본 경로
  계산(solve) ② 경로 확정(stage 2) ③ B06 기본 횡단 설계 확정(stage 3)까지 진행
- 수동 이력 보호: 프로젝트에 경로가 이미 있으면 건너뜀
- 단계별 실패 격리: 실패 단계에서 멈추고 로그·workflow 상태로만 기록
- AUTO_DESIGN_CHAIN_ENABLED config 플래그(기본 True)

typecheck·ruff·B03 unittest(7건) 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-04 18:09:15 +09:00
co-authored by Claude Opus 5
parent 6080d122bb
commit e2f6a101b6
11 changed files with 564 additions and 9 deletions
+65
View File
@@ -30,6 +30,8 @@ from B03_FileInput.B03_FileInput_Repository import (
get_project_storage_relative_path,
get_upload_session,
list_completed_chunk_indexes,
list_incomplete_upload_sessions,
list_project_input_files,
mark_upload_session_completed,
mark_upload_session_failed,
upsert_upload_chunk,
@@ -42,6 +44,9 @@ from B03_FileInput.B03_FileInput_Schema import (
FileUploadResponse,
UploadedFileResult,
UploadFinalizeRequest,
UploadOverviewFile,
UploadOverviewResponse,
UploadOverviewSession,
UploadStatusResponse,
)
from B03_FileInput.B03_FileInput_Service_WF1 import trigger_wf1_analysis_and_email
@@ -620,6 +625,66 @@ async def get_project_upload_status(
)
@router.get("/{project_id}/upload-overview", response_model=UploadOverviewResponse)
async def get_project_upload_overview(
project_id: UUID,
session: dict[str, Any] = Depends(verify_session),
) -> UploadOverviewResponse | JSONResponse:
"""B03 재접속 시 업로드 현황(정본) — 완료 파일 목록 + 중단 세션 + 완료 여부.
localStorage 기반 표시는 캐시를 지우거나 다른 PC로 가면 사라진다(2026-08-04 사용자
보고). 화면은 진입 시 이 응답을 정본으로 삼고 localStorage는 보조로만 쓴다.
"""
pool = get_db_pool()
try:
async with pool.acquire() as connection:
files = await list_project_input_files(connection, project_id)
sessions = await list_incomplete_upload_sessions(connection, project_id)
file_types, point_cloud_id = await get_project_input_readiness(connection, project_id)
async with connection.cursor(aiomysql.DictCursor) as cursor:
state = await get_workflow_state(cursor, str(project_id))
stages = (state or {}).get("stages") or []
analysis_complete = any(
int(stage.get("stage_no", -1)) == 1 and str(stage.get("state")) == "COMPLETE"
for stage in stages
)
return UploadOverviewResponse(
files=[
UploadOverviewFile(
input_file_id=int(row["id"]),
file_type=str(row["file_type"]),
original_filename=str(row["original_filename"]),
file_size_mb=float(row["file_size_mb"] or 0.0),
status=str(row["status"]),
uploaded_at=str(row["created_at"]) if row.get("created_at") else None,
)
for row in files
],
pending_sessions=[
UploadOverviewSession(
upload_session_id=str(row["id"]),
original_filename=str(row["original_filename"]),
file_size_bytes=int(row["file_size_bytes"]),
total_chunks=int(row["total_chunks"]),
completed_chunks=int(row["completed_chunks"]),
progress_percent=round(
100.0 * int(row["completed_chunks"]) / max(1, int(row["total_chunks"])), 1
),
updated_at=str(row["updated_at"]) if row.get("updated_at") else None,
)
for row in sessions
],
required_complete=_REQUIRED_FILE_TYPES <= file_types and point_cloud_id is not None,
analysis_complete=analysis_complete,
)
except Exception:
logger.exception("B03 업로드 현황 조회 실패: project_id=%s", project_id)
return JSONResponse(
status_code=500,
content={"status": "error", "message": "업로드 현황 조회 중 오류가 발생했습니다."},
)
@router.get("/{project_id}/workflow-state")
async def get_project_workflow_state(project_id: str):
pool = get_db_pool()