Files
Aislo/B08_Quantity/B08_Quantity_Router_StmateLibrary.py
T

113 lines
4.4 KiB
Python

"""B08 라이브러리 — STmate 출력 엑셀에서 **고정형 항목 뽑아 넣기** (PLAN 4장 · 2026-09-14 브레인 판정).
두 걸음:
① [읽기] 파일을 올리면 읽은 호표 수·구성 줄 수·못 읽은 사유를 돌려줌 — **아무것도 안 씀**.
사용자가 수를 보고 넣을지 정함(판정 「읽은 뒤 수를 보이고 묻는 걸음」).
② [넣기] 같은 파일 + 고른 호표 차례 + 우리 구조물 종류 → 서버가 **파일을 다시 읽어** 개인 단에 씀.
⚠ 브라우저가 보낸 줄을 받아 적지 않음(CLAUDE.md 5장) · 개인 단만(판정 Ⓗ) · 종류당 하나라 같은 종류 내 것은
덮어씀(판정 Ⓐ) · 종류는 사용자가 고름 — 이름으로 자동으로 안 붙임(판정 Ⓒ).
"""
import asyncio
import io
from typing import Any
from uuid import UUID
from fastapi import APIRouter, Depends, File, Form, UploadFile
from fastapi.responses import JSONResponse
from common_util.common_util_auth import verify_session
router = APIRouter(prefix="/api/projects", tags=["B08 Quantity"])
BASE = "/{project_id}/quantity/structure-sheets/library/stmate"
#: 출력 엑셀은 수백 KB — 넉넉히 두되 끝없이 받지 않음.
MAX_BYTES = 20 * 1024 * 1024
def _error(status: int, message: str, **extra: Any) -> JSONResponse:
return JSONResponse(
status_code=status, content={"status": "error", "message": message, **extra}
)
async def _read(file: UploadFile) -> dict[str, Any] | JSONResponse:
from B08_Quantity.B08_Quantity_Engine_StmateRecipe import read_recipes
data = await file.read(MAX_BYTES + 1)
if len(data) > MAX_BYTES:
return _error(413, "파일이 너무 큼(20MB 넘음)")
return await asyncio.to_thread(read_recipes, io.BytesIO(data))
@router.post(BASE + "/read")
async def read_stmate_recipes(
project_id: UUID,
file: UploadFile = File(...),
session: dict[str, Any] = Depends(verify_session),
) -> JSONResponse:
"""① 읽기만 — 호표 목록(차례·명칭·규격·단위·구성 줄 수)과 못 읽은 사유."""
read = await _read(file)
if isinstance(read, JSONResponse):
return read
hopyo = [
{
"no": h["no"],
"name": h["name"],
"spec": h["spec"],
"unit": h["unit"],
"rows": len(h["rows"]),
"percent_rows": sum(1 for row in h["rows"] if row["unit"] == "%"),
"contract_rows": h["contract_rows"],
}
for h in read["hopyo"]
]
return JSONResponse(
content={
"status": "success",
"file_name": file.filename or "",
"project": read["project"],
"hopyo": hopyo,
"counts": {"hopyo": len(hopyo), "rows": sum(h["rows"] for h in hopyo)},
"problems": read["problems"],
}
)
@router.post(BASE + "/save")
async def save_stmate_recipe(
project_id: UUID,
file: UploadFile = File(...),
hopyo_no: int = Form(...),
type_id: str = Form(..., min_length=1, max_length=100),
session: dict[str, Any] = Depends(verify_session),
) -> JSONResponse:
"""② 넣기 — 서버가 다시 읽은 호표 하나를 고정형 항목으로 **로그인한 사람 개인 단**에 씀."""
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
from B08_Quantity.B08_Quantity_Engine_StmateRecipe import recipe_item
from B08_Quantity.B08_Quantity_Engine_StructureLibrary import save_item_personal, tier_dirs
folder = tier_dirs(session.get("company_id"), session.get("user_id")).get("personal")
if folder is None:
return _error(403, "개인 라이브러리는 회사에 속한 사용자만 씁니다.")
if type_id not in structure_type_map():
return _error(400, f"모르는 구조물 종류: {type_id}")
read = await _read(file)
if isinstance(read, JSONResponse):
return read
hopyo = next((h for h in read["hopyo"] if h["no"] == hopyo_no), None)
if hopyo is None:
return _error(404, f"제{hopyo_no}호표를 읽지 못함", problems=read["problems"])
item = recipe_item(
hopyo, type_id=type_id, file_name=file.filename or "", project=read["project"]
)
code = await asyncio.to_thread(save_item_personal, folder, item)
return JSONResponse(
content={
"status": "success",
"code": code,
"name": item["name"],
"rows": len(item["rows"]),
"note": item["note"],
}
)