PLAN 8-4b 열 명세대로. B06 이 이미 낸 측점별 단면적을 다시 재지 않고
체적화만 함 — 새 수량을 낳지 않으므로 캐시·조작 경로 불필요(CLAUDE.md 5장).
열 구성 (실무 토적표 = 오솔길 1.BOM 36열과 1:1)
측점·거리·절토[토사·암 각 단면적/입적/보정량]·측구터파기[토사·암 각 3칸]
·보정량계·성토[단면적/입적]·유용토·차인토량·누가토량.
사면 계열(층따기·면고르기·법면보호공·지장목제거)은 사면길이가 아직 없어 일감 3.
계산 규칙
체적 = (앞 단면적 + 현 단면적)/2 x 거리. 첫 측점은 앞이 없어 체적 없음.
보정량 = 체적 x 다짐 환산계수 — 정의처는 EARTHWORK_CONVERSION_FACTORS 한 곳,
여기서 값을 다시 적지 않음(토사 0.90 / 리핑암 1.15 / 발파암 1.30).
값을 자르지 않음 — 품셈 1-2-2 는 표기 규칙이고 절사는 화면 몫(PLAN 8-16).
원가 쪽(줄마다 원 단위 절사)과 규칙이 반대라 섞지 말 것.
TODO(미결 · PLAN 8-4b) — 설계가 측구를 토사·암으로 안 나눠 줌(ditch_area_m2 한 값).
잠정으로 그 측점 절토 토사:암 면적비로 안분함. 측구는 절토부에 파므로 같은
지반을 만난다는 것이 근거. 설계가 측구 지반을 따로 내면 _split_ditch 만 교체.
API — GET /api/projects/{id}/quantity/{route_id}/earthwork-table,
경로 생략형은 워크플로 최신 노선으로. main.py 는 자기 두 줄만 추가.
검증 — tmp/tests/test_b08_earthwork_table.py 14건 통과.
거창 실무 BOM 실측값 재현(단면적 1.89 → 체적 9.45 → 보정 8.505,
다음 측점 18.10 → 16.29, 측구 0.18㎡ → 10m당 1.80 → 1.62).
공용 브라우저에서 실 API 호출로 route 150·측점 65곳 확인 — 측점 20 에서
(0.2723+1.9615)/2x20 = 22.338, x0.9 = 20.1042, 암 25.427 x1.15 = 29.24105,
측구 안분 0.0784+0.1016 = 0.18 로 전건 일치.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
65 lines
2.6 KiB
Python
65 lines
2.6 KiB
Python
"""B08 토적표 조회 라우터 (일감 2 · PLAN 8-4b).
|
|
|
|
값은 어디서 오나
|
|
측점별 단면적은 **B06 이 이미 낸 정본**이다(`cross_sections.data.design` 의
|
|
`cut_soil_area_m2`·`cut_rock_area_m2`·`fill_area_m2`·`ditch_area_m2`).
|
|
B08 은 그것을 다시 재지 않고 **평균단면적법으로 체적화만** 한다.
|
|
|
|
계산 자리 (CLAUDE.md 5장)
|
|
초기값은 서버가 한 번 계산해 영구저장한다. 여기서는 저장된 단면적을 읽어 표를 만든다 —
|
|
새 수량을 낳지 않으므로 캐시·조작 경로가 따로 필요 없다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from B06_Section.B06_Section_Repository import (
|
|
get_cross_section_designs,
|
|
get_workflow_route_context,
|
|
)
|
|
from B08_Quantity.B08_Quantity_Engine_EarthworkTable import StationArea, build_table
|
|
from config.config_db import run_with_connection
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/projects", tags=["B08 Quantity"])
|
|
|
|
|
|
def _stations(designs: list[dict[str, Any]]) -> list[StationArea]:
|
|
return [
|
|
StationArea.from_design(item["chainage_m"], item.get("design") or {}) for item in designs
|
|
]
|
|
|
|
|
|
@router.get("/{project_id}/quantity/{route_id}/earthwork-table")
|
|
async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
|
"""토적표 — 측점별 단면적을 평균단면적법으로 체적화한 표."""
|
|
try:
|
|
designs = await run_with_connection(get_cross_section_designs, route_id)
|
|
except Exception:
|
|
logger.exception("B08 토적표 조회 실패: project_id=%s route_id=%s", project_id, route_id)
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={"status": "error", "message": "토적표를 만들지 못했습니다."},
|
|
)
|
|
table = build_table(_stations(designs))
|
|
table["route_id"] = route_id
|
|
return JSONResponse(content=table)
|
|
|
|
|
|
@router.get("/{project_id}/quantity/earthwork-table")
|
|
async def get_earthwork_table_for_current_route(project_id: UUID) -> JSONResponse:
|
|
"""경로를 안 주면 워크플로가 보고 있는 경로로 낸다 — 화면이 route_id 를 모를 때 쓴다."""
|
|
context = await run_with_connection(get_workflow_route_context, project_id)
|
|
if not context or not context.get("route_id"):
|
|
return JSONResponse(
|
|
status_code=404,
|
|
content={"status": "error", "message": "이 프로젝트에 확정된 노선이 없습니다."},
|
|
)
|
|
return await get_earthwork_table(project_id, int(context["route_id"]))
|