Files
Aislo/B06_Section/B06_Section_Router_HaulPlan.py
T
eomsangdonandClaude Opus 5 465dbb955d feat(B08): 토량환산계수를 프로젝트가 고를 수 있게 열되 기본값은 불변
- 기본값 정의처는 config_system_design 한 곳 그대로. 고른 값은 프로젝트 설정
  quantity.conversion_factors_override 에 얹고, earthwork_conversion_factors() 한 함수가
  기본값 위에 얹어 풀어 냄.
- 그 함수를 거치는 자리 여섯 — 토적표·운반표·기초단가(B08), 유토곡선 컨텍스트·
  배분 계산·[저장] 재계산(B06). 상수를 직접 드는 자리를 없앰.
- 산출 조건 패널에 갈래별 계수 칸 신설 — 기본값·품셈 범위를 함께 보이고,
  「유토곡선·운반표·기초단가에도 같이 닿음」 안내 한 줄. 범위 밖은 막지 않고 사유를 받음.
- 품셈 체적변화율 범위를 서버 상수로 두고 화면에 내려보냄(프론트에 다시 적지 않음).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrXDD23Dvt2sR7q3X6oekp
2026-09-12 13:38:00 +09:00

87 lines
3.6 KiB
Python

"""유토 **배분(평형선·운반거리·장비)** 만 내주는 창구.
왜 따로 있나(2026-09-06 사용자 확정) — 배분 산식은 노하우가 몰린 자리라 브라우저 번들에
남기지 않는다. 화면은 누가토량까지만 스스로 내고(`common_util_mass_haul.ts`), 편집이
멈추면 그 결과를 여기로 보내 배분을 받아 쥔다. 그래서 유토곡선 패널을 펼치는 순간이
즉시가 된다(미리 받아 뒀으므로).
**계산은 한 벌이다** — 화면이 쓰던 TS 를 Node 진입점(`B06_Section_Server_Calc_Node.ts`)으로
그대로 돌린다(CLAUDE.md 5장). 파이썬으로 옮기면 저장 정본과 화면 값이 갈린다.
정본을 만들지 않는다 — 이 응답은 **표시 전용**이다. 저장 정본은 [저장]·[확정] 뒤
`recompute_server_side` 가 따로 낸다.
"""
from __future__ import annotations
import asyncio
import logging
from typing import Any
from uuid import UUID
from fastapi import APIRouter, Body
from fastapi.responses import JSONResponse
from B06_Section.B06_Section_Server_Calc_Prebuild import (
BUNDLE,
_mass_haul_context,
conversion_factors_for,
haul_inputs_for,
)
from common_util.common_util_node_bundle import run_bundle_json
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/projects", tags=["B06 Profile Cross"])
_NPM_SCRIPT = "build:server-calc"
# 측점 수 상한 — 정상 노선은 수백 곳이다. 그보다 크면 비정상 요청으로 본다.
_MAX_POINTS = 5000
@router.post("/{project_id}/sections/{route_id}/haul-plan", response_model=None)
async def compute_haul_plan(
project_id: UUID,
route_id: int,
payload: dict[str, Any] = Body(...),
) -> JSONResponse:
"""브라우저가 낸 누가토량 결과를 받아 **배분만** 돌려준다.
입력은 `common_util_mass_haul.computeMassHaul` 의 결과 한 벌이다(`points` 포함).
출력은 `{"haul_plan": {...} | null}` — 화면이 그대로 그린다.
"""
result = payload.get("result")
points = result.get("points") if isinstance(result, dict) else None
if not isinstance(points, list) or not points:
return JSONResponse(
status_code=400,
content={"status": "error", "message": "누가토량 결과가 비어 있습니다."},
)
if len(points) > _MAX_POINTS:
return JSONResponse(
status_code=400,
content={"status": "error", "message": "측점 수가 너무 많습니다."},
)
# 구조물 몫(공제·잔토)을 **넘겨야** 사토가 줄고 는다 — 인자 없이 부르면 늘 `None` 이라
# 통로만 있고 값이 안 흐른다(2026-09-09 실측으로 드러난 자리).
haul_inputs = await haul_inputs_for(project_id)
# 곡선이 쓰는 계수도 프로젝트가 고른 값으로 — 토적표·운반표와 같은 값이어야 한다.
factors = await conversion_factors_for(project_id)
try:
output = await asyncio.to_thread(
run_bundle_json,
BUNDLE,
_NPM_SCRIPT,
{
"haul_plan_for": result,
"context": _mass_haul_context(haul_inputs, factors),
},
)
except Exception:
logger.exception("유토 배분 계산 실패: project_id=%s route_id=%s", project_id, route_id)
return JSONResponse(
status_code=500,
content={"status": "error", "message": "유토 배분 계산에 실패했습니다."},
)
plan = output.get("haul_plan") if isinstance(output, dict) else None
return JSONResponse(content={"status": "success", "haul_plan": plan})