feat(B08): 성토/절토를 판정 한 벌로 받아 표준경사 열을 고름 (확정 ⑨ 마무리)

앞서 「성토 열로 잠정」 딱지를 달아 두었던 자리를 채움. 랩탑 메인이 낸 공용 함수
`common_util_structure_face_role(section_mode, side)` 를 **그대로** 부름 —
판정을 두 벌로 짜면 `both_cut` + 「자동」 예외에서 갈리므로 우리 쪽에 다시 안 짬.

- 새 저장 키 없음 — 이미 저장되는 `design.section_mode` 와 구조물 `options.side` 만 씀.
  라우터가 측점별 단면유형을 모아 넘기고, 구조물은 **가장 가까운 측점** 값을 씀.
- ⚠ 못 가르면 `None` 을 **성토로 눅이지 않음** — 종전값 1:0.3 으로 서되 **왜 못 갈랐는지**를
  근거 줄에 적음. 임의값이 금액으로 굳으면 안 됨.
- 근거 줄에 판정 문구가 그대로 실림 — 「… 1:0.35 · right_cut · 자동(성토 쪽) → 성토면」.

곁들여 — 전면 기울기 칸이 레지스트리에 생겨(`ebdf2988`) 「아직 칸이 없음」 문구와
없는-키 목록에서 뺌. ⚠ 표 밖 값(실무 도면의 S0.7·0.8)은 **막지 않고 그대로** 씀 —
접으면 사용자가 넣은 값이 조용히 다른 값이 됨. 시험으로 잠금.

검증 프로젝트 실측 — 62측점 전부 `right_cut`, 돌쌓기 「자동(성토 쪽)」 → 성토면.
잠정으로 쓰던 열과 **결과는 같고 근거가 값에 생김**. 돌쌓기(메) 21.190㎡(1:0.35) 유지.

시험 747 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 20:27:40 +09:00
co-authored by Claude Opus 5
parent 20738bd169
commit 1b03ad025a
2 changed files with 89 additions and 16 deletions
+26 -2
View File
@@ -25,6 +25,8 @@ from fastapi import APIRouter
from fastapi.responses import JSONResponse
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
from B06_Section.B06_Section_Repository import get_cross_section_designs
from B06_Section.B06_Section_Repository import get_workflow_route_context
from B05_Profile.B05_Profile_Structures_Repository import load_structures
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff, load_mapping, summarize
@@ -78,6 +80,28 @@ def _collect_structures(
return targets, names, sorted(set(skipped))
async def _section_modes(project_id: UUID) -> dict[float, str]:
"""측점별 단면유형(`left_cut` 등). 구조물이 **성토면인가 절토면인가**를 가릴 때 쓴다.
⚠ 새 저장 키를 만들지 않는다 — 이미 저장되는 `design.section_mode` 를 읽기만 한다.
못 읽으면 빈 표로 두고, 판정이 「가를 근거 없음」이 되게 한다(성토로 눅이지 않음).
"""
try:
context = await run_with_connection(get_workflow_route_context, project_id)
route_id = int((context or {}).get("route_id") or 0)
if not route_id:
return {}
designs = await run_with_connection(get_cross_section_designs, route_id)
except Exception:
logger.exception("B08 단면유형 조회 실패: project_id=%s", project_id)
return {}
return {
float(item["chainage_m"]): str((item.get("design") or {}).get("section_mode") or "")
for item in designs
if (item.get("design") or {}).get("section_mode")
}
@router.get("/{project_id}/quantity/material-summary")
async def get_material_summary(project_id: UUID) -> JSONResponse:
"""구조물 원단위와 자재총괄을 **한 응답**으로 낸다.
@@ -103,7 +127,7 @@ async def get_material_summary(project_id: UUID) -> JSONResponse:
content={"status": "error", "message": "구조물 정본을 읽지 못했습니다."},
)
unit_table = build_unit_table(structures, names)
unit_table = build_unit_table(structures, names, await _section_modes(project_id))
settings = quantity_settings(project_root)
material_table = build_material_table(
unit_table,
@@ -153,7 +177,7 @@ async def get_handoff(project_id: UUID) -> JSONResponse:
)
structures, names, skipped = _collect_structures(project_root)
unit_table = build_unit_table(structures, names)
unit_table = build_unit_table(structures, names, await _section_modes(project_id))
settings = quantity_settings(project_root)
material_table = build_material_table(
unit_table, supply_map=settings.get("material_supply") or {}