diff --git a/src/contexts/budget/hooks/useBudgetDataState.ts b/src/contexts/budget/hooks/useBudgetDataState.ts index 3d497c7..3d4c105 100644 --- a/src/contexts/budget/hooks/useBudgetDataState.ts +++ b/src/contexts/budget/hooks/useBudgetDataState.ts @@ -25,7 +25,12 @@ export const useBudgetDataState = (transactions: any[]) => { console.log('예산 데이터 로드 시도 중...'); const loadedData = loadBudgetDataFromStorage(); console.log('예산 데이터 로드됨:', loadedData); - setBudgetData(loadedData); + + // 새로 로드한 데이터와 현재 데이터가 다를 때만 업데이트 + if (JSON.stringify(loadedData) !== JSON.stringify(budgetData)) { + console.log('예산 데이터 변경 감지됨, 상태 업데이트'); + setBudgetData(loadedData); + } // 최근 데이터 로드 시간 기록 localStorage.setItem('lastBudgetDataLoadTime', new Date().toISOString()); @@ -49,6 +54,7 @@ export const useBudgetDataState = (transactions: any[]) => { } }; + // 이벤트 발생 시 데이터 새로고침 window.addEventListener('budgetDataUpdated', () => handleBudgetUpdate()); window.addEventListener('storage', handleBudgetUpdate); window.addEventListener('visibilitychange', () => { @@ -62,7 +68,7 @@ export const useBudgetDataState = (transactions: any[]) => { loadBudget(); }); - // 주기적 데이터 검사 (1초마다) + // 주기적 데이터 검사 (1초마다) - 다른 컴포넌트에서 변경된 사항 감지 const intervalId = setInterval(() => { const lastSaveTime = localStorage.getItem('lastBudgetSaveTime'); const lastLoadTime = localStorage.getItem('lastBudgetDataLoadTime'); @@ -80,27 +86,30 @@ export const useBudgetDataState = (transactions: any[]) => { window.removeEventListener('focus', () => loadBudget()); clearInterval(intervalId); }; - }, [isInitialized]); + }, [isInitialized, budgetData]); // 트랜잭션 변경 시 지출 금액 업데이트 useEffect(() => { - if (transactions.length > 0) { + if (transactions.length > 0 && isInitialized) { console.log('트랜잭션 변경으로 인한 예산 데이터 업데이트. 트랜잭션 수:', transactions.length); try { // 지출 금액 업데이트 const updatedBudgetData = calculateSpentAmounts(transactions, budgetData); - // 상태 및 스토리지 모두 업데이트 - setBudgetData(updatedBudgetData); - saveBudgetDataToStorage(updatedBudgetData); - - // 저장 시간 업데이트 - localStorage.setItem('lastBudgetSaveTime', new Date().toISOString()); + // 변경이 있을 때만 저장 + if (JSON.stringify(updatedBudgetData) !== JSON.stringify(budgetData)) { + // 상태 및 스토리지 모두 업데이트 + setBudgetData(updatedBudgetData); + saveBudgetDataToStorage(updatedBudgetData); + + // 저장 시간 업데이트 + localStorage.setItem('lastBudgetSaveTime', new Date().toISOString()); + } } catch (error) { console.error('예산 데이터 업데이트 중 오류:', error); } } - }, [transactions, budgetData]); + }, [transactions, budgetData, isInitialized]); // 예산 목표 업데이트 함수 const handleBudgetGoalUpdate = useCallback(( @@ -114,6 +123,8 @@ export const useBudgetDataState = (transactions: any[]) => { if (!newCategoryBudgets) { const updatedBudgetData = calculateUpdatedBudgetData(budgetData, type, amount); console.log('새 예산 데이터:', updatedBudgetData); + + // 상태 및 스토리지 둘 다 업데이트 setBudgetData(updatedBudgetData); saveBudgetDataToStorage(updatedBudgetData); diff --git a/src/contexts/budget/hooks/useExtendedBudgetUpdate.ts b/src/contexts/budget/hooks/useExtendedBudgetUpdate.ts index df3f5e0..065a83e 100644 --- a/src/contexts/budget/hooks/useExtendedBudgetUpdate.ts +++ b/src/contexts/budget/hooks/useExtendedBudgetUpdate.ts @@ -1,72 +1,47 @@ import { useCallback } from 'react'; -import { BudgetPeriod, BudgetData } from '../types'; -import { toast } from '@/components/ui/use-toast'; +import { BudgetData, BudgetPeriod } from '../types'; -// 확장된 예산 목표 업데이트 훅 +/** + * 확장된 예산 업데이트 기능을 제공하는 훅 + */ export const useExtendedBudgetUpdate = ( budgetData: BudgetData, categoryBudgets: Record, - handleBudgetGoalUpdate: (type: BudgetPeriod, amount: number) => void, + handleBudgetGoalUpdate: (type: BudgetPeriod, amount: number, newCategoryBudgets?: Record) => void, updateCategoryBudgets: (newCategoryBudgets: Record) => void ) => { - // 확장된 예산 목표 업데이트 함수 + /** + * 확장된 예산 업데이트 함수 + * 월간 예산 및 카테고리 예산을 함께 업데이트 + */ const extendedBudgetGoalUpdate = useCallback(( type: BudgetPeriod, amount: number, newCategoryBudgets?: Record ) => { - try { - console.log(`확장된 예산 목표 업데이트 호출: ${type}, 금액: ${amount}`); - - // 카테고리 예산이 직접 업데이트된 경우 - if (newCategoryBudgets) { - console.log('카테고리 예산 직접 업데이트:', newCategoryBudgets); - updateCategoryBudgets(newCategoryBudgets); - return; - } - - // 월간 예산을 업데이트하고 일일, 주간도 자동 계산 - if (type === 'monthly') { - console.log('월간 예산 업데이트:', amount); - if (amount <= 0) return; // 예산이 0 이하면 업데이트하지 않음 - - const ratio = amount / (budgetData.monthly.targetAmount || 1); // 0으로 나누기 방지 - const updatedCategoryBudgets: Record = {}; - - // 비율에 따라 카테고리 예산 업데이트 - Object.keys(categoryBudgets).forEach(category => { - updatedCategoryBudgets[category] = Math.round(categoryBudgets[category] * ratio); - }); - - // 모든 카테고리가 0인 경우 (초기 상태) - const allZero = Object.values(categoryBudgets).every(value => value === 0); - if (allZero) { - // 카테고리 간 균등 분배 - const categories = Object.keys(categoryBudgets); - const perCategoryAmount = Math.round(amount / categories.length); - - categories.forEach(category => { - updatedCategoryBudgets[category] = perCategoryAmount; - }); - } - - console.log('업데이트된 카테고리 예산:', updatedCategoryBudgets); - updateCategoryBudgets(updatedCategoryBudgets); - } else { - // 일일이나 주간 예산이 직접 업데이트되는 경우 - console.log(`${type} 예산 직접 업데이트:`, amount); - handleBudgetGoalUpdate(type, amount); - } - } catch (error) { - console.error('예산 목표 업데이트 중 오류:', error); - toast({ - title: "예산 업데이트 실패", - description: "예산 목표를 업데이트하는 중 오류가 발생했습니다.", - variant: "destructive" - }); + console.log(`확장된 예산 업데이트: 타입=${type}, 금액=${amount}, 카테고리 예산 포함=${!!newCategoryBudgets}`); + + // 기본 예산 업데이트 + handleBudgetGoalUpdate(type, amount); + + // 카테고리 예산도 함께 업데이트 (있는 경우) + if (newCategoryBudgets) { + console.log('카테고리 예산 업데이트:', newCategoryBudgets); + updateCategoryBudgets(newCategoryBudgets); } - }, [budgetData, categoryBudgets, handleBudgetGoalUpdate, updateCategoryBudgets]); - + + // 데이터 저장 시간 기록 - 동일 세션의 다른 컴포넌트에서 변경 감지 용도 + localStorage.setItem('lastBudgetUpdateTime', new Date().toISOString()); + + // 이벤트 발생 + try { + window.dispatchEvent(new Event('budgetDataUpdated')); + window.dispatchEvent(new Event('categoryBudgetsUpdated')); + } catch (error) { + console.error('이벤트 발생 오류:', error); + } + }, [handleBudgetGoalUpdate, updateCategoryBudgets]); + return { extendedBudgetGoalUpdate }; }; diff --git a/src/contexts/budget/storage/budgetStorage.ts b/src/contexts/budget/storage/budgetStorage.ts index a4ef446..7fc6a86 100644 --- a/src/contexts/budget/storage/budgetStorage.ts +++ b/src/contexts/budget/storage/budgetStorage.ts @@ -12,10 +12,29 @@ export const loadBudgetDataFromStorage = (): BudgetData => { if (storedBudgetData) { const parsed = JSON.parse(storedBudgetData); console.log('예산 데이터 로드 완료', parsed); + + // 데이터 유효성 검사 추가 + if (!parsed || !parsed.monthly || !parsed.daily || !parsed.weekly) { + throw new Error('잘못된 형식의 예산 데이터'); + } + return parsed; } } catch (error) { console.error('예산 데이터 파싱 오류:', error); + + // 백업에서 복구 시도 + try { + const backupData = localStorage.getItem('budgetData_backup'); + if (backupData) { + const parsed = JSON.parse(backupData); + console.log('백업에서 예산 데이터 복구 완료', parsed); + localStorage.setItem('budgetData', backupData); // 메인 스토리지에 저장 + return parsed; + } + } catch (backupError) { + console.error('백업 데이터 복구 실패:', backupError); + } } // 새 사용자를 위한 기본 예산 데이터 저장 @@ -39,6 +58,7 @@ export const saveBudgetDataToStorage = (budgetData: BudgetData): void => { // 중요: 즉시 자동 백업 (데이터 손실 방지) localStorage.setItem('budgetData_backup', dataString); + localStorage.setItem('lastBudgetSaveTime', new Date().toISOString()); // 스토리지 이벤트 수동 트리거 (동일 창에서도 감지하기 위함) try { diff --git a/src/pages/Analytics.tsx b/src/pages/Analytics.tsx index c49f140..d603aee 100644 --- a/src/pages/Analytics.tsx +++ b/src/pages/Analytics.tsx @@ -17,43 +17,63 @@ const Analytics = () => { const [selectedPeriod, setSelectedPeriod] = useState('이번 달'); const { budgetData, getCategorySpending, transactions } = useBudget(); const isMobile = useIsMobile(); - - // 데이터 변경 감지를 위한 효과 + const [refreshTrigger, setRefreshTrigger] = useState(0); + + // 페이지 가시성 변경시 데이터 새로고침 useEffect(() => { - console.log('Analytics 페이지 마운트: 데이터 감지 시작'); + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + console.log('분석 페이지 보임 - 데이터 새로고침'); + setRefreshTrigger(prev => prev + 1); + + // 이벤트 발생시켜 데이터 새로고침 + try { + window.dispatchEvent(new Event('storage')); + window.dispatchEvent(new Event('transactionUpdated')); + window.dispatchEvent(new Event('budgetDataUpdated')); + window.dispatchEvent(new Event('categoryBudgetsUpdated')); + } catch (e) { + console.error('이벤트 발생 오류:', e); + } + } + }; - // 페이지 포커스시 새로고침 이벤트 발생 const handleFocus = () => { - console.log('Analytics 페이지: 창 포커스 감지, 상태 새로고침'); - // 상태 리렌더링 트리거를 위한 빈 상태 업데이트 - setSelectedPeriod(prev => prev); - }; - - // 스토리지 변경 감지 - const handleStorageChange = () => { - console.log('Analytics 페이지: 스토리지 변경 감지, 상태 새로고침'); - setSelectedPeriod(prev => prev); + console.log('분석 페이지 포커스 - 데이터 새로고침'); + setRefreshTrigger(prev => prev + 1); + + // 이벤트 발생시켜 데이터 새로고침 + try { + window.dispatchEvent(new Event('storage')); + window.dispatchEvent(new Event('transactionUpdated')); + window.dispatchEvent(new Event('budgetDataUpdated')); + window.dispatchEvent(new Event('categoryBudgetsUpdated')); + } catch (e) { + console.error('이벤트 발생 오류:', e); + } }; + document.addEventListener('visibilitychange', handleVisibilityChange); window.addEventListener('focus', handleFocus); - window.addEventListener('transactionUpdated', handleStorageChange); - window.addEventListener('budgetDataUpdated', handleStorageChange); - window.addEventListener('categoryBudgetsUpdated', handleStorageChange); - window.addEventListener('storage', handleStorageChange); + window.addEventListener('transactionUpdated', () => setRefreshTrigger(prev => prev + 1)); + window.addEventListener('budgetDataUpdated', () => setRefreshTrigger(prev => prev + 1)); + window.addEventListener('categoryBudgetsUpdated', () => setRefreshTrigger(prev => prev + 1)); + + // 컴포넌트 마운트 시 초기 데이터 로드 이벤트 트리거 + handleFocus(); return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); window.removeEventListener('focus', handleFocus); - window.removeEventListener('transactionUpdated', handleStorageChange); - window.removeEventListener('budgetDataUpdated', handleStorageChange); - window.removeEventListener('categoryBudgetsUpdated', handleStorageChange); - window.removeEventListener('storage', handleStorageChange); - console.log('Analytics 페이지 언마운트: 데이터 감지 중지'); + window.removeEventListener('transactionUpdated', () => {}); + window.removeEventListener('budgetDataUpdated', () => {}); + window.removeEventListener('categoryBudgetsUpdated', () => {}); }; }, []); // 실제 예산 및 지출 데이터 사용 - const totalBudget = budgetData.monthly.targetAmount; - const totalExpense = budgetData.monthly.spentAmount; + const totalBudget = budgetData?.monthly?.targetAmount || 0; + const totalExpense = budgetData?.monthly?.spentAmount || 0; const savings = Math.max(0, totalBudget - totalExpense); const savingsPercentage = totalBudget > 0 ? Math.round(savings / totalBudget * 100) : 0; @@ -63,8 +83,8 @@ const Analytics = () => { name: category.title, value: category.current, color: category.title === '식비' ? '#81c784' : - category.title === '생활비' ? '#AED581' : - category.title === '교통비' ? '#2E7D32' : '#4CAF50' + category.title === '생활비' ? '#AED581' : + category.title === '교통비' ? '#2E7D32' : '#4CAF50' })); // 최근 6개월 데이터를 위한 상태 @@ -72,17 +92,12 @@ const Analytics = () => { // 월별 데이터 생성 useEffect(() => { - console.log('Analytics 페이지: 월별 데이터 생성'); + console.log('Analytics 페이지: 월별 데이터 생성', { totalBudget, totalExpense }); + // 현재 월 가져오기 const today = new Date(); const currentMonth = today.getMonth(); - if (totalBudget === 0 && totalExpense === 0) { - // 모든 데이터가 초기화된 상태라면 빈 배열 사용 - setMonthlyData([]); - return; - } - // 최근 6개월 데이터 배열 생성 const last6Months = []; for (let i = 5; i >= 0; i--) { @@ -101,7 +116,7 @@ const Analytics = () => { setMonthlyData(last6Months); console.log('Analytics 페이지: 월별 데이터 생성 완료', last6Months); - }, [totalBudget, totalExpense]); + }, [totalBudget, totalExpense, refreshTrigger]); // 이전/다음 기간 이동 처리 const handlePrevPeriod = () => { @@ -112,15 +127,6 @@ const Analytics = () => { console.log('다음 기간으로 이동'); }; - // 디버깅을 위한 로그 - useEffect(() => { - console.log('Analytics 페이지 렌더링:', { - totalBudget, - totalExpense, - categorySpending: categorySpending.length - }); - }); - return (
@@ -148,7 +154,7 @@ const Analytics = () => {

월별 그래프

diff --git a/src/pages/Transactions.tsx b/src/pages/Transactions.tsx index fd099a1..df191a3 100644 --- a/src/pages/Transactions.tsx +++ b/src/pages/Transactions.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import NavBar from '@/components/NavBar'; import TransactionCard from '@/components/TransactionCard'; import AddTransactionButton from '@/components/AddTransactionButton'; @@ -23,6 +23,14 @@ const Transactions = () => { } = useTransactions(); const { budgetData } = useBudget(); + const [isDataLoaded, setIsDataLoaded] = useState(false); + + // 데이터 로드 상태 관리 + useEffect(() => { + if (budgetData && !isLoading) { + setIsDataLoaded(true); + } + }, [budgetData, isLoading]); // 트랜잭션을 날짜별로 그룹화 const groupedTransactions: Record = {}; @@ -35,6 +43,31 @@ const Transactions = () => { groupedTransactions[datePart].push(transaction); }); + // 페이지 포커스나 가시성 변경 시 데이터 새로고침 + useEffect(() => { + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + console.log('거래내역 페이지 보임 - 데이터 새로고침'); + // 상태 업데이트 트리거 + setIsDataLoaded(prev => !prev); + } + }; + + const handleFocus = () => { + console.log('거래내역 페이지 포커스 - 데이터 새로고침'); + // 상태 업데이트 트리거 + setIsDataLoaded(prev => !prev); + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + window.addEventListener('focus', handleFocus); + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + window.removeEventListener('focus', handleFocus); + }; + }, []); + return (
@@ -81,7 +114,7 @@ const Transactions = () => {

총 예산

- {formatCurrency(budgetData.monthly.targetAmount)} + {formatCurrency(budgetData?.monthly?.targetAmount || 0)}