feat(B05,B06): 유토 배분을 서버로 옮기고 브라우저는 미리 받아 쓰게 함
배분·운반거리 산식(_balance 523 + _settle 235 = 758줄)을 브라우저 번들에서 빼기 위한
배선. 계산은 여전히 한 벌 — 서버가 같은 TS 를 Node 로 돈다(CLAUDE.md 5장).
- POST /projects/{id}/sections/{route_id}/haul-plan 신설
(B06_Section_Router_HaulPlan.py). 브라우저가 낸 누가토량 결과를 받아 배분만 돌려줌.
화면이 쓰는 꼴 그대로 내보내 그리기 코드를 안 건드림. 표시 전용 — 정본은 저장 때 따로.
- B05_Profile_Api_HaulPlan.ts 신설 — 편집이 멈추면(400ms) 조용히 받아 두는 선반입기.
늦게 온 응답은 버림(최신 요청만 채택). 못 받아도 곡선 자체는 그대로 보임.
- common_util_mass_haul.massHaulPayload 가 배분을 값으로 물던 것을 끊음(extra 인자).
이 커플링 때문에 화면에서 안 불러도 번들에 남았음.
- 저장 경로는 곡선도 배분도 안 만듦 — 서버가 정본을 내므로 balloon 위치만 보냄.
- 죽은 파일 B06_Section_UI_Section_View_MassHaul.ts(236줄) 삭제.
자체검증(공용 브라우저, 용화 route 149) — 엔드포인트 200 · 196ms.
서버 배분과 브라우저 computeHaulPlan 결과가 JSON 문자열까지 동일(blocks 1 · steps 2 ·
spoil/borrow/hauled/transferred/fill_total 전부 일치). 시험 400 통과·17 건너뜀.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""유토 **배분(평형선·운반거리·장비)** 만 내주는 창구.
|
||||
|
||||
왜 따로 있나(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})
|
||||
Reference in New Issue
Block a user