diff --git a/B04_PreProcess/B04_PreProcess_Service.py b/B04_PreProcess/B04_PreProcess_Service.py index 586d59161..a70f9613b 100644 --- a/B04_PreProcess/B04_PreProcess_Service.py +++ b/B04_PreProcess/B04_PreProcess_Service.py @@ -10,7 +10,13 @@ from B04_PreProcess.B04_PreProcess_Repository import ( list_surface_models, ) from common_util.common_util_surface_confirmation import ( + LAS_STASH_KEY, + AnalysisTerrain, + analysis_terrain_params, + design_terrain, + get_surface_confirmation_params, merge_surface_confirmation_params, + terrain_changes, ) from common_util.common_util_workflow_state import complete_stage @@ -52,3 +58,30 @@ async def confirm_surface_selection( async with connection.cursor() as cursor: await complete_stage(cursor, str(project_id), 1) return normalized + + +async def switch_design_terrain( + connection: aiomysql.Connection, + project_id: UUID, + terrain: AnalysisTerrain | None, +) -> tuple[int, AnalysisTerrain] | None: + """노선 [확인] 때 고른 분석 지형을 설계 지형으로(PLAN 133-2) — 확정 모델 · stage 1 params. + + 뒤 단계(종 · 횡단 · 배수 · 유토 · 3D)는 모두 stage 1 params · 확정 모델을 읽으므로 여기 한 곳만 + 바꾼다. 수치지형도로 갈 때 라이다 확정값을 적어 둠(되돌릴 자리). WF1 완료 전이는 안 건드림. + 바뀌면 (새 확정 모델 id, 이전 설계 지형) · 그대로면 None. 트랜잭션은 부르는 쪽. + """ + params = await get_surface_confirmation_params(connection, str(project_id)) + target = analysis_terrain_params(params, terrain) + if not terrain_changes(params, target): + return None + model_id = await find_surface_model_for_selection(connection, project_id, target) + await confirm_surface_model(connection, project_id, model_id) + previous = design_terrain(params) + extra = ( + {LAS_STASH_KEY: {key: params[key] for key in ("source_filter", "method", "smooth")}} + if previous == "las" + else None + ) + await merge_surface_confirmation_params(connection, str(project_id), target, extra) + return model_id, previous diff --git a/B05_Profile/B05_Profile_Api_Replan.ts b/B05_Profile/B05_Profile_Api_Replan.ts index 1a12fc0cc..3285d710d 100644 --- a/B05_Profile/B05_Profile_Api_Replan.ts +++ b/B05_Profile/B05_Profile_Api_Replan.ts @@ -21,21 +21,29 @@ import { readState, writeState } from "../A00_Common/b_page_state"; /** 편집 창 분석 지형 — las = 확정 지표면(라이다) · sheet = 수치지형도 서피스. */ export type AnalysisTerrain = "las" | "sheet"; -/** 고른 분석 지형 — 화면 상태 보관소 pref 통(노선 편집 고정과 같은 자리) · 기본 las. */ -export const analysisTerrain = (projectId: string): AnalysisTerrain => - readState("routeedit-terrain", projectId) === "sheet" ? "sheet" : "las"; +/** 고른 분석 지형 — 화면 상태 보관소 pref 통(노선 편집 고정과 같은 자리). 없으면 `fallback` + * (모달이 열 때 설계 지형으로 채움). [확인] 때 이 값이 설계 지형이 됨(PLAN 133-2). */ +export const analysisTerrain = ( + projectId: string, + fallback: AnalysisTerrain = "las", +): AnalysisTerrain => { + const saved = readState("routeedit-terrain", projectId); + return saved === "sheet" || saved === "las" ? saved : fallback; +}; export const setAnalysisTerrain = (projectId: string, terrain: AnalysisTerrain): void => writeState("routeedit-terrain", terrain, projectId); -/** 이 프로젝트가 고를 수 있는 분석 지형 — 둘이면 고르기를 띄운다. */ -export async function fetchTerrainSources(projectId: string): Promise { - const payload = await requestJson<{ sources: AnalysisTerrain[] }>( +/** 이 프로젝트가 고를 수 있는 분석 지형(둘이면 고르기를 띄운다)과 지금 설계 지형. */ +export async function fetchTerrainSources( + projectId: string, +): Promise<{ sources: AnalysisTerrain[]; design: AnalysisTerrain }> { + const payload = await requestJson<{ sources?: AnalysisTerrain[]; design?: AnalysisTerrain }>( `/projects/${projectId}/route/terrain-sources`, { method: "GET" }, 60000, ); - return payload.sources ?? []; + return { sources: payload.sources ?? [], design: payload.design === "sheet" ? "sheet" : "las" }; } /** 노선 재계산 대기 상한 — 배수유역 분석(90초대)까지 포함해 넉넉히 잡는다. */ diff --git a/B05_Profile/B05_Profile_Router_Replan.py b/B05_Profile/B05_Profile_Router_Replan.py index c928db5aa..10e2f4559 100644 --- a/B05_Profile/B05_Profile_Router_Replan.py +++ b/B05_Profile/B05_Profile_Router_Replan.py @@ -120,8 +120,8 @@ class RouteInitialRequest(BaseModel): criteria: RouteInitialCriteria | None = None # 추천 점수에 균형 종단(PLAN 63-4)을 넣을지 — 끄면 원지반만(63-4 전 점수). 파일에 안 적힘. with_profile: bool = True - # 분석 지형(PLAN 133-1) — 추천만 고른 지형으로 · [확인] 재계산은 확정 지표면 그대로. - terrain: Literal["las", "sheet"] = "las" + # 분석 지형(PLAN 133-1) — None = 지금 설계 지형 · [확인] 때 설계 지형이 됨(133-2). + terrain: Literal["las", "sheet"] | None = None class RouteReplanRequest(BaseModel): @@ -133,6 +133,8 @@ class RouteReplanRequest(BaseModel): balanced_profile: bool = False """「종단 반영」 켬으로 [적용]한 추천 그대로인가 — 켜면 서버가 균형 종단을 다시 셈해 1차 종단으로 쓴다(PLAN 74-4). 값은 받지 않는다 — 켬/끔 표시만.""" + terrain: Literal["las", "sheet"] | None = None + """편집 창에서 고른 분석 지형 = 설계 지형(PLAN 133-2). None 이면 지금 설계 지형 그대로.""" async def _project_paths(project_id: UUID) -> tuple[Path, str] | None: @@ -385,6 +387,7 @@ async def _recompute( project_root: Path, stored_path: str, balanced: RouteInitialRequest | None = None, + terrain: Literal["las", "sheet"] | None = None, ) -> dict[str, Any]: """노선을 갈아 끼운 뒤의 재계산 — 초기 업로드 체인과 같은 로직을 그대로 탄다. @@ -397,7 +400,10 @@ async def _recompute( _planned_route_points_in_project_crs, run_redesign_chain, ) - from B04_PreProcess.B04_PreProcess_Service import find_surface_model_for_selection + from B04_PreProcess.B04_PreProcess_Service import ( + find_surface_model_for_selection, + switch_design_terrain, + ) from B05_Profile.B05_Profile_Repository import get_latest_route from common_util.common_util_drainage_pipes import clear_pipe_points from common_util.common_util_surface_confirmation import get_surface_confirmation_params @@ -410,6 +416,9 @@ async def _recompute( pool = get_db_pool() async with pool.acquire() as connection: + await connection.begin() # 고른 지형 = 설계 지형 — 뒤 단계가 모두 이 확정값을 읽음 + switched = await switch_design_terrain(connection, project_id, terrain) + await connection.commit() selection = await get_surface_confirmation_params(connection, str(project_id)) latest = await get_latest_route(connection, project_id) # 지금 노선이 쓰던 지표면 모델이 1순위다 — 확정 선택값으로 다시 찾으면 프로젝트마다 @@ -417,6 +426,8 @@ async def _recompute( surface_model_id = ( int(latest["surface_model_id"]) if latest and latest.get("surface_model_id") else None ) + if switched: + surface_model_id = switched[0] if surface_model_id is None: try: surface_model_id = await find_surface_model_for_selection( @@ -475,6 +486,11 @@ async def _recompute( failure = await run_redesign_chain(project_id, int(surface_model_id), selection, target) marks.append(("재확정 체인", time.perf_counter())) _log_steps("노선 [확인] 재계산", marks) + if failure and switched: # 노선과 함께 설계 지형도 되돌림 + async with pool.acquire() as connection: + await connection.begin() + await switch_design_terrain(connection, project_id, switched[1]) + await connection.commit() if failure: # 체인이 끊기면 **노선만 새것으로 갈아 끼워진 채** 종횡단은 옛 노선에 남아 어긋난다 # (2026-09-06 실측: 배수유역·관은 새 노선, 종횡단은 옛 노선 → 배수관 측점 9 → 0). @@ -613,6 +629,7 @@ async def replan_route( project_root, stored_path, (request.initial or RouteInitialRequest()) if request.balanced_profile else None, + request.terrain, ) if "error" in result: # 노선을 되돌린다 — 실패했는데 새 노선만 남으면 화면·정본이 어긋난 채 굳는다. diff --git a/B05_Profile/B05_Profile_Router_RouteInitial.py b/B05_Profile/B05_Profile_Router_RouteInitial.py index 2e8c91200..9540d6bfc 100644 --- a/B05_Profile/B05_Profile_Router_RouteInitial.py +++ b/B05_Profile/B05_Profile_Router_RouteInitial.py @@ -211,11 +211,11 @@ async def balanced_profile_of( points: list[dict[str, float]], criteria: Any, design_curves: list[dict[str, Any]] | None = None, - terrain: Literal["las", "sheet"] = "las", + terrain: Literal["las", "sheet"] | None = None, ) -> dict[str, Any] | None: """노선 정점의 균형 종단 한 벌(`route_profile`) — [확인] 체인 · 편집 창 종단 띠가 함께 씀. - `terrain` 은 편집 창 종단 띠만 바꿈(PLAN 133-1) — [확인] 체인은 기본(확정 지표면). + `terrain` 은 편집 창 종단 띠만 바꿈(PLAN 133-1) — None = 지금 설계 지형([확인] 체인). """ from B05_Profile.B05_Profile_Engine_Grade import legal_grade_limit_pct from B05_Profile.B05_Profile_Engine_RouteInitial import load_terrain_and_streams @@ -293,7 +293,7 @@ class ProfilePreviewRequest(BaseModel): vertices: list[replan.RouteVertexInput] = Field(..., min_length=2) criteria: replan.RouteInitialCriteria | None = None - terrain: Literal["las", "sheet"] = "las" + terrain: Literal["las", "sheet"] | None = None @replan.router.post("/{project_id}/route/profile-preview", response_model=None) diff --git a/B05_Profile/B05_Profile_Router_Terrain.py b/B05_Profile/B05_Profile_Router_Terrain.py index 7f55fb0ad..559d64724 100644 --- a/B05_Profile/B05_Profile_Router_Terrain.py +++ b/B05_Profile/B05_Profile_Router_Terrain.py @@ -12,8 +12,8 @@ POST /api/projects/{id}/route/cross-preview → 고치던 노선의 한 측점 횡단 미리보기 GET /api/projects/{id}/route/terrain-sources → 고를 수 있는 분석 지형(라이다 · 수치지형도) -분석 지형(`terrain`, PLAN 133-1) — las = 확정 지표면 · sheet = 수치지형도 서피스. 모달 분석만 -고른 지형을 쓴다 — 확정 지표면 · 종 · 횡단 정본은 안 바뀐다. +분석 지형(`terrain`, PLAN 133-1) — las = 라이다 · sheet = 수치지형도 · None = 지금 설계 지형. +모달 분석만 고른 지형을 쓴다 — 설계 지형(확정)은 [확인] 때만 바뀜(133-2 · `/route/replan`). """ import asyncio @@ -38,6 +38,7 @@ from common_util.common_util_surface_confirmation import ( AnalysisTerrain, analysis_terrain_params, analysis_terrain_sources, + design_terrain, get_surface_confirmation_params, ) from common_util.common_util_surface_sampler import build_surface_sampler @@ -56,7 +57,7 @@ class ElevationRequest(BaseModel): """사업지 좌표계(m) 점 묶음 [[x, y], …].""" points: list[tuple[float, float]] = Field(..., min_length=1, max_length=MAX_POINTS) - terrain: AnalysisTerrain = "las" + terrain: AnalysisTerrain | None = None def _sample(project_root: Path, params: dict, points: list[tuple[float, float]]): @@ -131,7 +132,7 @@ class CrossPreviewRequest(BaseModel): station_interval_m: float | None = None #: 횡단 반폭(m) — 화면이 적용해 둔 값. 비면 서버 기본값. cross_half_width_m: float | None = Field(None, gt=0) - terrain: AnalysisTerrain = "las" + terrain: AnalysisTerrain | None = None def _cross_preview( @@ -254,4 +255,9 @@ async def read_terrain_sources(project_id: UUID) -> dict | JSONResponse: params = await get_surface_confirmation_params(connection, str(project_id)) models_dir = Path(resolve_stored_project_path(stored)) / _MODELS_SUBDIR sources = await asyncio.to_thread(analysis_terrain_sources, params, models_dir) - return {"status": "success", "project_id": str(project_id), "sources": sources} + return { + "status": "success", + "project_id": str(project_id), + "sources": sources, + "design": design_terrain(params), # 지금 설계 지형(PLAN 133-2) + } diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts index a5bb94258..a69bcf658 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts @@ -143,6 +143,11 @@ export async function openRouteEditModal( let meta: VWorldMeta | null = null; /** 바탕 등고선 한 벌 — LAS 것이거나 도엽 것. 고르기는 `_Contour` 몫. */ let contours: RouteEditContours | null = null; + /** 지금 설계 지형(PLAN 133-2) — 고른 분석 지형이 이와 다르면 [확인]이 설계 지형을 바꾼다. */ + let designTerrain: AnalysisTerrain = "las"; + /** [확인]에 실을 분석 지형 — 고르기가 안 보이면 null(설계 지형 그대로). */ + const pickedTerrain = (): AnalysisTerrain | null => + terrainSelect.closest("label")!.hidden ? null : analysisTerrain(projectId, designTerrain); /** 등고선 말고 함께 깔 도엽 레이어(하천중심선). */ let otherSheets: PreparedLayer[] = []; /** 고른 등고선 가닥 — 없으면 -1(계획서 0-9 ⑦). */ @@ -524,7 +529,9 @@ export async function openRouteEditModal( busy, projectId, nodes: () => ({ planned, curveOn, curveRadius }), - changed: () => history?.isDirty() ?? false, + // 분석 지형만 바꿔도 [확인]이 설계 지형을 바꿔 다시 셈한다(PLAN 133-2). + changed: () => (history?.isDirty() ?? false) || pickedTerrain() !== designTerrain, + terrain: pickedTerrain, initial, profileShown: strip.settle, close, @@ -536,7 +543,7 @@ export async function openRouteEditModal( const [plan, drainage, sources] = await Promise.all([ fetchRoutePlan(projectId), fetchDrainageLayers(projectId, () => {}), - fetchTerrainSources(projectId).catch((): AnalysisTerrain[] => []), + fetchTerrainSources(projectId).catch(() => null), ]); if (closed) return; expected = plan.expected as Vertex[]; @@ -561,9 +568,14 @@ export async function openRouteEditModal( if (!planned.length) planned = plannedLine.map((vertex) => [vertex[0], vertex[1]]); meta = drainage.meta; // 분석 지형 고르기(PLAN 133-1) — 라이다 · 수치지형도 둘 다 있을 때만 보임 · 기본 라이다. - const choosable = sources.includes("las") && sources.includes("sheet"); + // 고른 값이 없으면 지금 설계 지형(133-2) · 못 고르는 프로젝트는 설계 지형 그대로. + designTerrain = sources?.design ?? "las"; + const choosable = !!sources?.sources.includes("las") && !!sources?.sources.includes("sheet"); terrainSelect.closest("label")!.hidden = !choosable; - if (!choosable && analysisTerrain(projectId) === "sheet") setAnalysisTerrain(projectId, "las"); + setAnalysisTerrain( + projectId, + choosable ? analysisTerrain(projectId, designTerrain) : designTerrain, + ); terrainSelect.value = analysisTerrain(projectId); // 등고선은 따로 고른다(LAS 우선 · 수치지형도를 고르면 도엽). 나머지 도엽 레이어(하천중심선)만 배경으로 깐다. const loadBackground = () => @@ -587,6 +599,7 @@ export async function openRouteEditModal( status.textContent = `${routeHead()} · ${plan.edited ? "고친 계획노선" : "초기 폴리라인"} · ` + `${contours?.source === "las" ? "LAS 등고선" : "도엽 등고선"} · ` + + `설계 지형 ${designTerrain === "sheet" ? "수치지형도" : "라이다"} · ` + curveHint(); }; useBackground(await loadBackground()); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts index ff05796a6..ed8036b2a 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts @@ -25,6 +25,8 @@ export interface RouteApplyParams { nodes: () => { planned: Vertex[]; curveOn: boolean[]; curveRadius: Array }; /** 창을 연 뒤 노선이 바뀌었나 — 안 바뀌었으면 다시 계산하지 않는다. */ changed: () => boolean; + /** 설계 지형으로 보낼 분석 지형(PLAN 133-2) — 고르기가 안 보이면 null(지금 설계 지형 그대로). */ + terrain: () => string | null; /** 초기노선 갈래 — [확인]에 지금 초기값을 싣고, 결과 캐시를 비운 뒤 다시 적는다. */ initial: { current: () => unknown; @@ -94,6 +96,7 @@ export function bindRouteApply(params: RouteApplyParams): void { initial: params.initial.current(), // 띠 응답 전에 눌렀으면 기다린다 — 서버는 같은 노선 · 같은 셈으로 띠 계획선을 다시 낸다. balanced_profile: await params.profileShown(), + terrain: params.terrain(), }, ), ); diff --git a/B06_Section/B06_Section_Router_Stations.py b/B06_Section/B06_Section_Router_Stations.py index e6b68584d..4e3865346 100644 --- a/B06_Section/B06_Section_Router_Stations.py +++ b/B06_Section/B06_Section_Router_Stations.py @@ -62,9 +62,15 @@ async def _sampling_conditions(project_id: UUID, project_root: Path) -> dict[str from common_util.common_util_surface_confirmation import get_surface_confirmation_params snapshot = load_sampling_snapshot(project_root) - if snapshot is not None: - return snapshot params = await run_with_connection(get_surface_confirmation_params, str(project_id)) + surface = (params or {}).get("source_filter"), (params or {}).get("method") + # 설계 지형이 바뀌었으면(B04 재확정 · 노선 [확인] 지형 바꿈 PLAN 133-2) ①은 옛 지형 — 버림. + if snapshot is not None and ( + not all(surface) + or (snapshot["filter_key"], snapshot["method"], bool(snapshot.get("smooth"))) + == (*surface, bool(params.get("smooth"))) + ): + return snapshot if not params or not params.get("source_filter") or not params.get("method"): return None return { diff --git a/common_util/common_util_surface_confirmation.py b/common_util/common_util_surface_confirmation.py index 5c429adaa..863df3311 100644 --- a/common_util/common_util_surface_confirmation.py +++ b/common_util/common_util_surface_confirmation.py @@ -18,6 +18,9 @@ from config.config_system import ( AnalysisTerrain = Literal["las", "sheet"] #: 수치지형도 분석 지형 — LAS 없는 프로젝트 자동 확정(WF1)과 같은 모델 SHEET_ANALYSIS_FILTER = f"sheet_{SHEET_SURFACE_DEFAULT_METHOD}" +#: 설계 지형을 수치지형도로 바꿀 때 적어 두는 라이다 확정값 — 라이다로 되돌릴 자리(PLAN 133-2) +LAS_STASH_KEY = "las_selection" +_SURFACE_KEYS = ("source_filter", "method", "smooth") SURFACE_CONFIRM_PARAM_KEYS = ( "source_filter", @@ -67,6 +70,8 @@ async def get_surface_confirmation_params( for key in SURFACE_CONFIRM_PARAM_KEYS: if key in params and params[key] is not None: resolved[key] = params[key] + if isinstance(params.get(LAS_STASH_KEY), dict): + resolved[LAS_STASH_KEY] = params[LAS_STASH_KEY] return resolved @@ -74,17 +79,38 @@ def _is_sheet(params: dict[str, Any]) -> bool: return str(params.get("source_filter") or "").startswith("sheet_") -def analysis_terrain_params(params: dict[str, Any], terrain: AnalysisTerrain) -> dict[str, Any]: - """편집 모달 분석에 쓸 지표면 params — 확정 params 는 안 고침(정본 · 종 · 횡단 그대로).""" - if terrain != "sheet" or _is_sheet(params): +def design_terrain(params: dict[str, Any]) -> AnalysisTerrain: + """지금 설계 지형 — 확정 params 가 수치지형도면 sheet.""" + return "sheet" if _is_sheet(params) else "las" + + +def analysis_terrain_params( + params: dict[str, Any], terrain: AnalysisTerrain | None +) -> dict[str, Any]: + """고른 지형의 지표면 params — None · 지금 설계 지형과 같으면 확정 params 그대로. + + 설계 지형이 수치지형도여도 적어 둔 라이다 확정값(`LAS_STASH_KEY`)이 있으면 las 로 돌아감. + 라이다 없는 프로젝트는 무엇을 받아도 확정 params 그대로. + """ + if terrain is None or terrain == design_terrain(params): return params - return {**params, "source_filter": SHEET_ANALYSIS_FILTER, "method": "dtm", "smooth": True} + if terrain == "sheet": + return {**params, "source_filter": SHEET_ANALYSIS_FILTER, "method": "dtm", "smooth": True} + stash = params.get(LAS_STASH_KEY) + if not isinstance(stash, dict) or not stash.get("source_filter"): + return params + return {**params, **{key: stash[key] for key in _SURFACE_KEYS if key in stash}} + + +def terrain_changes(params: dict[str, Any], target: dict[str, Any]) -> bool: + """지표면(필터 · 표현 · 평활)이 바뀌는가.""" + return any(params.get(key) != target.get(key) for key in _SURFACE_KEYS) def analysis_terrain_sources(params: dict[str, Any], models_dir: Path) -> list[str]: - """고를 수 있는 분석 지형 — 확정이 LAS 이고 수치지형도 서피스가 있을 때만 둘.""" + """고를 수 있는 분석 지형 — 라이다 확정값(지금 것 · 적어 둔 것)과 수치지형도가 둘 다 있으면 둘.""" if _is_sheet(params): - return ["sheet"] + return ["las", "sheet"] if isinstance(params.get(LAS_STASH_KEY), dict) else ["sheet"] if (Path(models_dir) / f"dtm_{SHEET_ANALYSIS_FILTER}.npz").is_file(): return ["las", "sheet"] return ["las"] @@ -125,8 +151,9 @@ async def merge_surface_confirmation_params( connection: aiomysql.Connection, project_id: str, selection: dict[str, Any], + extra: dict[str, Any] | None = None, ) -> dict[str, Any]: - """기존 stage 1 params에 확정 선택값 전체를 병합 저장한다.""" + """기존 stage 1 params에 확정 선택값 전체를 병합 저장한다. `extra` 는 함께 적을 칸.""" normalized = { "source_filter": str(selection["source_filter"]), "method": str(selection["method"]), @@ -148,6 +175,7 @@ async def merge_surface_confirmation_params( raise LookupError("WF1 단계 상태를 찾을 수 없습니다.") params = _decode_params(row.get("params") if row else None) params.update(normalized) + params.update(extra or {}) await cursor.execute( """ UPDATE project_workflow_stages diff --git a/resources/tester/test_133_1_analysis_terrain.py b/resources/tester/test_133_1_analysis_terrain.py index 1d201148f..5a15686ee 100644 --- a/resources/tester/test_133_1_analysis_terrain.py +++ b/resources/tester/test_133_1_analysis_terrain.py @@ -99,9 +99,9 @@ def test_sheet_고르면_dtm_sheet_로_표고(tmp_path, monkeypatch): _patch_db(monkeypatch, root, LAS_PARAMS) assert _elevations("las")["z"] == [LAS_Z] assert _elevations("sheet")["z"] == [SHEET_Z] - # 기본값 = 지금 동작(확정 지표면 · 라이다) + # 기본값 = 지금 설계 지형(확정 지표면 · 133-2) request = terrain_router.ElevationRequest(points=[(5.0, 5.0)]) - assert request.terrain == "las" + assert request.terrain is None def test_확정_params_는_안_바뀜(): @@ -163,8 +163,8 @@ def test_화면_고르기_배선(): assert '