diff --git a/src/contexts/budget/hooks/useExtendedBudgetUpdate.ts b/src/contexts/budget/hooks/useExtendedBudgetUpdate.ts index 5b892b6..e59ec4c 100644 --- a/src/contexts/budget/hooks/useExtendedBudgetUpdate.ts +++ b/src/contexts/budget/hooks/useExtendedBudgetUpdate.ts @@ -1,191 +1,29 @@ -import { useCallback } from 'react'; -import { BudgetData, BudgetPeriod } from '../types'; -import { calculateUpdatedBudgetData } from '../budgetUtils'; -import { toast } from '@/components/ui/use-toast'; +import { BudgetPeriod } from '../types'; -// 확장된 예산 업데이트 로직을 제공하는 훅 +/** + * 예산 목표 업데이트 확장 함수를 제공하는 훅 + * 예산 목표 업데이트와 카테고리 예산 업데이트를 통합합니다. + */ export const useExtendedBudgetUpdate = ( - budgetData: BudgetData, - categoryBudgets: Record, - handleBudgetGoalUpdate: (type: BudgetPeriod, amount: number) => void, - updateCategoryBudgets: (budgets: Record) => void + handleBudgetUpdate: (type: BudgetPeriod, amount: number) => void, + setCategoryBudgets: (budgets: Record) => void ) => { - // 확장된 예산 업데이트 로직 - const extendedBudgetGoalUpdate = useCallback(( - type: BudgetPeriod, - amount: number, + // 예산 목표 업데이트 확장 함수 + const extendedBudgetUpdate = ( + type: BudgetPeriod, + amount: number, newCategoryBudgets?: Record ) => { - console.log(`확장된 예산 목표 업데이트 호출: ${type}, 금액: ${amount}, 카테고리 예산:`, newCategoryBudgets); + // 기본 예산 목표 업데이트 + handleBudgetUpdate(type, amount); - // 카테고리 예산이 제공된 경우 업데이트 + // 카테고리 예산 업데이트 (제공된 경우) if (newCategoryBudgets) { - try { - // 카테고리명 표준화 처리 - let updatedCategoryBudgets = { ...newCategoryBudgets }; - - // 교통비 값이 있으면 교통으로 통합 - if (updatedCategoryBudgets['교통비'] && !updatedCategoryBudgets['교통']) { - updatedCategoryBudgets['교통'] = updatedCategoryBudgets['교통비']; - delete updatedCategoryBudgets['교통비']; - } - - // 식비 값이 있으면 음식으로 통합 - if (updatedCategoryBudgets['식비'] && !updatedCategoryBudgets['음식']) { - updatedCategoryBudgets['음식'] = updatedCategoryBudgets['식비']; - delete updatedCategoryBudgets['식비']; - } - - // 생활비 값이 있으면 쇼핑으로 통합 - if (updatedCategoryBudgets['생활비'] && !updatedCategoryBudgets['쇼핑']) { - updatedCategoryBudgets['쇼핑'] = updatedCategoryBudgets['생활비']; - delete updatedCategoryBudgets['생활비']; - } - - // 카테고리 예산 저장 - updateCategoryBudgets(updatedCategoryBudgets); - - // 총액 계산 (0 확인) - const totalAmount = Object.values(updatedCategoryBudgets).reduce((sum, val) => sum + val, 0); - console.log('카테고리 예산 총합:', totalAmount, updatedCategoryBudgets); - - if (totalAmount <= 0) { - toast({ - title: "예산 설정 오류", - description: "유효한 예산 금액을 입력해주세요.", - variant: "destructive" - }); - return; - } - - // 항상 월간 예산으로 처리하여 일/주간 자동계산 보장 - handleBudgetGoalUpdate('monthly', totalAmount); - - // 명시적으로 BudgetData 업데이트 이벤트 발생 - window.dispatchEvent(new Event('budgetDataUpdated')); - - // 성공 토스트 표시 - toast({ - title: "카테고리 예산 설정 완료", - description: `월간 총 예산이 ${totalAmount.toLocaleString()}원으로 설정되었습니다.` - }); - - // 다중 이벤트 발생으로 UI 업데이트 보장 - // 0.5초 후 1차 업데이트 - setTimeout(() => { - console.log("예산 UI 업데이트 이벤트 발생 (1차)"); - window.dispatchEvent(new Event('budgetDataUpdated')); - }, 500); - - // 1.5초 후 2차 업데이트 - setTimeout(() => { - console.log("예산 UI 업데이트 이벤트 발생 (2차)"); - window.dispatchEvent(new Event('budgetDataUpdated')); - // 스토리지 이벤트도 발생시켜 데이터 로드 보장 - const savedData = localStorage.getItem('budgetData'); - if (savedData) { - window.dispatchEvent(new StorageEvent('storage', { - key: 'budgetData', - newValue: savedData - })); - } - }, 1500); - - // 3초 후 3차 업데이트 (최종 보장) - setTimeout(() => { - console.log("예산 UI 업데이트 이벤트 발생 (3차/최종)"); - window.dispatchEvent(new Event('budgetDataUpdated')); - const savedData = localStorage.getItem('budgetData'); - if (savedData) { - window.dispatchEvent(new StorageEvent('storage', { - key: 'budgetData', - newValue: savedData - })); - } - }, 3000); - } catch (error) { - console.error('카테고리 예산 업데이트 오류:', error); - toast({ - title: "예산 설정 오류", - description: "카테고리 예산을 업데이트하는 중 오류가 발생했습니다.", - variant: "destructive" - }); - } - } else { - // 카테고리 예산이 없는 경우, 선택된 기간 유형에 맞게 예산 설정 - if (amount <= 0) { - toast({ - title: "예산 설정 오류", - description: "유효한 예산 금액을 입력해주세요.", - variant: "destructive" - }); - return; - } - - // 어떤 타입이 들어오더라도 항상 월간으로 처리하고 일/주간은 자동계산 - if (type !== 'monthly') { - console.log(`${type} 입력을 월간 예산으로 변환합니다.`); - // 일간 입력인 경우 월간으로 변환 (30배) - if (type === 'daily') { - amount = amount * 30; - } - // 주간 입력인 경우 월간으로 변환 (4.3배) - else if (type === 'weekly') { - amount = Math.round(amount * 4.3); - } - - // 변환된 금액으로 월간 예산 설정 - handleBudgetGoalUpdate('monthly', amount); - } else { - // 원래 월간이면 그대로 설정 - handleBudgetGoalUpdate('monthly', amount); - } - - // 명시적으로 BudgetData 업데이트 이벤트 발생 - window.dispatchEvent(new Event('budgetDataUpdated')); - - // 성공 토스트 표시 - const periodText = type === 'daily' ? '일일' : type === 'weekly' ? '주간' : '월간'; - toast({ - title: "예산 설정 완료", - description: `${periodText} 예산이 ${amount.toLocaleString()}원으로 설정되었습니다.` - }); - - // 다중 이벤트 발생 - // 0.5초 후 1차 업데이트 - setTimeout(() => { - console.log("예산 UI 업데이트 이벤트 발생 (1차)"); - window.dispatchEvent(new Event('budgetDataUpdated')); - }, 500); - - // 1.5초 후 2차 업데이트 - setTimeout(() => { - console.log("예산 UI 업데이트 이벤트 발생 (2차)"); - window.dispatchEvent(new Event('budgetDataUpdated')); - const savedData = localStorage.getItem('budgetData'); - if (savedData) { - window.dispatchEvent(new StorageEvent('storage', { - key: 'budgetData', - newValue: savedData - })); - } - }, 1500); - - // 3초 후 3차 업데이트 (최종 보장) - setTimeout(() => { - console.log("예산 UI 업데이트 이벤트 발생 (3차/최종)"); - window.dispatchEvent(new Event('budgetDataUpdated')); - const savedData = localStorage.getItem('budgetData'); - if (savedData) { - window.dispatchEvent(new StorageEvent('storage', { - key: 'budgetData', - newValue: savedData - })); - } - }, 3000); + console.log('카테고리 예산 업데이트:', newCategoryBudgets); + setCategoryBudgets(newCategoryBudgets); } - }, [categoryBudgets, handleBudgetGoalUpdate, updateCategoryBudgets]); + }; - return { extendedBudgetGoalUpdate }; + return extendedBudgetUpdate; }; diff --git a/src/contexts/budget/hooks/useTransactionState.ts b/src/contexts/budget/hooks/useTransactionState.ts index 6da37df..9f68e7b 100644 --- a/src/contexts/budget/hooks/useTransactionState.ts +++ b/src/contexts/budget/hooks/useTransactionState.ts @@ -1,211 +1,74 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect } from 'react'; import { Transaction } from '../types'; -import { - loadTransactionsFromStorage, - saveTransactionsToStorage, - clearAllTransactions -} from '../storage'; -import { toast } from '@/hooks/useToast.wrapper'; // 래퍼 사용 -import { addToDeletedTransactions } from '@/utils/sync/transaction/deletedTransactionsTracker'; -import { - updateTitleUsage, - analyzeTransactionTitles -} from '@/utils/userTitlePreferences'; +import { loadTransactionsFromStorage, saveTransactionsToStorage } from '../storage/transactionStorage'; +import { v4 as uuidv4 } from 'uuid'; -// 트랜잭션 상태 관리 훅 +/** + * 트랜잭션 상태를 관리하는 훅 + * 트랜잭션 목록을 로드하고 추가, 수정, 삭제 기능을 제공합니다. + */ export const useTransactionState = () => { + // 로컬 스토리지에서 초기 트랜잭션 데이터 로드 const [transactions, setTransactions] = useState([]); - const [lastDeletedId, setLastDeletedId] = useState(null); - const [isDeleting, setIsDeleting] = useState(false); - // 초기 트랜잭션 로드 및 이벤트 리스너 설정 + // 초기 로드 useEffect(() => { - // 트랜잭션 로드 함수 - 비동기 처리로 변경 - const loadTransactions = async () => { - try { - console.log('[트랜잭션 상태] 트랜잭션 로드 시도 중...'); - - // 비동기 작업을 마이크로태스크로 지연 - await new Promise(resolve => queueMicrotask(() => resolve())); - - const storedTransactions = loadTransactionsFromStorage(); - console.log('[트랜잭션 상태] 트랜잭션 로드됨:', storedTransactions.length, '개'); - - // 상태 업데이트를 마이크로태스크로 지연 - queueMicrotask(() => { - setTransactions(storedTransactions); - - // 사용자 제목 선호도 분석 실행 (최근 50개 트랜잭션) - analyzeTransactionTitles(storedTransactions, 50); - }); - } catch (error) { - console.error('[트랜잭션 상태] 트랜잭션 로드 오류:', error); - } - }; - - // 초기 로드 - 지연 시간 추가 - setTimeout(() => { - loadTransactions(); - }, 100); // 지연된 초기 로드 - - // 이벤트 리스너 추가 - 최소한으로 유지 - const handleTransactionUpdate = (e?: StorageEvent) => { - if (!e || e.key === 'transactions' || e.key === null) { - loadTransactions(); - } - }; - - // 필수 이벤트만 등록 - const transactionUpdateHandler = () => handleTransactionUpdate(); - window.addEventListener('transactionUpdated', transactionUpdateHandler); - window.addEventListener('storage', handleTransactionUpdate); - - return () => { - window.removeEventListener('transactionUpdated', transactionUpdateHandler); - window.removeEventListener('storage', handleTransactionUpdate); - }; + try { + const storedTransactions = loadTransactionsFromStorage(); + console.log('로컬 스토리지에서 트랜잭션 로드:', storedTransactions.length); + setTransactions(storedTransactions); + } catch (error) { + console.error('트랜잭션 로드 중 오류 발생:', error); + // 오류 발생 시 빈 배열로 초기화 + setTransactions([]); + } }, []); - // 트랜잭션 추가 함수 - const addTransaction = useCallback((newTransaction: Transaction) => { - console.log('[트랜잭션 상태] 새 트랜잭션 추가:', newTransaction); - - // 현재 시간을 타임스탬프로 추가 - const transactionWithTimestamp = { - ...newTransaction, + // 트랜잭션 변경 시 로컬 스토리지에 저장 + useEffect(() => { + if (transactions.length > 0) { + console.log('트랜잭션 저장 중:', transactions.length); + saveTransactionsToStorage(transactions); + } + }, [transactions]); + + // 트랜잭션 추가 + const addTransaction = (transaction: Transaction) => { + const newTransaction = { + ...transaction, + id: transaction.id || uuidv4(), localTimestamp: new Date().toISOString() }; - - // 사용자 제목 선호도 업데이트 - updateTitleUsage(transactionWithTimestamp); - - setTransactions(prev => { - const updated = [transactionWithTimestamp, ...prev]; - saveTransactionsToStorage(updated); - return updated; - }); - }, []); + setTransactions(prevTransactions => [...prevTransactions, newTransaction]); + console.log('트랜잭션 추가됨:', newTransaction); + }; - // 트랜잭션 업데이트 함수 - const updateTransaction = useCallback((updatedTransaction: Transaction) => { - console.log('[트랜잭션 상태] 트랜잭션 업데이트:', updatedTransaction); - - // 현재 시간을 타임스탬프로 업데이트 - const transactionWithTimestamp = { - ...updatedTransaction, - localTimestamp: new Date().toISOString() - }; - - // 사용자 제목 선호도 업데이트 - updateTitleUsage(transactionWithTimestamp); - - setTransactions(prev => { - const updated = prev.map(transaction => - transaction.id === updatedTransaction.id ? transactionWithTimestamp : transaction - ); - saveTransactionsToStorage(updated); - return updated; - }); - }, []); + // 트랜잭션 업데이트 + const updateTransaction = (updatedTransaction: Transaction) => { + setTransactions(prevTransactions => + prevTransactions.map(transaction => + transaction.id === updatedTransaction.id + ? { ...updatedTransaction, localTimestamp: new Date().toISOString() } + : transaction + ) + ); + console.log('트랜잭션 업데이트됨:', updatedTransaction.id); + }; - // 트랜잭션 삭제 함수 - 성능 최적화 및 안정성 개선 - const deleteTransaction = useCallback((transactionId: string) => { - // 이미 삭제 중이면 중복 삭제 방지 - if (isDeleting) { - console.log('[트랜잭션 상태] 이미 삭제 작업이 진행 중입니다.'); - return; - } - - // 중복 삭제 방지 - if (lastDeletedId === transactionId) { - console.log('[트랜잭션 상태] 중복 삭제 요청 무시:', transactionId); - return; - } - - // 삭제 상태 설정 - 마이크로태스크로 지연 - queueMicrotask(() => { - setIsDeleting(true); - setLastDeletedId(transactionId); - - // 삭제 작업을 마이크로태스크로 진행하여 UI 차단 방지 - queueMicrotask(() => { - try { - console.log(`[트랜잭션 상태] 삭제 시작: ${transactionId}`); - - setTransactions(prev => { - // 삭제할 트랜잭션 찾기 - const transactionToDelete = prev.find(t => t.id === transactionId); - if (!transactionToDelete) { - console.log('[트랜잭션 상태] 삭제할 트랜잭션을 찾을 수 없음:', transactionId); - return prev; // 변경 없음 - } - - console.log(`[트랜잭션 상태] 삭제할 트랜잭션: "${transactionToDelete.title}", 금액: ${transactionToDelete.amount}원`); - - // 삭제할 항목 필터링 - 성능 최적화 - const updated = prev.filter(transaction => transaction.id !== transactionId); - - // 항목이 실제로 삭제되었는지 확인 - if (updated.length === prev.length) { - console.log('[트랜잭션 상태] 삭제할 트랜잭션을 찾을 수 없음:', transactionId); - return prev; // 변경 없음 - } - - // 클라우드 동기화를 위해 삭제된 트랜잭션 ID 추적 - addToDeletedTransactions(transactionId); - console.log(`[트랜잭션 상태] 삭제 트랜잭션 추적 목록에 추가: ${transactionId}`); - - // 저장소 업데이트를 마이크로태스크로 진행 - queueMicrotask(() => { - saveTransactionsToStorage(updated); - console.log(`[트랜잭션 상태] 로컬 저장소 업데이트 완료`); - - // 토스트 메시지 표시 - toast({ - title: "지출이 삭제되었습니다", - description: "지출 항목이 성공적으로 삭제되었습니다.", - }); - }); - - return updated; - }); - } catch (error) { - console.error('[트랜잭션 상태] 트랜잭션 삭제 중 오류 발생:', error); - toast({ - title: "삭제 실패", - description: "지출 항목 삭제 중 오류가 발생했습니다.", - variant: "destructive" - }); - } finally { - // 삭제 상태 초기화 (500ms 후) - 시간 단축 - setTimeout(() => { - setIsDeleting(false); - setLastDeletedId(null); - console.log('[트랜잭션 상태] 삭제 상태 초기화 완료'); - }, 500); - } - }); - }); - }, [lastDeletedId, isDeleting]); - - // 트랜잭션 초기화 함수 - const resetTransactions = useCallback(() => { - console.log('[트랜잭션 상태] 모든 트랜잭션 초기화'); - clearAllTransactions(); - setTransactions([]); - }, []); - - // 트랜잭션 개수가 변경될 때 로그 기록 - useEffect(() => { - console.log('[트랜잭션 상태] 현재 트랜잭션 개수:', transactions.length); - }, [transactions.length]); + // 트랜잭션 삭제 + const deleteTransaction = (id: string) => { + setTransactions(prevTransactions => + prevTransactions.filter(transaction => transaction.id !== id) + ); + console.log('트랜잭션 삭제됨:', id); + }; return { transactions, + setTransactions, addTransaction, updateTransaction, - deleteTransaction, - resetTransactions + deleteTransaction }; }; diff --git a/src/contexts/budget/useBudgetState.ts b/src/contexts/budget/useBudgetState.ts index d55eb78..298c264 100644 --- a/src/contexts/budget/useBudgetState.ts +++ b/src/contexts/budget/useBudgetState.ts @@ -1,92 +1,45 @@ -import { useState, useEffect } from 'react'; +import { useEffect } from 'react'; import { Transaction, BudgetData, BudgetPeriod, BudgetContextType } from './types'; -import { loadTransactionsFromStorage, saveTransactionsToStorage } from './storage/transactionStorage'; -import { v4 as uuidv4 } from 'uuid'; +import { useBudgetDataState } from './hooks/useBudgetDataState'; +import { useTransactionState } from './hooks/useTransactionState'; +import { useCategoryBudgetState } from './hooks/useCategoryBudgetState'; +import { useCategorySpending } from './hooks/useCategorySpending'; +import { useExtendedBudgetUpdate } from './hooks/useExtendedBudgetUpdate'; +/** + * 애플리케이션 예산 데이터 상태 관리를 위한 통합 훅 + * 트랜잭션, 예산 데이터, 카테고리 예산 등을 관리합니다. + */ export const useBudgetState = () => { - // 로컬 스토리지에서 초기 트랜잭션 데이터 로드 - const [transactions, setTransactions] = useState([]); - const [categoryBudgets, setCategoryBudgets] = useState>({}); - const [budgetData, setBudgetData] = useState({ - daily: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, - weekly: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, - monthly: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, - }); - const [selectedTab, setSelectedTab] = useState('monthly'); + // 트랜잭션 상태 관리 + const { + transactions, + addTransaction, + updateTransaction, + deleteTransaction + } = useTransactionState(); - useEffect(() => { - const storedTransactions = loadTransactionsFromStorage(); - setTransactions(storedTransactions); - }, []); + // 카테고리 예산 상태 관리 + const { categoryBudgets, setCategoryBudgets } = useCategoryBudgetState(); - useEffect(() => { - // 트랜잭션 변경 시 로컬 스토리지에 저장 - saveTransactionsToStorage(transactions); - }, [transactions]); - - // 트랜잭션 추가 - const addTransaction = (transaction: Transaction) => { - const newTransaction = { ...transaction, id: uuidv4() }; - setTransactions(prevTransactions => [...prevTransactions, newTransaction]); - }; - - // 트랜잭션 업데이트 - const updateTransaction = (updatedTransaction: Transaction) => { - setTransactions(prevTransactions => - prevTransactions.map(transaction => - transaction.id === updatedTransaction.id ? updatedTransaction : transaction - ) - ); - }; - - // 트랜잭션 삭제 - const deleteTransaction = (id: string) => { - setTransactions(prevTransactions => - prevTransactions.filter(transaction => transaction.id !== id) - ); - }; - - // 예산 목표 업데이트 - const handleBudgetGoalUpdate = (type: BudgetPeriod, amount: number, newCategoryBudgets?: Record) => { - setBudgetData(prev => ({ - ...prev, - [type]: { - ...prev[type], - targetAmount: amount, - }, - })); - if (newCategoryBudgets) { - setCategoryBudgets(newCategoryBudgets); - } - }; + // 예산 데이터 상태 관리 + const { + budgetData, + selectedTab, + setSelectedTab, + handleBudgetGoalUpdate, + resetBudgetData + } = useBudgetDataState(transactions); + + // 확장된 예산 업데이트 로직 - 기존 로직과 호환성 유지 + const extendedBudgetUpdate = useExtendedBudgetUpdate( + handleBudgetGoalUpdate, + setCategoryBudgets + ); // 카테고리별 지출 계산 - const getCategorySpending = () => { - const categorySpending: { [key: string]: { total: number; current: number } } = {}; - - // 초기화 - ['음식', '쇼핑', '교통', '기타'].forEach(category => { - categorySpending[category] = { total: categoryBudgets[category] || 0, current: 0 }; - }); - - // 지출 합산 - transactions.filter(tx => tx.type === 'expense').forEach(tx => { - if (categorySpending[tx.category]) { - categorySpending[tx.category].current += tx.amount; - } else { - // 새 카테고리인 경우 초기화 - categorySpending[tx.category] = { total: 0, current: tx.amount }; - } - }); - - // 배열로 변환 - return Object.entries(categorySpending).map(([title, { total, current }]) => ({ - title, - total, - current, - })); - }; + const { getCategorySpending } = useCategorySpending(transactions, categoryBudgets); // 결제 방법 통계 계산 함수 const getPaymentMethodStats = () => { @@ -121,16 +74,15 @@ export const useBudgetState = () => { return result; }; - - // 예산 데이터 재설정 함수 - const resetBudgetData = () => { - setBudgetData({ - daily: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, - weekly: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, - monthly: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, - }); - setCategoryBudgets({}); - }; + + // 디버깅 로그 추가 + useEffect(() => { + console.log('예산 상태 업데이트:', + '트랜잭션 수:', transactions.length, + '카테고리 예산:', categoryBudgets, + '예산 데이터:', budgetData + ); + }, [transactions, categoryBudgets, budgetData]); return { transactions, @@ -141,7 +93,7 @@ export const useBudgetState = () => { addTransaction, updateTransaction, deleteTransaction, - handleBudgetGoalUpdate, + handleBudgetGoalUpdate: extendedBudgetUpdate, // 확장된 버전 사용 getCategorySpending, getPaymentMethodStats, resetBudgetData,