"""유토 **배분(평형선·운반거리·장비)** 만 내주는 창구. 왜 따로 있나(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 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": "측점 수가 너무 많습니다."}, ) try: output = await asyncio.to_thread( run_bundle_json, BUNDLE, _NPM_SCRIPT, {"haul_plan_for": result, "context": _mass_haul_context()}, ) 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})