From a0e50d93e36f813606b7edb4715c60d36b61a643 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 6 Sep 2026 14:49:44 +0900 Subject: [PATCH] =?UTF-8?q?fix(B06):=20=EC=83=81=EC=84=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=97=90=EC=84=9C=20=EA=B3=84=ED=9A=8D=EC=9D=84=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EA=B3=84=EC=82=B0=ED=95=98=EB=8D=98=20?= =?UTF-8?q?=EC=9E=90=EB=A6=AC=EB=A5=BC=20=EC=A0=80=EC=9E=A5=20=EC=8B=9C?= =?UTF-8?q?=EC=A0=90=EC=9C=BC=EB=A1=9C=20=EC=98=AE=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 확정(2026-09-06): 읽을 때는 영구저장소에서 가져오기만 하고, 계산은 화면(캐시)에서 하며, 저장·확정에서만 영구저장소에 쓴다. - get_section_detail 이 조회할 때마다 돌리던 포장 구간 보정·세월교 노면 하강 보정을 제거. 조회는 저장분을 그대로 싣는다. - 두 보정을 recompute_server_side 로 옮겨 [저장]·[확정]·자동설계 체인에서 정본에 남긴다. 보정된 설계 위에서 구조물 면적·유토곡선이 나오도록 순서를 잡았다. - 설계 미지정 측점 폴백(attach_default_designs)만 조회에 남긴다 — 저장분이 있으면 아무 일도 하지 않는다. 검증: pytest 387 passed. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_Router.py | 27 ++------- .../B06_Section_Server_Calc_Prebuild.py | 59 ++++++++++++++++++- 2 files changed, 62 insertions(+), 24 deletions(-) diff --git a/B06_Section/B06_Section_Router.py b/B06_Section/B06_Section_Router.py index a865299e..81bcf2cc 100644 --- a/B06_Section/B06_Section_Router.py +++ b/B06_Section/B06_Section_Router.py @@ -42,8 +42,6 @@ from B06_Section.B06_Section_Router_Design import ( from B06_Section.B06_Section_Router_Design import ( attach_default_designs as _attach_default_designs, compute_default_designs as _compute_default_designs, - enforce_pavement_ranges as _enforce_pavement_ranges, - enforce_ford_surface_drops as _enforce_ford_surface_drops, stored_standard_cross_section as _stored_standard_cross_section, pavement_ranges as _pavement_ranges, paved_at as _paved_at, @@ -313,27 +311,14 @@ async def get_section_detail( if abs(float(section.get("chainage_m", 0.0)) - record["chainage_m"]) < 0.01: section["design"] = record["design"] break - # 포장 구간·물넘이 범위는 저장분이 비포장이어도 포장으로 맞춘다(2026-08-28). - # 표준횡단면은 확정 때 저장해 둔 사용자 값을 쓴다 — 없으면 config 기본값. + # 포장 구간·세월교 노면 하강 보정은 **저장 때**로 옮겼다(2026-09-06 사용자 확정: + # 「읽을 때는 영구저장소에서 가져오기만」). 조회는 저장분을 그대로 싣는다 — + # 보정은 `B06_Section_Server_Calc_Prebuild.recompute_server_side` 가 [저장]·[확정]과 + # 자동설계 체인에서 돌려 정본에 남긴다. standard = _stored_standard_cross_section(longitudinal) - await asyncio.to_thread( - _enforce_pavement_ranges, - detail["longitudinal"], - detail["cross_sections"], - project_root, - standard, - ) - # 세월교 측점은 구체 위 노면이 월류 높이만큼 낮게 앉는다 — 저장분이 옛 계획고면 - # 여기서 다시 계산한다(2026-08-30 사용자 확정). - await asyncio.to_thread( - _enforce_ford_surface_drops, - detail["longitudinal"], - detail["cross_sections"], - project_root, - standard, - ) # 지정값이 없는 측점은 기본값(토사/좌절토)으로 즉석 계산해 프리뷰로 채운다. - # (미저장 프리뷰: 실제 저장은 사용자가 카드를 조작하거나 확정할 때 이뤄진다.) + # 저장분이 있으면 **아무것도 하지 않는다**(측점마다 design 유무만 본다) — 아직 + # 저장된 적 없는 비정규 측점만 이 폴백을 탄다. await asyncio.to_thread( _attach_default_designs, detail["longitudinal"], diff --git a/B06_Section/B06_Section_Server_Calc_Prebuild.py b/B06_Section/B06_Section_Server_Calc_Prebuild.py index 158aa0a2..b73556e4 100644 --- a/B06_Section/B06_Section_Server_Calc_Prebuild.py +++ b/B06_Section/B06_Section_Server_Calc_Prebuild.py @@ -18,17 +18,21 @@ from __future__ import annotations import asyncio +import json import logging from pathlib import Path from typing import Any from uuid import UUID +from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path from B06_Section.B06_Section_Repository import ( get_longitudinal_section, merge_cross_section_design_patch, merge_longitudinal_section_data, + update_cross_section_design, ) 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 get_db_pool from config.config_system import ( EARTHWORK_CONVERSION_FACTORS, @@ -57,6 +61,26 @@ def _mass_haul_context() -> dict[str, Any]: } +def _enforce_stored_designs( + longitudinal: dict[str, Any], + sections: list[dict[str, Any]], + project_root: Path, + standard: dict[str, Any] | None, +) -> None: + """저장분 설계를 **쓰는 시점에** 바로잡는다 — 포장 구간·세월교 노면 하강. + + 예전에는 상세를 **읽을 때마다** 돌려 화면이 볼 때만 맞았다(저장분은 낡은 채로). + 2026-09-06 사용자 확정대로 「읽기는 영구저장소에서 가져오기만」이므로 이쪽으로 옮겼다. + """ + from B06_Section.B06_Section_Router_Design import ( + enforce_ford_surface_drops, + enforce_pavement_ranges, + ) + + enforce_pavement_ranges(longitudinal, sections, project_root, standard) + enforce_ford_surface_drops(longitudinal, sections, project_root, standard) + + async def recompute_server_side(project_id: UUID | str, route_id: int) -> int: """구조물 면적·유토곡선을 다시 계산해 정본에 얹는다. 고친 측점 수를 돌려준다.""" from B06_Section.B06_Section_Router import get_section_detail @@ -67,12 +91,33 @@ async def recompute_server_side(project_id: UUID | str, route_id: int) -> int: if payload is None: # JSONResponse = 실패 logger.warning("서버 재계산: 종횡단 상세를 못 받음 (route_id=%s)", route_id) return 0 + detail = payload(mode="json") + sections = detail.get("cross_sections") or [] + + pool = get_db_pool() + async with pool.acquire() as connection: + stored_path = await get_project_storage_relative_path(connection, project_uuid) + longitudinal_row = await get_longitudinal_section(connection, project_uuid, route_id) + project_root = Path(resolve_stored_project_path(stored_path)) + from B06_Section.B06_Section_Router_Design import stored_standard_cross_section + + standard = stored_standard_cross_section(longitudinal_row) + # 포장 구간·세월교 보정 — 고쳐진 설계 위에서 면적·유토곡선이 나와야 한다. + before = [json.dumps(item.get("design"), sort_keys=True, default=str) for item in sections] + await asyncio.to_thread( + _enforce_stored_designs, detail.get("longitudinal") or {}, sections, project_root, standard + ) + fixed = [ + item + for index, item in enumerate(sections) + if json.dumps(item.get("design"), sort_keys=True, default=str) != before[index] + ] output = await asyncio.to_thread( run_bundle_json, BUNDLE, _NPM_SCRIPT, - {"detail": payload(mode="json"), "context": _mass_haul_context()}, + {"detail": detail, "context": _mass_haul_context()}, ) if not isinstance(output, dict): return 0 @@ -80,7 +125,6 @@ async def recompute_server_side(project_id: UUID | str, route_id: int) -> int: mass_haul = output.get("mass_haul") updated = 0 - pool = get_db_pool() async with pool.acquire() as connection: # balloon 위치는 **사용자가 끌어 옮긴 화면값**이다 — 서버가 만들지 않으므로 # 저장분에서 떼어 새 유토곡선에 도로 붙인다(2026-09-06). @@ -93,6 +137,14 @@ async def recompute_server_side(project_id: UUID | str, route_id: int) -> int: await connection.begin() try: + for item in fixed: + await update_cross_section_design( + connection, + route_id=route_id, + chainage_m=float(item.get("chainage_m") or 0.0), + design=item["design"], + project_id=project_uuid, + ) for row in rows if isinstance(rows, list) else []: patch: dict[str, Any] = { key: float(row[key]) @@ -117,8 +169,9 @@ async def recompute_server_side(project_id: UUID | str, route_id: int) -> int: await connection.rollback() raise logger.info( - "서버 재계산: route_id=%s 구조물 측점 %s곳 갱신, 유토곡선 %s", + "서버 재계산: route_id=%s 설계 보정 %s곳, 구조물 면적 %s곳, 유토곡선 %s", route_id, + len(fixed), updated, "갱신" if isinstance(mass_haul, dict) else "없음", )