"""B03 파일 입력 FastAPI 라우터.""" import asyncio import logging from pathlib import Path from uuid import UUID from fastapi import APIRouter, File, UploadFile from fastapi.responses import JSONResponse from B03_FileInput.B03_FileInput_Engine import ( resolve_upload_destination, save_upload_stream, ) from B03_FileInput.B03_FileInput_Engine_Analyze import analyze_input_metadata from B03_FileInput.B03_FileInput_Repository import ( create_input_file, get_project_storage_relative_path, ) from B03_FileInput.B03_FileInput_Schema import ( FileUploadDescriptor, FileUploadResponse, UploadedFileResult, ) from common_util.common_util_json import atomic_write_json from common_util.common_util_storage import resolve_stored_project_path from common_util.common_util_workflow import load_project_workflow from config.config_db import get_db_pool from config.config_system import UPLOAD_MAX_FILES logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/projects", tags=["B03 File Input"]) @router.post("/{project_id}/files", response_model=FileUploadResponse) async def upload_project_files( project_id: UUID, files: list[UploadFile] = File(...), ) -> FileUploadResponse | JSONResponse: """프로젝트 입력 파일을 저장·분석하고 DB 메타데이터를 기록한다.""" if not files or len(files) > UPLOAD_MAX_FILES: return JSONResponse( status_code=400, content={ "status": "error", "message": f"파일은 1~{UPLOAD_MAX_FILES}개까지 가능합니다.", }, ) filenames = [upload.filename or "" for upload in files] if len({filename.casefold() for filename in filenames}) != len(filenames): return JSONResponse( status_code=400, content={"status": "error", "message": "동일한 파일명을 중복 업로드할 수 없습니다."}, ) las_count = sum(Path(filename).suffix.lower() in {".las", ".laz"} for filename in filenames) if las_count != 1: return JSONResponse( status_code=400, content={ "status": "error", "message": "LAS 또는 LAZ 파일을 정확히 1개 포함해야 합니다.", }, ) pool = get_db_pool() saved_paths: list[Path] = [] try: results: list[UploadedFileResult] = [] async with pool.acquire() as connection: stored_path = await get_project_storage_relative_path(connection, project_id) project_root = Path(resolve_stored_project_path(stored_path)) await connection.begin() try: for upload in files: preliminary = FileUploadDescriptor( original_filename=upload.filename or "", size_bytes=max(upload.size or 0, 1), ) destination = resolve_upload_destination(project_root, preliminary) written_bytes = await save_upload_stream(upload, destination) saved_paths.append(destination) descriptor = FileUploadDescriptor( original_filename=preliminary.original_filename, size_bytes=written_bytes, ) metadata = await asyncio.to_thread(analyze_input_metadata, destination) relative_path = destination.relative_to(project_root).as_posix() file_type = destination.suffix.lower().lstrip(".") crs_epsg = metadata.get("epsg") input_file_id = await create_input_file( connection, project_id=project_id, file_type=file_type, original_filename=descriptor.original_filename, relative_path=relative_path, file_size_bytes=written_bytes, upload_by=None, crs_epsg=int(crs_epsg) if crs_epsg is not None else None, metadata=metadata, ) results.append( UploadedFileResult( input_file_id=input_file_id, original_filename=descriptor.original_filename, file_type=file_type, relative_path=relative_path, size_bytes=written_bytes, metadata=metadata, ) ) await connection.commit() except Exception: await connection.rollback() raise stage_root = project_root / "B03_FileInput" atomic_write_json( stage_root / "metadata.json", {"project_id": str(project_id), "files": [result.model_dump() for result in results]}, ) workflow_path = project_root / "workflow.json" if not workflow_path.exists(): atomic_write_json(workflow_path, load_project_workflow(project_root)) return FileUploadResponse(project_id=str(project_id), files=results) except LookupError as exc: return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)}) except (OSError, ValueError) as exc: for saved_path in saved_paths: saved_path.unlink(missing_ok=True) return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) except Exception: for saved_path in saved_paths: saved_path.unlink(missing_ok=True) logger.exception("B03 파일 업로드 처리 실패: project_id=%s", project_id) return JSONResponse( status_code=500, content={"status": "error", "message": "파일 업로드 처리 중 오류가 발생했습니다."}, ) finally: for upload in files: await upload.close()