fix(b08): 산출 조건 [저장]이 운반계획을 다시 세움(브레인 ② · 지침 5장 「[저장]·[확정]은 서버가 정본으로 다시 계산」)

- 운반계획 밑수 칸(암 갈래 세트·구성비·시공법·계수·도쟈 한계거리)이 바뀐 저장만 B06 [저장]과 같은 서버 재계산을 부름 · 다른 칸 저장은 안 부름 · 못 하면 저장은 살리고 haul_plan_recomputed 거짓
- 종전엔 B06 을 다시 저장하기 전까지 토적표(새 계수) ↔ 운반표(옛 계획)가 갈렸음 — 계수 고르기 칸의 옛 틈도 함께 닫힘
- 936be972 화면에서 연암 60 리핑/보통암 40 발파 [저장](2.0초) → 유토곡선 깎기 6,296.8 = 토적표 6,296.81 · 토취 7,259.84 → 7,053.83 · 되돌림(설정 파일 · 재계산 원래 값)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-14 21:32:21 +09:00
co-authored by Claude Opus 5
parent 8f4ec48759
commit d57e97a1cd
3 changed files with 119 additions and 1 deletions
+15 -1
View File
@@ -45,6 +45,10 @@ from B08_Quantity.B08_Quantity_Engine_Handoff import load_mapping
from B08_Quantity.B08_Quantity_Engine_HaulSummary import build_table as build_haul_table
from B08_Quantity.B08_Quantity_Engine_HaulSummary import check_against_plan, summary_input_rows
from B08_Quantity.B08_Quantity_Engine_RockSplit import apply_rock_split
from B08_Quantity.B08_Quantity_Router_Earthwork_HaulPlan import HAUL_PLAN_KEYS
from B08_Quantity.B08_Quantity_Router_Earthwork_HaulPlan import (
recompute_haul_plan as _recompute_haul_plan,
)
from B08_Quantity.B08_Quantity_Engine_Preparation import build_table as build_preparation_table
from B08_Quantity.B08_Quantity_Engine_SlopeArea import build_table as build_slope_table
from B08_Quantity.B08_Quantity_Engine_SlopeLength import road_surface_area, station_slopes
@@ -626,6 +630,7 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) -
for name, method in values["rock_methods"].items()
if method in ROCK_METHODS
}
before = {key: quantity_settings(root).get(key) for key in HAUL_PLAN_KEYS}
try:
# ⚠ 고른 값을 **되돌릴 수 있어야** 하는 칸은 통째로 갈아 끼운다 — 병합이면
# 「안 정함」으로 되돌아가지 않는다(2026-09-07 화면에서 걸린 자리).
@@ -653,7 +658,16 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) -
status_code=500,
content={"status": "error", "message": "산출 조건을 저장하지 못했습니다."},
)
return JSONResponse(content={"status": "success", "quantity": saved.get("quantity") or {}})
after = quantity_settings(root)
changed = any(before[key] != after.get(key) for key in HAUL_PLAN_KEYS)
recomputed = await _recompute_haul_plan(project_id) if changed else False
return JSONResponse(
content={
"status": "success",
"quantity": saved.get("quantity") or {},
"haul_plan_recomputed": recomputed,
}
)
def _save_quantity(
@@ -0,0 +1,40 @@
"""산출 조건 [저장] 뒤 **운반계획 다시 세우기** — 2026-09-14 브레인 ②(지침 5장).
구성비·시공법·계수·도쟈 한계거리는 B06 유토곡선(운반계획)의 밑수다. B08 [저장]이 계획을 다시 안 세우면
B06 을 다시 저장하기 전까지 토적표(새 계수) ↔ 운반표(옛 계획)가 갈린다(계수 칸은 전부터 같은 틈).
⇒ 그 칸이 **바뀐 저장**만 B06 [저장]과 같은 서버 재계산을 부른다(무거워서 다른 칸 저장은 안 부름).
본 라우터(`B08_Quantity_Router_Earthwork.py`)가 700줄에 닿아 이 파일로 뗌.
"""
from __future__ import annotations
import logging
from uuid import UUID
from B06_Section.B06_Section_Repository import get_workflow_route_context
from config.config_db import run_with_connection
logger = logging.getLogger(__name__)
#: 운반계획의 밑수가 되는 산출 조건 칸.
HAUL_PLAN_KEYS = (
"rock_class_set",
"rock_classes",
"rock_ratios_pct",
"rock_methods",
"conversion_factors_override",
"dozer_haul_limit_m",
)
async def recompute_haul_plan(project_id: UUID) -> bool:
"""B06 [저장]과 같은 서버 재계산 — 못 하면 저장은 살리고 거짓(화면이 옛 계획임을 앎)."""
from B06_Section.B06_Section_Server_Calc_Prebuild import recompute_server_side
try:
context = await run_with_connection(get_workflow_route_context, project_id)
route_id = int((context or {}).get("route_id") or 0)
return bool(route_id) and await recompute_server_side(project_id, route_id) >= 0
except Exception:
logger.exception("B08 저장 뒤 운반계획 재계산 실패: project_id=%s", project_id)
return False
@@ -0,0 +1,64 @@
"""산출 조건 [저장]이 운반계획을 다시 세움 — 2026-09-14 브레인 ②(지침 5장 「[저장]·[확정]은 서버가 정본으로 다시 계산」).
구성비·시공법·계수·도쟈 한계거리는 B06 유토곡선(운반계획)의 밑수인데 B08 [저장]이 운반계획을 다시 안 셈
→ B06 을 다시 저장하기 전까지 토적표(새 계수) ↔ 운반표(옛 계획)가 갈렸음(계수 칸은 전부터 같은 틈).
⇒ 그 칸이 **바뀐 저장**만 서버 재계산(`recompute_server_side`)을 부름 · 다른 칸 저장은 안 부름(무거움).
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
import B08_Quantity.B08_Quantity_Router_Earthwork as earthwork_router # noqa: E402
PROJECT_ID = "56565656-5656-5656-5656-565656565656"
URL = f"/api/projects/{PROJECT_ID}/quantity/settings"
@pytest.fixture()
def calls(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> tuple[TestClient, list]:
recorded: list = []
async def fake_run(func, *args):
return "project"
async def fake_recompute(project_id):
recorded.append(str(project_id))
return True
monkeypatch.setattr(earthwork_router, "run_with_connection", fake_run)
monkeypatch.setattr(earthwork_router, "resolve_stored_project_path", lambda _p: str(tmp_path))
monkeypatch.setattr(earthwork_router, "_recompute_haul_plan", fake_recompute)
app = FastAPI()
app.include_router(earthwork_router.router)
return TestClient(app), recorded
def test_운반계획_밑수가_바뀐_저장만_재계산(calls) -> None:
client, recorded = calls
ratios = {"rock_ratios_pct": {"연암": 60, "보통암": 40}}
first = client.put(URL, json=ratios)
assert first.status_code == 200 and first.json()["haul_plan_recomputed"] is True
assert recorded == [PROJECT_ID]
# 같은 값을 다시 저장 — 안 부름
assert client.put(URL, json=ratios).json()["haul_plan_recomputed"] is False
# 운반계획과 무관한 칸 — 안 부름
assert (
client.put(URL, json={"topsoil_thickness_m": 0.2}).json()["haul_plan_recomputed"] is False
)
assert len(recorded) == 1
for body in (
{"rock_methods": {"연암": "ripping"}},
{"conversion_factors_override": {"soil": {"compacted": 0.85}}},
{"dozer_haul_limit_m": 70},
):
assert client.put(URL, json=body).json()["haul_plan_recomputed"] is True, body
assert len(recorded) == 4