diff --git a/src/components/RecentTransactionsSection.tsx b/src/components/RecentTransactionsSection.tsx index 271ed70..dd104ff 100644 --- a/src/components/RecentTransactionsSection.tsx +++ b/src/components/RecentTransactionsSection.tsx @@ -1,160 +1,42 @@ -import React, { useState, useCallback, useRef } from 'react'; +import React, { useState } from 'react'; import { Transaction } from '@/contexts/budget/types'; import TransactionEditDialog from './TransactionEditDialog'; import { ChevronRight } from 'lucide-react'; import { useBudget } from '@/contexts/budget/BudgetContext'; import { Link } from 'react-router-dom'; -import { categoryIcons } from '@/constants/categoryIcons'; -import TransactionIcon from './transaction/TransactionIcon'; -import { toast } from '@/hooks/useToast.wrapper'; +import { useRecentTransactions } from '@/hooks/transactions/useRecentTransactions'; +import RecentTransactionItem from './recent-transactions/RecentTransactionItem'; + interface RecentTransactionsSectionProps { transactions: Transaction[]; onUpdateTransaction?: (transaction: Transaction) => void; } + const RecentTransactionsSection: React.FC = ({ transactions, onUpdateTransaction }) => { const [selectedTransaction, setSelectedTransaction] = useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const { - updateTransaction, - deleteTransaction - } = useBudget(); + const { updateTransaction, deleteTransaction } = useBudget(); + + // 트랜잭션 삭제 관련 로직은 커스텀 훅으로 분리 + const { handleDeleteTransaction, isDeleting } = useRecentTransactions(deleteTransaction); - // 삭제 중인 ID 추적 - const deletingIdRef = useRef(null); - - // 타임아웃 추적 - const timeoutRef = useRef(null); - - // 삭제 요청 타임스탬프 추적 (급발진 방지) - const lastDeleteTimeRef = useRef>({}); const handleTransactionClick = (transaction: Transaction) => { setSelectedTransaction(transaction); setIsDialogOpen(true); }; - const handleUpdateTransaction = useCallback((updatedTransaction: Transaction) => { + + const handleUpdateTransaction = (updatedTransaction: Transaction) => { if (onUpdateTransaction) { onUpdateTransaction(updatedTransaction); } // 직접 컨텍스트를 통해 업데이트 updateTransaction(updatedTransaction); - }, [onUpdateTransaction, updateTransaction]); - - // 완전히 새로운 삭제 처리 함수 - const handleDeleteTransaction = useCallback(async (id: string): Promise => { - return new Promise(resolve => { - try { - // 삭제 진행 중인지 확인 - 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); - - // 항상 상태 정리 - setIsDeleting(false); - deletingIdRef.current = null; - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - toast({ - title: "오류 발생", - description: "처리 중 문제가 발생했습니다.", - variant: "destructive", - duration: 1500 - }); - resolve(false); - } - }); - }, [deleteTransaction, isDeleting]); - - // 컴포넌트 언마운트 시 타임아웃 정리 - React.useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, []); - const formatCurrency = (amount: number) => { - return amount.toLocaleString('ko-KR') + '원'; }; + return (
@@ -167,27 +49,11 @@ const RecentTransactionsSection: React.FC = ({
{transactions.length > 0 ? ( transactions.map(transaction => ( -
handleTransactionClick(transaction)} - className="flex justify-between py-2 cursor-pointer px-[5px] bg-transparent" - > -
- -
-

- {transaction.title} -

-

{transaction.date}

-
-
-
-

- -{formatCurrency(transaction.amount)} -

-

{transaction.category}

-
-
+ /> )) ) : (
@@ -208,4 +74,5 @@ const RecentTransactionsSection: React.FC = ({
); }; + export default RecentTransactionsSection; diff --git a/src/components/recent-transactions/RecentTransactionItem.tsx b/src/components/recent-transactions/RecentTransactionItem.tsx new file mode 100644 index 0000000..3691f93 --- /dev/null +++ b/src/components/recent-transactions/RecentTransactionItem.tsx @@ -0,0 +1,43 @@ + +import React from 'react'; +import { Transaction } from '@/contexts/budget/types'; +import TransactionIcon from '../transaction/TransactionIcon'; + +interface RecentTransactionItemProps { + transaction: Transaction; + onClick: () => void; +} + +const RecentTransactionItem: React.FC = ({ + transaction, + onClick +}) => { + const formatCurrency = (amount: number) => { + return amount.toLocaleString('ko-KR') + '원'; + }; + + return ( +
+
+ +
+

+ {transaction.title} +

+

{transaction.date}

+
+
+
+

+ -{formatCurrency(transaction.amount)} +

+

{transaction.category}

+
+
+ ); +}; + +export default RecentTransactionItem; diff --git a/src/hooks/transactions/useRecentTransactions.ts b/src/hooks/transactions/useRecentTransactions.ts new file mode 100644 index 0000000..ef9dc1f --- /dev/null +++ b/src/hooks/transactions/useRecentTransactions.ts @@ -0,0 +1,117 @@ + +import { useCallback, useRef, useState } from 'react'; +import { toast } from '@/hooks/useToast.wrapper'; + +/** + * 최근 거래내역 관련 로직을 처리하는 커스텀 훅 + * 삭제 로직과 상태 관리 등을 담당 + */ +export const useRecentTransactions = ( + deleteTransaction: (id: string) => void +) => { + const [isDeleting, setIsDeleting] = useState(false); + + // 삭제 중인 ID 추적 + const deletingIdRef = useRef(null); + + // 타임아웃 추적 + const timeoutRef = useRef(null); + + // 삭제 요청 타임스탬프 추적 (급발진 방지) + const lastDeleteTimeRef = useRef>({}); + + // 완전히 새로운 삭제 처리 함수 + const handleDeleteTransaction = useCallback(async (id: string): Promise => { + return new Promise(resolve => { + try { + // 삭제 진행 중인지 확인 + 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; + + // 안전장치: 타임아웃 설정 (최대 900ms) + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + console.warn('삭제 타임아웃 - 상태 초기화'); + setIsDeleting(false); + deletingIdRef.current = null; + resolve(true); // UI 응답성 위해 성공 간주 + }, 900); + + // 비동기 작업을 동기적으로 처리하여 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('삭제 처리 전체 오류:', error); + + // 항상 상태 정리 + setIsDeleting(false); + deletingIdRef.current = null; + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + toast({ + title: "오류 발생", + description: "처리 중 문제가 발생했습니다.", + variant: "destructive", + duration: 1500 + }); + resolve(false); + } + }); + }, [deleteTransaction, isDeleting]); + + // 컴포넌트 언마운트 시 타임아웃 정리 (리액트 컴포넌트에서 처리해야함) + const cleanupTimeouts = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }, []); + + return { + handleDeleteTransaction, + isDeleting, + cleanupTimeouts + }; +};