diff --git a/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionCore.ts b/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionCore.ts deleted file mode 100644 index 8c513a4..0000000 --- a/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionCore.ts +++ /dev/null @@ -1,105 +0,0 @@ - -import { useCallback, MutableRefObject } from 'react'; -import { Transaction } from '@/components/TransactionCard'; -import { toast } from '@/hooks/useToast.wrapper'; -import { handleDeleteStorage } from './deleteTransactionStorage'; -import { User } from '@supabase/supabase-js'; - -/** - * 트랜잭션 삭제 핵심 기능 - 완전히 재설계된 버전 - * 안정성과 속도를 모두 개선 - */ -export const useDeleteTransactionCore = ( - transactions: Transaction[], - setTransactions: React.Dispatch>, - user: User | null, - pendingDeletionRef: MutableRefObject> -) => { - return useCallback(async (id: string): Promise => { - try { - // 삭제 시작 - console.log(`[삭제 핵심] 시작: ${id}`); - - // 중복 삭제 방지 - if (pendingDeletionRef.current.has(id)) { - console.warn(`[삭제 핵심] 중복 요청: ${id}`); - return true; - } - - // 삭제 중 상태 등록 - pendingDeletionRef.current.add(id); - - // 안전장치: 2초 후에도 완료되지 않으면 강제로 상태 정리 - const safetyTimeoutId = setTimeout(() => { - if (pendingDeletionRef.current.has(id)) { - console.warn(`[삭제 핵심] 안전장치 실행: ${id}`); - pendingDeletionRef.current.delete(id); - } - }, 2000); - - // 지우려는 트랜잭션 검색 - const transactionToDelete = transactions.find(t => t.id === id); - - // 트랜잭션이 없으면 종료 - if (!transactionToDelete) { - clearTimeout(safetyTimeoutId); - pendingDeletionRef.current.delete(id); - console.warn(`[삭제 핵심] 트랜잭션 없음: ${id}`); - return true; - } - - // UI 업데이트 먼저 처리 (낙관적 UI 업데이트) - setTransactions(prev => prev.filter(t => t.id !== id)); - - // 백그라운드에서 스토리지 처리 시작 (비차단) - Promise.resolve().then(async () => { - try { - // 필터링된 트랜잭션 목록 - const updatedTransactions = transactions.filter(t => t.id !== id); - - // 스토리지 작업 실행 (약간 지연) - await handleDeleteStorage(updatedTransactions, id, user, pendingDeletionRef); - - console.log(`[삭제 핵심] 완료: ${id}`); - - // 작업 완료 후 이벤트 발생 - window.dispatchEvent(new Event('transactionDeleted')); - window.dispatchEvent(new CustomEvent('transactionChanged', { - detail: { type: 'delete', id } - })); - - // 토스트 메시지 - toast({ - title: "삭제 완료", - description: "항목이 삭제되었습니다.", - duration: 2000 - }); - } catch (error) { - console.error('[삭제 핵심] 백그라운드 작업 오류:', error); - } finally { - // 항상 상태 정리 보장 - clearTimeout(safetyTimeoutId); - pendingDeletionRef.current.delete(id); - } - }); - - // 성공 반환 (UI는 이미 업데이트됨) - return true; - } catch (error) { - console.error('[삭제 핵심] 오류:', error); - - // 항상 상태 정리 보장 - pendingDeletionRef.current.delete(id); - - // 오류 알림 - toast({ - title: "오류 발생", - description: "삭제 처리 중 문제가 발생했습니다.", - variant: "destructive", - duration: 2000 - }); - - return false; - } - }, [transactions, user, pendingDeletionRef, setTransactions]); -}; diff --git a/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionStorage.ts b/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionStorage.ts deleted file mode 100644 index 6c57ccd..0000000 --- a/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionStorage.ts +++ /dev/null @@ -1,83 +0,0 @@ - -import { MutableRefObject } from 'react'; -import { Transaction } from '@/components/TransactionCard'; -import { saveTransactionsToStorage } from '../../storageUtils'; -import { deleteTransactionFromServer } from '@/utils/sync/transaction/deleteTransaction'; -import { toast } from '@/hooks/useToast.wrapper'; -import { User } from '@supabase/supabase-js'; - -/** - * 스토리지 및 서버 동기화 처리 - 완전히 재설계된 버전 - * 오류 복원력과 성능을 모두 개선 - */ -export const handleDeleteStorage = async ( - updatedTransactions: Transaction[], - id: string, - user: User | null, - pendingDeletionRef: MutableRefObject> -): Promise => { - try { - console.log(`[스토리지 삭제] 시작: ${id}`); - - // 로컬 스토리지 저장 - try { - saveTransactionsToStorage(updatedTransactions); - console.log(`[스토리지 삭제] 로컬 스토리지 업데이트 완료: ${id}`); - } catch (storageError) { - console.error('[스토리지 삭제] 로컬 저장 실패:', storageError); - // 오류가 있어도 계속 진행 - } - - // 서버 동기화 (사용자 정보가 있을 때만) - if (user && user.id) { - try { - // 서버 삭제 요청 (별도 비동기 처리) - const serverPromise = deleteTransactionFromServer(user.id, id); - - // 타임아웃 설정 (3초) - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => { - reject(new Error('[스토리지 삭제] 서버 작업 타임아웃')); - }, 3000); - }); - - // Promise.race로 먼저 끝나는 작업 처리 - await Promise.race([serverPromise, timeoutPromise]) - .catch(err => { - console.warn('[스토리지 삭제] 서버 작업 문제:', err.message); - }); - } catch (syncError) { - console.error('[스토리지 삭제] 서버 동기화 오류:', syncError); - } - } else { - console.log('[스토리지 삭제] 사용자 정보 없음, 서버 동기화 건너뜀'); - } - - // 이벤트 발생 - try { - window.dispatchEvent(new Event('storage')); - window.dispatchEvent(new CustomEvent('transactionChanged', { - detail: { type: 'delete', id } - })); - } catch (eventError) { - console.error('[스토리지 삭제] 이벤트 발생 오류:', eventError); - } - - // 항상 대기 상태 제거 - if (pendingDeletionRef.current.has(id)) { - pendingDeletionRef.current.delete(id); - } - - console.log(`[스토리지 삭제] 모든 작업 완료: ${id}`); - return true; - } catch (error) { - console.error('[스토리지 삭제] 심각한 오류:', error); - - // 항상 대기 상태 제거 - if (pendingDeletionRef.current.has(id)) { - pendingDeletionRef.current.delete(id); - } - - return false; - } -}; diff --git a/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionUtils.ts b/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionUtils.ts deleted file mode 100644 index 52a3070..0000000 --- a/src/hooks/transactions/transactionOperations/deleteOperation/deleteTransactionUtils.ts +++ /dev/null @@ -1,62 +0,0 @@ - -import { Transaction } from '@/components/TransactionCard'; - -/** - * 날짜 기준 트랜잭션 정렬 함수 - */ -export const sortTransactionsByDate = (transactions: Transaction[]): Transaction[] => { - return transactions.sort((a, b) => { - try { - // 날짜 형식이 다양할 수 있으므로 안전하게 처리 - let dateA = new Date(); - let dateB = new Date(); - - // 타입 안전성 확보 - if (a.date && typeof a.date === 'string') { - // 이미 포맷팅된 날짜 문자열 감지 - if (!a.date.includes('오늘,') && !a.date.includes('년')) { - const testDate = new Date(a.date); - if (!isNaN(testDate.getTime())) { - dateA = testDate; - } - } - } - - if (b.date && typeof b.date === 'string') { - // 이미 포맷팅된 날짜 문자열 감지 - if (!b.date.includes('오늘,') && !b.date.includes('년')) { - const testDate = new Date(b.date); - if (!isNaN(testDate.getTime())) { - dateB = testDate; - } - } - } - - return dateB.getTime() - dateA.getTime(); - } catch (error) { - console.error('날짜 정렬 오류:', error); - return 0; // 오류 발생 시 순서 유지 - } - }); -}; - -/** - * 트랜잭션 목록에서 삭제 대상 트랜잭션 찾기 - */ -export const findTransactionById = (transactions: Transaction[], id: string): Transaction | undefined => { - return transactions.find(transaction => transaction.id === id); -}; - -/** - * 중복 트랜잭션 검사 - */ -export const hasDuplicateTransaction = (transactions: Transaction[], id: string): boolean => { - return transactions.some(transaction => transaction.id === id); -}; - -/** - * 트랜잭션 목록에서 특정 ID 제외하기 - */ -export const removeTransactionById = (transactions: Transaction[], id: string): Transaction[] => { - return transactions.filter(transaction => transaction.id !== id); -}; diff --git a/src/hooks/transactions/useTransactionsOperations.ts b/src/hooks/transactions/useTransactionsOperations.ts index 49d7677..c3015c7 100644 --- a/src/hooks/transactions/useTransactionsOperations.ts +++ b/src/hooks/transactions/useTransactionsOperations.ts @@ -1,6 +1,18 @@ -/** - * 트랜잭션 작업 기능 통합 훅 - * 안정성과 성능 모두 개선 - */ -export { useTransactionsOperations } from './transactionOperations'; +import { useCallback } from 'react'; +import { Transaction } from '@/components/TransactionCard'; + +export const useTransactionsOperations = ( + transactions: Transaction[], + setTransactions: React.Dispatch> +) => { + const updateTransaction = useCallback((updatedTransaction: Transaction) => { + setTransactions(prev => + prev.map(t => t.id === updatedTransaction.id ? updatedTransaction : t) + ); + }, [setTransactions]); + + return { + updateTransaction + }; +}; diff --git a/src/pages/Transactions.tsx b/src/pages/Transactions.tsx index 566022d..9b713a1 100644 --- a/src/pages/Transactions.tsx +++ b/src/pages/Transactions.tsx @@ -9,9 +9,6 @@ import TransactionsContent from '@/components/transactions/TransactionsContent'; import { Transaction } from '@/components/TransactionCard'; import { toast } from '@/hooks/useToast.wrapper'; -/** - * 거래내역 페이지 - 성능 및 안정성 개선 버전 - */ const Transactions = () => { const { transactions, @@ -23,130 +20,60 @@ const Transactions = () => { handleNextMonth, refreshTransactions, totalExpenses, - deleteTransaction } = useTransactions(); - const { budgetData } = useBudget(); - const [isDataLoaded, setIsDataLoaded] = useState(false); + const { budgetData, deleteTransaction: budgetDeleteTransaction } = useBudget(); const [isProcessing, setIsProcessing] = useState(false); - const [deletingId, setDeletingId] = useState(null); - - // 더블 클릭 방지용 래퍼 - const deletionTimestampRef = useRef>({}); - // 페이지 가시성 상태 추적 - const isVisibleRef = useRef(true); - // 타임아웃 ID 관리 - const timeoutIdsRef = useRef>>([]); - - // 타임아웃 정리 함수 - const clearAllTimeouts = useCallback(() => { - timeoutIdsRef.current.forEach(id => clearTimeout(id)); - timeoutIdsRef.current = []; - }, []); + const processingTimeoutRef = useRef(null); - // 데이터 로드 상태 관리 - useEffect(() => { - if (budgetData && !isLoading) { - setIsDataLoaded(true); - } - }, [budgetData, isLoading]); - - // 트랜잭션 삭제 핸들러 - 완전히 개선된 버전 + // 삭제 핸들러 - 홈 페이지와 동일한 방식으로 구현 const handleTransactionDelete = useCallback(async (id: string): Promise => { - // 삭제 중 또는 다른 작업 중인지 확인 - if (isProcessing || deletingId) { - console.log('이미 삭제 작업이 진행 중입니다:', deletingId); - return true; + if (isProcessing) { + console.log('이미 삭제 작업이 진행 중입니다'); + return false; } - - // 더블 클릭 방지 (2초 이내 동일 요청) - const now = Date.now(); - const lastDeletionTime = deletionTimestampRef.current[id] || 0; - if (now - lastDeletionTime < 2000) { - console.log('중복 삭제 요청 무시:', id); - return true; - } - - // 타임스탬프 업데이트 - deletionTimestampRef.current[id] = now; - + try { - // 삭제 상태 설정 setIsProcessing(true); - setDeletingId(id); - console.log('트랜잭션 삭제 시작 (ID):', id); + // 타임아웃 설정 + processingTimeoutRef.current = setTimeout(() => { + setIsProcessing(false); + }, 2000); + + // BudgetContext의 삭제 함수 사용 + budgetDeleteTransaction(id); - // 삭제 함수 호출 (Promise로 래핑) - const deletePromise = deleteTransaction(id); - - // 안전한 타임아웃 설정 (5초) - const timeoutPromise = new Promise((resolve) => { - const timeoutId = setTimeout(() => { - console.warn('삭제 타임아웃 - 강제 완료'); - resolve(true); // UI는 이미 업데이트되었으므로 성공으로 간주 - }, 5000); - - // 타임아웃 ID 관리 - timeoutIdsRef.current.push(timeoutId); - }); - - // 둘 중 하나가 먼저 완료되면 반환 - const result = await Promise.race([deletePromise, timeoutPromise]); - return result; + // 삭제 후 데이터 새로고침 + setTimeout(() => { + refreshTransactions(); + }, 500); + + return true; } catch (error) { - console.error('삭제 처리 중 오류:', error); + console.error('트랜잭션 삭제 중 오류:', error); toast({ title: "삭제 실패", - description: "지출 삭제 중 오류가 발생했습니다.", - variant: "destructive", - duration: 1500 + description: "지출 항목을 삭제하는데 문제가 발생했습니다.", + variant: "destructive" }); return false; } finally { - // 상태 초기화 + if (processingTimeoutRef.current) { + clearTimeout(processingTimeoutRef.current); + } setIsProcessing(false); - setDeletingId(null); - - // 새로고침 (약간 지연) - const refreshTimeoutId = setTimeout(() => { - if (!isLoading && isVisibleRef.current) { - refreshTransactions(); - } - }, 500); - - // 타임아웃 ID 관리 - timeoutIdsRef.current.push(refreshTimeoutId); } - }, [isProcessing, deletingId, deleteTransaction, isLoading, refreshTransactions]); + }, [isProcessing, budgetDeleteTransaction, refreshTransactions]); - // 페이지 포커스/가시성 관리 + // 컴포넌트 언마운트 시 타임아웃 정리 useEffect(() => { - const handleVisibilityChange = () => { - isVisibleRef.current = document.visibilityState === 'visible'; - - if (isVisibleRef.current && !isProcessing) { - console.log('거래내역 페이지 보임 - 데이터 새로고침'); - refreshTransactions(); - } - }; - - const handleFocus = () => { - if (!isProcessing && isVisibleRef.current) { - console.log('거래내역 페이지 포커스 - 데이터 새로고침'); - refreshTransactions(); - } - }; - - document.addEventListener('visibilitychange', handleVisibilityChange); - window.addEventListener('focus', handleFocus); - return () => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - window.removeEventListener('focus', handleFocus); - clearAllTimeouts(); + if (processingTimeoutRef.current) { + clearTimeout(processingTimeoutRef.current); + } }; - }, [refreshTransactions, isProcessing, clearAllTimeouts]); + }, []); // 트랜잭션을 날짜별로 그룹화 const groupTransactionsByDate = useCallback((transactions: Transaction[]): Record => { @@ -165,7 +92,6 @@ const Transactions = () => { return grouped; }, []); - // 로딩이나 처리 중이면 비활성화된 UI 상태 표시 const isDisabled = isLoading || isProcessing; const groupedTransactions = groupTransactionsByDate(transactions);