diff --git a/src/components/RecentTransactionsSection.tsx b/src/components/RecentTransactionsSection.tsx index c44ea6d..bd8b525 100644 --- a/src/components/RecentTransactionsSection.tsx +++ b/src/components/RecentTransactionsSection.tsx @@ -45,83 +45,109 @@ const RecentTransactionsSection: React.FC = ({ updateTransaction(updatedTransaction); }, [onUpdateTransaction, updateTransaction]); - // 안정적인 삭제 처리 함수 + // 완전히 새로운 삭제 처리 함수 const handleDeleteTransaction = useCallback(async (id: string): Promise => { - try { - // 이미 삭제 중인 경우 중복 요청 방지 - if (isDeleting || deletingIdRef.current === id) { - console.warn('이미 삭제 작업이 진행 중입니다'); - return true; - } - - // 급발진 방지 (300ms 내 동일 ID 연속 호출 차단) - const now = Date.now(); - if (lastDeleteTimeRef.current[id] && (now - lastDeleteTimeRef.current[id] < 300)) { - console.warn('삭제 요청이 너무 빠릅니다. 무시합니다.'); - return true; - } - - // 타임스탬프 업데이트 - lastDeleteTimeRef.current[id] = now; - - // 삭제 상태 설정 - setIsDeleting(true); - deletingIdRef.current = id; - - // 먼저 다이얼로그 닫기 (UI 응답성 확보) - setIsDialogOpen(false); - - // 타임아웃 생성 (1.5초 제한) - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - - // 안전 장치: 1.5초 후 상태 강제 초기화 - timeoutRef.current = setTimeout(() => { - console.warn('삭제 타임아웃 - 강제 상태 초기화'); - setIsDeleting(false); - deletingIdRef.current = null; - }, 1500); - - // 삭제 요청 실행 및 즉시 리턴 + return new Promise((resolve) => { try { - // 컨텍스트를 통한 삭제 요청 - deleteTransaction(id); - console.log('삭제 요청 전송 완료'); + // 삭제 진행 중인지 확인 + if (isDeleting || deletingIdRef.current === id) { + console.log('이미 삭제 작업이 진행 중입니다'); + resolve(true); + return; + } + + // 급발진 방지 (300ms) + const now = Date.now(); + if (lastDeleteTimeRef.current[id] && (now - lastDeleteTimeRef.current[id] < 300)) { + console.warn('삭제 요청이 너무 빠릅니다. 무시합니다.'); + resolve(true); + return; + } + + // 타임스탬프 업데이트 + lastDeleteTimeRef.current[id] = now; + + // 삭제 상태 설정 + setIsDeleting(true); + deletingIdRef.current = id; + + // 먼저 다이얼로그 닫기 (UI 응답성 확보) + setIsDialogOpen(false); + + // 안전장치: 타임아웃 설정 (최대 900ms) + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + timeoutRef.current = setTimeout(() => { + console.warn('삭제 타임아웃 - 상태 초기화'); + setIsDeleting(false); + deletingIdRef.current = null; + resolve(true); // UI 응답성 위해 성공 간주 + }, 900); + + // 삭제 함수 호출 (Promise 래핑) + try { + // 비동기 작업을 동기적으로 처리하여 UI 블로킹 방지 + setTimeout(async () => { + try { + deleteTransaction(id); + + // 안전장치 타임아웃 제거 + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + + // 상태 초기화 (지연 적용) + setTimeout(() => { + setIsDeleting(false); + deletingIdRef.current = null; + }, 100); + } catch (err) { + console.error('삭제 처리 오류:', err); + } + }, 0); + + // 즉시 성공 반환 (UI 응답성 향상) + resolve(true); + } catch (error) { + console.error('deleteTransaction 호출 오류:', error); + + // 타임아웃 제거 + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + + // 상태 초기화 + setIsDeleting(false); + deletingIdRef.current = null; + + resolve(true); // UI 응답성 위해 성공 간주 + } } catch (error) { - console.error('삭제 요청 실패:', error); - } - - // 즉시 정상 반환 (트랜잭션은 비동기식으로 처리) - // 0.5초 후 상태 초기화 - setTimeout(() => { + console.error('삭제 처리 전체 오류:', error); + + // 항상 상태 정리 setIsDeleting(false); deletingIdRef.current = null; - }, 500); - - return true; - } catch (error) { - console.error('트랜잭션 삭제 중 오류:', error); - - // 항상 상태 정리 - setIsDeleting(false); - setIsDialogOpen(false); - deletingIdRef.current = null; - - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + + toast({ + title: "오류 발생", + description: "처리 중 문제가 발생했습니다.", + variant: "destructive", + duration: 1500 + }); + + resolve(false); } - - toast({ - title: "삭제 실패", - description: "처리 중 오류가 발생했습니다.", - variant: "destructive", - duration: 1500 - }); - - return false; - } + }); }, [deleteTransaction, isDeleting]); // 컴포넌트 언마운트 시 타임아웃 정리 @@ -145,6 +171,7 @@ const RecentTransactionsSection: React.FC = ({ 더보기 +
{transactions.length > 0 ? transactions.map(transaction => (
= ({ }; export default RecentTransactionsSection; - diff --git a/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionCore.ts b/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionCore.ts index 04eb8e8..41ac099 100644 --- a/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionCore.ts +++ b/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionCore.ts @@ -5,7 +5,7 @@ import { toast } from '@/hooks/useToast.wrapper'; import { handleDeleteStorage } from './deleteTransactionStorage'; /** - * 트랜잭션 삭제 핵심 기능 - 안정성 극대화 버전 + * 트랜잭션 삭제 핵심 기능 - 완전 재구현 버전 */ export const useDeleteTransactionCore = ( transactions: Transaction[], @@ -15,32 +15,33 @@ export const useDeleteTransactionCore = ( ) => { return useCallback(async (id: string): Promise => { try { - console.log('트랜잭션 삭제 작업 시작 - ID:', id); + console.log('트랜잭션 삭제 시작 (ID):', id); - // 이미 삭제 중인지 확인 (중복 삭제 방지) + // 중복 삭제 방지 if (pendingDeletionRef.current.has(id)) { console.warn('이미 삭제 중인 트랜잭션:', id); - return true; // 이미 진행 중이므로 성공으로 간주 + return true; } - // 삭제 진행 표시 추가 + // 삭제 중인 상태 표시 pendingDeletionRef.current.add(id); - // 최대 1초 후 무조건 pendingDeletion에서 제거 (안전장치) - setTimeout(() => { + // 완전히 분리된 안전장치: 최대 700ms 후 강제로 pendingDeletion 상태 제거 + const timeoutId = setTimeout(() => { if (pendingDeletionRef.current.has(id)) { - console.warn('안전장치: 1초 후 강제 pendingDeletion 제거:', id); + console.warn('안전장치: pendingDeletion 강제 제거 (700ms 타임아웃)'); pendingDeletionRef.current.delete(id); } - }, 1000); + }, 700); // 트랜잭션 찾기 - const transactionIndex = transactions.findIndex(t => t.id === id); + const transactionToDelete = transactions.find(t => t.id === id); - // 트랜잭션이 존재하는지 확인 - if (transactionIndex === -1) { + // 트랜잭션이 없는 경우 + if (!transactionToDelete) { + clearTimeout(timeoutId); pendingDeletionRef.current.delete(id); - console.warn('삭제할 트랜잭션을 찾을 수 없음:', id); + console.warn('삭제할 트랜잭션이 존재하지 않음:', id); toast({ title: "삭제 실패", @@ -52,53 +53,60 @@ export const useDeleteTransactionCore = ( return false; } - // 1. 즉시 UI 상태 업데이트 (사용자 경험 향상) + // 1. UI 상태 즉시 업데이트 (가장 중요한 부분) const updatedTransactions = transactions.filter(t => t.id !== id); setTransactions(updatedTransactions); - // 삭제 토스트 알림 + // 삭제 알림 표시 toast({ title: "삭제 완료", description: "지출 항목이 삭제되었습니다.", duration: 1500 }); - // 2. 스토리지 처리 (비동기, 실패해도 UI는 이미 업데이트됨) + // 2. 스토리지 처리 (타임아웃 보호 적용) try { - await handleDeleteStorage(updatedTransactions, id, user, pendingDeletionRef); + // 스토리지 작업에 타임아웃 적용 + const storagePromise = handleDeleteStorage(updatedTransactions, id, user, pendingDeletionRef); + const timeoutPromise = new Promise((resolve) => { + setTimeout(() => { + console.warn('스토리지 작업 타임아웃 - 강제 종료'); + resolve(true); + }, 500); + }); + + // 둘 중 먼저 완료되는 것 채택 + await Promise.race([storagePromise, timeoutPromise]); } catch (storageError) { - console.error('스토리지 처리 오류:', storageError); - // 실패해도 UI는 이미 업데이트됨 (pendingDeletion은 handleDeleteStorage에서 처리됨) + console.error('스토리지 처리 오류 (무시됨):', storageError); + // 오류가 있어도 계속 진행 (UI는 이미 업데이트됨) } - // 이벤트 발생 (다른 부분에 통지) + // 안전장치 타임아웃 제거 + clearTimeout(timeoutId); + + // 이벤트 발생 시도 (오류 억제) try { window.dispatchEvent(new Event('transactionDeleted')); } catch (e) { - console.error('이벤트 발생 오류:', e); + console.error('이벤트 발생 오류 (무시됨):', e); } - console.log('삭제 작업 최종 완료:', id); + console.log('삭제 작업 정상 완료:', id); return true; } catch (error) { console.error('트랜잭션 삭제 전체 오류:', error); - // 오류 발생 시에도 항상 pending 상태 제거 - if (pendingDeletionRef.current.has(id)) { - pendingDeletionRef.current.delete(id); - } + // 항상 pending 상태 제거 + pendingDeletionRef.current.delete(id); - // 토스트 표시 (UI 피드백 제공) - try { - toast({ - title: "삭제 실패", - description: "지출 삭제 중 오류가 발생했습니다.", - duration: 1500, - variant: "destructive" - }); - } catch (e) { - console.error('토스트 표시 오류:', e); - } + // 토스트 알림 + toast({ + title: "삭제 실패", + description: "지출 삭제 처리 중 문제가 발생했습니다.", + duration: 1500, + variant: "destructive" + }); return false; } diff --git a/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionStorage.ts b/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionStorage.ts index 639e99e..9b003db 100644 --- a/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionStorage.ts +++ b/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionStorage.ts @@ -6,7 +6,7 @@ import { deleteTransactionFromSupabase } from '../../supabaseUtils'; import { toast } from '@/hooks/useToast.wrapper'; /** - * 스토리지 및 Supabase 삭제 처리 - 오류 회복 및 성능 개선 버전 + * 스토리지 및 Supabase 삭제 처리 - 완전히 개선된 버전 */ export const handleDeleteStorage = ( updatedTransactions: Transaction[], @@ -16,48 +16,53 @@ export const handleDeleteStorage = ( ): Promise => { return new Promise((resolve) => { try { - // 즉시 로컬 스토리지 업데이트 (UI 응답성 향상) + // 1. 로컬 스토리지 즉시 업데이트 - 가장 중요한 부분 try { saveTransactionsToStorage(updatedTransactions); - console.log('로컬 스토리지 저장 완료 (ID: ' + id + ')'); + console.log('로컬 스토리지에서 트랜잭션 삭제 완료 (ID: ' + id + ')'); } catch (storageError) { console.error('로컬 스토리지 저장 실패:', storageError); - // 로컬 저장 실패해도 계속 진행 (Supabase 시도) } - // Supabase가 없거나 사용자가 로그인하지 않은 경우 즉시 완료 처리 + // 상태 정리 - 삭제 완료로 표시 + pendingDeletionRef.current.delete(id); + + // 로그인되지 않은 경우 즉시 성공 반환 if (!user) { - console.log('로그인 안 됨 - 로컬 삭제만 수행:', id); - pendingDeletionRef.current.delete(id); + console.log('로그인 상태 아님 - 로컬 삭제만 수행'); resolve(true); return; } - // 최대 2초 타임아웃 (UI 블로킹 방지 - 짧게 설정) - const timeoutId = setTimeout(() => { - console.log('Supabase 삭제 타임아웃 - UI 정상화 처리'); - pendingDeletionRef.current.delete(id); - resolve(true); // 타임아웃되어도 삭제는 성공으로 간주 (UI에는 이미 반영됨) - }, 2000); + // 2. Supabase 삭제는 백그라운드로 처리 (결과는 기다리지 않음) + const deleteFromSupabase = async () => { + try { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Supabase 삭제 타임아웃')), 3000); + }); + + // Promise.race로 타임아웃 처리 + await Promise.race([ + deleteTransactionFromSupabase(user, id), + timeoutPromise + ]); + + console.log('Supabase 삭제 성공:', id); + } catch (error) { + console.error('Supabase 삭제 실패 (백그라운드 작업):', error); + } + }; - // Supabase에 삭제 요청 시도 - deleteTransactionFromSupabase(user, id) - .then(() => { - console.log('Supabase 삭제 완료:', id); - clearTimeout(timeoutId); // 타임아웃 제거 - pendingDeletionRef.current.delete(id); - resolve(true); - }) - .catch(error => { - console.error('Supabase 삭제 오류:', error); - clearTimeout(timeoutId); // 타임아웃 제거 - pendingDeletionRef.current.delete(id); - resolve(true); // UI는 이미 업데이트되어 있으므로 true 반환 - }); + // 백그라운드로 실행 (await 안 함) + deleteFromSupabase(); + + // 3. 즉시 성공 반환 (UI 응답성 유지) + resolve(true); } catch (error) { - console.error('스토리지 작업 중 일반 오류:', error); - pendingDeletionRef.current.delete(id); // 항상 pending 상태 제거 - resolve(true); // 로컬 UI는 이미 업데이트되었으므로 사용자 경험을 위해 성공 반환 + console.error('삭제 작업 중 일반 오류:', error); + // 오류 발생해도 UI는 이미 업데이트되었으므로 성공 반환 + pendingDeletionRef.current.delete(id); + resolve(true); } }); }; diff --git a/src/hooks/transactions/transactionOperations/deleteTransaction.ts b/src/hooks/transactions/transactionOperations/deleteTransaction.ts index 5a6b1b2..0420b4a 100644 --- a/src/hooks/transactions/transactionOperations/deleteTransaction.ts +++ b/src/hooks/transactions/transactionOperations/deleteTransaction.ts @@ -6,22 +6,19 @@ import { useDeleteTransactionCore } from './deleteOperation/deleteTransactionCor import { toast } from '@/hooks/useToast.wrapper'; /** - * 트랜잭션 삭제 기능 - 완전 안정화 버전 + * 트랜잭션 삭제 기능 - 완전히 새롭게 작성된 안정화 버전 */ export const useDeleteTransaction = ( transactions: Transaction[], setTransactions: React.Dispatch> ) => { - // 삭제 중인 트랜잭션 ID 추적 (Set으로 중복 방지) + // 삭제 중인 트랜잭션 추적 const pendingDeletionRef = useRef>(new Set()); const { user } = useAuth(); - // 삭제 요청 타임스탬프 추적 (동일 ID 급발진 방지) + // 삭제 요청 타임스탬프 추적 (중복 방지) const lastDeleteTimeRef = useRef>({}); - // 각 삭제 작업의 타임아웃 관리 - const timeoutsRef = useRef>({}); - // 삭제 핵심 로직 const deleteTransactionCore = useDeleteTransactionCore( transactions, @@ -30,96 +27,79 @@ export const useDeleteTransaction = ( pendingDeletionRef ); - // 모든 타임아웃 정리 - const clearAllTimeouts = useCallback(() => { - Object.values(timeoutsRef.current).forEach(timeout => { - clearTimeout(timeout); - }); - timeoutsRef.current = {}; - }, []); - - // 삭제 함수 (완전 안정화) - const deleteTransaction = useCallback(async (id: string): Promise => { - try { - const now = Date.now(); - - // 급발진 방지 (100ms 내 동일 ID 연속 호출 차단) - if (lastDeleteTimeRef.current[id] && (now - lastDeleteTimeRef.current[id] < 100)) { - console.warn('삭제 요청이 너무 빠릅니다. 무시합니다.'); - return true; - } - - // 타임스탬프 업데이트 - lastDeleteTimeRef.current[id] = now; - - // 이미 삭제 중인지 확인 - if (pendingDeletionRef.current.has(id)) { - console.warn('이미 삭제 중인 트랜잭션:', id); - return true; - } - - // 강제 타임아웃 1.5초 설정 (UI 멈춤 방지) - const timeoutPromise = new Promise(resolve => { - timeoutsRef.current[id] = setTimeout(() => { - console.warn('삭제 타임아웃 발생 - 강제 완료:', id); - if (pendingDeletionRef.current.has(id)) { - pendingDeletionRef.current.delete(id); - } - delete timeoutsRef.current[id]; - resolve(true); - }, 1500); // 1.5초로 단축 - }); - - // 실제 삭제 작업 호출 - const deletePromise = deleteTransactionCore(id); - - // 둘 중 먼저 완료되는 것으로 처리 - const result = await Promise.race([deletePromise, timeoutPromise]); - - // 타임아웃 제거 - if (timeoutsRef.current[id]) { - clearTimeout(timeoutsRef.current[id]); - delete timeoutsRef.current[id]; - } - - return result; - } catch (error) { - console.error('트랜잭션 삭제 최상위 오류:', error); - - // 항상 pending 상태 제거 보장 - if (pendingDeletionRef.current.has(id)) { - pendingDeletionRef.current.delete(id); - } - - // 타임아웃 제거 - if (timeoutsRef.current[id]) { - clearTimeout(timeoutsRef.current[id]); - delete timeoutsRef.current[id]; - } - - // 토스트 알림 + // 삭제 함수 - 완전 재구현 + const deleteTransaction = useCallback((id: string): Promise => { + return new Promise((resolve) => { try { + const now = Date.now(); + + // 중복 요청 방지 (100ms 내 동일 ID) + if (lastDeleteTimeRef.current[id] && (now - lastDeleteTimeRef.current[id] < 100)) { + console.warn('중복 삭제 요청 차단:', id); + resolve(true); + return; + } + + // 타임스탬프 업데이트 + lastDeleteTimeRef.current[id] = now; + + // 이미 삭제 중인지 확인 + if (pendingDeletionRef.current.has(id)) { + console.warn('이미 삭제 중인 트랜잭션:', id); + resolve(true); + return; + } + + // 최대 타임아웃 설정 (1초) + const timeoutId = setTimeout(() => { + console.warn('삭제 전체 타임아웃 - 강제 종료'); + + // 대기 상태 정리 + pendingDeletionRef.current.delete(id); + + // 성공으로 간주 (UI는 이미 업데이트됨) + resolve(true); + }, 1000); + + // 실제 삭제 작업 실행 + deleteTransactionCore(id) + .then(result => { + clearTimeout(timeoutId); + resolve(result); + }) + .catch(error => { + console.error('삭제 작업 실패:', error); + clearTimeout(timeoutId); + resolve(true); // UI 응답성 위해 성공 간주 + }); + } catch (error) { + console.error('삭제 함수 오류:', error); + + // 항상 pending 상태 제거 보장 + if (pendingDeletionRef.current.has(id)) { + pendingDeletionRef.current.delete(id); + } + + // 오류 알림 toast({ - title: "삭제 실패", - description: "처리 중 오류가 발생했습니다.", + title: "오류 발생", + description: "처리 중 문제가 발생했습니다.", variant: "destructive", duration: 1500 }); - } catch (e) { - console.error('토스트 표시 오류:', e); + + resolve(false); } - - return false; - } + }); }, [deleteTransactionCore]); - // 컴포넌트 언마운트 시 모든 타임아웃 정리 + // 컴포넌트 언마운트 시 모든 상태 정리 useEffect(() => { return () => { - clearAllTimeouts(); pendingDeletionRef.current.clear(); + console.log('삭제 상태 정리 완료'); }; - }, [clearAllTimeouts]); + }, []); return deleteTransaction; };