"""B09 자재 견적서 파일 길(PLAN 166-4) — 자재 줄마다 여러 파일 · DB 표 없음(폴더가 목록). GET /api/projects/{id}/estimation/quotes?key= 목록 [{name, size, at}] POST /api/projects/{id}/estimation/quotes?key= 올리기(`files` 여러 개 · 같은 이름 = 교체) GET /api/projects/{id}/estimation/quotes/file?key=&name= 내려받기 DELETE /api/projects/{id}/estimation/quotes?key=&name= 지우기 → 목록 자재 키 · 파일 이름은 쿼리로(키에 `/` · `|` 가 섞임) · 저장 자리 = `B09_Estimation_Store.quote_dir`. `B09_Estimation_Router` 가 끼워 넣음(같은 머리 · 같은 권한). """ from __future__ import annotations import asyncio from datetime import datetime from pathlib import Path from typing import Any from uuid import UUID from fastapi import APIRouter, File, HTTPException, Query, UploadFile from fastapi.responses import FileResponse from B03_FileInput.B03_FileInput_Engine import save_upload_stream from B08_Quantity.B08_Quantity_Router_Hydro import _project_root from B09_Estimation import B09_Estimation_Store as store router = APIRouter() PATH = "/{project_id}/estimation/quotes" def _listing(folder: Path) -> list[dict[str, Any]]: return [ { "name": f.name, "size": f.stat().st_size, "at": datetime.fromtimestamp(f.stat().st_mtime).isoformat(timespec="seconds"), } for f in store.quote_files(folder) ] async def _folder(project_id: UUID, key: str) -> Path: root = await _project_root(project_id) try: return store.quote_dir(root, key) except ValueError as error: raise HTTPException(status_code=400, detail=str(error)) from error async def _file(project_id: UUID, key: str, name: str) -> Path: folder = await _folder(project_id, key) path = folder / name if store.safe_name(name) != name or not path.is_file(): raise HTTPException(status_code=404, detail="견적서 파일이 없습니다.") return path @router.get(PATH) async def list_quotes(project_id: UUID, key: str = Query(...)) -> list[dict[str, Any]]: folder = await _folder(project_id, key) return await asyncio.to_thread(_listing, folder) @router.post(PATH) async def upload_quotes( project_id: UUID, key: str = Query(...), files: list[UploadFile] = File(...) ) -> list[dict[str, Any]]: folder = await _folder(project_id, key) folder.mkdir(parents=True, exist_ok=True) for upload in files: name = store.safe_name(upload.filename or "") if not name: raise HTTPException(status_code=400, detail="파일 이름이 없습니다.") try: await save_upload_stream(upload, folder / name) except ValueError as error: raise HTTPException(status_code=400, detail=str(error)) from error return await asyncio.to_thread(_listing, folder) @router.get(PATH + "/file") async def download_quote(project_id: UUID, key: str = Query(...), name: str = Query(...)): path = await _file(project_id, key, name) return FileResponse(path, filename=name) @router.delete(PATH) async def delete_quote( project_id: UUID, key: str = Query(...), name: str = Query(...) ) -> list[dict[str, Any]]: path = await _file(project_id, key, name) await asyncio.to_thread(path.unlink) return await asyncio.to_thread(_listing, path.parent)