"""B06 횡단 [상세] 모달의 **B07 횡단 수량표 값** 창구(138-4). 값은 B07 횡단도가 쓰는 `_quantity_table` 한 벌을 **브라우저 캐시 설계**로 돌린 것이다 — 화면이 TS 로 다시 짜지 않는다(CLAUDE.md 5장 ③ 서버 단독). 칸 배치도 B07 표 정의를 그대로 실어 보내 화면은 그리기만 한다. **표시 전용** — 정본을 쓰지 않는다. 모달을 열 때 부른다. """ from __future__ import annotations import asyncio import logging from pathlib import Path from typing import Any from uuid import UUID from fastapi import APIRouter from fastapi.responses import JSONResponse from pydantic import BaseModel, Field from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path from B06_Section.B06_Section_Repository import get_longitudinal_section from B06_Section.B06_Section_Router_Design import read_cross_design_inputs from B07_DesignDetail.B07_DesignDetail_Engine_Cad import _format from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import ( _CROSS_HEADER_KEYS, _CROSS_LEFT_ROWS, _CROSS_MIDDLE_ROWS, _CROSS_RIGHT_ROWS, ) from B07_DesignDetail.B07_DesignDetail_Router_Support import _quantity_table 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=["B06 Profile Cross"]) class CrossQuantityTableRequest(BaseModel): chainage_m: float = Field(..., ge=0) # 브라우저 캐시의 그 측점 설계(조작 반영본) — 없으면 원본 칸만. design: dict[str, Any] | None = None def cross_quantity_sheet(source: dict[str, Any], design: dict[str, Any] | None) -> dict[str, Any]: """B07 횡단 수량표 한 장 — 값(숫자) · 글(B07 표와 같은 꼴) · 칸 배치.""" table = _quantity_table(source, design) text = { key: _format(value) if isinstance(value, (int, float)) else "-" for key, value in table.items() } return { "quantity_table": table, "text": text, "layout": { "header": [list(row) for row in _CROSS_HEADER_KEYS], "left": [list(row) for row in _CROSS_LEFT_ROWS], "middle": [list(row) for row in _CROSS_MIDDLE_ROWS], "right": [list(row) for row in _CROSS_RIGHT_ROWS], }, } @router.post("/{project_id}/sections/{route_id}/cross-quantity-table", response_model=None) async def cross_quantity_table( project_id: UUID, route_id: int, request: CrossQuantityTableRequest ) -> JSONResponse: pool = get_db_pool() async with pool.acquire() as connection: longitudinal = await get_longitudinal_section(connection, project_id, route_id) stored_path = await get_project_storage_relative_path(connection, project_id) if not longitudinal or not stored_path: return JSONResponse( status_code=404, content={"status": "error", "message": "종횡단 상세 결과가 없습니다."}, ) try: *_, source = await asyncio.to_thread( read_cross_design_inputs, Path(resolve_stored_project_path(stored_path)), str(longitudinal["longitudinal_file_path"]), request.chainage_m, ) sheet = cross_quantity_sheet(source, request.design) except (FileNotFoundError, ValueError) as error: return JSONResponse(status_code=404, content={"status": "error", "message": str(error)}) except Exception: logger.exception( "횡단 수량표 계산 실패: project_id=%s chainage=%s", project_id, request.chainage_m ) return JSONResponse( status_code=500, content={"status": "error", "message": "횡단 수량표를 만들지 못했습니다."}, ) return JSONResponse(content={"status": "success", **sheet})