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(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 ( <> {showInitialLoading ? : null} ); } export default App;