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 { 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 INITIAL_LOADING_MIN_VISIBLE_MS = 450; function shouldRetryChunkLoad(errorMessage: string) { return /Failed to fetch dynamically imported module|Importing a module script failed|Load failed|ChunkLoadError/i.test( errorMessage, ); } function retryChunkLoadOnce(errorMessage: string) { if (typeof window === 'undefined' || typeof sessionStorage === 'undefined') { return false; } if (!shouldRetryChunkLoad(errorMessage)) { return false; } if (sessionStorage.getItem(CHUNK_LOAD_RETRY_SESSION_KEY) === '1') { return false; } sessionStorage.setItem(CHUNK_LOAD_RETRY_SESSION_KEY, '1'); window.location.reload(); return true; } function App() { const { currentPage } = useAppStore(); const lastTrackedPageIdRef = useRef(null); const [showInitialLoading, setShowInitialLoading] = useState(true); useLayoutEffect(() => bindViewportCssVars(), []); useEffect(() => { if (typeof window === 'undefined') { return undefined; } 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, }, }); }; 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, }, }); }; window.addEventListener('error', handleError); window.addEventListener('unhandledrejection', handleUnhandledRejection); return () => { window.removeEventListener('error', handleError); window.removeEventListener('unhandledrejection', handleUnhandledRejection); }; }, []); 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;