From 0dbe4d5f310c7b5dee3d9be9e1bab5026669319c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 3 Nov 2026 15:50:38 +0900 Subject: [PATCH] =?UTF-8?q?feat(765-4):=20=EC=A7=80=ED=98=95=20=EC=B9=A9?= =?UTF-8?q?=20=3D=20=EB=B2=8C=20=EB=A7=9E=EB=B0=94=EA=BF=88=20=C2=B7=20LAS?= =?UTF-8?q?=20=EB=82=98=EC=A4=91=20=EC=98=AC=EB=A6=AC=EA=B8=B0=20=EB=92=A4?= =?UTF-8?q?=20=3D=20=ED=99=95=EC=9D=B8=EC=B0=BD=20=E2=86=92=20=EC=A0=84?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=8B=A4=EC=8B=9C(=EB=91=90=20=EB=B2=8C?= =?UTF-8?q?=20=C2=B7=20B05~B09=20=EC=B4=88=EA=B8=B0=ED=99=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B05_Profile_Router_TerrainSwitch: 칩 = swap_design_set(떠난 벌 [저장] 고침 살림 · 묶음 없음 · 묵음 = 한 번 지음 · 실패 = 통째 되돌림) · force = switch_terrain → run_surface_change_chain(765-5) - 확인창 글: 바꿈 = 미리 지어 둔 그 지형 설계로 바꿔 끼움 · 전처리 다시 = 두 벌 새로 · B05~B09 초기화 · LAS 올림 끝 = 늘 전처리 다시 - 시험: test_626_1 칩 시험을 맞바꿈 · 전처리 다시 기준으로(옛 노선 그대로 다시 셈 길 없음) · test_637 force 시험 · 화면 줄 · test_763_3 서피스 바꾸는 자리 목록에 지형 칩 Co-Authored-By: Claude Opus 5.5 --- .../B05_Profile_Router_TerrainSwitch.py | 76 +++++++------------ .../B05_Profile_UI_RouteEdit_Terrain.ts | 29 +++---- .../tester/test_626_1_recommend_preview.py | 69 ++++++++--------- resources/tester/test_637_lidar.py | 16 ++-- resources/tester/test_763_3_surface_change.py | 2 + 5 files changed, 82 insertions(+), 110 deletions(-) diff --git a/B05_Profile/B05_Profile_Router_TerrainSwitch.py b/B05_Profile/B05_Profile_Router_TerrainSwitch.py index ac821e0b0..343e7e88c 100644 --- a/B05_Profile/B05_Profile_Router_TerrainSwitch.py +++ b/B05_Profile/B05_Profile_Router_TerrainSwitch.py @@ -1,15 +1,15 @@ -"""B05 설계 지형 바꿈(PLAN 626-1 · 요구 530 · 결정 A④ · D2) — 평면 도구줄 지형 칩. +"""B05 설계 지형 바꿈(PLAN 626-1 · 765-4 · 요구 530 · 605 · 606-1) — 평면 도구줄 지형 칩. -`POST /api/projects/{id}/route/design-terrain` 본문 `{terrain: "las" | "sheet"}` → -설계 지형만 바꾸고(`switch_design_terrain`) 노선 저장 체인을 다시 탐 -(종단 · 횡단 · 배수 · 관 · 구조물 따라감 · -계획고 옮김 없음 = 옛 · 새 계획 폴리라인 같음). 노선 · 노드 · 곡선표 = 지금 저장본 그대로(안 씀). -초기값(`initial_snapshot/`) · 추천 저장본 · 예정노선 안 씀(옛 추천 [적용] 길 `as_initial` 대신). -[저장]과 같은 프로젝트 잠금 · 떠 두고 실패면 되돌림(`common_util_save_guard`) · -같은 지형 = 바꿀 것 없음(`force` = 같은 지형도 체인 다시 탐 — LAS 갈음 뒤 · PLAN 637-2). +`POST /api/projects/{id}/route/design-terrain` 본문 `{terrain: "las" | "sheet", force}` +- 칩 = 맞바꿈(`common_util_design_sets.swap_design_set`) — 지금 벌(노선 · 종단 · 횡단 · 배수 · + 토공 · 수량 · 초기값)을 묶음으로 떠 두고 전처리가 미리 지은 고른 지형 벌을 꺼내 끼움 · 떠난 벌의 + [저장] 고침은 묶음에 살아 돌아오면 그대로 · 묶음 없음 · 지문 어긋남 = 그 자리에서 한 번 지음 · + 실패 = 통째 되돌림 · 같은 지형 = 바꿀 것 없음. +- `force` = LAS 나중 올리기 뒤 전처리 다시(765-5) — 고른 지형을 설계 지형으로 · B05~B09 초기화 · + 두 벌 다시(`run_surface_change_chain`). +[저장]과 같은 프로젝트 잠금(`common_util_save_guard`). 계산 자리 = ③ 서버 단독. """ -import asyncio import logging from typing import Literal from uuid import UUID @@ -18,10 +18,6 @@ from fastapi.responses import JSONResponse from pydantic import BaseModel from B05_Profile.B05_Profile_Router_Terrain import router -from common_util.common_util_surface_confirmation import ( - design_terrain, - get_surface_confirmation_params, -) from config.config_db import get_db_pool logger = logging.getLogger(__name__) @@ -29,7 +25,7 @@ logger = logging.getLogger(__name__) class DesignTerrainRequest(BaseModel): terrain: Literal["las", "sheet"] - #: 같은 지형도 다시 셈 — LAS 갈음 뒤 설계 지형이 라이다인 B(PLAN 637-2) + #: 전처리 다시(B05~B09 초기화 · 두 벌) — LAS 나중 올리기 뒤(765-5) force: bool = False @@ -37,45 +33,25 @@ class DesignTerrainRequest(BaseModel): async def switch_route_design_terrain( project_id: UUID, body: DesignTerrainRequest ) -> dict | JSONResponse: - """설계 지형을 바꾸고 뒤 단계를 모두 다시 셈 — 노선 값 · 초기값은 그대로.""" - from B05_Profile.B05_Profile_Router_Replan import ( - _PROJECT_PATH_MISSING, - _planned_line, - _project_paths, - _recompute, - ) - from B06_Section.B06_Section_Router_DesignSave import _restore + """설계 지형 벌 맞바꿈 · `force` = 전처리 다시.""" + from B03_FileInput.B03_FileInput_Service_SurfaceChange import run_surface_change_chain + from B05_Profile.B05_Profile_Router_Replan import _PROJECT_PATH_MISSING, _project_paths + from common_util import common_util_design_sets as sets from common_util import common_util_save_guard as guard - lock = guard.project_lock(str(project_id)) - async with lock: + async with guard.project_lock(str(project_id)): paths = await _project_paths(project_id) if paths is None: return JSONResponse(status_code=404, content=_PROJECT_PATH_MISSING) - project_root, stored_path = paths - pool = get_db_pool() - async with pool.acquire() as connection: - now = design_terrain(await get_surface_confirmation_params(connection, str(project_id))) - if now == body.terrain and not body.force: - return {"status": "success", "design": now, "changed": False} - rows = await guard.snapshot_rows(connection, str(project_id), surface=True) - files = await asyncio.to_thread( - guard.snapshot_storage, project_root, guard.STORAGE_SCOPE + guard.PIPE_SCOPE - ) try: - line = await asyncio.to_thread(_planned_line, project_root) - result = await _recompute( - project_id, project_root, stored_path, None, body.terrain, (line, line), False - ) - if "error" in result: - await _restore(project_id, rows, files) - return JSONResponse( - status_code=409, content={"status": "error", "message": result["error"]} - ) - except Exception: - await _restore(project_id, rows, files) - raise - finally: - files.discard() - logger.info("설계 지형 바꿈: project_id=%s %s → %s", project_id, now, body.terrain) - return {"status": "success", "design": body.terrain, "changed": True, **result} + if body.force: + async with get_db_pool().acquire() as connection: + model_id = await sets.switch_terrain(connection, project_id, body.terrain) + await run_surface_change_chain(project_id, model_id) + result = {"changed": True, "design": body.terrain, "preprocessed": True} + else: + result = await sets.swap_design_set(project_id, paths[0], body.terrain) + except (LookupError, RuntimeError) as exc: + return JSONResponse(status_code=409, content={"status": "error", "message": str(exc)}) + logger.info("설계 지형 바꿈: project_id=%s %s", project_id, result) + return {"status": "success", **result} diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Terrain.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Terrain.ts index 50bc03725..4e5c21157 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Terrain.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Terrain.ts @@ -2,10 +2,11 @@ * B05_Profile_UI_RouteEdit_Terrain.ts * 평면 설계 지형 칩(PLAN 626-1 · 요구 530 · 결정 A④ · D2) — 공용 지형 칩(`map_terrain_chip`)으로 설계 지형 보기 · 바꾸기. * 고를 수 있는 지형 = `/route/terrain-sources`(없는 쪽 막힘) · 바꾸면 확인창 → `POST /route/design-terrain` - * (설계 지형 바꿈 · 뒤 단계 모두 다시 셈 · 노선 값 · 초기값 · 추천 저장본 그대로 · 잠금 막) → 초안 비움 → 페이지 다시 세움. + * (765-4 맞바꿈 — 전처리가 미리 지은 그 지형 벌로 바꿔 끼움 · 떠난 벌의 [저장] 고침은 살아 돌아오면 그대로 · 잠금 막) + * → 초안 비움 → 페이지 다시 세움. * 취소 · 실패 = 칩을 지금 지형으로 되돌림. 옛 길(추천 [적용] 안 지형 고르기 · 509-5) 대신. * 칩 옆 [LAS 올리기](637-3 · 공용 `map_lidar_upload` · 예정노선과 같은 단추) → `POST /route/lidar` → 진행 서클(1 초 묻기) - * → 끝나면 칩 다시 읽고 확인창 「설계 지형을 라이다로 바꿈?」(이미 라이다 = 갈음 → `force` 다시 셈) · + * → 끝나면 칩 다시 읽고 확인창 「라이다로 전처리 다시」(`force` — 두 벌 새로 · B05~B09 초기화 · 765-5) · * 노선을 다 못 덮음(`covered` 거짓) = 라이다로 바꿈 막고 알림 · 설계 값은 확인창을 눌러야만 바뀜. * 부르는 곳 = `_Chrome`(도구줄 `terrain` 칸) · `_RouteEdit`(지형 읽은 뒤 `load`). * ========================================================================== */ @@ -82,7 +83,7 @@ export function createRouteTerrain(projectId: string): RouteTerrain { note(job); load({ sources: job.sources, design: job.design === "sheet" ? "sheet" : "las" }); if (uncovered != null) return warnUncovered(); - await change("las", design === "las"); + await change("las", true); // 전처리 다시 — 두 벌 새로 · B05~B09 초기화(765-5) } const note = (info: RouteLidar): void => { @@ -106,26 +107,28 @@ export function createRouteTerrain(projectId: string): RouteTerrain { if (info.running && !upload.busy()) void upload.follow(); } - /** 설계 지형 바꿈 — `force` = 같은 지형 다시 셈(라이다 갈음 뒤). */ + /** 설계 지형 바꿈(벌 맞바꿈) — `force` = 전처리 다시(LAS 나중 올리기 뒤). */ async function change(next: AnalysisTerrain, force = false): Promise { if (busy || upload.busy() || (next === design && !force)) return chip.set(toChip(design)); if (next === "las" && uncovered != null) { warnUncovered(); return chip.set(toChip(design)); } + const name = TERRAIN_NAMES[toChip(next)]; const ok = await askConfirm({ title: force - ? "설계 지형 다시 셈" + ? "라이다로 전처리 다시" : next === "las" ? "설계 지형을 라이다로 바꿈?" : "설계 지형 바꿈", - message: - (force - ? "새로 올린 라이다로 설계 지형을 다시 셈합니다 — 종단 · 횡단 · 배수 · 토공을 모두 다시 셈합니다. " - : `설계 지형을 ${TERRAIN_NAMES[toChip(next)]}(으)로 바꿉니다 — 종단 · 횡단 · 배수 · 토공을 모두 다시 셈합니다. `) + - "평면 노선 · 초기값([초기화])은 그대로 · 저장 안 한 고침은 버립니다." + - (force ? " 안 하면 설계 값은 옛 라이다 셈 그대로입니다." : ""), - okLabel: force ? "다시 셈" : "바꿈", + message: force + ? "새로 올린 라이다로 전처리를 다시 합니다 — 라이다 · 수치지형도 설계(노선 · 종단 · 횡단 · 배수 · " + + "토공 · 수량)를 새로 짓고 B05~B09 를 초기화합니다. 저장한 고침 · 초기값([초기화])도 새 값으로 " + + "바뀝니다. 안 하면 설계 값은 옛 지형 셈 그대로입니다." + : `설계 지형을 ${name}(으)로 바꿉니다 — 전처리 때 미리 지어 둔 ${name} 설계(노선 · 종단 · 횡단 · ` + + "배수 · 토공 · 수량)로 바꿔 끼웁니다. 지금 지형에서 [저장]한 고침은 남아 돌아오면 그대로 · " + + "저장 안 한 고침은 버립니다. 초기값([초기화])은 지형마다 따로입니다.", + okLabel: force ? "전처리 다시" : "바꿈", }); if (!ok) return chip.set(toChip(design)); busy = true; @@ -140,7 +143,7 @@ export function createRouteTerrain(projectId: string): RouteTerrain { clearDrafts(projectId); forgetDesignTerrainGrid(projectId); // 새 지형 격자로(509-2) showToast( - `설계 지형을 ${TERRAIN_NAMES[toChip(next)]}(으)로 ${force ? "다시 셈했" : "바꿨"}습니다.`, + force ? "라이다로 전처리를 다시 했습니다." : `설계 지형을 ${name}(으)로 바꿨습니다.`, "success", ); await rebuildAfterRouteSave(projectId); diff --git a/resources/tester/test_626_1_recommend_preview.py b/resources/tester/test_626_1_recommend_preview.py index 1cfe04a24..81429b83a 100644 --- a/resources/tester/test_626_1_recommend_preview.py +++ b/resources/tester/test_626_1_recommend_preview.py @@ -2,19 +2,18 @@ - 옛 추천 모달 · 좌측 「노선 추천」 칸 · [적용](`as_initial`) 화면 부름 없음(서버 길 · 시험은 남김). - 미리보기 = 저장본 넷(영선 · 토공 · 기울기 · 비중) 하나만 · 끔 · 다시 셈(`recompute`) 없음. -- 지형 칩 → `POST /route/design-terrain` — 노선 그대로(옛 = 새 선) · 초기값 폴더 안 씀 · 같은 지형 = 그대로. +- 지형 칩 → `POST /route/design-terrain` — 벌 맞바꿈(765-4 · `swap_design_set`) · + 같은 지형 = 그대로 · `force` = 전처리 다시(765-5). """ import asyncio import re from pathlib import Path -from types import SimpleNamespace from uuid import uuid4 from B05_Profile import B05_Profile_Router_Replan as replan from B05_Profile import B05_Profile_Router_TerrainSwitch as switch -from B06_Section import B06_Section_Router_DesignSave as design_save -from common_util import common_util_save_guard as guard +from common_util import common_util_design_sets as sets ROOT = Path(__file__).resolve().parents[2] B05 = ROOT / "B05_Profile" @@ -60,22 +59,25 @@ def test_지형_칩_확인창_새_길() -> None: assert "rebuildAfterRouteSave(projectId)" in src -def _run(monkeypatch, now: str, want: str): +def _run(monkeypatch, now: str, want: str, force: bool = False): + """칩 요청 — 맞바꿈 · 전처리 다시가 받은 값만 잰다(본체 = test_765_design_sets).""" calls: dict = {} - line = [[0.0, 0.0], [10.0, 0.0]] async def paths(_pid): return Path("root"), "stored" - async def recompute(*args): - calls["args"] = args - return {"route_id": 7} + async def swap(pid, root, target): + calls["swap"] = (root, target) + if target == now: + return {"changed": False, "design": now} + return {"changed": True, "design": target, "built": False} - async def rows(*_a, **_k): - return {} + async def switch_terrain(_c, _pid, terrain): + calls["switch"] = terrain + return 9 - async def restore(*_a): - calls["restored"] = True + async def chain(pid, model_id): + calls["chain"] = model_id class Pool: def acquire(self): @@ -88,38 +90,31 @@ def _run(monkeypatch, now: str, want: str): return Ctx() - async def params(*_a): - return {} - - def storage(root, scope): - calls["scope"] = scope - return SimpleNamespace(discard=lambda: None) + import B03_FileInput.B03_FileInput_Service_SurfaceChange as change monkeypatch.setattr(replan, "_project_paths", paths) - monkeypatch.setattr(replan, "_planned_line", lambda _root: line) - monkeypatch.setattr(replan, "_recompute", recompute) - monkeypatch.setattr(design_save, "_restore", restore) - monkeypatch.setattr(guard, "snapshot_rows", rows) - monkeypatch.setattr(guard, "snapshot_storage", storage) + monkeypatch.setattr(sets, "swap_design_set", swap) + monkeypatch.setattr(sets, "switch_terrain", switch_terrain) + monkeypatch.setattr(change, "run_surface_change_chain", chain) monkeypatch.setattr(switch, "get_db_pool", lambda: Pool()) - monkeypatch.setattr(switch, "get_surface_confirmation_params", params) - monkeypatch.setattr(switch, "design_terrain", lambda _p: now) - body = switch.DesignTerrainRequest(terrain=want) + body = switch.DesignTerrainRequest(terrain=want, force=force) result = asyncio.run(switch.switch_route_design_terrain(uuid4(), body)) - return result, calls, line + return result, calls -def test_지형_바꿈_노선_그대로_초기값_안씀(monkeypatch) -> None: - result, calls, line = _run(monkeypatch, "las", "sheet") +def test_지형_바꿈_벌_맞바꿈(monkeypatch) -> None: + result, calls = _run(monkeypatch, "las", "sheet") assert result["changed"] is True and result["design"] == "sheet" - _pid, _root, _stored, balanced, terrain, lines, initial = calls["args"] - assert balanced is None and terrain == "sheet" and initial is False - assert lines == (line, line) # 계획고 옮김 없음 · 노선 안 씀 - assert guard.INITIAL_SCOPE[0] not in calls["scope"] - assert "restored" not in calls + assert calls == {"swap": (Path("root"), "sheet")} # 전처리 다시 아님 def test_같은_지형이면_그대로(monkeypatch) -> None: - result, calls, _ = _run(monkeypatch, "sheet", "sheet") + result, calls = _run(monkeypatch, "sheet", "sheet") assert result == {"status": "success", "design": "sheet", "changed": False} - assert "args" not in calls and "scope" not in calls + assert "chain" not in calls + + +def test_force_는_전처리_다시(monkeypatch) -> None: + result, calls = _run(monkeypatch, "las", "las", force=True) + assert result["preprocessed"] is True and result["design"] == "las" + assert calls == {"switch": "las", "chain": 9} # 맞바꿈 아님 · 두 벌 다시 diff --git a/resources/tester/test_637_lidar.py b/resources/tester/test_637_lidar.py index ab63aad3b..baa43d32e 100644 --- a/resources/tester/test_637_lidar.py +++ b/resources/tester/test_637_lidar.py @@ -347,14 +347,10 @@ def test_지형_바꿈_force_같은_지형도_다시_셈(monkeypatch) -> None: from B05_Profile import B05_Profile_Router_TerrainSwitch as switch - request = switch.DesignTerrainRequest - assert request(terrain="las").force is False - monkeypatch.setattr( - switch, "DesignTerrainRequest", lambda terrain: request(terrain=terrain, force=True) - ) - result, calls, line = base._run(monkeypatch, "las", "las") - assert result["changed"] is True and calls["args"][4] == "las" - assert calls["args"][5] == (line, line) # 노선 그대로 + assert switch.DesignTerrainRequest(terrain="las").force is False + result, calls = base._run(monkeypatch, "las", "las", force=True) + # LAS 갈음 뒤 = 전처리 다시(765-5 · 두 벌 · B05~B09 초기화) + assert result["changed"] is True and calls["switch"] == "las" and calls["chain"] == 9 # ── 637-3 B05 화면 ────────────────────────────────────────────────────────── @@ -367,8 +363,8 @@ def test_B05_칩_옆_올리기_확인창_force_덮임_막음() -> None: assert "const lidarPath = `/projects/${projectId}/route/lidar`;" in ui assert "root.append(chip.root, upload.button, circle.root);" in ui # 칩 옆 · 서클 assert "createProgressCircle({ size: 16, inline: true })" in ui - # 끝 = 칩 다시 · 못 덮음 = 막고 알림 · 이미 라이다 = force 다시 셈 · 확인창 거쳐서만 - assert 'await change("las", design === "las");' in ui + # 끝 = 칩 다시 · 못 덮음 = 막고 알림 · 전처리 다시(force · 765-5) · 확인창 거쳐서만 + assert 'await change("las", true);' in ui assert "if (uncovered != null) return warnUncovered();" in ui assert 'if (next === "las" && uncovered != null) {' in ui assert "JSON.stringify({ terrain: next, force })" in ui diff --git a/resources/tester/test_763_3_surface_change.py b/resources/tester/test_763_3_surface_change.py index fb030451c..de7ef93cd 100644 --- a/resources/tester/test_763_3_surface_change.py +++ b/resources/tester/test_763_3_surface_change.py @@ -244,6 +244,8 @@ _ALLOWED = { "B05_Profile/B05_Profile_Router_Replan.py", # 라이다 확정값만 적어 둠 · 설계 지형 그대로(637-2) "B05_Profile/B05_Profile_Router_Lidar.py", + # 지형 칩 = 벌 맞바꿈 · LAS 나중 올리기 뒤 전처리 다시(765-4 · 5 · 요구 605-1 · 606-1) + "B05_Profile/B05_Profile_Router_TerrainSwitch.py", }