merge(511-B · 511-C 합치기): origin/sub_laptop_4(511-C 유토 배분 E2 · 판 haul 줄) 받음 — 공유 파일 충돌 풂

- Node 입구 design_calc_node.ts 등록부 밖 id = 시험 길 · 단계 · 화면 길(PAGE_IDS) · haul.view
- calc_runner = PAGE_IDS(cross.refresh) · VIEW_IDS(haul.view) 둘 다 받음
- calc_sites.py 머리 글 = B-4 cross · 511-C haul(기본 server) 함께 · 묶음 13 줄
- 시험 309_2 · 309_2_browser · 511_b1 = 열린 줄에 haul 더함 · 화면 시험 「지금 서버」 = haul 기본 server 로 1 줄(바꾼 뒤 2 줄)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 15:55:17 +09:00
co-authored by Claude Opus 5.5
19 changed files with 403 additions and 65 deletions
+25 -11
View File
@@ -5,8 +5,9 @@
멈추면 그 결과를 여기로 보내 배분을 받아 쥔다. 그래서 유토곡선 패널을 펼치는 순간이
즉시가 된다(미리 받아 뒀으므로).
**계산은 한 벌이다** — 화면이 쓰던 TS 를 Node 진입점(`B06_Section_Server_Calc_Node.ts`)으로
그대로 돌린다(CLAUDE.md 5장). 파이썬으로 옮기면 저장 정본과 화면 값이 갈린다.
**계산은 한 벌이다** — 화면이 쓰던 TS(`computeHaulPlan`)를 늘 떠 있는 Node 시험 길 `haul.view`
(`design_calc_node_trial.ts` · 511-C)로 그대로 돌린다(CLAUDE.md 5장). 파이썬으로 옮기면 저장 정본과
화면 값이 갈린다.
정본을 만들지 않는다 — 이 응답은 **표시 전용**이다. 저장 정본은 [저장]·[확정] 뒤
`recompute_server_side` 가 따로 낸다.
@@ -28,21 +29,19 @@ from fastapi.responses import JSONResponse
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
from B06_Section.B06_Section_Server_Calc_Prebuild import (
BUNDLE,
_mass_haul_context,
conversion_factors_for,
haul_inputs_for,
haul_limits_for,
)
from common_util.common_util_calc_runner import run_calc_served
from common_util.common_util_derived_store import read_derived, write_derived
from common_util.common_util_node_bundle import run_bundle_json
from common_util.common_util_storage import resolve_stored_project_path
from config.config_db import run_with_connection
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/projects", tags=["B06 Profile Cross"])
_NPM_SCRIPT = "build:server-calc"
# 측점 수 상한 — 정상 노선은 수백 곳이다. 그보다 크면 비정상 요청으로 본다.
_MAX_POINTS = 5000
# 열쇠(누가토량 결과 + 계수 · 장비 경계) → 배분 — 같은 열쇠면 Node 를 다시 안 띄움
@@ -64,6 +63,26 @@ def _keep(key: str, plan: Any) -> None:
_KEPT.popitem(last=False)
def _run_view(result: dict[str, Any], context: dict[str, Any]) -> dict[str, Any] | None:
"""화면 꼴 배분(그림용 · 잔진동 거름) — 옛 server-calc `haul_plan_for` 갈래와 같은 인자."""
options = {
key: context.get(key)
for key in (
"collected_stone_deduction_m3",
"collected_stone_by_ground_m3",
"collected_stone_ground_unknown_m3",
"structure_spoil_m3",
"structure_spoil_points",
)
}
options["conversion"] = context.get("earthwork_conversion")
options["drawing"] = True
args = [result, context.get("haul_equipment_limits"), options]
answer = run_calc_served(["haul.view"], {"args": {"haul.view": args}})
results = answer.get("results") if isinstance(answer, dict) else None
return {"haul_plan": results["haul.view"]} if isinstance(results, dict) else None
@router.post("/{project_id}/sections/{route_id}/haul-plan", response_model=None)
async def compute_haul_plan(
project_id: UUID,
@@ -114,12 +133,7 @@ async def compute_haul_plan(
_keep(key, stored["value"])
return JSONResponse(content={"status": "success", "haul_plan": stored["value"]})
try:
output = await asyncio.to_thread(
run_bundle_json,
BUNDLE,
_NPM_SCRIPT,
{"haul_plan_for": result, "context": context},
)
output = await asyncio.to_thread(_run_view, result, context)
except Exception:
logger.exception("유토 배분 계산 실패: project_id=%s route_id=%s", project_id, route_id)
return JSONResponse(
@@ -13,8 +13,9 @@
**보안**이다 — 저장 경로가 브라우저 계산이면 유토 배분·운반거리 코드가 번들에 남아야
해서 화면에서 안 그려도 뺄 수 없다. 대가는 저장 대기 850ms → 약 980ms(Node 131ms).
**계산을 다시 짜지 않는다.** 화면이 쓰는 TS 를 Node 진입점(`B06_Section_Server_Calc_Node.ts`)
으로 감싸 그대로 돌린다. 파이썬으로 포팅하면 같은 기하가 두 벌이 되어 「그림은 이런데
**계산을 다시 짜지 않는다.** 화면이 쓰는 TS 를 늘 떠 있는 Node(`design_calc_node.ts` ·
`structure.layouts` · `masshaul.cumulative` · `haul.plan` · 511-C)로 그대로 돌린다.
파이썬으로 포팅하면 같은 기하가 두 벌이 되어 「그림은 이런데
수량은 저렇다」가 생긴다.
실패는 던진다 — 부르는 쪽([저장] · [확정])이 `recomputed: false` 로 화면에 알린다(388-2).
@@ -38,7 +39,9 @@ from B06_Section.B06_Section_Repository import (
merge_longitudinal_section_data,
)
from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_design_batches
from common_util.common_util_node_bundle import bundle_stale, run_bundle_json
from common_util.common_util_calc_runner import BUNDLE as NODE_BUNDLE
from common_util.common_util_calc_runner import run_calc, run_calc_served
from common_util.common_util_node_bundle import bundle_stale
from common_util.common_util_project_settings import (
haul_equipment_limits,
mixed_conversion_factors,
@@ -56,6 +59,7 @@ from config.config_system import (
logger = logging.getLogger(__name__)
ROOT = Path(__file__).resolve().parents[1]
# 옛 server-calc 묶음 — 정본 길은 늘 떠 있는 Node(`_run_node`) · 이 묶음은 시험 대조만(B-6 걷음 몫)
BUNDLE = ROOT / "config" / "server_calc_node" / "B06_Section_Server_Calc_Node.js"
# 곡선부 확폭 입력 칸(파이썬 `curve_widening_args` · TS `SaveSection`)
CURVE_KEYS = ("plan_radius_m", "curve_outer_side", "curve_widening_m")
@@ -89,16 +93,37 @@ def _node_fingerprint(node_input: dict[str, Any]) -> str:
json.dumps(node_input, sort_keys=True, default=str).encode("utf-8"),
usedforsecurity=False,
)
digest.update(str(BUNDLE.stat().st_mtime_ns if BUNDLE.is_file() else 0).encode())
digest.update(str(NODE_BUNDLE.stat().st_mtime_ns if NODE_BUNDLE.is_file() else 0).encode())
return digest.hexdigest()
_NODE_IDS = ("structure.layouts", "masshaul.cumulative", "haul.plan")
def _run_node(node_input: dict[str, Any]) -> dict[str, Any] | None:
"""늘 떠 있는 Node(못 부르면 새 프로세스) 셋 → 옛 server-calc 답 꼴
`{areas, mass_haul, extra_walls}`."""
answer = run_calc_served(_NODE_IDS, node_input)
results = answer.get("results") if isinstance(answer, dict) else None
if not isinstance(results, dict):
results = run_calc(_NODE_IDS, node_input)
if not isinstance(results, dict):
return None
layouts = results.get("structure.layouts") or {}
cumulative = results.get("masshaul.cumulative")
return {
"areas": layouts.get("areas"),
"mass_haul": {**cumulative, **(results.get("haul.plan") or {})} if cumulative else None,
"extra_walls": layouts.get("extra_walls"),
}
def _node_output(node_input: dict[str, Any]) -> Any:
"""같은 입력이면 지난 결과 · 아니면 Node(면적 · 유토곡선)를 돌려 기억한다."""
key = _node_fingerprint(node_input)
if key in _NODE_CACHE and not bundle_stale(BUNDLE):
if key in _NODE_CACHE and not bundle_stale(NODE_BUNDLE):
return _NODE_CACHE[key]
output = run_bundle_json(BUNDLE, _NPM_SCRIPT, node_input)
output = _run_node(node_input)
if output is None: # 실패는 기억하지 않음 — 다음 [저장]이 다시 셈
return None
if len(_NODE_CACHE) >= _NODE_CACHE_LIMIT: