fix(b07): 횡단도 [확정]은 설계 상태만 올림 — 입력 셋으로 다시 계산한 값으로 정본을 덮던 것 걷음(브레인 ②)
- 옛 길: 지반·단면·측구 쪽 셋만으로 compute_cross_design 을 돌려 설계를 통째로 update
→ 암선·절토경사·표준 횡단·구조물 트림·사용자 입력(측구 끔)이 사라짐
- 936be972 실측(읽기만): 62측점 전부 단면적 바뀜 · 절토 5,868.92 → 4,650.76㎥(−20.8%) · 성토 +5.8% · 종점 1078.01 측구 끔→켬
- 지금: patch = {status: confirmed} 뿐 · 응답 설계는 저장값 그대로 · _recompute_confirmed_design 지움
- 확인: 실제 라우터로 23장 전부 [확정] → 62측점 상태 뺀 값 차이 0 · 상태 되돌림
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
@@ -43,7 +43,6 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support import (
|
||||
_invalidate_drawing,
|
||||
_read_drawing,
|
||||
_read_json,
|
||||
_recompute_confirmed_design,
|
||||
_store_confirmed_drawing,
|
||||
landuse_source,
|
||||
lidar_source,
|
||||
@@ -405,7 +404,7 @@ async def confirm_design_drawing(
|
||||
) -> DesignDrawingConfirmResponse | JSONResponse:
|
||||
"""현재 편집 도면을 영구 저장하고 도면별 확정 상태를 기록한다.
|
||||
|
||||
횡단도 확정 시 B06 지정 잠정치를 동일 엔진으로 재계산해 확정치로 승격·저장한다.
|
||||
횡단도 확정 시 담긴 측점 설계의 **상태만** 확정으로 올린다(값은 B06 정본 그대로).
|
||||
"""
|
||||
try:
|
||||
route_id, project_root, longitudinal_path, bypass = await _confirmed_source(project_id)
|
||||
@@ -444,9 +443,10 @@ async def confirm_design_drawing(
|
||||
quantity_tables or None,
|
||||
)
|
||||
|
||||
# 횡단도면이면 확정 단면적을 재계산한다 (재계산 실패는 도면 확정을 막지 않음).
|
||||
# 장은 담긴 측점 전부를 함께 확정한다.
|
||||
recomputed: list[tuple[int, dict[str, Any]]] = []
|
||||
# 횡단도면이면 담긴 측점 설계를 확정으로 올린다 — **상태만** 바꾼다. 장은 담긴 측점 전부.
|
||||
# B07 CAD 에는 설계를 고치는 자리가 없어 덮을 값이 없다. 예전에는 입력 셋(지반·단면·측구
|
||||
# 쪽)만으로 단면적을 다시 계산해 설계를 통째로 덮어, 암선·절토경사·표준 횡단·구조물
|
||||
# 트림과 사용자 입력(측구 끔)이 사라졌다(2026-09-14 936be972 실측 62측점 · 브레인 ②).
|
||||
cross_match = _CROSS_ID.fullmatch(drawing_id)
|
||||
pool = get_db_pool()
|
||||
targets: list[int] = []
|
||||
@@ -454,39 +454,21 @@ async def confirm_design_drawing(
|
||||
targets = list(sheet["chainages"])
|
||||
elif item.kind == "cross" and cross_match:
|
||||
targets = [int(cross_match.group(1))]
|
||||
for chainage_int in targets:
|
||||
designation = designs.get(chainage_int)
|
||||
if not designation:
|
||||
continue
|
||||
try:
|
||||
recomputed.append(
|
||||
(
|
||||
chainage_int,
|
||||
await asyncio.to_thread(
|
||||
_recompute_confirmed_design,
|
||||
longitudinal_path,
|
||||
f"cross_{chainage_int:05d}m",
|
||||
designation,
|
||||
),
|
||||
)
|
||||
)
|
||||
except (ValueError, KeyError, FileNotFoundError, OSError):
|
||||
logger.warning(
|
||||
"B07 확정 단면적 재계산 실패 (도면 확정은 유지): drawing_id=%s 측점=%s",
|
||||
drawing_id,
|
||||
chainage_int,
|
||||
exc_info=True,
|
||||
)
|
||||
confirmed_designs = [
|
||||
(chainage_int, {**designs[chainage_int], "status": "confirmed"})
|
||||
for chainage_int in targets
|
||||
if designs.get(chainage_int)
|
||||
]
|
||||
|
||||
async with pool.acquire() as connection:
|
||||
await connection.begin()
|
||||
try:
|
||||
for chainage_int, confirmed_design in recomputed:
|
||||
for chainage_int, _design in confirmed_designs:
|
||||
await merge_cross_section_design_by_round(
|
||||
connection,
|
||||
route_id=route_id,
|
||||
chainage_int=chainage_int,
|
||||
patch=confirmed_design,
|
||||
patch={"status": "confirmed"},
|
||||
)
|
||||
async with connection.cursor() as cursor:
|
||||
if all_confirmed:
|
||||
@@ -502,7 +484,7 @@ async def confirm_design_drawing(
|
||||
id=drawing_id,
|
||||
confirmed=True,
|
||||
all_confirmed=all_confirmed,
|
||||
design=recomputed[0][1] if len(recomputed) == 1 else None,
|
||||
design=confirmed_designs[0][1] if len(confirmed_designs) == 1 else None,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
|
||||
|
||||
@@ -646,32 +646,3 @@ def _invalidate_drawing(project_root: Path, drawing_id: str) -> None:
|
||||
if entry:
|
||||
entry["confirmed"] = False
|
||||
_write_manifest(project_root, manifest)
|
||||
|
||||
|
||||
def _recompute_confirmed_design(
|
||||
longitudinal_path: Path, cross_stem: str, designation: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""B06 지정값과 현재 계획고로 절·성토 단면적을 재계산해 확정치(status=confirmed)로 만든다.
|
||||
|
||||
B07 CAD에는 아직 편집 가능한 설계선이 없으므로, 저장된 지정값(지반유형·단면유형·
|
||||
측구위치)과 계획고로 동일 엔진을 재실행해 확정 시점 값을 고정한다.
|
||||
"""
|
||||
longitudinal = _read_json(longitudinal_path)
|
||||
cross_path = longitudinal_path.parent.parent / "cross_sections" / f"{cross_stem}.json"
|
||||
source = _read_json(cross_path)
|
||||
samples = source.get("samples")
|
||||
if not isinstance(samples, list):
|
||||
raise ValueError("횡단 상세 파일 형식이 올바르지 않습니다.")
|
||||
design_elevation = design_elevation_from_longitudinal(
|
||||
longitudinal, float(source.get("chainage_m", 0.0))
|
||||
)
|
||||
design = compute_cross_design(
|
||||
samples,
|
||||
design_elevation,
|
||||
ground_type=designation["ground_type"],
|
||||
section_mode=designation["section_mode"],
|
||||
ditch_side=designation.get("ditch_side"),
|
||||
**curve_widening_args(source),
|
||||
)
|
||||
design["status"] = "confirmed"
|
||||
return design
|
||||
|
||||
@@ -86,7 +86,7 @@ class DesignDrawingConfirmResponse(BaseModel):
|
||||
id: str
|
||||
confirmed: bool
|
||||
all_confirmed: bool
|
||||
# 횡단도 확정 시 재계산된 확정 설계(status=confirmed). 종단도·재계산 불가 시 None.
|
||||
# 횡단도 확정 시 저장된 설계(상태만 status=confirmed). 종단도·장·설계 없음이면 None.
|
||||
design: dict[str, Any] | None = None
|
||||
|
||||
|
||||
|
||||
@@ -418,7 +418,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
// **기다린다**: 안 기다리면 오버레이가 먼저 걷혀, 버튼은 [수정]인데 CAD는 아직
|
||||
// 편집이 열린 어긋난 순간이 생긴다.
|
||||
await loadDrawing(currentDrawing, currentIndex);
|
||||
// 확정 시 재계산된 확정 단면적으로 지반/계획 정보 패널을 갱신한다.
|
||||
// 확정한 설계(B06 정본 그대로 · 상태만 확정)로 지반/계획 정보 패널을 갱신한다.
|
||||
if (currentDrawing.kind === "cross") {
|
||||
infoPanelHost.replaceChildren(
|
||||
buildDesignInfoPanel(
|
||||
|
||||
@@ -181,7 +181,7 @@ function infoRow(label: string, value: string): HTMLElement {
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택 횡단도의 지반정보/계획정보를 2분할로 렌더한다 (잠정치, B07 확정 시 재계산).
|
||||
* 선택 횡단도의 지반정보/계획정보를 2분할로 렌더한다 (B06 정본 값 · B07 확정은 상태만 올림).
|
||||
*
|
||||
* **장(여러 측점을 담은 도면)에는 측점 단위 값이 없다** — 서버가 `design` 을 넘기지
|
||||
* 않는데도 제목만 「측점 …」으로 달려 어느 측점 값인지 오해됐다(2026-09-03 정리).
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""B07 횡단도 [확정]은 설계 **상태만** 올린다 — 정본 설계값을 덮지 않는다 (2026-09-14 브레인 ②).
|
||||
|
||||
실측(936be972 · 읽기만): 옛 [확정]은 입력 셋(지반·단면·측구 쪽)만으로 단면적을 다시 계산해
|
||||
설계를 통째로 덮었다 — 62측점 전부 단면적이 바뀌고(절토 −20.8% · 성토 +5.8%) 암선·절토경사·
|
||||
표준 횡단·구조물 트림이 빠졌으며, 종점 1078.01 은 **사용자가 끈 측구가 켜졌다**.
|
||||
B07 CAD 에는 설계를 고치는 자리가 없으므로 덮을 값이 없다 — 「반만 계산할 거면 반만 덮는다」.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from uuid import UUID
|
||||
|
||||
import B07_DesignDetail.B07_DesignDetail_Router as router
|
||||
from B07_DesignDetail.B07_DesignDetail_Schema import DesignDrawingConfirmRequest
|
||||
|
||||
STORED = {
|
||||
"status": "provisional",
|
||||
"ground_type": "ripping_rock",
|
||||
"section_mode": "left_cut",
|
||||
"ditch_side": "left",
|
||||
"ditch_enabled": False,
|
||||
"rock_boundary_offset_m": 0.8,
|
||||
"cut_slope_ratio": 0.5,
|
||||
"cut_area_m2": 48.894,
|
||||
"fill_area_m2": 54.84,
|
||||
}
|
||||
|
||||
|
||||
class _Cursor:
|
||||
async def __aenter__(self) -> _Cursor:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class _Connection:
|
||||
async def begin(self) -> None: ...
|
||||
|
||||
async def commit(self) -> None: ...
|
||||
|
||||
async def rollback(self) -> None: ...
|
||||
|
||||
def cursor(self) -> _Cursor:
|
||||
return _Cursor()
|
||||
|
||||
|
||||
class _Acquire:
|
||||
async def __aenter__(self) -> _Connection:
|
||||
return _Connection()
|
||||
|
||||
async def __aexit__(self, *_: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def test_확정은_상태만_올리고_설계값을_안_덮는다(monkeypatch, tmp_path: Path) -> None:
|
||||
patches: list[dict] = []
|
||||
|
||||
async def source(_project_id: UUID) -> tuple[int, Path, Path, bool]:
|
||||
return 182, tmp_path, tmp_path / "longitudinal.json", False
|
||||
|
||||
async def designs(_route_id: int) -> dict[int, dict]:
|
||||
return {700: dict(STORED)}
|
||||
|
||||
async def merge(_connection: object, **kwargs: object) -> bool:
|
||||
patches.append(dict(kwargs["patch"])) # type: ignore[arg-type]
|
||||
return True
|
||||
|
||||
async def stage(*_: object) -> None: ...
|
||||
|
||||
monkeypatch.setattr(router, "_confirmed_source", source)
|
||||
monkeypatch.setattr(router, "_designs_by_chainage", designs)
|
||||
monkeypatch.setattr(
|
||||
router, "_drawing_list", lambda *_: [SimpleNamespace(id="cross_00700m", kind="cross")]
|
||||
)
|
||||
monkeypatch.setattr(router, "extract_quantity_table", lambda *_: None)
|
||||
monkeypatch.setattr(router, "_store_confirmed_drawing", lambda *_: False)
|
||||
monkeypatch.setattr(router, "get_db_pool", lambda: SimpleNamespace(acquire=_Acquire))
|
||||
monkeypatch.setattr(router, "merge_cross_section_design_by_round", merge)
|
||||
monkeypatch.setattr(router, "start_stage", stage)
|
||||
monkeypatch.setattr(router, "complete_stage", stage)
|
||||
# 옛 길 — 입력 셋으로 다시 계산한 값(암 절토경사가 빠져 절토가 줄어든 모양)을 흉내 낸다.
|
||||
monkeypatch.setattr(
|
||||
router,
|
||||
"_recompute_confirmed_design",
|
||||
lambda *_: {**STORED, "ditch_enabled": True, "cut_area_m2": 7.527, "status": "confirmed"},
|
||||
raising=False,
|
||||
)
|
||||
|
||||
response = asyncio.run(
|
||||
router.confirm_design_drawing(
|
||||
UUID("936be972-11bc-46c2-8bf3-b15d8de7df0d"),
|
||||
"cross_00700m",
|
||||
DesignDrawingConfirmRequest(drawing={}),
|
||||
)
|
||||
)
|
||||
|
||||
# 덮는 것은 상태 하나뿐 — 단면적·사용자 입력(측구 끔)은 그대로.
|
||||
assert patches == [{"status": "confirmed"}]
|
||||
# 화면 정보 패널이 받는 설계도 저장값 그대로(상태만 확정).
|
||||
assert response.design == {**STORED, "status": "confirmed"}
|
||||
Reference in New Issue
Block a user