fix(종횡단): 342-1 계획선 한 벌 · 남은 낡음 표시 · 흙 배분 열쇠

- 횡단 다시 셈이 화면이 쥔 선(측점 한 벌 · 관 지점 · 옮긴 꼭짓점)을 받음 — 종단도가 선을 맡기고 B06 진입 정합 · 3D [업데이트] 앞 다시 셈도 같은 선 · 낡음 판정도 그 선 기준(staleAgainstScreen)
- 다시 셈 뒤에도 낡은 측점이 남으면 유토곡선은 그리고 그 측점만 표시 · 콘솔 경고 한 줄 · 흙 배분도 건너뛰지 않음(09-03 「새 값만 보여주기」 바꿈 · 사용자 확정 10-16)
- 흙 배분: 실패한 열쇠 다시 보냄 · 받은 열쇠(누가토량 결과 전체)는 다시 안 물음 · 서버도 같은 열쇠 기억 · 묶음 실패는 500
- 옛 시험 셋 새 동작으로 맞춤 · 서버 열쇠 시험 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
2026-10-16 11:15:22 +09:00
co-authored by Claude Opus 5.5
parent 2879f9bea9
commit 48ddca36a2
13 changed files with 290 additions and 108 deletions
+29 -5
View File
@@ -15,7 +15,10 @@
from __future__ import annotations
import asyncio
import hashlib
import json
import logging
from collections import OrderedDict
from typing import Any
from uuid import UUID
@@ -37,6 +40,16 @@ router = APIRouter(prefix="/api/projects", tags=["B06 Profile Cross"])
_NPM_SCRIPT = "build:server-calc"
# 측점 수 상한 — 정상 노선은 수백 곳이다. 그보다 크면 비정상 요청으로 본다.
_MAX_POINTS = 5000
# 열쇠(누가토량 결과 + 계수 · 장비 경계) → 배분 — 같은 열쇠면 Node 를 다시 안 띄움
# (342-1 · 부록 4 R25). ponytail: 프로세스 메모리 · 최근 몇 벌만 — 재시작 뒤 첫 열기는
# 다시 셈(_derived 저장본은 343 틀 뒤).
_KEPT: OrderedDict[str, Any] = OrderedDict()
_KEPT_MAX = 32
def _plan_key(result: dict[str, Any], context: Any) -> str:
body = json.dumps({"result": result, "context": context}, sort_keys=True, default=str)
return hashlib.sha1(body.encode("utf-8")).hexdigest()
@router.post("/{project_id}/sections/{route_id}/haul-plan", response_model=None)
@@ -69,15 +82,17 @@ async def compute_haul_plan(
factors = await conversion_factors_for(project_id)
# 장비 거리 경계도 프로젝트 값으로 — 도쟈 한계거리를 고쳤으면 배분이 그 값으로 선다.
limits = await haul_limits_for(project_id)
context = _mass_haul_context(haul_inputs, factors, limits)
key = _plan_key(result, context)
if key in _KEPT:
_KEPT.move_to_end(key)
return JSONResponse(content={"status": "success", "haul_plan": _KEPT[key]})
try:
output = await asyncio.to_thread(
run_bundle_json,
BUNDLE,
_NPM_SCRIPT,
{
"haul_plan_for": result,
"context": _mass_haul_context(haul_inputs, factors, limits),
},
{"haul_plan_for": result, "context": context},
)
except Exception:
logger.exception("유토 배분 계산 실패: project_id=%s route_id=%s", project_id, route_id)
@@ -85,5 +100,14 @@ async def compute_haul_plan(
status_code=500,
content={"status": "error", "message": "유토 배분 계산에 실패했습니다."},
)
plan = output.get("haul_plan") if isinstance(output, dict) else None
# 묶음 실패(None)는 실패로 — 성공 + 빈 배분으로 주면 화면이 그 열쇠를 다시 안 물었다(342-1).
if not isinstance(output, dict):
return JSONResponse(
status_code=500,
content={"status": "error", "message": "유토 배분 계산에 실패했습니다."},
)
plan = output.get("haul_plan")
_KEPT[key] = plan
if len(_KEPT) > _KEPT_MAX:
_KEPT.popitem(last=False)
return JSONResponse(content={"status": "success", "haul_plan": plan})