"""B07 표준도(구조물도) 라우터 — 장 목록 조회와 **제원 입력**. 표준도는 도면이자 **입력 화면**이다(PLAN 4-5b). `phase: "detail"` 칸(돌 종류·조달·뒷길이· 전면 기울기)을 그리는 화면이 없어(2026-09-09 실측) 그 자리를 여기가 맡는다. **장 하나 = 제원 조합 하나**라 한 번 고치면 그 조합의 개소 전부에 걸린다. ⚠ 값을 여기서 셈하지 않는다 — 정본(`structures.json`)에 적기만 하고 표·그림은 다음 조회에서 그 정본으로 다시 선다. 표준도가 두 번째 정본이 되면 안 된다(CLAUDE.md 5장). """ import asyncio import logging from pathlib import Path from uuid import UUID from fastapi import APIRouter from fastapi.responses import JSONResponse from pydantic import BaseModel, ConfigDict, Field from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path from common_util.common_util_storage import resolve_stored_project_path from config.config_db import get_db_pool logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/projects", tags=["B07 Design Detail"]) class StandardSheetSpecRequest(BaseModel): """표준도 장 하나의 제원. **빈 값(null)은 「정한 적 없음」**이라 그 칸을 지운다.""" model_config = ConfigDict(extra="forbid") sheet_key: str base_revision: int = Field(ge=0) stone_kind: str | None = None stone_supply: str | None = None back_len_cm: int | str | None = None face_slope_ratio: float | str | None = None @router.put("/{project_id}/standard-sheets/spec") async def put_standard_sheet_spec( project_id: UUID, payload: StandardSheetSpecRequest ) -> JSONResponse: """장 하나의 제원을 고쳐 **그 조합의 구조물 전부**에 반영한다. ⚠ 값을 여기서 셈하지 않는다 — 정본(`structures.json`)에 적기만 하고, 표·그림은 다음 조회에서 그 정본으로 다시 선다. 표준도가 두 번째 정본이 되면 안 된다(CLAUDE.md 5장). """ from B05_Profile.B05_Profile_Structures_Repository import load_structures, save_structures from B05_Profile.B05_Profile_Structures_Schema import structure_type_map from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardSheet import standard_payload from B07_DesignDetail.B07_DesignDetail_Engine_Standard_Edit import ( apply_spec, clean_spec, drop_unregistered, ) try: pool = get_db_pool() 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)).resolve() except Exception: logger.exception("B07 표준도 제원 저장 실패(경로): project_id=%s", project_id) return JSONResponse( status_code=404, content={"status": "error", "message": "프로젝트 저장 폴더를 찾지 못했습니다."}, ) sheets = (await asyncio.to_thread(standard_payload, project_root)).get("sheets") or [] picked = next((s for s in sheets if s.get("key") == payload.sheet_key), None) if picked is None: return JSONResponse( status_code=404, content={"status": "error", "message": "그 표준도 장을 찾지 못했습니다."}, ) member_ids = { str(m.get("structure_id")) for m in picked.get("members") or [] if m.get("structure_id") } spec, notes = clean_spec(payload.model_dump(exclude={"sheet_key", "base_revision"})) # ⚠ 등록부에 없는 칸은 저장소가 거절한다 — 한 칸 때문에 **전부** 못 저장되지 않게 거른다. type_id = str(picked.get("type_id") or "") definition = structure_type_map().get(type_id) allowed = {field.key for field in definition.options} if definition else set() spec, missing = drop_unregistered(type_id, spec, allowed) notes.extend(missing) try: revision, stored = await asyncio.to_thread(load_structures, str(project_root)) updated, changed = apply_spec(stored, member_ids, spec) new_revision = await asyncio.to_thread( save_structures, str(project_root), updated, base_revision=payload.base_revision ) except Exception as exc: logger.exception("B07 표준도 제원 저장 실패: project_id=%s", project_id) return JSONResponse( status_code=409, content={"status": "error", "message": f"제원을 저장하지 못했습니다 — {exc}"}, ) return JSONResponse( content={ "status": "success", "project_id": str(project_id), "revision": new_revision, "previous_revision": revision, "changed": changed, # 범위 밖 값·표에 없는 규격은 **막지 않고 알린다**(실무에 1:0.7 이 실재). "notes": notes, } ) @router.get("/{project_id}/standard-sheets") async def get_standard_sheets(project_id: UUID) -> JSONResponse: """표준도(구조물도) **장 목록 + 하단표**. ⚠ 수량을 여기서 새로 셈하지 않는다 — B08 원단위 전개를 그대로 받아 **제원 조합으로 묶고 단위당으로 접기만** 한다(계산 자리는 한 곳, CLAUDE.md 5장). """ from B07_DesignDetail.B07_DesignDetail_Engine_Standard_Sheet import build_standard_sheets from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table as build_unit_table from B08_Quantity.B08_Quantity_Router_Material import _collect_structures try: pool = get_db_pool() async with pool.acquire() as connection: stored_path = await get_project_storage_relative_path(connection, project_id) project_root = str(Path(resolve_stored_project_path(stored_path)).resolve()) except Exception: logger.exception("B07 표준도 조회 실패(경로): project_id=%s", project_id) return JSONResponse( status_code=404, content={"status": "error", "message": "프로젝트 저장 폴더를 찾지 못했습니다."}, ) try: structures, names, skipped = await asyncio.to_thread(_collect_structures, project_root) unit_table = await asyncio.to_thread(build_unit_table, structures, names) except Exception: logger.exception("B07 표준도 전개 실패: project_id=%s", project_id) return JSONResponse( status_code=500, content={"status": "error", "message": "구조물 원단위를 전개하지 못했습니다."}, ) payload = build_standard_sheets(unit_table) payload["status"] = "success" payload["project_id"] = str(project_id) # 왜 안 실렸는지 — 「구조물이 없다」와 「걸러졌다」를 화면이 가릴 수 있어야 한다. payload["skipped_structures"] = skipped return JSONResponse(content=payload)