From cd6c92d7dec0da2efadf9217d35ed766866aa6a7 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 16 Mar 2025 09:41:07 +0000 Subject: [PATCH] Refactor toast duplicate prevention Refactor the toast wrapper to improve the logic for preventing duplicate toast messages. --- src/hooks/use-toast.ts | 13 ++- src/hooks/useToast.wrapper.ts | 153 +++++++++++++++++++++++++++------- 2 files changed, 135 insertions(+), 31 deletions(-) diff --git a/src/hooks/use-toast.ts b/src/hooks/use-toast.ts index 4094035..6c277ef 100644 --- a/src/hooks/use-toast.ts +++ b/src/hooks/use-toast.ts @@ -1,3 +1,4 @@ + import * as React from "react" const TOAST_LIMIT = 5 // 최대 5개로 제한 @@ -134,6 +135,14 @@ let memoryState: State = { toasts: [] } let lastAction: { type: string; id?: string; time: number } | null = null function dispatch(action: Action) { + // 마지막 액션 정보 추출 + let actionId: string | undefined = undefined; + if ('toast' in action && action.toast) { + actionId = action.toast.id; + } else if ('toastId' in action) { + actionId = action.toastId; + } + // 동일한 토스트에 대한 중복 액션 방지 const now = Date.now(); const isSameAction = lastAction && @@ -141,7 +150,7 @@ function dispatch(action: Action) { ((action.type === actionTypes.ADD_TOAST && lastAction.time > now - 1000) || // ADD 액션은 1초 내 중복 방지 (action.type !== actionTypes.ADD_TOAST && - action.toast?.id === lastAction.id && + actionId === lastAction.id && lastAction.time > now - 300)); // 다른 액션은 300ms 내 중복 방지 if (isSameAction) { @@ -152,7 +161,7 @@ function dispatch(action: Action) { // 액션 추적 업데이트 lastAction = { type: action.type, - id: action.toast?.id, + id: actionId, time: now }; diff --git a/src/hooks/useToast.wrapper.ts b/src/hooks/useToast.wrapper.ts index ed274d8..1ef301f 100644 --- a/src/hooks/useToast.wrapper.ts +++ b/src/hooks/useToast.wrapper.ts @@ -1,52 +1,139 @@ import { useToast as useOriginalToast, toast as originalToast, ToasterToast } from '@/hooks/use-toast'; -// 토스트 이벤트 추적을 위한 히스토리 및 설정 -let toastHistory: { message: string; timestamp: number }[] = []; -const DEBOUNCE_TIME = 1000; // 1초 내에 동일 메시지 방지 -const HISTORY_LIMIT = 10; // 히스토리에 유지할 최대 토스트 개수 -const CLEAR_INTERVAL = 30000; // 30초마다 오래된 히스토리 정리 - -// 히스토리 정리 함수 -const cleanupHistory = () => { - const now = Date.now(); - toastHistory = toastHistory.filter(item => (now - item.timestamp) < 10000); // 10초 이상 지난 항목 제거 +/** + * 토스트 중복 방지를 위한 설정값 + */ +const TOAST_CONFIG = { + DEFAULT_DURATION: 3000, // 기본 토스트 표시 시간 (ms) + DEBOUNCE_TIME: 1500, // 동일 메시지 무시 시간 (ms) + HISTORY_LIMIT: 10, // 히스토리에 저장할 최대 토스트 수 + CLEANUP_INTERVAL: 30000, // 히스토리 정리 주기 (ms) + HISTORY_RETENTION: 10000 // 히스토리 보관 기간 (ms) }; -// 주기적 히스토리 정리 -setInterval(cleanupHistory, CLEAR_INTERVAL); +/** + * 토스트 메시지 히스토리 인터페이스 + */ +interface ToastHistoryItem { + message: string; // 메시지 내용 (title + description) + timestamp: number; // 생성 시간 + variant?: string; // 토스트 종류 (default/destructive) +} -// 중복 토스트 방지 래퍼 함수 +/** + * 토스트 히스토리 관리 클래스 + */ +class ToastHistoryManager { + private history: ToastHistoryItem[] = []; + private cleanupInterval: ReturnType; + + constructor() { + // 주기적으로 오래된 히스토리 정리 + this.cleanupInterval = setInterval( + () => this.cleanup(), + TOAST_CONFIG.CLEANUP_INTERVAL + ); + } + + /** + * 새 토스트를 히스토리에 추가 + */ + add(message: string, variant?: string): void { + this.history.push({ + message, + timestamp: Date.now(), + variant + }); + + // 히스토리 크기 제한 + if (this.history.length > TOAST_CONFIG.HISTORY_LIMIT) { + this.history.shift(); + } + } + + /** + * 오래된 히스토리 정리 + */ + cleanup(): void { + const now = Date.now(); + this.history = this.history.filter( + item => (now - item.timestamp) < TOAST_CONFIG.HISTORY_RETENTION + ); + } + + /** + * 최근에 동일한 토스트가 표시되었는지 확인 + */ + isDuplicate(message: string, variant?: string): boolean { + const now = Date.now(); + + return this.history.some(item => + item.message === message && + item.variant === variant && + (now - item.timestamp) < TOAST_CONFIG.DEBOUNCE_TIME + ); + } + + /** + * 히스토리 초기화 (테스트용) + */ + clear(): void { + this.history = []; + } + + /** + * 정리 타이머 해제 (메모리 누수 방지) + */ + dispose(): void { + clearInterval(this.cleanupInterval); + } +} + +// 싱글톤 인스턴스 생성 +const toastHistory = new ToastHistoryManager(); + +/** + * 메시지 내용 추출 (title + description) + */ +const extractMessage = (params: Omit): string => { + return [ + params.title?.toString() || '', + params.description?.toString() || '' + ].filter(Boolean).join(' - '); +}; + +/** + * 중복 방지 토스트 표시 함수 + */ const debouncedToast = (params: Omit) => { - const currentMessage = params.description?.toString() || params.title?.toString() || ''; - const now = Date.now(); + const message = extractMessage(params); - // 유사한 메시지가 최근에 표시되었는지 확인 - const isDuplicate = toastHistory.some(item => - item.message === currentMessage && (now - item.timestamp) < DEBOUNCE_TIME - ); + // 빈 메시지 무시 + if (!message.trim()) { + console.warn('빈 토스트 메시지가 무시되었습니다'); + return; + } - // 중복이면 무시 - if (isDuplicate) { - console.log('중복 토스트 감지로 무시됨:', currentMessage); + // 중복 검사 + if (toastHistory.isDuplicate(message, params.variant)) { + console.log('중복 토스트 감지로 무시됨:', message); return; } // 히스토리에 추가 - toastHistory.push({ message: currentMessage, timestamp: now }); - - // 히스토리 크기 제한 - if (toastHistory.length > HISTORY_LIMIT) { - toastHistory.shift(); // 가장 오래된 항목 제거 - } + toastHistory.add(message, params.variant); // 실제 토스트 표시 originalToast({ ...params, - duration: params.duration || 3000, // 기본 3초로 설정 + duration: params.duration || TOAST_CONFIG.DEFAULT_DURATION, }); }; +/** + * 토스트 훅 래퍼 + */ export const useToast = () => { const toast = useOriginalToast(); return { @@ -55,4 +142,12 @@ export const useToast = () => { }; }; +/** + * 토스트 함수 래퍼 (훅을 사용하지 않는 컨텍스트용) + */ export const toast = debouncedToast; + +// 메모리 누수 방지를 위한 정리 함수 (필요시 호출) +export const disposeToastHistory = () => { + toastHistory.dispose(); +};