'use client'; import { createContext, useCallback, useMemo, useRef, useState, type ReactNode, } from 'react'; export type ToastVariant = 'info' | 'success' | 'danger' | 'warning'; export type AlertVariant = 'info' | 'danger'; const DEFAULT_TOAST_DURATION_MS = 3000; export interface ToastItem { id: number; variant: ToastVariant; message: ReactNode; } export interface AlertItem { variant: AlertVariant; title: string; message?: ReactNode; actions?: ReactNode; } export interface ShowToastOptions { variant?: ToastVariant; message: ReactNode; /** 자동 소멸까지의 시간(ms). 기본 3000ms. */ durationMs?: number; } export interface ShowAlertOptions { variant?: AlertVariant; title: string; message?: ReactNode; actions?: ReactNode; } export interface FeedbackContextValue { toasts: ToastItem[]; alert: AlertItem | null; spinnerCount: number; showToast: (options: ShowToastOptions) => void; dismissToast: (id: number) => void; showAlert: (options: ShowAlertOptions) => void; hideAlert: () => void; showSpinner: () => void; hideSpinner: () => void; } export const FeedbackContext = createContext(null); /** * 전역 피드백(토스트 큐 / 알럿 1건 / 스피너 카운트) 상태 보유 — 루트 레이아웃에 한 번만 * 마운트한다. 실제 비주얼(Toast·Alert·Spinner)은 FeedbackHost가 이 상태를 구독해 그린다. * children은 Server Component가 렌더한 결과를 그대로 통과시켜 RSC를 유지한다. */ export function FeedbackProvider({ children }: { children: ReactNode }) { const [toasts, setToasts] = useState([]); const [alert, setAlert] = useState(null); const [spinnerCount, setSpinnerCount] = useState(0); const nextToastId = useRef(0); const dismissToast = useCallback((id: number) => { setToasts((prev) => prev.filter((toast) => toast.id !== id)); }, []); const showToast = useCallback( (options: ShowToastOptions) => { const id = nextToastId.current++; setToasts((prev) => [ ...prev, { id, variant: options.variant ?? 'info', message: options.message }, ]); setTimeout(() => { dismissToast(id); }, options.durationMs ?? DEFAULT_TOAST_DURATION_MS); }, [dismissToast] ); const showAlert = useCallback((options: ShowAlertOptions) => { setAlert({ variant: options.variant ?? 'info', title: options.title, message: options.message, actions: options.actions, }); }, []); const hideAlert = useCallback(() => { setAlert(null); }, []); const showSpinner = useCallback(() => { setSpinnerCount((prev) => prev + 1); }, []); const hideSpinner = useCallback(() => { setSpinnerCount((prev) => Math.max(0, prev - 1)); }, []); const value = useMemo( () => ({ toasts, alert, spinnerCount, showToast, dismissToast, showAlert, hideAlert, showSpinner, hideSpinner, }), [ toasts, alert, spinnerCount, showToast, dismissToast, showAlert, hideAlert, showSpinner, hideSpinner, ] ); return ( {children} ); }