fix(B05·B06): 지형 격자 자리 = 서버 표본기와 같은 params DTM 행 · 격자 없는 지형 안 닫힌 측점 「지형 범위 밖」 배지 (PLAN 578-1 · 요구 438 · 439)
- 까닭: 확정 모델 행이 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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")}`;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user