From 419148901b17af6c0933445016676d3bc12e7db0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 23:43:58 +0900 Subject: [PATCH] =?UTF-8?q?fix(b07):=20=ED=9A=A1=EB=8B=A8=EB=8F=84=20[?= =?UTF-8?q?=ED=99=95=EC=A0=95]=EC=9D=80=20=EC=84=A4=EA=B3=84=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A7=8C=20=EC=98=AC=EB=A6=BC=20=E2=80=94=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=85=8B=EC=9C=BC=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=ED=95=9C=20=EA=B0=92=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B8=EC=9D=84=20=EB=8D=AE=EB=8D=98=20=EA=B2=83=20?= =?UTF-8?q?=EA=B1=B7=EC=9D=8C(=EB=B8=8C=EB=A0=88=EC=9D=B8=20=E2=91=A1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 옛 길: 지반·단면·측구 쪽 셋만으로 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) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- B07_DesignDetail/B07_DesignDetail_Router.py | 44 +++----- .../B07_DesignDetail_Router_Support.py | 29 ----- B07_DesignDetail/B07_DesignDetail_Schema.py | 2 +- B07_DesignDetail/B07_DesignDetail_UI_Page.ts | 2 +- .../B07_DesignDetail_UI_Panels.ts | 2 +- .../tester/test_b07_confirm_keeps_design.py | 104 ++++++++++++++++++ 6 files changed, 120 insertions(+), 63 deletions(-) create mode 100644 resources/tester/test_b07_confirm_keeps_design.py diff --git a/B07_DesignDetail/B07_DesignDetail_Router.py b/B07_DesignDetail/B07_DesignDetail_Router.py index 9c1bd58d..20768b82 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router.py +++ b/B07_DesignDetail/B07_DesignDetail_Router.py @@ -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)}) diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support.py b/B07_DesignDetail/B07_DesignDetail_Router_Support.py index debe0f24..08d99000 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support.py @@ -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 diff --git a/B07_DesignDetail/B07_DesignDetail_Schema.py b/B07_DesignDetail/B07_DesignDetail_Schema.py index ecd6f96b..acfc367e 100644 --- a/B07_DesignDetail/B07_DesignDetail_Schema.py +++ b/B07_DesignDetail/B07_DesignDetail_Schema.py @@ -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 diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts index db6ed620..4fda6b0b 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts @@ -418,7 +418,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { // **기다린다**: 안 기다리면 오버레이가 먼저 걷혀, 버튼은 [수정]인데 CAD는 아직 // 편집이 열린 어긋난 순간이 생긴다. await loadDrawing(currentDrawing, currentIndex); - // 확정 시 재계산된 확정 단면적으로 지반/계획 정보 패널을 갱신한다. + // 확정한 설계(B06 정본 그대로 · 상태만 확정)로 지반/계획 정보 패널을 갱신한다. if (currentDrawing.kind === "cross") { infoPanelHost.replaceChildren( buildDesignInfoPanel( diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts b/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts index dab77c73..a3fde826 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts @@ -181,7 +181,7 @@ function infoRow(label: string, value: string): HTMLElement { } /** - * 선택 횡단도의 지반정보/계획정보를 2분할로 렌더한다 (잠정치, B07 확정 시 재계산). + * 선택 횡단도의 지반정보/계획정보를 2분할로 렌더한다 (B06 정본 값 · B07 확정은 상태만 올림). * * **장(여러 측점을 담은 도면)에는 측점 단위 값이 없다** — 서버가 `design` 을 넘기지 * 않는데도 제목만 「측점 …」으로 달려 어느 측점 값인지 오해됐다(2026-09-03 정리). diff --git a/resources/tester/test_b07_confirm_keeps_design.py b/resources/tester/test_b07_confirm_keeps_design.py new file mode 100644 index 00000000..f3327380 --- /dev/null +++ b/resources/tester/test_b07_confirm_keeps_design.py @@ -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"}