Files
Aislo/B03_FileInput/B03_FileInput_Schema.py
T
eomsangdonandClaude Opus 5 e2f6a101b6 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>
2026-08-04 18:09:15 +09:00

145 lines
4.2 KiB
Python

"""B03 파일 입력 요청·응답 검증 모델."""
from pathlib import PurePath
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, model_validator
from config.config_system import UPLOAD_ALLOWED_EXT, UPLOAD_CHUNK_SIZE_BYTES, UPLOAD_MAX_MB
class FileUploadDescriptor(BaseModel):
"""업로드 전에 검증할 단일 파일의 이름과 크기."""
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
original_filename: str = Field(min_length=1, max_length=255)
size_bytes: int = Field(gt=0)
@model_validator(mode="after")
def validate_file(self) -> "FileUploadDescriptor":
filename = self.original_filename
if PurePath(filename).name != filename or "/" in filename or "\\" in filename:
raise ValueError("파일명에는 경로가 포함될 수 없습니다.")
extension = PurePath(filename).suffix.lower()
if extension not in UPLOAD_ALLOWED_EXT:
allowed = ", ".join(UPLOAD_ALLOWED_EXT)
raise ValueError(f"허용되지 않은 파일 확장자입니다. 허용 형식: {allowed}")
maximum_bytes = UPLOAD_MAX_MB * 1024 * 1024
if self.size_bytes > maximum_bytes:
raise ValueError(f"파일 크기는 {UPLOAD_MAX_MB}MB를 초과할 수 없습니다.")
return self
class UploadedFileResult(BaseModel):
"""저장 완료된 단일 입력 파일 정보."""
input_file_id: int
original_filename: str
file_type: str
relative_path: str
size_bytes: int
metadata: dict[str, Any]
class FileUploadResponse(BaseModel):
"""B03 다중 파일 업로드 성공 응답."""
status: str = "success"
project_id: str
files: list[UploadedFileResult]
class ChunkSessionCreateRequest(FileUploadDescriptor):
"""청크 업로드 세션 생성 요청."""
chunk_size_bytes: int = Field(default=UPLOAD_CHUNK_SIZE_BYTES, gt=0)
class ChunkSessionCreateResponse(BaseModel):
"""청크 업로드 세션 생성 응답."""
status: str = "success"
project_id: str
upload_session_id: str
original_filename: str
file_size_bytes: int
chunk_size_bytes: int
total_chunks: int
completed_chunks: int = 0
class ChunkUploadResponse(BaseModel):
"""단일 청크 저장 응답."""
status: str = "success"
upload_session_id: str
chunk_index: int
completed_chunks: int
total_chunks: int
chunk_hash: str
class UploadFinalizeRequest(BaseModel):
"""청크 업로드 완료 및 병합 요청."""
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
session_id: str = Field(min_length=1, max_length=36)
total_chunks: int = Field(gt=0)
complete_upload: bool = True
class UploadStatusResponse(BaseModel):
"""청크 업로드 상태 조회 응답."""
status: str = "success"
upload_session_id: str
upload_status: str
original_filename: str
file_size_bytes: int
chunk_size_bytes: int
total_chunks: int
completed_chunks: int
completed_chunk_indexes: list[int]
class UploadOverviewFile(BaseModel):
"""재접속 현황 — 업로드 완료된 입력 파일 한 건(서버 정본)."""
input_file_id: int
file_type: str
original_filename: str
file_size_mb: float
status: str
uploaded_at: str | None = None
class UploadOverviewSession(BaseModel):
"""재접속 현황 — 중단된 청크 업로드 세션 한 건(이어올리기 안내용)."""
upload_session_id: str
original_filename: str
file_size_bytes: int
total_chunks: int
completed_chunks: int
progress_percent: float
updated_at: str | None = None
class UploadOverviewResponse(BaseModel):
"""B03 재접속 시 업로드 현황 — localStorage가 아니라 이 응답이 정본이다.
`required_complete`는 필수 파일(LAS/LAZ 1 + csv·prj·tfw)이 모두 업로드된 상태,
`analysis_complete`는 WF1 분석(stage 1)까지 끝난 상태다. 두 값이 모두 참이면
화면은 "완료" 배지를 띄우고, 완료 슬롯에 재업로드하면 교체 경고 모달을 띄운다.
"""
status: str = "success"
files: list[UploadOverviewFile]
pending_sessions: list[UploadOverviewSession]
required_complete: bool
analysis_complete: bool