All files / components/shared/ui ErrorBoundary.tsx

94.44% Statements 17/18
90.9% Branches 10/11
100% Functions 7/7
94.44% Lines 17/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 124 125 126 127                                                                    48x     49x 49x       48x       24x             1x   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 = () => {
    // Mark as not recovered yet
    this.didRecover = false;
 
    // Try to recover by resetting error state
    this.setState({ hasError: false, error: null });
 
    // Give React a chance to re-render, then reload if recovery failed
    setTimeout(() => {
      Iif (!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">
              {this.state.error?.message ?? "Unknown error occurred"}
            </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>
      );
    }
 
    // Mark recovery as successful when rendering children
    this.didRecover = true;
    return this.props.children;
  }
}