fix(B07): 무한 로딩 두 자리 — 미리받기가 클릭을 막던 것과 응답 없는 CAD

무한 로딩으로 보이던 자리가 둘이었음.

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) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 18:12:52 +09:00
co-authored by Claude Opus 5
parent f5cd898c16
commit 107883a354
+68 -10
View File
@@ -134,6 +134,50 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
frameFields: Record<string, string>;
}
| 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<void> {
return request;
};
/** 목록 전체를 순서대로 미리 받는다. 화면을 막지 않도록 한 번에 하나씩만 간다. */
/** 목록 전체를 순서대로 미리 받는다. 화면을 막지 않도록 한 번에 하나씩만 간다.
*
* ⚠ **사용자가 누른 도면에 자리를 내준다** (2026-09-09 — 무한 로딩으로 보이던 자리).
* 도면이 31 장이고 한 장에 0.3~1.0초라, 미리 받기가 도는 동안 클릭하면 그 요청이
* **앞의 여러 장 뒤에 줄을 서서** 30초 넘게 안 열렸다. 화면은 「불러오는 중」 그대로였다.
* 한 장 받을 때마다 **클릭이 끼어들 틈**을 주고, 클릭이 도는 동안은 쉬어 준다.
*/
const prefetchAllDrawings = async (): Promise<void> => {
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<void> {
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<void> {
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<void> {
);
} 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<void> {
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;
}