From d57e97a1cdeb23ccfa3913cf0513d6f3c285a652 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 21:32:21 +0900 Subject: [PATCH] =?UTF-8?q?fix(b08):=20=EC=82=B0=EC=B6=9C=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20[=EC=A0=80=EC=9E=A5]=EC=9D=B4=20=EC=9A=B4=EB=B0=98?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=EC=9D=84=20=EB=8B=A4=EC=8B=9C=20=EC=84=B8?= =?UTF-8?q?=EC=9B=80(=EB=B8=8C=EB=A0=88=EC=9D=B8=20=E2=91=A1=20=C2=B7=20?= =?UTF-8?q?=EC=A7=80=EC=B9=A8=205=EC=9E=A5=20=E3=80=8C[=EC=A0=80=EC=9E=A5]?= =?UTF-8?q?=C2=B7[=ED=99=95=EC=A0=95]=EC=9D=80=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EA=B0=80=20=EC=A0=95=EB=B3=B8=EC=9C=BC=EB=A1=9C=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EA=B3=84=EC=82=B0=E3=80=8D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운반계획 밑수 칸(암 갈래 세트·구성비·시공법·계수·도쟈 한계거리)이 바뀐 저장만 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) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- B08_Quantity/B08_Quantity_Router_Earthwork.py | 16 ++++- .../B08_Quantity_Router_Earthwork_HaulPlan.py | 40 ++++++++++++ .../test_b08_settings_recompute_haul.py | 64 +++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 B08_Quantity/B08_Quantity_Router_Earthwork_HaulPlan.py create mode 100644 resources/tester/test_b08_settings_recompute_haul.py diff --git a/B08_Quantity/B08_Quantity_Router_Earthwork.py b/B08_Quantity/B08_Quantity_Router_Earthwork.py index 842db03c..0f64e5c1 100644 --- a/B08_Quantity/B08_Quantity_Router_Earthwork.py +++ b/B08_Quantity/B08_Quantity_Router_Earthwork.py @@ -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( diff --git a/B08_Quantity/B08_Quantity_Router_Earthwork_HaulPlan.py b/B08_Quantity/B08_Quantity_Router_Earthwork_HaulPlan.py new file mode 100644 index 00000000..e392c850 --- /dev/null +++ b/B08_Quantity/B08_Quantity_Router_Earthwork_HaulPlan.py @@ -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 diff --git a/resources/tester/test_b08_settings_recompute_haul.py b/resources/tester/test_b08_settings_recompute_haul.py new file mode 100644 index 00000000..0975d9e9 --- /dev/null +++ b/resources/tester/test_b08_settings_recompute_haul.py @@ -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