Files
Aislo/B07_DesignDetail/B07_DesignDetail_Router_Standard.py
T
eomsangdonandClaude Opus 5 918e7c12da fix(B07): 표준도가 단면유형을 안 넘겨 기울기 판정이 통째로 꺼져 있던 것
표준도가 `build_table` 을 직접 부르면서 `section_modes` 를 안 넘겨, 성절토를 가를 근거가
없다며 **전 구조물이 종전값 1:0.3** 으로 섰음. 값이 없는 게 아니라 **안 넘긴 것**이었음.

`section_modes_from_designs` 한 벌을 그대로 씀(부르는 쪽마다 다시 짜면 B08 과 갈림).
노선은 `get_workflow_route_context` 로 찾고, 못 찾으면 빈 표 — 성토로 눅이지 않음.

⚠ 고치고 나니 **제목·그림이 표와 갈렸음** — 표는 판정된 1:0.35, 제목은 다시 판정하며
근거를 못 받아 1:0.3. 실측으로 잡았음. 그래서 **표를 만든 그 판정을 시트에 실어**
제목·그림이 같은 값을 쓰게 함(`face`·`face_reason`). 자리를 고르는 것도 B08 이 쓰는
`_section_mode_at` 과 판정 한 벌 `structure_face_role` 을 그대로 부름.

실측 (프로젝트 936be972) — 제목과 표가 **같은 값**
```
큰돌쌓기  H=2.5  제목 1:0.3   표 0.3    면적 2.6101
돌쌓기(메) H=2.0  제목 1:0.35  표 0.35   면적 2.1190   ← 메쌓기가 갈림
돌쌓기(찰) H=2.5  제목 1:0.3   표 0.3    면적 2.6101
옹벽      H=2.0  기울기 없음(판정 대상 아님)
```
표 산출근거에 판정 경로가 그대로 뜸 — 「품셈 13-4-4 [주]⑪ 표준경사 · 메쌓기 성토 ·
직고 2m ≤3m → 1:0.35 · right_cut · 자동(성토 쪽) → 성토면」.

앞서 `face_slope_ratio({}, wet=…)` 로 확인한 「근거 없음」은 `face` 를 안 준 호출이라
**정상 동작**이었음 — 함수가 성토로 안 눅이게 짠 결과.

자체검증 — 회귀 580 통과 · 0 실패. 세 라우터 모두 700줄 안(588 · 190 · 692).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 21:01:46 +09:00

191 lines
8.7 KiB
Python

"""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"])
async def section_modes_of_project(project_id: UUID) -> dict[float, str]:
"""프로젝트에서 노선을 찾아 단면유형 표를 낸다 — 노선을 모르면 빈 표."""
from B06_Section.B06_Section_Repository import get_workflow_route_context
from config.config_db import run_with_connection
try:
context = await run_with_connection(get_workflow_route_context, project_id)
route_id = int((context or {}).get("route_id") or 0)
except Exception:
logger.exception("B07 노선 조회 실패: project_id=%s", project_id)
return {}
return await section_modes_of(route_id) if route_id else {}
async def section_modes_of(route_id: int) -> dict[float, str]:
"""측점별 단면유형(`left_cut` 등) — 구조물이 **성토면인가 절토면인가**를 가르는 근거.
⚠ 이것을 안 넘기면 판정이 통째로 「가를 근거 없음」으로 떨어져 **전 구조물이 종전값
1:0.3 으로 선다**(2026-09-09 실측). 값이 없는 것이 아니라 **안 넘긴 것**이었다.
⚠ 표를 만드는 셈은 `section_modes_from_designs` 한 벌을 쓴다 — 부르는 쪽마다 다시
짜면 B08 과 갈린다.
"""
from B06_Section.B06_Section_Repository import get_cross_section_designs
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import section_modes_from_designs
from config.config_db import run_with_connection
try:
designs = await run_with_connection(get_cross_section_designs, route_id)
except Exception:
logger.exception("B07 단면유형 조회 실패: route_id=%s", route_id)
return {}
return section_modes_from_designs(designs)
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": "프로젝트 저장 폴더를 찾지 못했습니다."},
)
modes = await section_modes_of_project(project_id)
payload_sheets = await asyncio.to_thread(standard_payload, project_root, modes)
sheets = payload_sheets.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": "프로젝트 저장 폴더를 찾지 못했습니다."},
)
modes = await section_modes_of_project(project_id)
try:
structures, names, skipped = await asyncio.to_thread(_collect_structures, project_root)
unit_table = await asyncio.to_thread(build_unit_table, structures, names, modes)
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, modes)
payload["status"] = "success"
payload["project_id"] = str(project_id)
# 왜 안 실렸는지 — 「구조물이 없다」와 「걸러졌다」를 화면이 가릴 수 있어야 한다.
payload["skipped_structures"] = skipped
return JSONResponse(content=payload)