diff --git a/src/hooks/transactions/deleteTransaction.ts b/src/hooks/transactions/deleteTransaction.ts new file mode 100644 index 0000000..e5652f3 --- /dev/null +++ b/src/hooks/transactions/deleteTransaction.ts @@ -0,0 +1,105 @@ + +import { useCallback, useRef, useEffect } from 'react'; +import { Transaction } from '@/components/TransactionCard'; +import { useAuth } from '@/contexts/auth/AuthProvider'; +import { useDeleteTransactionCore } from './transactionOperations/deleteTransactionCore'; +import { toast } from '@/hooks/useToast.wrapper'; + +/** + * 트랜잭션 삭제 기능 - 완전히 새롭게 작성된 최적화 버전 + */ +export const useDeleteTransaction = ( + transactions: Transaction[], + setTransactions: React.Dispatch> +) => { + // 삭제 중인 트랜잭션 추적 + const pendingDeletionRef = useRef>(new Set()); + const { user } = useAuth(); + + // 삭제 요청 타임스탬프 (중복 방지) + const lastDeleteTimeRef = useRef>({}); + + // 삭제 핵심 함수 + const deleteTransactionCore = useDeleteTransactionCore( + transactions, + setTransactions, + user, + pendingDeletionRef + ); + + // 삭제 함수 (안정성 최적화) + 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('삭제 전체 타임아웃 - 강제 종료'); + + // pending 상태 정리 + pendingDeletionRef.current.delete(id); + + // 타임아웃 처리 + 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: "처리 중 문제가 발생했습니다.", + variant: "destructive", + duration: 1500 + }); + + resolve(false); + } + }); + }, [deleteTransactionCore]); + + // 컴포넌트 언마운트 시 모든 상태 정리 + useEffect(() => { + return () => { + pendingDeletionRef.current.clear(); + console.log('삭제 상태 정리 완료'); + }; + }, []); + + return deleteTransaction; +}; diff --git a/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionStorage.ts b/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionStorage.ts index 0c71fae..ba346bf 100644 --- a/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionStorage.ts +++ b/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionStorage.ts @@ -2,11 +2,11 @@ import { MutableRefObject } from 'react'; import { Transaction } from '@/components/TransactionCard'; import { saveTransactionsToStorage } from '../../storageUtils'; -import { deleteTransactionFromSupabase } from '../../supabaseUtils'; +import { deleteTransactionFromServer } from '@/utils/sync/transaction/deleteTransaction'; import { toast } from '@/hooks/useToast.wrapper'; /** - * 스토리지 및 Supabase 삭제 처리 - Supabase Cloud에 최적화된 버전 + * 스토리지 및 Supabase 삭제 처리 - 안정성 개선 버전 */ export const handleDeleteStorage = ( updatedTransactions: Transaction[], @@ -16,7 +16,7 @@ export const handleDeleteStorage = ( ): Promise => { return new Promise((resolve) => { try { - // 1. 로컬 스토리지 즉시 업데이트 - 가장 중요한 부분 + // 즉시 로컬 저장소 업데이트 (가장 중요한 부분) try { saveTransactionsToStorage(updatedTransactions); console.log('로컬 스토리지에서 트랜잭션 삭제 완료 (ID: ' + id + ')'); @@ -24,46 +24,36 @@ export const handleDeleteStorage = ( console.error('로컬 스토리지 저장 실패:', storageError); } - // 상태 정리 - 삭제 완료로 표시 + // 삭제 완료 상태로 업데이트 (pending 제거) pendingDeletionRef.current.delete(id); - // 로그인되지 않은 경우 즉시 성공 반환 - if (!user) { - console.log('로그인 상태 아님 - 로컬 삭제만 수행'); - resolve(true); - return; + // 로그인된 경우에만 서버 동기화 시도 + if (user && user.id) { + try { + // 비동기 작업 실행 (결과 기다리지 않음) + deleteTransactionFromServer(user.id, id) + .then(() => { + console.log('서버 삭제 완료:', id); + }) + .catch(serverError => { + console.error('서버 삭제 실패 (무시됨):', serverError); + }); + } catch (syncError) { + console.error('서버 동기화 요청 실패 (무시됨):', syncError); + } } - // 2. Supabase Cloud 삭제는 백그라운드로 처리 (결과는 기다리지 않음) - const deleteFromSupabase = async () => { - try { - // Supabase Cloud에 최적화된 삭제 요청 - await Promise.race([ - deleteTransactionFromSupabase(user, id), - new Promise((_, reject) => setTimeout(() => reject(new Error('Supabase 삭제 타임아웃')), 5000)) - ]); - - console.log('Supabase 삭제 성공:', id); - } catch (error) { - console.error('Supabase 삭제 실패 (백그라운드 작업):', error); - // 삭제 실패 알림 (선택적) - toast({ - title: "동기화 문제", - description: "클라우드 데이터 삭제에 실패했습니다. 나중에 다시 시도합니다.", - variant: "default" - }); - } - }; - - // 백그라운드로 실행 (await 안 함) - deleteFromSupabase(); - - // 3. 즉시 성공 반환 (UI 응답성 유지) + // 항상 성공으로 간주 (UI 응답성 우선) resolve(true); } catch (error) { - console.error('삭제 작업 중 일반 오류:', error); - // 오류 발생해도 UI는 이미 업데이트되었으므로 성공 반환 - pendingDeletionRef.current.delete(id); + console.error('트랜잭션 삭제 스토리지 전체 오류:', error); + + // 안전하게 pending 상태 제거 + if (pendingDeletionRef.current.has(id)) { + pendingDeletionRef.current.delete(id); + } + + // 심각한 오류 발생해도 UI는 이미 업데이트되었으므로 성공 반환 resolve(true); } }); diff --git a/src/hooks/transactions/transactionOperations/deleteTransactionCore.ts b/src/hooks/transactions/transactionOperations/deleteTransactionCore.ts new file mode 100644 index 0000000..9186259 --- /dev/null +++ b/src/hooks/transactions/transactionOperations/deleteTransactionCore.ts @@ -0,0 +1,114 @@ + +import { useCallback, MutableRefObject } from 'react'; +import { Transaction } from '@/components/TransactionCard'; +import { toast } from '@/hooks/useToast.wrapper'; +import { handleDeleteStorage } from './deleteOperation/deleteTransactionStorage'; + +/** + * 트랜잭션 삭제 핵심 기능 - 완전히 재구현된 버전 + */ +export const useDeleteTransactionCore = ( + transactions: Transaction[], + setTransactions: React.Dispatch>, + user: any, + pendingDeletionRef: MutableRefObject> +) => { + return useCallback(async (id: string): Promise => { + try { + console.log('트랜잭션 삭제 시작 (ID):', id); + + // 중복 삭제 방지 + if (pendingDeletionRef.current.has(id)) { + console.warn('이미 삭제 중인 트랜잭션:', id); + return true; + } + + // 삭제 상태 표시 + pendingDeletionRef.current.add(id); + + // 안전장치: 최대 700ms 후 자동으로 pending 상태 제거 + const timeoutId = setTimeout(() => { + if (pendingDeletionRef.current.has(id)) { + console.warn('안전장치: 삭제 타임아웃으로 pending 상태 자동 제거'); + pendingDeletionRef.current.delete(id); + } + }, 700); + + // 트랜잭션 찾기 + const transactionToDelete = transactions.find(t => t.id === id); + + // 트랜잭션이 없으면 오류 반환 + if (!transactionToDelete) { + clearTimeout(timeoutId); + pendingDeletionRef.current.delete(id); + console.warn('삭제할 트랜잭션이 존재하지 않음:', id); + + toast({ + title: "삭제 실패", + description: "항목을 찾을 수 없습니다.", + variant: "destructive", + duration: 1500 + }); + + return false; + } + + // 1. UI 상태 즉시 업데이트 (사용자 경험 최우선) + const updatedTransactions = transactions.filter(t => t.id !== id); + setTransactions(updatedTransactions); + + // 성공 알림 표시 + toast({ + title: "삭제 완료", + description: "지출 항목이 삭제되었습니다.", + duration: 1500 + }); + + // 2. 스토리지 처리 (UI 블로킹 없음) + try { + // 스토리지 작업에 타임아웃 적용 (500ms 내에 완료되지 않으면 중단) + 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는 이미 업데이트됨) + } + + // 안전장치 타임아웃 제거 + clearTimeout(timeoutId); + + // 업데이트 이벤트 발생 (오류 무시) + try { + window.dispatchEvent(new Event('transactionDeleted')); + } catch (e) { + console.error('이벤트 발생 오류 (무시됨):', e); + } + + console.log('삭제 작업 정상 완료:', id); + return true; + } catch (error) { + console.error('트랜잭션 삭제 전체 오류:', error); + + // 항상 pending 상태 제거 보장 + pendingDeletionRef.current.delete(id); + + // 오류 알림 + toast({ + title: "삭제 실패", + description: "지출 삭제 처리 중 문제가 발생했습니다.", + duration: 1500, + variant: "destructive" + }); + + return false; + } + }, [transactions, setTransactions, user, pendingDeletionRef]); +}; diff --git a/src/hooks/transactions/transactionOperations/index.ts b/src/hooks/transactions/transactionOperations/index.ts index f41c3cf..78c70ac 100644 --- a/src/hooks/transactions/transactionOperations/index.ts +++ b/src/hooks/transactions/transactionOperations/index.ts @@ -1,23 +1,28 @@ -import { useUpdateTransaction } from './updateTransaction'; -import { useDeleteTransaction } from './deleteTransaction'; -import { TransactionOperationReturn } from './types'; +import { useCallback } from 'react'; +import { Transaction } from '@/components/TransactionCard'; +import { useDeleteTransaction } from '../deleteTransaction'; +import { updateTransaction } from './updateTransaction'; /** - * 트랜잭션 작업 관련 훅 - * 트랜잭션 업데이트, 삭제 기능을 제공합니다. + * 트랜잭션 작업 통합 훅 */ export const useTransactionsOperations = ( - transactions: any[], - setTransactions: React.Dispatch> -): TransactionOperationReturn => { - const updateTransaction = useUpdateTransaction(transactions, setTransactions); + transactions: Transaction[], + setTransactions: React.Dispatch> +) => { + // 삭제 기능 (전용 훅 사용) const deleteTransaction = useDeleteTransaction(transactions, setTransactions); + // 업데이트 기능 + const handleUpdateTransaction = useCallback(( + updatedTransaction: Transaction + ) => { + updateTransaction(transactions, setTransactions, updatedTransaction); + }, [transactions, setTransactions]); + return { - updateTransaction, + updateTransaction: handleUpdateTransaction, deleteTransaction }; }; - -export * from './types'; diff --git a/src/hooks/transactions/useTransactionsOperations.ts b/src/hooks/transactions/useTransactionsOperations.ts index 5e70eed..690eaf7 100644 --- a/src/hooks/transactions/useTransactionsOperations.ts +++ b/src/hooks/transactions/useTransactionsOperations.ts @@ -1,5 +1,2 @@ -import { useTransactionsOperations } from './transactionOperations'; - -// 기존 훅을 그대로 내보내기 -export { useTransactionsOperations }; +export { useTransactionsOperations } from './transactionOperations'; diff --git a/src/utils/sync/transaction/deleteTransaction.ts b/src/utils/sync/transaction/deleteTransaction.ts index b73421b..9403d44 100644 --- a/src/utils/sync/transaction/deleteTransaction.ts +++ b/src/utils/sync/transaction/deleteTransaction.ts @@ -4,13 +4,15 @@ import { isSyncEnabled } from '../syncSettings'; import { toast } from '@/hooks/useToast.wrapper'; /** - * 특정 트랜잭션 ID 삭제 처리 + * 특정 트랜잭션 ID 삭제 처리 - 안정성 개선 버전 */ export const deleteTransactionFromServer = async (userId: string, transactionId: string): Promise => { if (!isSyncEnabled()) return; try { console.log(`트랜잭션 삭제 요청: ${transactionId}`); + + // 삭제 요청 (타임아웃 처리 없음 - 불필요한 복잡성 제거) const { error } = await supabase .from('transactions') .delete() @@ -25,11 +27,16 @@ export const deleteTransactionFromServer = async (userId: string, transactionId: console.log(`트랜잭션 ${transactionId} 삭제 완료`); } catch (error) { console.error('트랜잭션 삭제 중 오류:', error); - // 에러 발생 시 토스트 알림 + + // 오류 메시지 (중요도 낮음) toast({ - title: "삭제 동기화 실패", - description: "서버에서 트랜잭션을 삭제하는데 문제가 발생했습니다.", - variant: "destructive" + title: "동기화 문제", + description: "서버에서 삭제 중 문제가 발생했습니다.", + variant: "default", + duration: 1500 }); + + // 오류 다시 던지기 (호출자가 처리하도록) + throw error; } };