import { CURRENT_PROJECT_ID_KEY, ROUTES, type RoutePath } from "@config/config_frontend"; import { createButton } from "@ui/ui_template_elements"; import { createGeneralLayout } from "@ui/ui_template_general_layout"; import { t } from "@ui/ui_template_locale"; import { createProgressCircle } from "@ui/ui_template_progress"; import { markProjectPreloaded, preloadSurfaceAssets, PRELOAD_NO_SURFACE, purgeOtherProjects, readPreloadTarget, } from "../A00_Common/b_asset_cache"; import { navigateTo } from "../A00_Common/router"; import { fetchSurfaceStatus } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import "./B11_Status_UI_Style.css"; /* ============================================================================= * 자료 준비 화면 (B11) * * 대시보드에서 프로젝트를 골라 작업 화면으로 들어갈 때, 먼저 이 화면이 3D 지표면과 * 등고선을 브라우저에 담아 둔다. 담아 두면 B04·B05가 그것을 그대로 쓰므로 화면이 바로 뜬다. * 이미 담겨 있으면 순식간에 지나간다. * * 자료를 준비하지 못하면(분석 실패·저장 경로 문제) 넘어가지 않고 사유를 알린다. * ========================================================================== */ /** 준비에 실패한 채 그냥 넘어갔을 때 남기는 표식 — 어떤 확정 구성과도 일치하지 않는다. */ const PRELOAD_SKIPPED_SIGNATURE = "skipped"; /** 초기 설계 계산이 끝날 때까지 기다린다. 상태 조회가 실패하면 기다리지 않는다 — * 조회가 막혔다고 사용자를 준비 화면에 가두면 안 된다. */ const DESIGNING_POLL_MS = 3000; const DESIGNING_MAX_WAIT_MS = 20 * 60 * 1000; async function waitWhileDesigning( projectId: string, onWait: (label: string) => void, ): Promise { const deadline = Date.now() + DESIGNING_MAX_WAIT_MS; while (Date.now() < deadline) { let status; try { status = await fetchSurfaceStatus(projectId); } catch { return; } if (status.current_stage !== "initial_design") return; onWait(status.message || "초기 설계를 계산하는 중…"); await new Promise((resolve) => window.setTimeout(resolve, DESIGNING_POLL_MS)); } } export async function renderB11Loading(root: HTMLElement): Promise { const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY); const target = (readPreloadTarget() ?? ROUTES.B03_FILE_INPUT) as RoutePath; const progress = createProgressCircle({ label: t("B11_Loading_Start"), size: 96 }); const message = document.createElement("p"); message.className = "b11-loading__message"; message.textContent = t("B11_Loading_Message"); const actions = document.createElement("div"); actions.className = "b11-loading__actions"; const body = document.createElement("div"); body.className = "b11-loading__body"; body.append(progress.root, message, actions); const layout = createGeneralLayout({ pageClass: "b11-status", title: t("B11_Loading_Title"), subtitle: t("B11_Loading_Subtitle"), content: [body], }); root.replaceChildren(layout.root); if (!projectId) { showFailure(t("B11_Loading_NoProject")); return; } try { // 초기 설계 체인이 도는 동안은 여기서 기다린다(2026-08-29 사용자 확정) — 자료를 담아도 // 계산이 안 끝난 화면으로 보내면 반쯤 된 값을 만지게 된다. await waitWhileDesigning(projectId, (label) => progress.set(0, label)); // 다른 프로젝트 자료가 남아 있으면 지운다 — 프로젝트끼리 섞이면 안 된다. await purgeOtherProjects(projectId); const signature = await preloadSurfaceAssets(projectId, (label, ratio) => { progress.set(ratio, label); }); // 표식은 담아 둔 구성 그대로 남긴다 — 확정이 바뀌면 다음 진입에서 다시 준비한다. markProjectPreloaded(projectId, signature); navigateTo(target); } catch (error) { // 확정 지표면이 아직 없는 것은 고장이 아니라 순서 문제다 — 장애 안내 대신 할 일을 알린다. if (error instanceof Error && error.message === PRELOAD_NO_SURFACE) { showNoSurface(); return; } const detail = error instanceof Error ? ` (${error.message})` : ""; showFailure(`${t("B11_Loading_Failed")}${detail}`); } function showNoSurface(): void { progress.root.hidden = true; message.textContent = t("B11_Loading_NoSurface"); actions.replaceChildren( createButton({ label: t("B11_Loading_Btn_FileInput"), variant: "filled", onClick: () => navigateTo(ROUTES.B03_FILE_INPUT), }), createButton({ label: t("B11_Loading_Btn_Dashboard"), variant: "ghost", onClick: () => navigateTo(ROUTES.B01_ACCOUNT), }), ); } function showFailure(text: string): void { progress.root.hidden = true; message.textContent = text; message.classList.add("b11-loading__message--error"); actions.replaceChildren( createButton({ label: t("B11_Loading_Btn_Continue"), variant: "ghost", // 준비를 못 했어도 같은 세션에서 다시 붙잡지 않도록 표시해 둔다. // 확정 구성을 모르는 상태이므로 전용 표식을 남긴다 — 확정이 정상화되면 표식이 // 어긋나 준비 화면이 다시 뜬다. onClick: () => { if (projectId) markProjectPreloaded(projectId, PRELOAD_SKIPPED_SIGNATURE); navigateTo(target); }, }), createButton({ label: t("B11_Loading_Btn_Dashboard"), variant: "filled", onClick: () => navigateTo(ROUTES.B01_ACCOUNT), }), ); } }