"""프로젝트 집계 항목 복사본 길 — `/api/projects/{id}/items…` (PLAN 25-1b · 25-2b · 25-2e). GET …/items 목록(이름 · 원본 · 가져온날 · 마스터 바뀜) POST …/items {summary_item} 미리 가져오기(구조물에 안 이은 항목) GET …/items/{폴더}/{종류} {종류, 판, 내용} PUT …/items/{폴더}/{종류} {판, 내용} — 판이 다르면 409 · 면적 · 재료는 서버 계산값 POST …/items/{폴더}/refresh 새로 가져오기(입력값 · 고른자재는 같은 키면 살림) POST …/items/{폴더}/calc 복사본만으로 계산(세 비목 · 계 · 빠진 입력) 종류 = 도면 · 면적 · 재료 · 자재조합 · 입력연결 · 입력값 · 고른자재. ⚠ 등록은 `main.py`(로그인만) — 프로젝트 권한(같은 회사)은 여기서 `_project` 로 봄(403 · 404). """ from __future__ import annotations import asyncio from typing import Any, Literal from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from common_util import common_util_spreadsheet as spreadsheet from common_util.common_util_auth import verify_session from common_util.common_util_summary_item import parse_summary_item from M02_MasterTemplete import M02_Item_Copy as copies from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layer_routes router = APIRouter(prefix="/api/projects", tags=["M02 Project Items"]) Kind = Literal["도면", "면적", "재료", "자재조합", "입력연결", "입력값", "고른자재"] class WriteBody(BaseModel): 판: str | None = None 내용: Any class PrefetchBody(BaseModel): summary_item: dict[str, Any] async def _root(session: dict[str, Any], project_id: str): return (await layer_routes._project(session, project_id))["root"] async def _run(fn, *args): try: return await asyncio.to_thread(fn, *args) except copies.Stale as error: raise HTTPException( status_code=409, detail={"message": str(error), "판": error.current} ) from error except LookupError as error: raise HTTPException(status_code=404, detail=str(error)) from error except ValueError as error: raise HTTPException(status_code=400, detail=str(error)) from error @router.get("/{project_id}/items") async def list_items( project_id: str, session: dict[str, Any] = Depends(verify_session) ) -> dict[str, Any]: return {"항목": await _run(copies.list_items, await _root(session, project_id))} @router.post("/{project_id}/items") async def prefetch_item( project_id: str, body: PrefetchBody, session: dict[str, Any] = Depends(verify_session) ) -> dict[str, Any]: try: item = parse_summary_item(body.summary_item) except ValueError as error: raise HTTPException(status_code=400, detail=str(error)) from error return await _run(copies.prefetch, await _root(session, project_id), item) @router.get("/{project_id}/items/{folder}/{kind}") async def read_item_file( project_id: str, folder: str, kind: Kind, session: dict[str, Any] = Depends(verify_session) ) -> dict[str, Any]: return await _run(copies.read_file, await _root(session, project_id), folder, kind) def _recalc(document: dict) -> dict: try: return spreadsheet.with_server_values(document) except spreadsheet.RecalcError as error: raise HTTPException(status_code=503, detail=str(error)) from error @router.put("/{project_id}/items/{folder}/{kind}") async def write_item_file( project_id: str, folder: str, kind: Kind, body: WriteBody, session: dict[str, Any] = Depends(verify_session), ) -> dict[str, Any]: root = await _root(session, project_id) return await _run(copies.write_file, root, folder, kind, body.판, body.내용, _recalc) @router.post("/{project_id}/items/{folder}/refresh") async def refresh_item( project_id: str, folder: str, session: dict[str, Any] = Depends(verify_session) ) -> dict[str, Any]: return await _run(copies.refresh, await _root(session, project_id), folder) @router.post("/{project_id}/items/{folder}/calc") async def calc_item( project_id: str, folder: str, session: dict[str, Any] = Depends(verify_session) ) -> dict[str, Any]: return await _run(copies.calc, await _root(session, project_id), folder)