/** * Mandu Boundary Components * * Loading (Suspense) 및 Error (ErrorBoundary) UI 래퍼 * * @module runtime/boundary */ import React, { Suspense, Component, type ReactNode, type ErrorInfo } from "react"; // ═══════════════════════════════════════════════════════════════════════════ // Types // ═══════════════════════════════════════════════════════════════════════════ export interface LoadingBoundaryProps { /** 로딩 중 표시할 컴포넌트 */ fallback: ReactNode; /** 자식 컴포넌트 */ children: ReactNode; } export interface ErrorBoundaryProps { /** 에러 발생 시 표시할 컴포넌트 */ fallback: React.ComponentType; /** 자식 컴포넌트 */ children: ReactNode; /** 에러 발생 시 콜백 */ onError?: (error: Error, errorInfo: ErrorInfo) => void; } export interface ErrorFallbackProps { /** * 발생한 에러. * * Phase 6.3 redaction contract: * - In dev (`isDev === true`): the original Error is passed through with * full `.stack`, identical to throw time. * - In prod (`isDev === false`): a shallow clone is passed with * `.message` preserved and `.stack` set to the top 3 frames only. * The `digest` prop correlates the redacted client view with the * full stack emitted to server logs. * * The enforcement point is `loadPageData` / `renderPageSSR` error * paths in `runtime/server.ts` — user-land `error.tsx` components * simply consume the props they're given. */ error: Error; /** 에러 정보 */ errorInfo?: ErrorInfo; /** 리셋 함수 (에러 상태 초기화) */ resetError: () => void; /** * Short opaque identifier correlating this error with the server-side * log entry. Present in both dev and prod. Users display it in their * error.tsx so support teams can find the exact failure. */ digest?: string; } interface ErrorBoundaryState { hasError: boolean; error: Error | null; errorInfo: ErrorInfo | null; } // ═══════════════════════════════════════════════════════════════════════════ // Loading Boundary (Suspense Wrapper) // ═══════════════════════════════════════════════════════════════════════════ /** * Loading Boundary - Suspense 래퍼 * * @example * ```tsx * }> * * * ``` */ export function LoadingBoundary({ fallback, children }: LoadingBoundaryProps): React.ReactElement { return {children}; } // ═══════════════════════════════════════════════════════════════════════════ // Error Boundary (Class Component) // ═══════════════════════════════════════════════════════════════════════════ /** * Error Boundary - 에러 캐치 및 fallback UI 표시 * * @example * ```tsx * * * * ``` */ export class ErrorBoundary extends Component { constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false, error: null, errorInfo: null, }; } static getDerivedStateFromError(error: Error): Partial { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: ErrorInfo): void { this.setState({ errorInfo }); this.props.onError?.(error, errorInfo); } resetError = (): void => { this.setState({ hasError: false, error: null, errorInfo: null, }); }; render(): ReactNode { const { hasError, error, errorInfo } = this.state; const { fallback: Fallback, children } = this.props; if (hasError && error) { return ( ); } return children; } } // ═══════════════════════════════════════════════════════════════════════════ // Default Fallback Components // ═══════════════════════════════════════════════════════════════════════════ /** * 기본 로딩 컴포넌트 */ export function DefaultLoading(): React.ReactElement { return (
Loading...
); } /** * 기본 에러 컴포넌트 */ export function DefaultError({ error, resetError }: ErrorFallbackProps): React.ReactElement { return (

Something went wrong

        {error.message}
      
); } // ═══════════════════════════════════════════════════════════════════════════ // Combined Boundary // ═══════════════════════════════════════════════════════════════════════════ export interface PageBoundaryProps { /** 로딩 컴포넌트 (옵션) */ loadingComponent?: ReactNode; /** 에러 컴포넌트 (옵션) */ errorComponent?: React.ComponentType; /** 자식 컴포넌트 */ children: ReactNode; /** 에러 발생 시 콜백 */ onError?: (error: Error, errorInfo: ErrorInfo) => void; } /** * Page Boundary - Loading + Error 래퍼 * * @example * ```tsx * } * errorComponent={CustomError} * > * * * ``` */ export function PageBoundary({ loadingComponent, errorComponent, children, onError, }: PageBoundaryProps): React.ReactElement { const LoadingFallback = loadingComponent ?? ; const ErrorFallback = errorComponent ?? DefaultError; return ( {children} ); }