Files
ai-code-app/src/App.tsx

136 lines
4.1 KiB
TypeScript

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