From acc09b041072bba31f4327f05dff9ab86340f935 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 29 Oct 2026 13:32:24 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05=C2=B7B06):=20=EC=A7=80=ED=98=95=20?= =?UTF-8?q?=EA=B2=A9=EC=9E=90=20=EC=9E=90=EB=A6=AC=20=3D=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=ED=91=9C=EB=B3=B8=EA=B8=B0=EC=99=80=20=EA=B0=99?= =?UTF-8?q?=EC=9D=80=20params=20DTM=20=ED=96=89=20=C2=B7=20=EA=B2=A9?= =?UTF-8?q?=EC=9E=90=20=EC=97=86=EB=8A=94=20=EC=A7=80=ED=98=95=20=EC=95=88?= =?UTF-8?q?=20=EB=8B=AB=ED=9E=8C=20=EC=B8=A1=EC=A0=90=20=E3=80=8C=EC=A7=80?= =?UTF-8?q?=ED=98=95=20=EB=B2=94=EC=9C=84=20=EB=B0=96=E3=80=8D=20=EB=B0=B0?= =?UTF-8?q?=EC=A7=80=20(PLAN=20578-1=20=C2=B7=20=EC=9A=94=EA=B5=AC=20438?= =?UTF-8?q?=20=C2=B7=20439)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 까닭: 확정 모델 행이 stage 1 params 와 어긋나면(검증_sub1 확정 = sheet · params = classification) terrain-sources 의 grid = null → 브라우저는 미교차 넓힘을 못 하고 ±20 m 에서 멈춤 · 서버 표본기 · Node /calc 는 params 를 따라 넓힘 → 소단 밀린 측점 화면 ≠ 저장 - 고침: `_confirmed_model` → `_design_model` — params 의 `dtm_{filter}.npz` DTM 행(확정 행 먼저) · 표본 폭 상수는 그대로(통째 격자가 이미 최대) - 횡단 카드: 격자 없는(DTM 아닌) 지형에서 사면 미교차 = 부품 배지 「지형 범위 밖」(값은 [저장] 때 서버) - 시험: test_344_1_terrain_grid_route 에 어긋난 확정 행 지킴 · test_133_1 · test_505_2 바뀐 이름 Co-Authored-By: Claude Opus 5.5 --- B05_Profile/B05_Profile_Router_Terrain.py | 18 +++++++------ .../B06_Section_UI_Cross_Card_Chrome.ts | 12 ++++++++- .../tester/test_133_1_analysis_terrain.py | 4 +-- .../tester/test_344_1_terrain_grid_route.py | 25 +++++++++++++++++++ resources/tester/test_505_2_terrain_sheet.py | 2 +- ui_template/ui_template_locale_b2.ts | 6 +++++ 6 files changed, 56 insertions(+), 11 deletions(-) diff --git a/B05_Profile/B05_Profile_Router_Terrain.py b/B05_Profile/B05_Profile_Router_Terrain.py index ee918e151..7315ea67c 100644 --- a/B05_Profile/B05_Profile_Router_Terrain.py +++ b/B05_Profile/B05_Profile_Router_Terrain.py @@ -126,7 +126,7 @@ async def read_terrain_sources(project_id: UUID) -> dict | JSONResponse: content={"status": "error", "message": "프로젝트 저장 경로를 찾을 수 없습니다."}, ) params = await get_surface_confirmation_params(connection, str(project_id)) - confirmed = await _confirmed_model(connection, project_id) + model = await _design_model(connection, project_id, params) models_dir = Path(resolve_stored_project_path(stored)) / _MODELS_SUBDIR sources = await choosable_terrains(connection, project_id, params, models_dir) return { @@ -134,7 +134,7 @@ async def read_terrain_sources(project_id: UUID) -> dict | JSONResponse: "project_id": str(project_id), "sources": sources, "design": design_terrain(params), # 지금 설계 지형(PLAN 133-2) - "grid": await asyncio.to_thread(_design_grid, params, models_dir, confirmed), + "grid": await asyncio.to_thread(_design_grid, params, models_dir, model), } @@ -156,17 +156,21 @@ async def choosable_terrains( return sources -async def _confirmed_model( - connection: aiomysql.Connection, project_id: UUID +async def _design_model( + connection: aiomysql.Connection, project_id: UUID, params: dict ) -> tuple[int, str, str] | None: - """확정 지표면 모델 (id, 표현, 파일 경로) — 없으면 None.""" + """설계 지형 DTM 모델 (id, 표현, 파일 경로) — 서버 표본기처럼 stage 1 params 의 `dtm_{filter}.npz` + (확정 행 먼저). 확정 행이 params 와 어긋나도 서버와 같은 바닥을 받게(578-1) · 없으면 None.""" + name = f"dtm_{params.get('source_filter') or ''}.npz" async with connection.cursor() as cursor: await cursor.execute( "SELECT id, model_type, model_file_path FROM surface_models" - " WHERE project_id = %s AND status = 'CONFIRMED' ORDER BY created_at DESC LIMIT 1", + " WHERE project_id = %s AND model_type = 'dtm' AND status IN ('CONFIRMED', 'COMPLETE')" + " ORDER BY status = 'CONFIRMED' DESC, created_at DESC", (str(project_id),), ) - row = await cursor.fetchone() + rows = await cursor.fetchall() + row = next((r for r in rows if Path(str(r[2] or "")).name == name), None) return (int(row[0]), str(row[1]), str(row[2] or "")) if row else None diff --git a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts index 3a5679da4..85f3c3b92 100644 --- a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts +++ b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts @@ -6,6 +6,8 @@ * 만든다. 여기 있는 것은 전부 **읽어서 붙이는 값**이라 카드 상태(줌·선택)를 건드리지 않는다. * ========================================================================== */ +import { currentProjectId } from "../A00_Common/b_project_switch"; +import { designTerrainGridNow } from "../B05_Profile/B05_Profile_Api_TerrainGrid"; import type { CrossSection } from "./B06_Section_Api_Fetch"; import { buildCutSlopeControl, type CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope"; import { buildDesignControls, sectionModeLabel } from "./B06_Section_UI_Cross_Design"; @@ -108,7 +110,15 @@ export function appendCardHeader( meta.className = "b06-cross-card__meta"; // 사면이 계산 반폭 끝까지 원지반을 못 만난 측점 — 면적이 거기서 잘려 유토곡선까지 // 그 값을 쌓는다. 계산은 손대지 않고 사실만 알린다(2026-09-03 사용자 확정). - if (section.design?.slope_unclosed) { + // 지형 격자가 없으면(DTM 아닌 지형) 브라우저는 받은 표본 끝에서 멈춤 — 값은 [저장] 때 서버가 넓혀 셈(578-1) + if (section.design?.slope_unclosed && !designTerrainGridNow(currentProjectId())) { + const outside = document.createElement("span"); + outside.className = "ui-badge ui-badge--pill ui-badge--warning b06-cross-card__outside"; + outside.setAttribute("data-size", "20"); + outside.textContent = L("B06_Cross_TerrainOutside"); + outside.title = L("B06_Cross_TerrainOutside_Tip"); + meta.append(outside); + } else if (section.design?.slope_unclosed) { const openSlope = document.createElement("span"); openSlope.className = "b06-cross-card__warning"; openSlope.textContent = `⚠ ${L("B06_Cross_SlopeUnclosed")}`; diff --git a/resources/tester/test_133_1_analysis_terrain.py b/resources/tester/test_133_1_analysis_terrain.py index 0c4e97281..87df11b00 100644 --- a/resources/tester/test_133_1_analysis_terrain.py +++ b/resources/tester/test_133_1_analysis_terrain.py @@ -75,10 +75,10 @@ def _patch_db(monkeypatch, project_root: Path, params: dict) -> None: monkeypatch.setattr(terrain_router, "get_surface_confirmation_params", confirmed) monkeypatch.setattr(terrain_router, "resolve_stored_project_path", lambda _s: project_root) - async def no_model(_connection, _project_id): # 격자 받을 자리(344-1)는 따로 시험 + async def no_model(_connection, _project_id, _params): # 격자 받을 자리(344-1)는 따로 시험 return None - monkeypatch.setattr(terrain_router, "_confirmed_model", no_model) + monkeypatch.setattr(terrain_router, "_design_model", no_model) PID = "1bb0f876-983f-4ca5-9cc7-f59ee1f77e0a" diff --git a/resources/tester/test_344_1_terrain_grid_route.py b/resources/tester/test_344_1_terrain_grid_route.py index 05c363bc3..1d4ec8669 100644 --- a/resources/tester/test_344_1_terrain_grid_route.py +++ b/resources/tester/test_344_1_terrain_grid_route.py @@ -141,3 +141,28 @@ def test_지반고_자릿수_기본_3_묶음은_6(tmp_path, monkeypatch): ) assert plain["z"] == [round(5.5 * 0.1234567, 3)] assert fine["z"] == [round(5.5 * 0.1234567, 6)] + + +def test_격자_모델은_params_파일_확정_행이_어긋나도(monkeypatch): + """578-1 — 확정 행(sheet)이 params(csf)와 어긋나도 서버 표본기와 같은 `dtm_{filter}.npz` 행을 고름.""" + rows = [ + (9, "dtm", "B04_PreProcess/models/dtm_sheet_laplace.npz"), # 확정(어긋남) + (7, "dtm", "B04_PreProcess/models/dtm_csf.npz"), + ] + + class _Cursor: + async def execute(self, *_args): + return None + + async def fetchall(self): + return rows + + class _Connection: + @asynccontextmanager + async def cursor(self): + yield _Cursor() + + pick = asyncio.run(terrain_router._design_model(_Connection(), PID, PARAMS)) + assert pick == (7, "dtm", "B04_PreProcess/models/dtm_csf.npz") + other = {**PARAMS, "source_filter": "pmf"} + assert asyncio.run(terrain_router._design_model(_Connection(), PID, other)) is None diff --git a/resources/tester/test_505_2_terrain_sheet.py b/resources/tester/test_505_2_terrain_sheet.py index 2464e7f5b..f739c17f4 100644 --- a/resources/tester/test_505_2_terrain_sheet.py +++ b/resources/tester/test_505_2_terrain_sheet.py @@ -70,7 +70,7 @@ def sources(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(terrain, "get_db_pool", lambda: _Pool()) monkeypatch.setattr(terrain, "get_project_storage_relative_path", _async("p")) monkeypatch.setattr(terrain, "get_surface_confirmation_params", _async(dict(LAS))) - monkeypatch.setattr(terrain, "_confirmed_model", _async(None)) + monkeypatch.setattr(terrain, "_design_model", _async(None)) monkeypatch.setattr(terrain, "resolve_stored_project_path", lambda stored: tmp_path) monkeypatch.setattr(terrain, "_design_grid", lambda *args: None) diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 288889187..a8cb7c849 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -318,6 +318,12 @@ export const ui_locales_b2 = { "사면이 계산 반폭 끝까지 원지반과 만나지 않아 절·성토 면적이 그 자리에서 잘렸습니다. 유토곡선·수량도 잘린 값을 씁니다.", "The slope never meets existing ground within the computed half-width, so the cut/fill area is truncated there. The mass haul curve and quantities use the truncated value.", ], + /* 지형 격자를 못 받은 지형(DTM 아님)에서 사면이 받은 지반 표본 밖으로 나간 측점(578-1). */ + B06_Cross_TerrainOutside: ["지형 범위 밖", "Outside terrain range"], + B06_Cross_TerrainOutside_Tip: [ + "사면이 화면이 받은 지반 표본 밖으로 나가 화면 면적은 그 끝에서 잘린 값입니다. [저장] 때 서버가 지형 끝까지 넓혀 다시 셉니다.", + "The slope runs past the ground samples this screen received, so the area shown is cut off there. On [Save] the server extends to the terrain edge and recomputes.", + ], /* 성토사면 경사길이 — 구조물이 사면을 끊기 전 **본래** 길이(2026-09-03 사용자 지시). */ B06_Cross_FillSlope: ["성토사면", "Fill slope"], B06_Cross_FillSlope_Tip: [