계단이 값에만 있던 것을 화면까지 연결함. 배선 — 세션 열쇠 `berm`(측점키 → 폭·간격·기울기)을 등록표에 두고, `readBermSession` 으로 읽어 ① 브라우저 재계산(`refreshCrossDesigns`)과 ② 서버 미리보기(`cross-design/preview` 의 `berms`) 양쪽에 실음. 암 경계선 오프셋과 같은 길이라 「계획선을 고치면 계단이 사라지는」 일이 없음. 실화면 확인(8001·5174, `/api/health` `stale:false`) — 측점 4120.0m 에 소단을 놓고 계획고를 한 칸 올렸다 내려 전 구간 재계산을 태움. · 폭 0.5m · 간격 3.0m → 절토 6.84 → 8.10㎡ (토사 2.42→3.24 · 암 4.42→4.86) · 폭 1.0m · 간격 2.0m → 절토 6.84 → 12.96㎡, 횡단도에 **계단이 눈으로 보임** · 소단을 안 놓은 옆 측점(4100.0m)은 3.77㎡ 그대로 — 놓은 곳만 달라짐 · 되돌린 뒤 6.84㎡ 로 복귀. [저장]·[확정] 안 눌렀으므로 정본은 그대로. `cut_slope_segments` 신설 — 절토 사면을 경사 구간별로 쪼갠 목록(파이썬·TS 짝). 법정 경사 검사가 읽을 값임(다른 창 요청). 소단이 서면 사면 전체를 하나로 재는 「실효 경사」가 완만해져 **위반이 사라진 것처럼** 보이므로(폭 1.0·간격 2 이면 설계 1:1 이 실효 1:1.71), 검사는 소단을 뺀 구간 자체를 봐야 함. 실측 — 소단을 놓아도 구간별 경사비는 1.0 그대로 나옴. 평탄부(소단)와 지반 만난 뒤 구간은 싣지 않음. 자체검증 — 거울 시험에 사면 구간 대조를 더해 파이썬·TS 일치 확인. 전체 539 passed · 18 skipped. TS 타입 검사 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
74 lines
3.0 KiB
Python
74 lines
3.0 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
|
|
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})
|