201 lines
6.0 KiB
TypeScript
201 lines
6.0 KiB
TypeScript
import { App as AntdApp } from 'antd';
|
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
import { getOrCreateClientId } from './app/main/clientIdentity';
|
|
import { reportClientError } from './app/main/errorLogApi';
|
|
import { AppShell } from './app/main';
|
|
import { InitialLoadingOverlay } from './app/main/InitialLoadingOverlay';
|
|
import { ReleasePendingMainModal } from './app/main/ReleasePendingMainModal';
|
|
import { buildChatPath } from './app/main/routes';
|
|
import { isPreviewRuntime } from './app/main/previewRuntime';
|
|
import { bindViewportCssVars } from './app/main/viewportCssVars';
|
|
import { reportVisitorPageView } from './features/history/api';
|
|
import { useAppStore } from './store';
|
|
|
|
const CHUNK_LOAD_RETRY_SESSION_KEY = 'ai-code-app.chunk-load-retried';
|
|
const CACHE_RECOVERY_SESSION_KEY = 'ai-code-app.cache-recovery-completed';
|
|
const INITIAL_LOADING_MIN_VISIBLE_MS = 450;
|
|
const CACHE_RECOVERY_NOTICE = '캐시된 화면 정보가 맞지 않아 홈으로 이동합니다. 다시 열어 주세요.';
|
|
const CACHE_RECOVERY_DELAY_MS = 900;
|
|
|
|
function shouldRetryChunkLoad(errorMessage: string) {
|
|
return /Failed to fetch dynamically imported module|Importing a module script failed|Load failed|ChunkLoadError/i.test(
|
|
errorMessage,
|
|
);
|
|
}
|
|
|
|
function shouldRecoverFromCacheError(errorMessage: string) {
|
|
return /Failed to fetch dynamically imported module|Importing a module script failed|Load failed|ChunkLoadError|Loading chunk|Failed to load module script|does not provide an export named|Cannot find module/i.test(
|
|
errorMessage,
|
|
);
|
|
}
|
|
|
|
function retryChunkLoadOnce(errorMessage: string) {
|
|
if (typeof window === 'undefined' || typeof sessionStorage === 'undefined') {
|
|
return false;
|
|
}
|
|
|
|
if (!shouldRetryChunkLoad(errorMessage)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
if (sessionStorage.getItem(CHUNK_LOAD_RETRY_SESSION_KEY) === '1') {
|
|
return false;
|
|
}
|
|
|
|
sessionStorage.setItem(CHUNK_LOAD_RETRY_SESSION_KEY, '1');
|
|
window.location.reload();
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getHomeRecoveryUrl() {
|
|
if (typeof window === 'undefined') {
|
|
return buildChatPath('live');
|
|
}
|
|
|
|
return new URL(buildChatPath('live'), window.location.origin).toString();
|
|
}
|
|
|
|
function tryRecoverToHomeFromCacheError(errorMessage: string, notify: (text: string) => void) {
|
|
if (typeof window === 'undefined' || typeof sessionStorage === 'undefined') {
|
|
return false;
|
|
}
|
|
|
|
if (isPreviewRuntime()) {
|
|
return false;
|
|
}
|
|
|
|
if (!shouldRecoverFromCacheError(errorMessage)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
if (sessionStorage.getItem(CACHE_RECOVERY_SESSION_KEY) === '1') {
|
|
return false;
|
|
}
|
|
|
|
sessionStorage.setItem(CACHE_RECOVERY_SESSION_KEY, '1');
|
|
notify(CACHE_RECOVERY_NOTICE);
|
|
window.setTimeout(() => {
|
|
window.location.replace(getHomeRecoveryUrl());
|
|
}, CACHE_RECOVERY_DELAY_MS);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function App() {
|
|
const { message } = AntdApp.useApp();
|
|
const { currentPage } = useAppStore();
|
|
const lastTrackedPageIdRef = useRef<string | null>(null);
|
|
const [showInitialLoading, setShowInitialLoading] = useState(true);
|
|
|
|
useLayoutEffect(() => bindViewportCssVars(), []);
|
|
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') {
|
|
return undefined;
|
|
}
|
|
|
|
const notifyCacheRecovery = (text: string) => {
|
|
message.warning({
|
|
content: text,
|
|
duration: 1.5,
|
|
});
|
|
};
|
|
|
|
const handleError = (event: ErrorEvent) => {
|
|
const reportedError = event.error instanceof Error ? event.error : null;
|
|
const errorMessage = event.message || reportedError?.message || '클라이언트 오류가 발생했습니다.';
|
|
|
|
if (retryChunkLoadOnce(errorMessage)) {
|
|
return;
|
|
}
|
|
|
|
void reportClientError({
|
|
errorType: 'window.error',
|
|
errorName: reportedError?.name ?? null,
|
|
errorMessage,
|
|
stackTrace: reportedError?.stack ?? null,
|
|
requestPath: `${window.location.pathname}${window.location.search}${window.location.hash}`,
|
|
context: {
|
|
filename: event.filename || null,
|
|
line: event.lineno || null,
|
|
column: event.colno || null,
|
|
},
|
|
});
|
|
|
|
tryRecoverToHomeFromCacheError(errorMessage, notifyCacheRecovery);
|
|
};
|
|
|
|
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
|
|
const reason = event.reason;
|
|
const reportedError = reason instanceof Error ? reason : null;
|
|
const errorMessage =
|
|
reportedError?.message || (typeof reason === 'string' ? reason : '처리되지 않은 Promise 거절이 발생했습니다.');
|
|
|
|
if (retryChunkLoadOnce(errorMessage)) {
|
|
return;
|
|
}
|
|
|
|
void reportClientError({
|
|
errorType: 'unhandledrejection',
|
|
errorName: reportedError?.name ?? null,
|
|
errorMessage,
|
|
stackTrace: reportedError?.stack ?? null,
|
|
requestPath: `${window.location.pathname}${window.location.search}${window.location.hash}`,
|
|
context: {
|
|
reasonType: typeof reason,
|
|
},
|
|
});
|
|
|
|
tryRecoverToHomeFromCacheError(errorMessage, notifyCacheRecovery);
|
|
};
|
|
|
|
window.addEventListener('error', handleError);
|
|
window.addEventListener('unhandledrejection', handleUnhandledRejection);
|
|
|
|
return () => {
|
|
window.removeEventListener('error', handleError);
|
|
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
|
|
};
|
|
}, [message]);
|
|
|
|
useEffect(() => {
|
|
getOrCreateClientId();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const hideTimer = window.setTimeout(() => {
|
|
setShowInitialLoading(false);
|
|
}, INITIAL_LOADING_MIN_VISIBLE_MS);
|
|
|
|
return () => {
|
|
window.clearTimeout(hideTimer);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (lastTrackedPageIdRef.current === currentPage.id) {
|
|
return;
|
|
}
|
|
|
|
lastTrackedPageIdRef.current = currentPage.id;
|
|
void reportVisitorPageView(currentPage);
|
|
}, [currentPage]);
|
|
|
|
return (
|
|
<>
|
|
<AppShell />
|
|
<ReleasePendingMainModal />
|
|
{showInitialLoading ? <InitialLoadingOverlay /> : null}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default App;
|