Files
Aislo/M02_MasterTemplete/M02_Project_Items_Router.py
T
eomsangdonandClaude Opus 5.5 0d8f967262 feat(M02): 집계 항목 복사본 완전체 — 입력연결 · 입력값 · 고른자재 · 단가(닫힌 묶음) · 가져온날 · 원본판 · 복사본 길 · 새로 가져오기 · 미리 가져오기 · 복사본만으로 계산 (PLAN 25-2b · 25-2e)
- 스냅샷 나눔: 원본 모으기(M02_Item_Source) · 맞추기(M02_Item_Snapshot) · 복사본 읽기 · 쓰기(M02_Item_Copy) · 단가 묶음 · 계산(M02_Item_Prices)
- 단가.json = 담은 로직 · 하위 로직 · 요소 · 준용 · 찾기 표 · 재료 고르기 후보 · 지역 · 계약종별 후보(마스터 파일 차례 그대로)
- 길 /api/projects/{id}/items(목록 · 미리 가져오기) · /{폴더}/{종류}(GET · PUT 판 409 · 면적 · 재료 서버 계산값) · /refresh · /calc
- 고친 복사본은 구조물에서 빠져도 안 지움 · 옛 복사본은 읽을 때 채움

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
2026-09-29 23:26:23 +09:00

114 lines
4.3 KiB
Python

"""프로젝트 집계 항목 복사본 길 — `/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)