All files / components/shared/ui ErrorBoundary.tsx

88.88% Statements 16/18
77.77% Branches 7/9
85.71% Functions 6/7
88.88% Lines 16/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123                                                                    48x     49x 49x       48x       24x             1x 1x   1x   1x               101x 49x 2x     47x                                                       1x                                     52x 52x      
import React, { Component, ReactNode } from "react";
 
/**
 * Props for the ErrorBoundary component
 */
interface Props {
  /** Child components to be wrapped by the error boundary */
  readonly children: ReactNode;
  /** Optional custom fallback UI to display instead of default error UI */
  readonly fallback?: ReactNode;
}
 
/**
 * State for the ErrorBoundary component
 */
interface State {
  /** Whether an error has been caught */
  readonly hasError: boolean;
  /** The caught error object, null if no error */
  readonly error: Error | null;
}
 
/**
 * ErrorBoundary component to catch and display errors gracefully.
 * Prevents black screen errors and provides user-friendly error UI with Korean/English bilingual support.
 *
 * @example
 * ```tsx
 * <ErrorBoundary>
 *   <App />
 * </ErrorBoundary>
 * ```
 */
export class ErrorBoundary extends Component<Props, State> {
  private didRecover = false;
 
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
 
  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }
 
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error("ErrorBoundary caught error:", error, errorInfo);
  }
 
  /**
   * Reset error state and attempt recovery without full page reload.
   * Full reload is used as a last resort if recovery fails.
   */
  private handleReset = () => {
    this.didRecover = false;
 
    this.setState({ hasError: false, error: null });
 
    setTimeout(() => {
      if (!this.didRecover) {
        window.location.reload();
      }
    }, 100);
  };
 
  render() {
    if (this.state.hasError) {
      if (this.props.fallback) {
        return this.props.fallback;
      }
 
      return (
        <div
          className="error-boundary"
          role="alert"
          aria-live="assertive"
          data-testid="error-boundary"
        >
          <div className="error-boundary__container">
            <h1 className="error-boundary__title">
              오류 발생 | Error Occurred
            </h1>
 
            <p className="error-boundary__message">
              화면을 복구할 수 없습니다. 다시 시작하거나 이전 화면으로 돌아가세요. | The screen could not recover. Restart or go back.
            </p>
 
            <div className="error-boundary__actions">
              <button
                type="button"
                onClick={this.handleReset}
                className="error-boundary__button error-boundary__button--primary"
                data-testid="error-boundary-restart-button"
              >
                다시 시작 | Restart
              </button>
 
              <button
                type="button"
                onClick={() => window.history.back()}
                className="error-boundary__button error-boundary__button--secondary"
                data-testid="error-boundary-back-button"
              >
                뒤로 | Back
              </button>
            </div>
 
            {process.env.NODE_ENV === "development" && this.state.error && (
              <details className="error-boundary__details">
                <summary>기술 정보 | Technical Details</summary>
                <pre>{this.state.error.stack}</pre>
              </details>
            )}
          </div>
        </div>
      );
    }
 
    this.didRecover = true;
    return this.props.children;
  }
}