"use client"; /** * V2 ErrorBoundary - 컴포넌트별 에러 격리 * * 특징: * - 각 컴포넌트의 에러가 다른 컴포넌트에 영향을 주지 않음 * - 폴백 UI 제공 * - 재시도 기능 * - 에러 로깅 및 이벤트 발행 */ import React, { Component, ErrorInfo, ReactNode } from "react"; import { v2EventBus, V2_EVENTS } from "../events"; import { AlertCircle, RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; interface V2ErrorBoundaryProps { /** 자식 컴포넌트 */ children: ReactNode; /** 컴포넌트 ID (에러 추적용) */ componentId: string; /** 컴포넌트 타입 (에러 추적용) */ componentType: string; /** 사용자 정의 폴백 UI */ fallback?: ReactNode | ((error: Error, retry: () => void) => ReactNode); /** 폴백 UI 표시 방식 */ fallbackStyle?: "minimal" | "compact" | "full"; /** 에러 발생 시 콜백 */ onError?: (error: Error, errorInfo: ErrorInfo) => void; /** 복구 가능 여부 */ recoverable?: boolean; /** 자동 재시도 횟수 (0이면 자동 재시도 안 함) */ autoRetryCount?: number; /** 자동 재시도 간격 (ms) */ autoRetryDelay?: number; } interface V2ErrorBoundaryState { hasError: boolean; error: Error | null; errorInfo: ErrorInfo | null; retryCount: number; } export class V2ErrorBoundary extends Component< V2ErrorBoundaryProps, V2ErrorBoundaryState > { private retryTimeoutId: ReturnType | null = null; static defaultProps = { fallbackStyle: "compact" as const, recoverable: true, autoRetryCount: 0, autoRetryDelay: 3000, }; constructor(props: V2ErrorBoundaryProps) { super(props); this.state = { hasError: false, error: null, errorInfo: null, retryCount: 0, }; } static getDerivedStateFromError(error: Error): Partial { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: ErrorInfo): void { const { componentId, componentType, onError, autoRetryCount = 0 } = this.props; const { retryCount } = this.state; // 상태 업데이트 this.setState({ errorInfo }); // 에러 로깅 console.error( `[V2ErrorBoundary] 컴포넌트 에러 - ${componentType}(${componentId}):`, error ); console.error("Component Stack:", errorInfo.componentStack); // 에러 이벤트 발행 v2EventBus.emitSync(V2_EVENTS.COMPONENT_ERROR, { componentId, componentType, error, recoverable: this.props.recoverable ?? true, }); // 사용자 정의 에러 핸들러 호출 onError?.(error, errorInfo); // 자동 재시도 if (autoRetryCount > 0 && retryCount < autoRetryCount) { this.scheduleAutoRetry(); } } componentWillUnmount(): void { if (this.retryTimeoutId) { clearTimeout(this.retryTimeoutId); } } private scheduleAutoRetry = (): void => { const { autoRetryDelay = 3000 } = this.props; this.retryTimeoutId = setTimeout(() => { this.handleRetry(); }, autoRetryDelay); }; private handleRetry = (): void => { const { componentId, componentType } = this.props; // 복구 이벤트 발행 v2EventBus.emitSync(V2_EVENTS.COMPONENT_RECOVER, { componentId, componentType, }); this.setState((prev) => ({ hasError: false, error: null, errorInfo: null, retryCount: prev.retryCount + 1, })); }; private renderMinimalFallback(): ReactNode { const { recoverable = true } = this.props; return (
오류 발생 {recoverable && ( )}
); } private renderCompactFallback(): ReactNode { const { componentType, recoverable = true } = this.props; const { error } = this.state; return (
{componentType} 로드 실패
{error && (

{error.message.substring(0, 100)} {error.message.length > 100 ? "..." : ""}

)} {recoverable && ( )}
); } private renderFullFallback(): ReactNode { const { componentId, componentType, recoverable = true } = this.props; const { error, errorInfo, retryCount } = this.state; return ( {componentType} 컴포넌트 오류

컴포넌트 ID: {componentId}

{error && (

에러 메시지: {error.message}

)} {retryCount > 0 && (

재시도 횟수: {retryCount}회

)} {process.env.NODE_ENV === "development" && errorInfo && (
스택 트레이스 보기
                  {errorInfo.componentStack}
                
)} {recoverable && ( )}
); } render(): ReactNode { const { children, fallback, fallbackStyle = "compact" } = this.props; const { hasError, error } = this.state; if (!hasError) { return children; } // 사용자 정의 폴백 if (fallback) { if (typeof fallback === "function") { return fallback(error!, this.handleRetry); } return fallback; } // 기본 폴백 스타일별 렌더링 switch (fallbackStyle) { case "minimal": return this.renderMinimalFallback(); case "full": return this.renderFullFallback(); case "compact": default: return this.renderCompactFallback(); } } } // ============================================================================ // 함수형 래퍼 (HOC) // ============================================================================ interface WithV2ErrorBoundaryOptions { componentType: string; fallbackStyle?: "minimal" | "compact" | "full"; recoverable?: boolean; } /** * V2 에러 바운더리 HOC * * @example * ```typescript * const SafeComponent = withV2ErrorBoundary(MyComponent, { * componentType: "MyComponent", * fallbackStyle: "compact", * }); * ``` */ export function withV2ErrorBoundary

( WrappedComponent: React.ComponentType

, options: WithV2ErrorBoundaryOptions ): React.FC

{ const { componentType, fallbackStyle, recoverable } = options; const WithErrorBoundary: React.FC

= (props) => { const componentId = props.componentId ?? `${componentType}_${Date.now()}`; return ( ); }; WithErrorBoundary.displayName = `WithV2ErrorBoundary(${componentType})`; return WithErrorBoundary; } // ============================================================================ // 훅 기반 에러 리포팅 (ErrorBoundary 외부에서 에러 보고용) // ============================================================================ /** * V2 에러 리포팅 훅 * * ErrorBoundary가 잡지 못하는 비동기 에러 등을 보고할 때 사용 * * @example * ```typescript * const reportError = useV2ErrorReporter("my-component", "MyComponent"); * * const handleClick = async () => { * try { * await someAsyncOperation(); * } catch (error) { * reportError(error as Error); * } * }; * ``` */ export function useV2ErrorReporter( componentId: string, componentType: string ): (error: Error, recoverable?: boolean) => void { return React.useCallback( (error: Error, recoverable = true) => { console.error( `[V2ErrorReporter] ${componentType}(${componentId}):`, error ); v2EventBus.emitSync(V2_EVENTS.COMPONENT_ERROR, { componentId, componentType, error, recoverable, }); }, [componentId, componentType] ); }