import React, { Component, ErrorInfo, ReactNode } from 'react'; interface Props { children: ReactNode; fallback?: ReactNode; componentName?: string; onError?: (error: Error, errorInfo: ErrorInfo) => void; autoRecover?: boolean; maxRetries?: number; } interface State { hasError: boolean; error?: Error; errorInfo?: ErrorInfo; retryCount: number; isRecovering: boolean; } class ErrorBoundary extends Component { public state: State = { hasError: false, retryCount: 0, isRecovering: false }; private retryTimeout?: NodeJS.Timeout; private readonly maxRetries: number; constructor(props: Props) { super(props); this.maxRetries = props.maxRetries || 3; } public static getDerivedStateFromError(error: Error): Partial { return { hasError: true, error }; } public componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error('Uncaught error:', error, errorInfo); // Recharts 관련 오류인 경우 추가 정보 로깅 if (this.isRechartsError(error)) { this.logRechartsError(error, errorInfo); } // 일반적인 React 오류 로깅 this.logGeneralError(error, errorInfo); // 에러 콜백 호출 if (this.props.onError) { this.props.onError(error, errorInfo); } this.setState({ errorInfo }); // 자동 복구 로직 if (this.props.autoRecover && this.state.retryCount < this.maxRetries) { this.scheduleAutoRecovery(); } } private isRechartsError(error: Error): boolean { return error.message.includes('Invariant failed') || error.stack?.includes('generateCategoricalChart') || error.message.includes('Recharts') || error.stack?.includes('recharts') || error.message.includes('categorical') || error.message.includes('chart'); } private logRechartsError(error: Error, errorInfo: ErrorInfo) { console.error('🚨 Recharts 차트 오류 감지:', { error: error.message, stack: error.stack, componentStack: errorInfo.componentStack, timestamp: new Date().toISOString(), userAgent: navigator.userAgent, url: window.location.href, componentName: this.props.componentName || 'Unknown', retryCount: this.state.retryCount }); // 추가 디버깅 정보 수집 this.logChartDebugInfo(); } private logGeneralError(error: Error, errorInfo: ErrorInfo) { console.error('🔴 일반 오류 감지:', { error: error.message, stack: error.stack, componentStack: errorInfo.componentStack, timestamp: new Date().toISOString(), componentName: this.props.componentName || 'Unknown', retryCount: this.state.retryCount }); } private logChartDebugInfo() { try { // DOM에서 차트 관련 요소들 찾기 const chartElements = document.querySelectorAll('[class*="recharts"]'); console.log('📊 차트 요소 개수:', chartElements.length); // 데이터 관련 정보 수집 const dataElements = document.querySelectorAll('[data-testid*="chart"]'); console.log('📈 데이터 요소 개수:', dataElements.length); // 메모리 사용량 확인 if ('memory' in performance) { console.log('💾 메모리 사용량:', (performance as any).memory); } // React DevTools 정보 if ((window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__) { console.log('🔧 React DevTools 감지됨'); } } catch (debugError) { console.error('디버깅 정보 수집 중 오류:', debugError); } } private scheduleAutoRecovery() { if (this.retryTimeout) { clearTimeout(this.retryTimeout); } const delay = Math.min(1000 * Math.pow(2, this.state.retryCount), 10000); // 지수 백오프, 최대 10초 this.setState({ isRecovering: true }); this.retryTimeout = setTimeout(() => { this.setState(prevState => ({ hasError: false, error: undefined, errorInfo: undefined, retryCount: prevState.retryCount + 1, isRecovering: false })); }, delay); } private handleRetry = () => { this.setState(prevState => ({ hasError: false, error: undefined, errorInfo: undefined, retryCount: prevState.retryCount + 1, isRecovering: false })); }; private handleReload = () => { window.location.reload(); }; private getErrorMessage(): string { if (!this.state.error) return '알 수 없는 오류가 발생했습니다.'; const error = this.state.error; // Recharts 관련 오류 메시지 if (this.isRechartsError(error)) { return '차트 데이터 처리 중 오류가 발생했습니다.'; } // 일반적인 React 오류 if (error.message.includes('React')) { return '컴포넌트 렌더링 중 오류가 발생했습니다.'; } // 네트워크 오류 if (error.message.includes('Network') || error.message.includes('fetch')) { return '네트워크 연결에 문제가 있습니다.'; } // 타입 오류 if (error.message.includes('Type') || error.message.includes('undefined')) { return '데이터 형식에 문제가 있습니다.'; } return error.message || '알 수 없는 오류가 발생했습니다.'; } private getErrorDetails(): string { if (!this.state.error) return ''; const error = this.state.error; if (this.isRechartsError(error)) { return '차트 라이브러리에서 데이터 구조를 처리할 수 없습니다. 데이터를 확인하거나 페이지를 새로고침해주세요.'; } if (error.message.includes('Network') || error.message.includes('fetch')) { return '인터넷 연결을 확인하고 다시 시도해주세요.'; } if (error.message.includes('Type') || error.message.includes('undefined')) { return '데이터 형식을 확인하고 다시 시도해주세요.'; } return '페이지를 새로고침하거나 잠시 후 다시 시도해주세요.'; } private getRetryMessage(): string { if (this.state.retryCount === 0) return ''; if (this.state.isRecovering) { return `자동 복구 시도 중... (${this.state.retryCount}/${this.maxRetries})`; } return `재시도 횟수: ${this.state.retryCount}/${this.maxRetries}`; } public componentWillUnmount() { if (this.retryTimeout) { clearTimeout(this.retryTimeout); } } public render() { if (this.state.hasError) { return this.props.fallback || (

오류가 발생했습니다

{this.props.componentName && (

컴포넌트: {this.props.componentName}

)}

{this.getErrorMessage()}

{this.getErrorDetails()}

{this.getRetryMessage() && (

{this.getRetryMessage()}

)}
); } return this.props.children; } } export default ErrorBoundary;