fix(B06): 상세 조회에서 계획을 다시 계산하던 자리를 저장 시점으로 옮김

사용자 확정(2026-09-06): 읽을 때는 영구저장소에서 가져오기만 하고, 계산은
화면(캐시)에서 하며, 저장·확정에서만 영구저장소에 쓴다.

- get_section_detail 이 조회할 때마다 돌리던 포장 구간 보정·세월교 노면 하강
  보정을 제거. 조회는 저장분을 그대로 싣는다.
- 두 보정을 recompute_server_side 로 옮겨 [저장]·[확정]·자동설계 체인에서
  정본에 남긴다. 보정된 설계 위에서 구조물 면적·유토곡선이 나오도록 순서를
  잡았다.
- 설계 미지정 측점 폴백(attach_default_designs)만 조회에 남긴다 — 저장분이
  있으면 아무 일도 하지 않는다.

검증: pytest 387 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 14:49:44 +09:00
co-authored by Claude Opus 5
parent aad78ec942
commit a0e50d93e3
2 changed files with 62 additions and 24 deletions
+6 -21
View File
@@ -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"],
@@ -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 "없음",
)