perf(B04/B05): 확정 지표면 요약 API 신설 + 포인트클라우드 24MB 수신 제거
- GET /surface/confirmed 추가: 확정 구성(모델·필터·표현·평활·등고선간격)과 지형 가장자리만 반환(수 KB, 0.02s). 진입 판정·준비화면·B05의 단일 출처. - B05 진입이 받던 포인트클라우드 JSON 23.8MB 제거 — 실제로 쓰던 값은 bounds뿐. - 준비 표식을 프로젝트 ID에서 확정 signature로 변경: B04에서 다시 확정하면 대시보드 복귀·새 브라우저·B그룹 단계 이동 어느 경로로 들어와도 최신본을 담는다. - preloadSurfaceAssets가 평활 여부를 추측하던 부분 제거(확정 저장값 사용) — 추측이 어긋나면 같은 지형을 두 번 내려받았다. - B04 진입 시 필터·표현·평활·등고선간격을 확정본 값으로 초기화(확정 없으면 기존 기본값). - 모델 확정 직후 준비 표식과 B05 세션 캐시를 비워 옛 지형이 남지 않게 함. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+36
-41
@@ -10,6 +10,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { fetchConfirmedSurface } from "../B04_wf1_Surface/B04_wf1_Surface_Api_Fetch";
|
||||
|
||||
const DB_NAME = "aislo-asset-cache";
|
||||
const DB_VERSION = 1;
|
||||
@@ -205,27 +206,44 @@ export async function fetchCachedJson<T>(
|
||||
}
|
||||
|
||||
/* ── 준비 화면 연동 ────────────────────────────────────────────────────────
|
||||
* 어느 프로젝트를 이미 준비했는지, 준비가 끝나면 어느 화면으로 보낼지를 탭 단위로 기억한다.
|
||||
* 대시보드로 나갔다가 같은 프로젝트로 돌아오면 준비 화면을 건너뛴다(2026-08-01 사용자 지시). */
|
||||
const PRELOADED_PROJECT_KEY = "frd_preloaded_project";
|
||||
* 무엇을 이미 준비했는지, 준비가 끝나면 어느 화면으로 보낼지를 탭 단위로 기억한다.
|
||||
* 대시보드로 나갔다가 같은 프로젝트로 돌아오면 준비 화면을 건너뛴다(2026-08-01 사용자 지시).
|
||||
*
|
||||
* 표식은 프로젝트 번호가 아니라 **확정 구성(signature)** 이다. 관리자가 B04에서 다른
|
||||
* 필터·표현으로 다시 확정하면 표식이 달라져 준비 화면이 한 번 더 돌고 새 자료를 담는다.
|
||||
* 프로젝트 번호만 봤다면 옛 자료를 계속 쓰게 된다(2026-08-01 사용자 지시). */
|
||||
const PRELOADED_SIGNATURE_KEY = "frd_preloaded_signature";
|
||||
const PRELOAD_TARGET_KEY = "frd_preload_target";
|
||||
|
||||
export function isProjectPreloaded(projectId: string): boolean {
|
||||
const preloadStamp = (projectId: string, signature: string): string => `${projectId}|${signature}`;
|
||||
|
||||
export function isProjectPreloaded(projectId: string, signature: string): boolean {
|
||||
try {
|
||||
return window.sessionStorage.getItem(PRELOADED_PROJECT_KEY) === projectId;
|
||||
return (
|
||||
window.sessionStorage.getItem(PRELOADED_SIGNATURE_KEY) === preloadStamp(projectId, signature)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function markProjectPreloaded(projectId: string): void {
|
||||
export function markProjectPreloaded(projectId: string, signature: string): void {
|
||||
try {
|
||||
window.sessionStorage.setItem(PRELOADED_PROJECT_KEY, projectId);
|
||||
window.sessionStorage.setItem(PRELOADED_SIGNATURE_KEY, preloadStamp(projectId, signature));
|
||||
} catch {
|
||||
/* 세션 저장 실패는 준비 화면이 한 번 더 뜨는 정도의 영향뿐이다. */
|
||||
}
|
||||
}
|
||||
|
||||
/** 담아 둔 표식을 지운다 — 확정이 바뀌어 자료를 다시 담아야 할 때 호출한다. */
|
||||
export function clearPreloadMark(): void {
|
||||
try {
|
||||
window.sessionStorage.removeItem(PRELOADED_SIGNATURE_KEY);
|
||||
} catch {
|
||||
/* 지우지 못해도 다음 표식 비교에서 불일치로 걸러진다. */
|
||||
}
|
||||
}
|
||||
|
||||
export function setPreloadTarget(route: string): void {
|
||||
try {
|
||||
window.sessionStorage.setItem(PRELOAD_TARGET_KEY, route);
|
||||
@@ -245,49 +263,25 @@ export function readPreloadTarget(): string | null {
|
||||
/** 준비 화면이 표시할 단계 안내. ratio가 null이면 진행률을 모른다는 뜻이다. */
|
||||
export type PreloadReporter = (label: string, ratio: number | null) => void;
|
||||
|
||||
/** 사용자가 고른 등고선 간격(B05에서 저장한 값). 없으면 1.0m.
|
||||
* B04(관리자 확인용 화면)도 이 값을 시작값으로 쓴다 — 사용자가 정한 값이 우선이다. */
|
||||
export async function fetchUserContourInterval(projectId: string): Promise<number> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/route/latest`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!response.ok) return 1.0;
|
||||
const data = (await response.json()) as {
|
||||
surface_params?: { contour_interval_m?: number };
|
||||
};
|
||||
const interval = data.surface_params?.contour_interval_m;
|
||||
return typeof interval === "number" && interval > 0 ? interval : 1.0;
|
||||
} catch {
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
/** 확정된 지표면의 3D 파일과 그 등고선을 보관함에 채운다(준비 화면에서 호출).
|
||||
*
|
||||
* 사용자가 실제로 보는 것은 이 둘이라 이것만 챙긴다 — 포인트클라우드·배수유역은 제외
|
||||
* (2026-08-01 사용자 지시). 이미 보관돼 있으면 거의 즉시 끝난다.
|
||||
* 확정 지표면을 찾지 못하면 오류를 던져 준비 화면이 안내 문구를 띄우게 한다. */
|
||||
* 평활 여부·등고선 간격은 짐작하지 않고 확정 저장값을 그대로 쓴다 — 짐작하면 B04·B05가
|
||||
* 서로 다른 파일을 받아 같은 지형을 두 번 내려받게 된다.
|
||||
* 확정 지표면을 찾지 못하면 오류를 던져 준비 화면이 안내 문구를 띄우게 한다.
|
||||
* 반환값은 담아 둔 구성의 signature — 호출측이 준비 표식으로 저장한다. */
|
||||
export async function preloadSurfaceAssets(
|
||||
projectId: string,
|
||||
report: PreloadReporter = () => {},
|
||||
): Promise<void> {
|
||||
): Promise<string> {
|
||||
report("확정된 지표면을 확인하는 중…", null);
|
||||
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/surface/models`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!response.ok) throw new Error("지표면 목록을 불러오지 못했습니다.");
|
||||
const data = (await response.json()) as {
|
||||
models?: Array<{ id: number; model_type?: string; status?: string }>;
|
||||
};
|
||||
const confirmed = (data.models ?? []).find((model) => model.status === "CONFIRMED");
|
||||
if (!confirmed) throw new Error("확정된 지표면 모델이 없습니다.");
|
||||
const confirmed = await fetchConfirmedSurface(projectId);
|
||||
if (!confirmed.model_id) throw new Error("확정된 지표면 모델이 없습니다.");
|
||||
|
||||
// 스무딩을 지원하는 방식(dtm·tin)은 화면 기본값이 스무딩 적용본이다.
|
||||
const method = (confirmed.model_type ?? "").toLowerCase();
|
||||
const smooth = method === "dtm" || method === "tin";
|
||||
const base = `${API_BASE_URL}/projects/${projectId}/surface/models/${confirmed.id}`;
|
||||
const interval = await fetchUserContourInterval(projectId);
|
||||
const smooth = confirmed.smooth ?? false;
|
||||
const interval = confirmed.contour_interval_m ?? 1.0;
|
||||
const base = `${API_BASE_URL}/projects/${projectId}/surface/models/${confirmed.model_id}`;
|
||||
|
||||
report("3D 지표면을 준비하는 중…", 0);
|
||||
await fetchCachedBytes(projectId, `${base}/preview?smooth=${smooth}`, {
|
||||
@@ -301,4 +295,5 @@ export async function preloadSurfaceAssets(
|
||||
{ onProgress: (ratio) => report("등고선을 준비하는 중…", ratio) },
|
||||
);
|
||||
report("준비 완료", 1);
|
||||
return confirmed.signature;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type RoutePath,
|
||||
} from "@config/config_frontend";
|
||||
import type { WorkflowStage } from "@ui/ui_template_workflow_layout";
|
||||
import { fetchConfirmedSurface } from "../B04_wf1_Surface/B04_wf1_Surface_Api_Fetch";
|
||||
import { isProjectPreloaded, setPreloadTarget } from "./b_asset_cache";
|
||||
import { navigateTo } from "./router";
|
||||
|
||||
@@ -37,13 +38,28 @@ export async function fetchWorkflowState(projectId: string): Promise<WorkflowSta
|
||||
|
||||
export function goToWorkflowStage(projectId: string, route: RoutePath): void {
|
||||
localStorage.setItem(CURRENT_PROJECT_ID_KEY, projectId);
|
||||
// 이 프로젝트를 아직 준비하지 않았으면 준비 화면(B11)을 먼저 거친다. 거기서 3D 지표면과
|
||||
// 등고선을 브라우저에 담고 원래 가려던 화면으로 넘겨준다. 같은 프로젝트로 다시 들어오면
|
||||
// 건너뛴다(2026-08-01 사용자 지시).
|
||||
if (!isProjectPreloaded(projectId)) {
|
||||
setPreloadTarget(route);
|
||||
navigateTo(ROUTES.B11_LOADING);
|
||||
void routeAfterPreloadCheck(projectId, route);
|
||||
}
|
||||
|
||||
/**
|
||||
* 담아 둔 자료가 지금 확정본과 같은지 확인하고 화면을 정한다.
|
||||
*
|
||||
* 확정 구성을 묻는 요청은 수 KB라 이동할 때마다 물어도 부담이 없다. 담아 둔 표식과 다르면
|
||||
* (관리자가 B04에서 다시 확정했거나, 새 브라우저이거나, 다른 프로젝트를 들렀다 온 경우)
|
||||
* 준비 화면(B11)을 거쳐 최신 자료를 담고 원래 가려던 화면으로 넘어간다(2026-08-01 사용자 지시).
|
||||
* 확인에 실패하면 준비 화면으로 보내 거기서 사유를 안내한다.
|
||||
*/
|
||||
async function routeAfterPreloadCheck(projectId: string, route: RoutePath): Promise<void> {
|
||||
let signature: string | null = null;
|
||||
try {
|
||||
signature = (await fetchConfirmedSurface(projectId)).signature;
|
||||
} catch {
|
||||
signature = null;
|
||||
}
|
||||
if (signature && isProjectPreloaded(projectId, signature)) {
|
||||
navigateTo(route);
|
||||
return;
|
||||
}
|
||||
navigateTo(route);
|
||||
setPreloadTarget(route);
|
||||
navigateTo(ROUTES.B11_LOADING);
|
||||
}
|
||||
|
||||
@@ -111,6 +111,29 @@ export interface SurfaceModelListResponse {
|
||||
models: SurfaceModelSummary[];
|
||||
}
|
||||
|
||||
/** 확정 지표면 요약 (SurfaceConfirmedResponse).
|
||||
* 포인트 배열 없이 확정 구성과 지형 가장자리만 담는다 — 진입 판정·준비화면·B05 공용. */
|
||||
export interface SurfaceConfirmedResponse {
|
||||
status: string;
|
||||
project_id: string;
|
||||
model_id: number | null;
|
||||
source_filter: string | null;
|
||||
method: string | null;
|
||||
smooth: boolean | null;
|
||||
contour_interval_m: number | null;
|
||||
/** 확정 구성이 바뀌었는지 한 줄로 비교하기 위한 값. */
|
||||
signature: string;
|
||||
point_count: number | null;
|
||||
bounds: {
|
||||
x_min: number;
|
||||
x_max: number;
|
||||
y_min: number;
|
||||
y_max: number;
|
||||
z_min: number;
|
||||
z_max: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
/** 공통 fetch 헬퍼: 타임아웃 + 인증 헤더 + 오류 응답 변환.
|
||||
*
|
||||
* `timeoutMs`를 주면 그 값으로 끊는다. 배수유역 격자 해석처럼 수십 초가 걸리는 요청은
|
||||
@@ -199,6 +222,14 @@ export async function fetchSurfacePointCloud(
|
||||
);
|
||||
}
|
||||
|
||||
/** 확정 지표면 구성 + 지형 가장자리만 조회한다(수 KB).
|
||||
* 포인트클라우드 전체(수십 MB)를 받지 않고도 3D 좌표 환산에 필요한 값을 얻는다. */
|
||||
export async function fetchConfirmedSurface(projectId: string): Promise<SurfaceConfirmedResponse> {
|
||||
return requestJson<SurfaceConfirmedResponse>(`/projects/${projectId}/surface/confirmed`, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchSurfaceGroundStats(
|
||||
projectId: string,
|
||||
): Promise<SurfaceGroundStatsResponse> {
|
||||
|
||||
@@ -28,6 +28,7 @@ from B04_wf1_Surface.B04_wf1_Surface_Repository import (
|
||||
from B04_wf1_Surface.B04_wf1_Surface_Schema import (
|
||||
SurfaceAnalyzeRequest,
|
||||
SurfaceAnalyzeResponse,
|
||||
SurfaceConfirmedResponse,
|
||||
SurfaceConfirmRequest,
|
||||
SurfaceConfirmResponse,
|
||||
SurfaceGroundStatsResponse,
|
||||
@@ -42,7 +43,10 @@ from common_util.common_util_auth import require_system_admin
|
||||
from common_util.common_util_http_cache import cached_file_response
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from common_util.common_util_surface_confirmation import surface_confirmation_defaults
|
||||
from common_util.common_util_surface_confirmation import (
|
||||
get_surface_confirmation_params,
|
||||
surface_confirmation_defaults,
|
||||
)
|
||||
from common_util.common_util_workflow_state import (
|
||||
fail_stage,
|
||||
start_stage,
|
||||
@@ -368,6 +372,80 @@ async def get_surface_point_cloud(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/surface/confirmed", response_model=SurfaceConfirmedResponse)
|
||||
async def get_confirmed_surface(project_id: UUID) -> SurfaceConfirmedResponse | JSONResponse:
|
||||
"""확정 지표면 구성과 지형 가장자리만 반환한다(포인트 배열 없음).
|
||||
|
||||
B05 3D 배치·B11 준비화면·진입 판정이 모두 이 응답 하나를 기준으로 삼는다.
|
||||
구성이 바뀌면 signature가 달라지므로 프론트가 담아 둔 자료의 갱신 여부를 판단할 수 있다.
|
||||
"""
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
models = await list_surface_models(connection, project_id)
|
||||
params = await get_surface_confirmation_params(connection, str(project_id))
|
||||
|
||||
confirmed = next((model for model in models if model["status"] == "CONFIRMED"), None)
|
||||
source_filter = params.get("source_filter")
|
||||
|
||||
# 가장자리는 B05가 3D 마커 좌표를 환산할 때 쓰므로, 기존 포인트클라우드 응답과
|
||||
# 같은 파일(확정 필터의 지면 포인트)에서 읽어 값이 어긋나지 않게 한다.
|
||||
processed_dir = Path(resolve_stored_project_path(stored_path)) / "B04_wf1_Surface"
|
||||
processed_dir = processed_dir / "processed"
|
||||
source_path = processed_dir / "structured.npz"
|
||||
if source_filter:
|
||||
filtered = processed_dir / f"ground_points_{source_filter}.npz"
|
||||
if filtered.is_file():
|
||||
source_path = filtered
|
||||
|
||||
bounds_payload: dict[str, float] | None = None
|
||||
point_count: int | None = None
|
||||
if source_path.is_file():
|
||||
with np.load(source_path) as stored:
|
||||
bounds = np.asarray(stored["bounds"], dtype=np.float64)
|
||||
if "point_count" in stored:
|
||||
point_count = int(stored["point_count"])
|
||||
bounds_payload = {
|
||||
"x_min": float(bounds[0, 0]),
|
||||
"x_max": float(bounds[0, 1]),
|
||||
"y_min": float(bounds[1, 0]),
|
||||
"y_max": float(bounds[1, 1]),
|
||||
"z_min": float(bounds[2, 0]),
|
||||
"z_max": float(bounds[2, 1]),
|
||||
}
|
||||
|
||||
signature = "|".join(
|
||||
str(value)
|
||||
for value in (
|
||||
confirmed["id"] if confirmed else "none",
|
||||
source_filter,
|
||||
params.get("method"),
|
||||
params.get("smooth"),
|
||||
params.get("contour_interval_m"),
|
||||
)
|
||||
)
|
||||
return SurfaceConfirmedResponse(
|
||||
project_id=str(project_id),
|
||||
model_id=int(confirmed["id"]) if confirmed else None,
|
||||
source_filter=source_filter,
|
||||
method=params.get("method"),
|
||||
smooth=params.get("smooth"),
|
||||
contour_interval_m=params.get("contour_interval_m"),
|
||||
signature=signature,
|
||||
point_count=point_count,
|
||||
bounds=bounds_payload,
|
||||
)
|
||||
except LookupError as exc:
|
||||
return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)})
|
||||
except Exception:
|
||||
logger.exception("B04 확정 지표면 요약 조회 실패: project_id=%s", project_id)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "확정 지표면 정보를 불러오지 못했습니다."},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/surface/ground-stats", response_model=SurfaceGroundStatsResponse)
|
||||
async def get_surface_ground_stats(project_id: UUID) -> SurfaceGroundStatsResponse | JSONResponse:
|
||||
"""manifest에서 필터별 지면 포인트 통계를 반환한다."""
|
||||
|
||||
@@ -110,6 +110,25 @@ class SurfacePointCloudSampleResponse(BaseModel):
|
||||
rgb: list[list[int]] | None = None
|
||||
|
||||
|
||||
class SurfaceConfirmedResponse(BaseModel):
|
||||
"""확정 지표면 요약 — 화면 진입 판정·준비화면·B05가 공통으로 쓰는 단일 출처.
|
||||
|
||||
포인트 배열 없이 확정값과 지형 가장자리만 담아 수 KB로 유지한다.
|
||||
signature는 확정 구성이 바뀌었는지 프론트가 한 줄로 비교하기 위한 값이다.
|
||||
"""
|
||||
|
||||
status: str = "success"
|
||||
project_id: str
|
||||
model_id: int | None = None
|
||||
source_filter: str | None = None
|
||||
method: str | None = None
|
||||
smooth: bool | None = None
|
||||
contour_interval_m: float | None = None
|
||||
signature: str
|
||||
point_count: int | None = None
|
||||
bounds: dict[str, float] | None = None
|
||||
|
||||
|
||||
class SurfaceGroundStatsResponse(BaseModel):
|
||||
"""필터별 지면 포인트 통계 응답."""
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { fetchDashboardMe } from "../B01_Dashboard/B01_Dashboard_Api_Fetch";
|
||||
import { fetchUserContourInterval, purgeOtherProjects } from "../A00_Common/b_asset_cache";
|
||||
import { clearPreloadMark, purgeOtherProjects } from "../A00_Common/b_asset_cache";
|
||||
import { clearRouteLatestCache } from "../B05_wf2_Route/B05_wf2_Route_Api_Fetch";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import {
|
||||
fetchWorkflowState,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
} from "../A00_Common/b_workflow_nav";
|
||||
import {
|
||||
confirmSurfaceModel,
|
||||
fetchConfirmedSurface,
|
||||
fetchSurfacePointCloud,
|
||||
fetchSurfaceStatus,
|
||||
listSurfaceInputFiles,
|
||||
@@ -320,16 +322,24 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
|
||||
async function loadProjectData(projectId: string): Promise<void> {
|
||||
const [inputs, status, modelResponse, contourInterval] = await Promise.all([
|
||||
const [inputs, status, modelResponse, confirmed] = await Promise.all([
|
||||
listSurfaceInputFiles(projectId),
|
||||
fetchSurfaceStatus(projectId),
|
||||
listSurfaceModels(projectId),
|
||||
// 등고선 간격은 사용자가 B05에서 저장한 값을 시작값으로 쓴다. 여기서 바꿔도 DB에는
|
||||
// 저장하지 않는다 — 관리자 확인용이라 사용자 설정을 건드리지 않는다(2026-08-01).
|
||||
fetchUserContourInterval(projectId),
|
||||
// 확정본 구성(필터·표현·평활·등고선 간격)을 그대로 시작값으로 쓴다. 여기서 바꿔도
|
||||
// DB에는 저장하지 않는다 — 관리자 확인용이라 사용자 설정을 건드리지 않는다(2026-08-01).
|
||||
fetchConfirmedSurface(projectId),
|
||||
]);
|
||||
models = modelResponse.models;
|
||||
terrainViewer.setContourInterval(contourInterval);
|
||||
// 확정본과 같은 조합에서 시작해야 B05와 같은 파일을 보고, 보관함도 한 벌만 쓴다.
|
||||
// 확정 이력이 없을 때만 개발 기본값(csf·dtm)으로 둔다.
|
||||
if (confirmed.model_id) {
|
||||
if (confirmed.source_filter) filterGroup.select.value = confirmed.source_filter;
|
||||
if (confirmed.method) methodGroup.select.value = confirmed.method;
|
||||
terrainViewer.setSmoothing(confirmed.smooth ?? false);
|
||||
}
|
||||
if (confirmed.contour_interval_m)
|
||||
terrainViewer.setContourInterval(confirmed.contour_interval_m);
|
||||
renderInputFiles(inputs.files);
|
||||
renderStatus(status);
|
||||
viewer.setLoading("포인트 데이터 로딩 중…");
|
||||
@@ -360,6 +370,10 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
smooth: terrainViewer.isSmoothingEnabled(),
|
||||
contour_interval_m: terrainViewer.getContourInterval(),
|
||||
});
|
||||
// 확정본이 바뀌었으므로 브라우저가 담아 둔 옛 자료를 더 이상 쓰지 않게 한다.
|
||||
// 준비 표식을 지우면 아래 goToWorkflowStage가 준비 화면을 거쳐 새 자료를 담는다.
|
||||
clearPreloadMark();
|
||||
clearRouteLatestCache(projectId);
|
||||
showToast(
|
||||
L("B04_Surface_Confirm_Success")
|
||||
.replace("{filter}", filterGroup.select.value)
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface SurfaceTerrainViewer {
|
||||
onCameraChange: (listener: (state: SurfaceCameraState) => void) => void;
|
||||
onAxesVisibilityChange: (listener: (visible: boolean) => void) => void;
|
||||
isSmoothingEnabled: () => boolean;
|
||||
/** 스무딩 시작값을 정한다(확정본 저장값). 다시 그리지는 않는다. */
|
||||
setSmoothing: (enabled: boolean) => void;
|
||||
getContourInterval: () => number;
|
||||
/** 등고선 간격 시작값을 정한다(사용자가 B05에서 저장한 값). 다시 그리지는 않는다. */
|
||||
setContourInterval: (interval: number) => void;
|
||||
@@ -703,6 +705,10 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
isSmoothingEnabled() {
|
||||
return !smoothCheck.disabled && smoothCheck.checked;
|
||||
},
|
||||
setSmoothing(enabled) {
|
||||
smoothPreferred = enabled;
|
||||
syncSmoothingSupport();
|
||||
},
|
||||
setContourInterval(interval) {
|
||||
if (Number.isFinite(interval) && interval > 0) intervalInput.value = String(interval);
|
||||
},
|
||||
|
||||
@@ -261,6 +261,19 @@ export async function fetchLatestRoute(projectId: string): Promise<RouteLatestRe
|
||||
});
|
||||
}
|
||||
|
||||
/** B05가 최신 경로·확정 설정값을 탭 세션에 담아 둘 때 쓰는 키(유일한 정의처). */
|
||||
export const routeLatestCacheKey = (projectId: string): string => `b05:latest:${projectId}`;
|
||||
|
||||
/** 담아 둔 최신 경로 값을 버린다. B04에서 지표면을 다시 확정하면 옛 확정값이 남아
|
||||
* B05가 이전 지형을 그리게 되므로, 확정 직후 이 값을 지운다. */
|
||||
export function clearRouteLatestCache(projectId: string): void {
|
||||
try {
|
||||
window.sessionStorage.removeItem(routeLatestCacheKey(projectId));
|
||||
} catch {
|
||||
/* 세션 접근 실패 시에는 다음 진입에서 DB를 읽게 되므로 그대로 둔다. */
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 배수유역도 (B05_wf2_Route_Router_Drainage.py) ───────────────────────── */
|
||||
|
||||
/** 관 매설 구조물 측점 후보 1개. reason: stream=세류 교차, spacing=300m 보충. */
|
||||
|
||||
@@ -10,13 +10,14 @@ import {
|
||||
WORKFLOW_STEP_ROUTES,
|
||||
} from "../A00_Common/b_workflow_nav";
|
||||
import {
|
||||
fetchSurfacePointCloud,
|
||||
fetchConfirmedSurface,
|
||||
listSurfaceModels,
|
||||
type SurfaceModelSummary,
|
||||
} from "../B04_wf1_Surface/B04_wf1_Surface_Api_Fetch";
|
||||
import {
|
||||
confirmRoute,
|
||||
fetchLatestRoute,
|
||||
routeLatestCacheKey,
|
||||
saveDrainageBoundary,
|
||||
solveRoute,
|
||||
updateContourInterval,
|
||||
@@ -246,7 +247,7 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
||||
* 확정 이력이 있으면 매 진입마다 DB(latest) 조회 대신 브라우저 세션 캐시를
|
||||
* 우선 사용해 응답속도를 높인다. 캐시 미스면 latest를 조회해 적재하고,
|
||||
* solve·확정 성공 시 신선한 값으로 갱신한다(세션 = 탭 단위, 탭 종료 시 소멸). */
|
||||
const latestCacheKey = `b05:latest:${activeProjectId}`;
|
||||
const latestCacheKey = routeLatestCacheKey(activeProjectId);
|
||||
|
||||
function readLatestCache(): RouteLatestResponse | null {
|
||||
try {
|
||||
@@ -672,17 +673,16 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
||||
if (!confirmedSurface) {
|
||||
showToast("확정된 지표면 모델이 없습니다.", "error");
|
||||
} else {
|
||||
const cloud = await fetchSurfacePointCloud(
|
||||
activeProjectId,
|
||||
latestResponse.surface_params.source_filter,
|
||||
);
|
||||
// 지형 가장자리만 필요하다 — 포인트클라우드 전체(수십 MB)는 받지 않는다.
|
||||
const confirmed = await fetchConfirmedSurface(activeProjectId);
|
||||
if (!confirmed.bounds) throw new Error("지표면 범위 정보를 찾을 수 없습니다.");
|
||||
await viewer.loadSurface(
|
||||
activeProjectId,
|
||||
confirmedSurface.id,
|
||||
latestResponse.surface_params.method,
|
||||
latestResponse.surface_params.smooth,
|
||||
latestResponse.surface_params.contour_interval_m,
|
||||
toBounds(cloud.bounds),
|
||||
toBounds(confirmed.bounds),
|
||||
);
|
||||
// 지형이 올라온 뒤에야 마커·측점선이 지형 위에 놓인다.
|
||||
renderLatest(latestResponse);
|
||||
|
||||
@@ -22,6 +22,9 @@ import "./B11_Status_UI_Style.css";
|
||||
* 자료를 준비하지 못하면(분석 실패·저장 경로 문제) 넘어가지 않고 사유를 알린다.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 준비에 실패한 채 그냥 넘어갔을 때 남기는 표식 — 어떤 확정 구성과도 일치하지 않는다. */
|
||||
const PRELOAD_SKIPPED_SIGNATURE = "skipped";
|
||||
|
||||
export async function renderB11Loading(root: HTMLElement): Promise<void> {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const target = (readPreloadTarget() ?? ROUTES.B03_FILE_INPUT) as RoutePath;
|
||||
@@ -54,10 +57,11 @@ export async function renderB11Loading(root: HTMLElement): Promise<void> {
|
||||
try {
|
||||
// 다른 프로젝트 자료가 남아 있으면 지운다 — 프로젝트끼리 섞이면 안 된다.
|
||||
await purgeOtherProjects(projectId);
|
||||
await preloadSurfaceAssets(projectId, (label, ratio) => {
|
||||
const signature = await preloadSurfaceAssets(projectId, (label, ratio) => {
|
||||
progress.set(ratio, label);
|
||||
});
|
||||
markProjectPreloaded(projectId);
|
||||
// 표식은 담아 둔 구성 그대로 남긴다 — 확정이 바뀌면 다음 진입에서 다시 준비한다.
|
||||
markProjectPreloaded(projectId, signature);
|
||||
navigateTo(target);
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? ` (${error.message})` : "";
|
||||
@@ -73,8 +77,10 @@ export async function renderB11Loading(root: HTMLElement): Promise<void> {
|
||||
label: t("B11_Loading_Btn_Continue"),
|
||||
variant: "ghost",
|
||||
// 준비를 못 했어도 같은 세션에서 다시 붙잡지 않도록 표시해 둔다.
|
||||
// 확정 구성을 모르는 상태이므로 전용 표식을 남긴다 — 확정이 정상화되면 표식이
|
||||
// 어긋나 준비 화면이 다시 뜬다.
|
||||
onClick: () => {
|
||||
if (projectId) markProjectPreloaded(projectId);
|
||||
if (projectId) markProjectPreloaded(projectId, PRELOAD_SKIPPED_SIGNATURE);
|
||||
navigateTo(target);
|
||||
},
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user