From 107883a35444590f0a307ab305b0641ff8b98e68 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 18:12:52 +0900 Subject: [PATCH] =?UTF-8?q?fix(B07):=20=EB=AC=B4=ED=95=9C=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EB=91=90=20=EC=9E=90=EB=A6=AC=20=E2=80=94=20?= =?UTF-8?q?=EB=AF=B8=EB=A6=AC=EB=B0=9B=EA=B8=B0=EA=B0=80=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=EC=9D=84=20=EB=A7=89=EB=8D=98=20=EA=B2=83=EA=B3=BC=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EC=97=86=EB=8A=94=20CAD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 무한 로딩으로 보이던 자리가 둘이었음. 1) 미리받기가 클릭 앞을 막음 (진짜 원인) 진입 직후 도면 31 장을 순서대로 미리 받는데, 한 장이 0.3~1.0초라 그 사이 클릭한 도면이 앞의 여러 장 뒤에 줄을 서서 30초 넘게 안 열렸음. 화면은 「불러오는 중」 그대로였음. - 클릭이 도는 동안 미리받기가 쉬게 함 - 클릭이 이미 받아 둔 장은 건너뜀 2) CAD 가 아무 말도 안 하면 끝이 없었음 끝을 알리는 것이 drawing-loaded · drawing-error 두 통지뿐이라, iframe 이 안 뜨거나(ready 없음) 도면을 여는 중에 멈추면 영원히 돎. - 지켜보는 시계 추가 (ready 20초 · 도면 15초) - iframe 자체가 못 뜨면 기다리지 않고 바로 끊음 - 화면 표시만 끊고 요청은 살려 둠 — 늦게 오면 그대로 받음 곁들여 B07 의 「모든 도면 확정 뒤에 이동」 가드를 개발환경에서만 품 (B09 로 가는 길. 운영에서는 그대로 막음). 확인: 단계 표시줄 B08·B09 가 눌리고 B07 → B08 이동됨. tsc 통과. Co-Authored-By: Claude Opus 5 (1M context) --- B07_DesignDetail/B07_DesignDetail_UI_Page.ts | 78 +++++++++++++++++--- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts index 3cb61056..fa37be21 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts @@ -134,6 +134,50 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { frameFields: Record; } | undefined; + // ⚠ CAD 는 iframe 이라 **저쪽이 아무 말도 안 하면 화면이 영원히 「불러오는 중」에 머문다** + // (2026-09-09 사용자 보고 — 무한 로딩). 끝을 알리는 것은 `drawing-loaded` · + // `drawing-error` 두 통지뿐이고, 그것이 안 오는 길이 둘 있다. + // ① iframe 이 아예 안 뜸 — `dist/` 가 없거나 스크립트가 죽음 ⇒ `ready` 가 안 옴 + // ② 떴는데 도면을 여는 중에 멈춤 ⇒ `loaded` 도 `error` 도 안 옴 + // 아래 시계가 그 자리를 끊는다. ⚠ **화면 표시만 끊는다** — 뒤늦게 응답이 오면 + // 그대로 받아 정상으로 되돌아간다(요청을 취소하지 않는다). + const CAD_READY_TIMEOUT_MS = 20000; + const CAD_LOAD_TIMEOUT_MS = 15000; + let loadWatchdog: number | undefined; + + const failCad = (detail: string): void => { + cadHost.dataset.loading = "false"; + cadHost.dataset.error = detail; + showToast(detail, "error"); + }; + + const stopLoadWatchdog = (): void => { + if (loadWatchdog === undefined) return; + window.clearTimeout(loadWatchdog); + loadWatchdog = undefined; + }; + + const startLoadWatchdog = (): void => { + stopLoadWatchdog(); + // 아직 `ready` 를 못 받았으면 iframe 이 뜨기를 기다리는 중이라 더 길게 준다. + const wait = cadReady ? CAD_LOAD_TIMEOUT_MS : CAD_READY_TIMEOUT_MS; + loadWatchdog = window.setTimeout(() => { + loadWatchdog = undefined; + if (cadHost.dataset.loading !== "true") return; + failCad( + cadReady + ? "CAD 가 도면을 여는 데 너무 오래 걸립니다. 다시 눌러 보세요." + : "CAD 화면이 응답하지 않습니다. 새로고침해도 같으면 CAD 빌드(dist)를 확인하세요.", + ); + }, wait); + }; + + // iframe 자체가 못 뜨는 경우 — 이때는 `ready` 가 영영 안 오므로 기다릴 것 없이 끊는다. + frame.addEventListener("error", () => { + stopLoadWatchdog(); + failCad("CAD 화면을 불러오지 못했습니다. CAD 빌드(dist)를 확인하세요."); + }); + let currentDrawing: DesignDrawingItem | undefined; let currentIndex = -1; let currentConfirmed = false; @@ -261,10 +305,19 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { return request; }; - /** 목록 전체를 순서대로 미리 받는다. 화면을 막지 않도록 한 번에 하나씩만 간다. */ + /** 목록 전체를 순서대로 미리 받는다. 화면을 막지 않도록 한 번에 하나씩만 간다. + * + * ⚠ **사용자가 누른 도면에 자리를 내준다** (2026-09-09 — 무한 로딩으로 보이던 자리). + * 도면이 31 장이고 한 장에 0.3~1.0초라, 미리 받기가 도는 동안 클릭하면 그 요청이 + * **앞의 여러 장 뒤에 줄을 서서** 30초 넘게 안 열렸다. 화면은 「불러오는 중」 그대로였다. + * 한 장 받을 때마다 **클릭이 끼어들 틈**을 주고, 클릭이 도는 동안은 쉬어 준다. + */ const prefetchAllDrawings = async (): Promise => { if (!projectId) return; for (const item of drawings) { + // 클릭이 도는 중이면 끝날 때까지 기다린다 — 미리 받기는 급하지 않다. + while (loadInFlight) await new Promise((done) => window.setTimeout(done, 120)); + if (drawingCache.has(item.id)) continue; // 클릭이 이미 받아 둔 장은 건너뛴다 try { await requestDrawing(item); } catch { @@ -294,6 +347,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { if (button) button.dataset.loading = "true"; cadHost.dataset.loading = "true"; cadHost.dataset.error = ""; // 앞선 실패 표시를 지운다 + startLoadWatchdog(); // 저쪽이 말이 없으면 여기서 끊는다 try { const response = await requestDrawing(drawing); currentDrawing = drawing; @@ -304,10 +358,8 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { updateInfoPanel(drawing, response); sendLoad(response.drawing, buildMeta(drawing, response, index)); } catch (error) { - cadHost.dataset.loading = "false"; - cadHost.dataset.error = - error instanceof Error ? error.message : "CAD 도면을 불러오지 못했습니다."; - showToast(cadHost.dataset.error, "error"); + stopLoadWatchdog(); // 여기서 이미 끝났다 — 시계를 두면 늦게 또 오류를 띄운다 + failCad(error instanceof Error ? error.message : "CAD 도면을 불러오지 못했습니다."); if (currentDrawing) highlightActive(currentDrawing.id); } finally { loadInFlight = false; @@ -493,19 +545,22 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { ); } else if (message.type === CAD_READY_MESSAGE) { cadReady = true; - if (pendingLoad) + if (pendingLoad) { sendLoad( pendingLoad.drawing, pendingLoad.meta, pendingLoad.frameEdit, pendingLoad.frameFields, ); + // 기다리던 것이 「iframe 이 뜨기」에서 「도면이 열리기」로 바뀌었다 — 시계를 다시 건다. + if (cadHost.dataset.loading === "true") startLoadWatchdog(); + } } else if (message.type === CAD_LOADED_MESSAGE) { + stopLoadWatchdog(); cadHost.dataset.loading = "false"; } else if (message.type === CAD_ERROR_MESSAGE) { - cadHost.dataset.error = message.detail ?? "CAD 도면을 표시하지 못했습니다."; - cadHost.dataset.loading = "false"; - showToast(cadHost.dataset.error, "error"); + stopLoadWatchdog(); + failCad(message.detail ?? "CAD 도면을 표시하지 못했습니다."); } else if (message.type === CAD_CHANGED_MESSAGE) { // 편집 통지는 **미저장 표시**만 세운다. 확정을 푸는 것은 [수정] 하나뿐이다 // (2026-09-01 사용자 확정) — 예전에는 이 통지가 확정을 풀어, 되돌리기나 색 @@ -551,7 +606,10 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { routes: WORKFLOW_STEP_ROUTES, onStepClick: (stepIndex) => { if (!projectId) return; - if (stepIndex > 5 && !allDrawingsConfirmed) { + // ⚠ 개발에서는 막지 않는다 (2026-09-09 사용자 지시) — B08·B09 를 확정 전에 봐야 + // 화면 검증이 되고, 그 우회 단추가 **B08 안에** 있어 여기서 막히면 닿을 길이 없다. + // 운영에서는 그대로 막는다. + if (stepIndex > 5 && !allDrawingsConfirmed && !import.meta.env.DEV) { showToast("모든 설계 도면을 확정한 뒤 다음 단계로 이동할 수 있습니다.", "warning"); return; }