`phase: "detail"` 칸(돌 종류·조달·뒷길이·전면 기울기)을 **그리는 화면이 없었음**
(2026-09-09 실측: B06 구조물 폼은 b05 phase 열 칸만 그림. 화면 글자 전수에서 「조달」·
「뒷길이」·「돌 종류」 0건). 그 자리를 표준도가 맡음 — PLAN 4-5b 「표준도는 입력 화면이기도 함」.
B06 은 「어디에·몇 m」(배치), 표준도는 「어떤 제원」.
**장 하나 = 제원 조합 하나**라 한 번 고치면 그 조합의 개소 전부에 걸림.
지킨 것
· **자동값을 저장에 박지 않음** — 빈 칸이 「정한 적 없음」의 뜻(확정 ⑨). 비우면 그 키를
지움. 판정된 기울기는 칸이 아니라 **회색 도움말**로만 비춤.
· **막지 않음** — 실무 도면에 1:0.7·1:0.8 이 실재(구조물도 53장). 품셈 범위(0.20~0.50)
밖이어도 값은 받고 안내만.
· **값을 여기서 셈하지 않음** — 정본에 적기만 하고 표·그림은 다음 조회에서 그 정본으로
다시 섬. 표준도가 두 번째 정본이 되면 안 됨.
⚠ 만들다 잡은 것 셋
① `face_slope_ratio` 가 **구조물 등록부에 없어** 저장이 통째로 거절됐음
(`ValueError: 돌쌓기(찰)에 정의되지 않은 옵션입니다`). 한 칸 때문에 전부 못 저장되는
것은 나쁨 — 등록부에 없는 칸은 **빼고 이름으로 알림**(조용히 버리면 저장된 줄 앎).
그 칸 신설은 랩탑 메인 몫.
② 저장 뒤 표·그림을 다시 받으면서 **폼이 새로 그려져 안내가 지워졌음.** 밖에서 들고
있다가 다시 넣음.
③ **장 제목에 제원이 들어 있는데 좌측 단추 글자가 안 따라왔음.** 목록을 통째로 다시
받지 않고 표준도 줄만 갈아 끼움.
실화면 확인 (프로젝트 936be972, 개발 우회로로 열어서)
「표준도 2장」에서 돌 종류 견치돌·뒷길이 55 로 저장 →
단추 글자가 「…뒷길이 35㎝ 야면석·호박돌」 → 「…뒷길이 55㎝ 견치돌」로 **즉시 바뀜**
기울기 0.7 저장 → 안내 셋: 「1개소에 반영했습니다」 · 「1:0.7 는 품셈 표준경사 표
범위(1:0.2~1:0.5) 밖입니다 — 값은 그대로 씁니다」 · 「「전면 기울기」 칸이 masonry_dry
등록부에 아직 없어 저장하지 못했습니다」
조달 「구입」 저장 → 그 장의 **2개소에만** 들어가고 다른 장은 그대로(정본 실측)
확인 뒤 **지정받은 제원으로 되돌리고 우회로도 닫음**(목록 409 복귀).
700줄 제한으로 표준도 라우터를 `B07_DesignDetail_Router_Standard.py` 로 뗌(579 + 153).
자체검증 — 새 시험 7건 + 회귀 580 통과 · 0 실패, `tsc --noEmit` 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
154 lines
6.9 KiB
Python
154 lines
6.9 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"])
|
|
|
|
|
|
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)
|