refactor(feedback): 전역 토스트 제거 및 HUD 오버레이 피드백 도입

This commit is contained in:
2026-03-04 21:56:51 +09:00
parent 06dbee8d63
commit 836679753e
5 changed files with 30 additions and 128 deletions

View File

@@ -1,8 +1,7 @@
'use client';
import type { ReactNode } from 'react';
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { cn } from '@/shared/lib/cn';
import { createContext, useCallback, useContext, useMemo } from 'react';
interface ToastPayload {
title: string;
@@ -14,10 +13,6 @@ interface ToastPayload {
};
}
interface ToastItem extends ToastPayload {
id: number;
}
interface ToastContextValue {
pushToast: (payload: ToastPayload) => void;
}
@@ -25,58 +20,13 @@ interface ToastContextValue {
const ToastContext = createContext<ToastContextValue | null>(null);
export const ToastProvider = ({ children }: { children: ReactNode }) => {
const [toasts, setToasts] = useState<ToastItem[]>([]);
const removeToast = useCallback((id: number) => {
setToasts((current) => current.filter((toast) => toast.id !== id));
}, []);
const pushToast = useCallback((payload: ToastPayload) => {
const id = Date.now() + Math.floor(Math.random() * 10000);
const durationMs = payload.durationMs ?? 2400;
setToasts((current) => [...current, { id, ...payload }]);
window.setTimeout(() => {
removeToast(id);
}, durationMs);
}, [removeToast]);
const pushToast = useCallback((_payload: ToastPayload) => {}, []);
const value = useMemo(() => ({ pushToast }), [pushToast]);
return (
<ToastContext.Provider value={value}>
{children}
<div className="pointer-events-none fixed bottom-4 right-4 z-[70] flex w-[min(92vw,340px)] flex-col gap-2">
{toasts.map((toast) => (
<div
key={toast.id}
className={cn(
'pointer-events-auto rounded-xl border border-white/15 bg-slate-950/92 px-4 py-3 text-sm text-white shadow-lg shadow-slate-950/60',
'animate-[toast-in_180ms_ease-out] motion-reduce:animate-none',
)}
>
<p className="font-medium">{toast.title}</p>
{toast.description ? (
<p className="mt-1 text-xs text-white/70">{toast.description}</p>
) : null}
{toast.action ? (
<div className="mt-2 flex justify-end">
<button
type="button"
onClick={() => {
toast.action?.onClick();
removeToast(toast.id);
}}
className="rounded-full border border-white/25 bg-white/[0.08] px-2.5 py-1 text-[11px] font-medium text-white/88 transition-colors hover:bg-white/[0.16]"
>
{toast.action.label}
</button>
</div>
) : null}
</div>
))}
</div>
</ToastContext.Provider>
);
};