File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'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<FeedbackContextValue | null>(null);
/**
* 전역 피드백(토스트 큐 / 알럿 1건 / 스피너 카운트) 상태 보유 — 루트 레이아웃에 한 번만
* 마운트한다. 실제 비주얼(Toast·Alert·Spinner)은 FeedbackHost가 이 상태를 구독해 그린다.
* children은 Server Component가 렌더한 결과를 그대로 통과시켜 RSC를 유지한다.
*/
export function FeedbackProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<ToastItem[]>([]);
const [alert, setAlert] = useState<AlertItem | null>(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<FeedbackContextValue>(
() => ({
toasts,
alert,
spinnerCount,
showToast,
dismissToast,
showAlert,
hideAlert,
showSpinner,
hideSpinner,
}),
[
toasts,
alert,
spinnerCount,
showToast,
dismissToast,
showAlert,
hideAlert,
showSpinner,
hideSpinner,
]
);
return (
<FeedbackContext.Provider value={value}>{children}</FeedbackContext.Provider>
);
}