From 9db57e10db48a2369b8524581c78bbe6bd49ff32 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:39:10 +0000 Subject: [PATCH] Refactor useTransactionsOperations hook Refactor the useTransactionsOperations.ts file into smaller modules for better code organization and maintainability, without changing any functionality. --- .../deleteTransaction.ts | 219 +++++++++++++++ .../transactionOperations/index.ts | 23 ++ .../transactionOperations/types.ts | 12 + .../updateTransaction.ts | 48 ++++ .../transactions/useTransactionsOperations.ts | 252 +----------------- 5 files changed, 305 insertions(+), 249 deletions(-) create mode 100644 src/hooks/transactions/transactionOperations/deleteTransaction.ts create mode 100644 src/hooks/transactions/transactionOperations/index.ts create mode 100644 src/hooks/transactions/transactionOperations/types.ts create mode 100644 src/hooks/transactions/transactionOperations/updateTransaction.ts diff --git a/src/hooks/transactions/transactionOperations/deleteTransaction.ts b/src/hooks/transactions/transactionOperations/deleteTransaction.ts new file mode 100644 index 0000000..79f45b9 --- /dev/null +++ b/src/hooks/transactions/transactionOperations/deleteTransaction.ts @@ -0,0 +1,219 @@ + +import { useCallback, useRef } from 'react'; +import { Transaction } from '@/components/TransactionCard'; +import { useAuth } from '@/contexts/auth/AuthProvider'; +import { toast } from '@/hooks/useToast.wrapper'; +import { saveTransactionsToStorage } from '../storageUtils'; +import { deleteTransactionFromSupabase } from '../supabaseUtils'; + +/** + * 트랜잭션 삭제 기능 + * 로컬 스토리지와 Supabase에서 트랜잭션을 삭제합니다. + */ +export const useDeleteTransaction = ( + transactions: Transaction[], + setTransactions: React.Dispatch> +) => { + // 현재 진행 중인 삭제 작업 추적을 위한 ref + const pendingDeletionRef = useRef>(new Set()); + const { user } = useAuth(); + + // 트랜잭션 삭제 - 안정성과 성능 개선 버전 + return useCallback((id: string): Promise => { + // pendingDeletionRef 초기화 확인 + if (!pendingDeletionRef.current) { + pendingDeletionRef.current = new Set(); + } + + // 기존 promise를 변수로 저장해서 참조 가능하게 함 + const promiseObj = new Promise((resolve, reject) => { + // 삭제 작업 취소 플래그 초기화 + let isCanceled = false; + let timeoutId: ReturnType | null = null; + + try { + console.log('트랜잭션 삭제 작업 시작 - ID:', id); + + // 이미 삭제 중인 트랜잭션인지 확인 + if (pendingDeletionRef.current.has(id)) { + console.warn('이미 삭제 중인 트랜잭션입니다:', id); + reject(new Error('이미 삭제 중인 트랜잭션입니다')); + return; + } + + // 삭제할 트랜잭션이 존재하는지 확인 및 데이터 복사 보관 + const transactionToDelete = transactions.find(t => t.id === id); + if (!transactionToDelete) { + console.warn('삭제할 트랜잭션이 존재하지 않음:', id); + reject(new Error('트랜잭션이 존재하지 않습니다')); + return; + } + + // 삭제 중인 상태로 표시 + pendingDeletionRef.current.add(id); + + // 즉시 상태 업데이트 (현재 상태 복사를 통한 안전한 처리) + const originalTransactions = [...transactions]; // 복구를 위한 상태 복사 + const updatedTransactions = transactions.filter(transaction => transaction.id !== id); + + // UI 업데이트 - 동기식 처리 + setTransactions(updatedTransactions); + + // 사용자 인터페이스 응답성 감소 전에 이벤트 발생 처리 + try { + // 상태 업데이트 바로 후 크로스 스레드 통신 방지 + setTimeout(() => { + try { + window.dispatchEvent(new Event('transactionUpdated')); + } catch (innerError) { + console.warn('이벤트 발생 중 비치명적 오류:', innerError); + } + }, 0); + } catch (eventError) { + console.warn('이벤트 디스패치 설정 오류:', eventError); + } + + // UI 스레드 블록하지 않는 너비로 requestAnimationFrame 사용 + requestAnimationFrame(() => { + if (isCanceled) { + console.log('작업이 취소되었습니다.'); + return; + } + + // 백그라운드 작업은 너비로 처리 + timeoutId = setTimeout(() => { + try { + if (isCanceled) { + console.log('백그라운드 작업이 취소되었습니다.'); + return; + } + + // 로컬 스토리지 업데이트 + saveTransactionsToStorage(updatedTransactions); + + // Supabase 업데이트 + if (user) { + deleteTransactionFromSupabase(user, id) + .then(() => { + if (!isCanceled) { + console.log('Supabase 트랜잭션 삭제 성공'); + // 성공 로그만 추가, UI 업데이트는 이미 수행됨 + } + }) + .catch(error => { + console.error('Supabase 삭제 오류:', error); + handleDeleteError(error, isCanceled, id, transactionToDelete); + }) + .finally(() => { + if (!isCanceled) { + // 작업 완료 후 보류 중인 삭제 목록에서 제거 + pendingDeletionRef.current?.delete(id); + } + }); + } else { + // 사용자 정보 없을 경우 목록에서 제거 + pendingDeletionRef.current?.delete(id); + } + } catch (storageError) { + console.error('스토리지 작업 중 오류:', storageError); + pendingDeletionRef.current?.delete(id); + } + }, 0); + }); + + // 상태 업데이트가 이미 수행되었으므로 즉시 성공 반환 + console.log('트랜잭션 삭제 UI 업데이트 완료'); + resolve(true); + + // 취소 기능을 가진 Promise 객체 생성 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (promiseObj as any).cancel = () => { + isCanceled = true; + + if (timeoutId !== null) { + clearTimeout(timeoutId); + } + + pendingDeletionRef.current?.delete(id); + console.log('트랜잭션 삭제 작업 취소 완료'); + }; + } catch (error) { + console.error('트랜잭션 삭제 초기화 중 오류:', error); + + // 오류 발생 시 토스트 표시 + toast({ + title: "시스템 오류", + description: "지출 삭제 중 오류가 발생했습니다.", + duration: 2000, + variant: "destructive" + }); + + // 캣치된 모든 오류에서 보류 삭제 표시 제거 + pendingDeletionRef.current?.delete(id); + reject(error); + } + }); + + return promiseObj; + }, [transactions, setTransactions, user]); + + // 오류 처리 헬퍼 함수 + function handleDeleteError( + error: any, + isCanceled: boolean, + id: string, + transactionToDelete: Transaction + ) { + if (!isCanceled) { + // 현재 상태에 해당 트랜잭션이 이미 있는지 확인 + setTransactions(prevState => { + // 동일 트랜잭션이 없을 경우에만 추가 + const hasDuplicate = prevState.some(t => t.id === id); + if (hasDuplicate) return prevState; + + // 삭제되었던 트랜잭션 다시 추가 + const newState = [...prevState, transactionToDelete]; + + // 날짜 기준 정렬 - 안전한 경로 + return sortTransactionsByDate(newState); + }); + } + } + + // 날짜 기준 트랜잭션 정렬 함수 + function 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; // 오류 발생 시 순서 유지 + } + }); + } +}; diff --git a/src/hooks/transactions/transactionOperations/index.ts b/src/hooks/transactions/transactionOperations/index.ts new file mode 100644 index 0000000..f41c3cf --- /dev/null +++ b/src/hooks/transactions/transactionOperations/index.ts @@ -0,0 +1,23 @@ + +import { useUpdateTransaction } from './updateTransaction'; +import { useDeleteTransaction } from './deleteTransaction'; +import { TransactionOperationReturn } from './types'; + +/** + * 트랜잭션 작업 관련 훅 + * 트랜잭션 업데이트, 삭제 기능을 제공합니다. + */ +export const useTransactionsOperations = ( + transactions: any[], + setTransactions: React.Dispatch> +): TransactionOperationReturn => { + const updateTransaction = useUpdateTransaction(transactions, setTransactions); + const deleteTransaction = useDeleteTransaction(transactions, setTransactions); + + return { + updateTransaction, + deleteTransaction + }; +}; + +export * from './types'; diff --git a/src/hooks/transactions/transactionOperations/types.ts b/src/hooks/transactions/transactionOperations/types.ts new file mode 100644 index 0000000..887b889 --- /dev/null +++ b/src/hooks/transactions/transactionOperations/types.ts @@ -0,0 +1,12 @@ + +import { Transaction } from '@/components/TransactionCard'; + +export interface TransactionOperationProps { + transactions: Transaction[]; + setTransactions: React.Dispatch>; +} + +export interface TransactionOperationReturn { + updateTransaction: (updatedTransaction: Transaction) => void; + deleteTransaction: (id: string) => Promise; +} diff --git a/src/hooks/transactions/transactionOperations/updateTransaction.ts b/src/hooks/transactions/transactionOperations/updateTransaction.ts new file mode 100644 index 0000000..c381577 --- /dev/null +++ b/src/hooks/transactions/transactionOperations/updateTransaction.ts @@ -0,0 +1,48 @@ + +import { useCallback } from 'react'; +import { Transaction } from '@/components/TransactionCard'; +import { useAuth } from '@/contexts/auth/AuthProvider'; +import { toast } from '@/hooks/useToast.wrapper'; +import { saveTransactionsToStorage } from '../storageUtils'; +import { updateTransactionInSupabase } from '../supabaseUtils'; +import { TransactionOperationProps } from './types'; + +/** + * 트랜잭션 업데이트 기능 + * 로컬 스토리지와 Supabase에 트랜잭션을 업데이트합니다. + */ +export const useUpdateTransaction = ( + transactions: Transaction[], + setTransactions: React.Dispatch> +) => { + const { user } = useAuth(); + + return useCallback((updatedTransaction: Transaction) => { + const updatedTransactions = transactions.map(transaction => + transaction.id === updatedTransaction.id ? updatedTransaction : transaction + ); + + // 로컬 스토리지 업데이트 + saveTransactionsToStorage(updatedTransactions); + + // 상태 업데이트 + setTransactions(updatedTransactions); + + // Supabase 업데이트 시도 + if (user) { + updateTransactionInSupabase(user, updatedTransaction); + } + + // 이벤트 발생 + window.dispatchEvent(new Event('transactionUpdated')); + + // 약간의 지연을 두고 토스트 표시 + setTimeout(() => { + toast({ + title: "지출이 수정되었습니다", + description: `${updatedTransaction.title} 항목이 업데이트되었습니다.`, + duration: 3000 + }); + }, 100); + }, [transactions, setTransactions, user]); +}; diff --git a/src/hooks/transactions/useTransactionsOperations.ts b/src/hooks/transactions/useTransactionsOperations.ts index 10cf0c7..5e70eed 100644 --- a/src/hooks/transactions/useTransactionsOperations.ts +++ b/src/hooks/transactions/useTransactionsOperations.ts @@ -1,251 +1,5 @@ -import { useCallback, useRef } from 'react'; -import { Transaction } from '@/components/TransactionCard'; -import { useAuth } from '@/contexts/auth/AuthProvider'; -import { toast } from '@/hooks/useToast.wrapper'; -import { saveTransactionsToStorage } from './storageUtils'; -import { - updateTransactionInSupabase, - deleteTransactionFromSupabase -} from './supabaseUtils'; +import { useTransactionsOperations } from './transactionOperations'; -/** - * 트랜잭션 작업 관련 훅 - * 트랜잭션 업데이트, 삭제 기능을 제공합니다. - */ -export const useTransactionsOperations = ( - transactions: Transaction[], - setTransactions: React.Dispatch> -) => { - // 현재 진행 중인 삭제 작업 추적을 위한 ref - const pendingDeletionRef = useRef>(new Set()); - const { user } = useAuth(); - - // 트랜잭션 업데이트 - const updateTransaction = useCallback((updatedTransaction: Transaction) => { - const updatedTransactions = transactions.map(transaction => - transaction.id === updatedTransaction.id ? updatedTransaction : transaction - ); - - // 로컬 스토리지 업데이트 - saveTransactionsToStorage(updatedTransactions); - - // 상태 업데이트 - setTransactions(updatedTransactions); - - // Supabase 업데이트 시도 - if (user) { - updateTransactionInSupabase(user, updatedTransaction); - } - - // 이벤트 발생 - window.dispatchEvent(new Event('transactionUpdated')); - - // 약간의 지연을 두고 토스트 표시 - setTimeout(() => { - toast({ - title: "지출이 수정되었습니다", - description: `${updatedTransaction.title} 항목이 업데이트되었습니다.`, - duration: 3000 - }); - }, 100); - }, [transactions, setTransactions, user]); - - // 트랜잭션 삭제 - 안정성과 성능 개선 버전 (버그 수정 및 메모리 누수 방지) - const deleteTransaction = useCallback((id: string): Promise => { - // pendingDeletionRef 초기화 확인 - if (!pendingDeletionRef.current) { - pendingDeletionRef.current = new Set(); - } - - // 기존 promise를 변수로 저장해서 참조 가능하게 함 - const promiseObj = new Promise((resolve, reject) => { - // 삭제 작업 취소 플래그 초기화 - let isCanceled = false; - let timeoutId: ReturnType | null = null; - - try { - console.log('트랜잭션 삭제 작업 시작 - ID:', id); - - // 이미 삭제 중인 트랜잭션인지 확인 - if (pendingDeletionRef.current.has(id)) { - console.warn('이미 삭제 중인 트랜잭션입니다:', id); - reject(new Error('이미 삭제 중인 트랜잭션입니다')); - return; - } - - // 삭제할 트랜잭션이 존재하는지 확인 및 데이터 복사 보관 - const transactionToDelete = transactions.find(t => t.id === id); - if (!transactionToDelete) { - console.warn('삭제할 트랜잭션이 존재하지 않음:', id); - reject(new Error('트랜잭션이 존재하지 않습니다')); - return; - } - - // 삭제 중인 상태로 표시 - pendingDeletionRef.current.add(id); - - // 즉시 상태 업데이트 (현재 상태 복사를 통한 안전한 처리) - const originalTransactions = [...transactions]; // 복구를 위한 상태 복사 - const updatedTransactions = transactions.filter(transaction => transaction.id !== id); - - // UI 업데이트 - 동기식 처리 - setTransactions(updatedTransactions); - - // 사용자 인터페이스 응답성 감소 전에 이벤트 발생 처리 - try { - // 상태 업데이트 바로 후 크로스 스레드 통신 방지 - setTimeout(() => { - try { - window.dispatchEvent(new Event('transactionUpdated')); - } catch (innerError) { - console.warn('이벤트 발생 중 비치명적 오류:', innerError); - } - }, 0); - } catch (eventError) { - console.warn('이벤트 디스패치 설정 오류:', eventError); - } - - // UI 스레드 블록하지 않는 너비로 requestAnimationFrame 사용 - requestAnimationFrame(() => { - if (isCanceled) { - console.log('작업이 취소되었습니다.'); - return; - } - - // 백그라운드 작업은 너비로 처리 - timeoutId = setTimeout(() => { - try { - if (isCanceled) { - console.log('백그라운드 작업이 취소되었습니다.'); - return; - } - - // 로컬 스토리지 업데이트 - saveTransactionsToStorage(updatedTransactions); - - // Supabase 업데이트 - if (user) { - deleteTransactionFromSupabase(user, id) - .then(() => { - if (!isCanceled) { - console.log('Supabase 트랜잭션 삭제 성공'); - // 성공 로그만 추가, UI 업데이트는 이미 수행됨 - } - }) - .catch(error => { - console.error('Supabase 삭제 오류:', error); - - // 비동기 작업 실패 시 새로운 상태를 확인하여 상태 복원 로직 실행 - if (!isCanceled) { - // 현재 상태에 해당 트랜잭션이 이미 있는지 확인 - const currentTransactions = [...transactions]; - const exists = currentTransactions.some(t => t.id === id); - - if (!exists) { - console.log('서버 삭제 실패, 상태 복원 시도...'); - // 현재 상태에 없을 경우에만 상태 복원 시도 - setTransactions(prevState => { - // 동일 트랜잭션이 없을 경우에만 추가 - const hasDuplicate = prevState.some(t => t.id === id); - if (hasDuplicate) return prevState; - - // 삭제되었던 트랜잭션 다시 추가 - const newState = [...prevState, transactionToDelete]; - - // 날짜 기준 정렬 - 안전한 경로 - return newState.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; // 오류 발생 시 순서 유지 - } - }); - }); - } - } - }) - .finally(() => { - if (!isCanceled) { - // 작업 완료 후 보류 중인 삭제 목록에서 제거 - pendingDeletionRef.current?.delete(id); - } - }); - } else { - // 사용자 정보 없을 경우 목록에서 제거 - pendingDeletionRef.current?.delete(id); - } - } catch (storageError) { - console.error('스토리지 작업 중 오류:', storageError); - pendingDeletionRef.current?.delete(id); - } - }, 0); // 흥미로운 사실: setTimeout(fn, 0)은 requestAnimationFrame 이후에 실행되어 UI 업데이트 완료 후 처리됨 - }); - - // 상태 업데이트가 이미 수행되었으므로 즉시 성공 반환 - console.log('트랜잭션 삭제 UI 업데이트 완료'); - resolve(true); - - // 취소 기능을 가진 Promise 객체 생성 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (promiseObj as any).cancel = () => { - isCanceled = true; - - if (timeoutId !== null) { - clearTimeout(timeoutId); - } - - pendingDeletionRef.current?.delete(id); - console.log('트랜잭션 삭제 작업 취소 완료'); - }; - } catch (error) { - console.error('트랜잭션 삭제 초기화 중 오류:', error); - - // 오류 발생 시 토스트 표시 - toast({ - title: "시스템 오류", - description: "지출 삭제 중 오류가 발생했습니다.", - duration: 2000, - variant: "destructive" - }); - - // 캣치된 모든 오류에서 보류 삭제 표시 제거 - pendingDeletionRef.current?.delete(id); - reject(error); - } - }); - - return promiseObj; - }, [transactions, setTransactions, user]); - - return { - updateTransaction, - deleteTransaction - }; -}; +// 기존 훅을 그대로 내보내기 +export { useTransactionsOperations };