feat(B11): 자료 준비 화면 신설 — 작업 화면 진입 전 3D·등고선 선적재
- B11_Status_UI_Loading + 라우트 b11-loading: 공통 프로그레스 서클로 단계·진행률 표시, 끝나면 원래 가려던 화면으로 자동 이동 - goToWorkflowStage: 아직 준비하지 않은 프로젝트면 준비 화면 경유, 같은 프로젝트로 다시 들어오면 건너뜀(탭 단위 기억). 다른 프로젝트면 보관분 교체 - 선적재 범위: 확정 지표면 3D + 그 등고선만 (종단·횡단은 0.86MB·0.06초로 이미 즉시라 제외, 배수유역·포인트클라우드도 제외) - 실패 시 자동 이동하지 않고 담당자 연락 안내 + [그래도 이동]/[대시보드로] - B04 등고선 간격 시작값을 B05 저장값과 공유(B04 변경은 DB에 쓰지 않음) - 새 문구는 ui_locales에 한국어·영어 등록
This commit is contained in:
+93
-27
@@ -204,35 +204,101 @@ export async function fetchCachedJson<T>(
|
||||
return JSON.parse(new TextDecoder().decode(bytes)) as T;
|
||||
}
|
||||
|
||||
/** 화면에 쓰기 전에 미리 받아 둔다(대시보드에서 B그룹으로 들어갈 때). 실패는 무시한다. */
|
||||
export function prefetchAsset(projectId: string, url: string): void {
|
||||
void fetchCachedBytes(projectId, url).catch(() => {
|
||||
/* 미리 받기 실패는 화면 동작에 영향을 주지 않는다. */
|
||||
});
|
||||
}
|
||||
/* ── 준비 화면 연동 ────────────────────────────────────────────────────────
|
||||
* 어느 프로젝트를 이미 준비했는지, 준비가 끝나면 어느 화면으로 보낼지를 탭 단위로 기억한다.
|
||||
* 대시보드로 나갔다가 같은 프로젝트로 돌아오면 준비 화면을 건너뛴다(2026-08-01 사용자 지시). */
|
||||
const PRELOADED_PROJECT_KEY = "frd_preloaded_project";
|
||||
const PRELOAD_TARGET_KEY = "frd_preload_target";
|
||||
|
||||
/** 확정된 지표면의 3D 파일과 등고선을 미리 받아 둔다.
|
||||
*
|
||||
* 사용자가 실제로 보는 것은 이 둘이라 이것만 챙긴다(포인트클라우드는 제외 — 2026-08-01
|
||||
* 사용자 지시). 이미 보관함에 있으면 아무 것도 하지 않는다. */
|
||||
export async function prefetchSurfaceAssets(projectId: string): Promise<void> {
|
||||
export function isProjectPreloaded(projectId: string): boolean {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/surface/models`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!response.ok) return;
|
||||
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) return;
|
||||
// 스무딩 지원 방식(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}`;
|
||||
prefetchAsset(projectId, `${base}/preview?smooth=${smooth}`);
|
||||
prefetchAsset(projectId, `${base}/contour?interval=1&smooth=${smooth}&recalculate=false`);
|
||||
return window.sessionStorage.getItem(PRELOADED_PROJECT_KEY) === projectId;
|
||||
} catch {
|
||||
/* 미리 받기는 실패해도 화면 동작에 영향을 주지 않는다. */
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function markProjectPreloaded(projectId: string): void {
|
||||
try {
|
||||
window.sessionStorage.setItem(PRELOADED_PROJECT_KEY, projectId);
|
||||
} catch {
|
||||
/* 세션 저장 실패는 준비 화면이 한 번 더 뜨는 정도의 영향뿐이다. */
|
||||
}
|
||||
}
|
||||
|
||||
export function setPreloadTarget(route: string): void {
|
||||
try {
|
||||
window.sessionStorage.setItem(PRELOAD_TARGET_KEY, route);
|
||||
} catch {
|
||||
/* 저장 실패 시 준비 화면이 기본 화면으로 보낸다. */
|
||||
}
|
||||
}
|
||||
|
||||
export function readPreloadTarget(): string | null {
|
||||
try {
|
||||
return window.sessionStorage.getItem(PRELOAD_TARGET_KEY);
|
||||
} catch {
|
||||
return 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 사용자 지시). 이미 보관돼 있으면 거의 즉시 끝난다.
|
||||
* 확정 지표면을 찾지 못하면 오류를 던져 준비 화면이 안내 문구를 띄우게 한다. */
|
||||
export async function preloadSurfaceAssets(
|
||||
projectId: string,
|
||||
report: PreloadReporter = () => {},
|
||||
): Promise<void> {
|
||||
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("확정된 지표면 모델이 없습니다.");
|
||||
|
||||
// 스무딩을 지원하는 방식(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);
|
||||
|
||||
report("3D 지표면을 준비하는 중…", 0);
|
||||
await fetchCachedBytes(projectId, `${base}/preview?smooth=${smooth}`, {
|
||||
onProgress: (ratio) => report("3D 지표면을 준비하는 중…", ratio),
|
||||
});
|
||||
|
||||
report("등고선을 준비하는 중…", null);
|
||||
await fetchCachedBytes(
|
||||
projectId,
|
||||
`${base}/contour?interval=${interval}&smooth=${smooth}&recalculate=false`,
|
||||
{ onProgress: (ratio) => report("등고선을 준비하는 중…", ratio) },
|
||||
);
|
||||
report("준비 완료", 1);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type RoutePath,
|
||||
} from "@config/config_frontend";
|
||||
import type { WorkflowStage } from "@ui/ui_template_workflow_layout";
|
||||
import { prefetchSurfaceAssets, purgeOtherProjects } from "./b_asset_cache";
|
||||
import { isProjectPreloaded, setPreloadTarget } from "./b_asset_cache";
|
||||
import { navigateTo } from "./router";
|
||||
|
||||
export interface WorkflowState {
|
||||
@@ -37,8 +37,13 @@ export async function fetchWorkflowState(projectId: string): Promise<WorkflowSta
|
||||
|
||||
export function goToWorkflowStage(projectId: string, route: RoutePath): void {
|
||||
localStorage.setItem(CURRENT_PROJECT_ID_KEY, projectId);
|
||||
// 다른 프로젝트 자료를 지우고, 이 프로젝트의 지표면 3D·등고선을 미리 받아 둔다.
|
||||
// 화면 이동을 막지 않도록 뒤에서 돌린다.
|
||||
void purgeOtherProjects(projectId).then(() => prefetchSurfaceAssets(projectId));
|
||||
// 이 프로젝트를 아직 준비하지 않았으면 준비 화면(B11)을 먼저 거친다. 거기서 3D 지표면과
|
||||
// 등고선을 브라우저에 담고 원래 가려던 화면으로 넘겨준다. 같은 프로젝트로 다시 들어오면
|
||||
// 건너뛴다(2026-08-01 사용자 지시).
|
||||
if (!isProjectPreloaded(projectId)) {
|
||||
setPreloadTarget(route);
|
||||
navigateTo(ROUTES.B11_LOADING);
|
||||
return;
|
||||
}
|
||||
navigateTo(route);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ const routeTable: Partial<Record<RoutePath, () => Promise<PageRenderer>>> = {
|
||||
(await import("../B10_Payment/B10_Payment_UI_Page")).renderB10Payment,
|
||||
[ROUTES.B11_STATUS]: async () =>
|
||||
(await import("../B11_Status/B11_Status_UI_Page")).renderB11Status,
|
||||
[ROUTES.B11_LOADING]: async () =>
|
||||
(await import("../B11_Status/B11_Status_UI_Loading")).renderB11Loading,
|
||||
};
|
||||
|
||||
/** 로그인 여부 (토큰 존재 확인) */
|
||||
@@ -112,6 +114,7 @@ export async function renderCurrentRoute(outlet: HTMLElement): Promise<void> {
|
||||
ROUTES.B09_WF6_ESTIMATION,
|
||||
ROUTES.B10_PAYMENT,
|
||||
ROUTES.B11_STATUS,
|
||||
ROUTES.B11_LOADING,
|
||||
];
|
||||
|
||||
if (workflowRoutes.includes(route)) {
|
||||
|
||||
Reference in New Issue
Block a user