Fix budget data persistence issue

Addresses the problem where budget data was not persisting across page transitions, causing budget and expense information to disappear from the expense page and only expense data to appear on the analytics page.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-16 07:29:25 +00:00
parent 84553d4fee
commit 23ba0f7e90
5 changed files with 158 additions and 113 deletions

View File

@@ -25,7 +25,12 @@ export const useBudgetDataState = (transactions: any[]) => {
console.log('예산 데이터 로드 시도 중...'); console.log('예산 데이터 로드 시도 중...');
const loadedData = loadBudgetDataFromStorage(); const loadedData = loadBudgetDataFromStorage();
console.log('예산 데이터 로드됨:', loadedData); console.log('예산 데이터 로드됨:', loadedData);
// 새로 로드한 데이터와 현재 데이터가 다를 때만 업데이트
if (JSON.stringify(loadedData) !== JSON.stringify(budgetData)) {
console.log('예산 데이터 변경 감지됨, 상태 업데이트');
setBudgetData(loadedData); setBudgetData(loadedData);
}
// 최근 데이터 로드 시간 기록 // 최근 데이터 로드 시간 기록
localStorage.setItem('lastBudgetDataLoadTime', new Date().toISOString()); localStorage.setItem('lastBudgetDataLoadTime', new Date().toISOString());
@@ -49,6 +54,7 @@ export const useBudgetDataState = (transactions: any[]) => {
} }
}; };
// 이벤트 발생 시 데이터 새로고침
window.addEventListener('budgetDataUpdated', () => handleBudgetUpdate()); window.addEventListener('budgetDataUpdated', () => handleBudgetUpdate());
window.addEventListener('storage', handleBudgetUpdate); window.addEventListener('storage', handleBudgetUpdate);
window.addEventListener('visibilitychange', () => { window.addEventListener('visibilitychange', () => {
@@ -62,7 +68,7 @@ export const useBudgetDataState = (transactions: any[]) => {
loadBudget(); loadBudget();
}); });
// 주기적 데이터 검사 (1초마다) // 주기적 데이터 검사 (1초마다) - 다른 컴포넌트에서 변경된 사항 감지
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
const lastSaveTime = localStorage.getItem('lastBudgetSaveTime'); const lastSaveTime = localStorage.getItem('lastBudgetSaveTime');
const lastLoadTime = localStorage.getItem('lastBudgetDataLoadTime'); const lastLoadTime = localStorage.getItem('lastBudgetDataLoadTime');
@@ -80,27 +86,30 @@ export const useBudgetDataState = (transactions: any[]) => {
window.removeEventListener('focus', () => loadBudget()); window.removeEventListener('focus', () => loadBudget());
clearInterval(intervalId); clearInterval(intervalId);
}; };
}, [isInitialized]); }, [isInitialized, budgetData]);
// 트랜잭션 변경 시 지출 금액 업데이트 // 트랜잭션 변경 시 지출 금액 업데이트
useEffect(() => { useEffect(() => {
if (transactions.length > 0) { if (transactions.length > 0 && isInitialized) {
console.log('트랜잭션 변경으로 인한 예산 데이터 업데이트. 트랜잭션 수:', transactions.length); console.log('트랜잭션 변경으로 인한 예산 데이터 업데이트. 트랜잭션 수:', transactions.length);
try { try {
// 지출 금액 업데이트 // 지출 금액 업데이트
const updatedBudgetData = calculateSpentAmounts(transactions, budgetData); const updatedBudgetData = calculateSpentAmounts(transactions, budgetData);
// 변경이 있을 때만 저장
if (JSON.stringify(updatedBudgetData) !== JSON.stringify(budgetData)) {
// 상태 및 스토리지 모두 업데이트 // 상태 및 스토리지 모두 업데이트
setBudgetData(updatedBudgetData); setBudgetData(updatedBudgetData);
saveBudgetDataToStorage(updatedBudgetData); saveBudgetDataToStorage(updatedBudgetData);
// 저장 시간 업데이트 // 저장 시간 업데이트
localStorage.setItem('lastBudgetSaveTime', new Date().toISOString()); localStorage.setItem('lastBudgetSaveTime', new Date().toISOString());
}
} catch (error) { } catch (error) {
console.error('예산 데이터 업데이트 중 오류:', error); console.error('예산 데이터 업데이트 중 오류:', error);
} }
} }
}, [transactions, budgetData]); }, [transactions, budgetData, isInitialized]);
// 예산 목표 업데이트 함수 // 예산 목표 업데이트 함수
const handleBudgetGoalUpdate = useCallback(( const handleBudgetGoalUpdate = useCallback((
@@ -114,6 +123,8 @@ export const useBudgetDataState = (transactions: any[]) => {
if (!newCategoryBudgets) { if (!newCategoryBudgets) {
const updatedBudgetData = calculateUpdatedBudgetData(budgetData, type, amount); const updatedBudgetData = calculateUpdatedBudgetData(budgetData, type, amount);
console.log('새 예산 데이터:', updatedBudgetData); console.log('새 예산 데이터:', updatedBudgetData);
// 상태 및 스토리지 둘 다 업데이트
setBudgetData(updatedBudgetData); setBudgetData(updatedBudgetData);
saveBudgetDataToStorage(updatedBudgetData); saveBudgetDataToStorage(updatedBudgetData);

View File

@@ -1,72 +1,47 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { BudgetPeriod, BudgetData } from '../types'; import { BudgetData, BudgetPeriod } from '../types';
import { toast } from '@/components/ui/use-toast';
// 확장된 예산 목표 업데이트 훅 /**
* 확장된 예산 업데이트 기능을 제공하는 훅
*/
export const useExtendedBudgetUpdate = ( export const useExtendedBudgetUpdate = (
budgetData: BudgetData, budgetData: BudgetData,
categoryBudgets: Record<string, number>, categoryBudgets: Record<string, number>,
handleBudgetGoalUpdate: (type: BudgetPeriod, amount: number) => void, handleBudgetGoalUpdate: (type: BudgetPeriod, amount: number, newCategoryBudgets?: Record<string, number>) => void,
updateCategoryBudgets: (newCategoryBudgets: Record<string, number>) => void updateCategoryBudgets: (newCategoryBudgets: Record<string, number>) => void
) => { ) => {
// 확장된 예산 목표 업데이트 함수 /**
* 확장된 예산 업데이트 함수
* 월간 예산 및 카테고리 예산을 함께 업데이트
*/
const extendedBudgetGoalUpdate = useCallback(( const extendedBudgetGoalUpdate = useCallback((
type: BudgetPeriod, type: BudgetPeriod,
amount: number, amount: number,
newCategoryBudgets?: Record<string, number> newCategoryBudgets?: Record<string, number>
) => { ) => {
try { console.log(`확장된 예산 업데이트: 타입=${type}, 금액=${amount}, 카테고리 예산 포함=${!!newCategoryBudgets}`);
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<string, number> = {};
// 비율에 따라 카테고리 예산 업데이트
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); handleBudgetGoalUpdate(type, amount);
// 카테고리 예산도 함께 업데이트 (있는 경우)
if (newCategoryBudgets) {
console.log('카테고리 예산 업데이트:', newCategoryBudgets);
updateCategoryBudgets(newCategoryBudgets);
} }
// 데이터 저장 시간 기록 - 동일 세션의 다른 컴포넌트에서 변경 감지 용도
localStorage.setItem('lastBudgetUpdateTime', new Date().toISOString());
// 이벤트 발생
try {
window.dispatchEvent(new Event('budgetDataUpdated'));
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
} catch (error) { } catch (error) {
console.error('예산 목표 업데이트 중 오류:', error); console.error('이벤트 발생 오류:', error);
toast({
title: "예산 업데이트 실패",
description: "예산 목표를 업데이트하는 중 오류가 발생했습니다.",
variant: "destructive"
});
} }
}, [budgetData, categoryBudgets, handleBudgetGoalUpdate, updateCategoryBudgets]); }, [handleBudgetGoalUpdate, updateCategoryBudgets]);
return { extendedBudgetGoalUpdate }; return { extendedBudgetGoalUpdate };
}; };

View File

@@ -12,10 +12,29 @@ export const loadBudgetDataFromStorage = (): BudgetData => {
if (storedBudgetData) { if (storedBudgetData) {
const parsed = JSON.parse(storedBudgetData); const parsed = JSON.parse(storedBudgetData);
console.log('예산 데이터 로드 완료', parsed); console.log('예산 데이터 로드 완료', parsed);
// 데이터 유효성 검사 추가
if (!parsed || !parsed.monthly || !parsed.daily || !parsed.weekly) {
throw new Error('잘못된 형식의 예산 데이터');
}
return parsed; return parsed;
} }
} catch (error) { } catch (error) {
console.error('예산 데이터 파싱 오류:', 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('budgetData_backup', dataString);
localStorage.setItem('lastBudgetSaveTime', new Date().toISOString());
// 스토리지 이벤트 수동 트리거 (동일 창에서도 감지하기 위함) // 스토리지 이벤트 수동 트리거 (동일 창에서도 감지하기 위함)
try { try {

View File

@@ -17,43 +17,63 @@ const Analytics = () => {
const [selectedPeriod, setSelectedPeriod] = useState('이번 달'); const [selectedPeriod, setSelectedPeriod] = useState('이번 달');
const { budgetData, getCategorySpending, transactions } = useBudget(); const { budgetData, getCategorySpending, transactions } = useBudget();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [refreshTrigger, setRefreshTrigger] = useState(0);
// 데이터 변경 감지를 위한 효과 // 페이지 가시성 변경시 데이터 새로고침
useEffect(() => { 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 = () => { const handleFocus = () => {
console.log('Analytics 페이지: 창 포커스 감지, 상태 새로고침'); console.log('분석 페이지 포커스 - 데이터 새로고침');
// 상태 리렌더링 트리거를 위한 빈 상태 업데이트 setRefreshTrigger(prev => prev + 1);
setSelectedPeriod(prev => prev);
}; // 이벤트 발생시켜 데이터 새로고침
try {
// 스토리지 변경 감지 window.dispatchEvent(new Event('storage'));
const handleStorageChange = () => { window.dispatchEvent(new Event('transactionUpdated'));
console.log('Analytics 페이지: 스토리지 변경 감지, 상태 새로고침'); window.dispatchEvent(new Event('budgetDataUpdated'));
setSelectedPeriod(prev => prev); window.dispatchEvent(new Event('categoryBudgetsUpdated'));
} catch (e) {
console.error('이벤트 발생 오류:', e);
}
}; };
document.addEventListener('visibilitychange', handleVisibilityChange);
window.addEventListener('focus', handleFocus); window.addEventListener('focus', handleFocus);
window.addEventListener('transactionUpdated', handleStorageChange); window.addEventListener('transactionUpdated', () => setRefreshTrigger(prev => prev + 1));
window.addEventListener('budgetDataUpdated', handleStorageChange); window.addEventListener('budgetDataUpdated', () => setRefreshTrigger(prev => prev + 1));
window.addEventListener('categoryBudgetsUpdated', handleStorageChange); window.addEventListener('categoryBudgetsUpdated', () => setRefreshTrigger(prev => prev + 1));
window.addEventListener('storage', handleStorageChange);
// 컴포넌트 마운트 시 초기 데이터 로드 이벤트 트리거
handleFocus();
return () => { return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('focus', handleFocus); window.removeEventListener('focus', handleFocus);
window.removeEventListener('transactionUpdated', handleStorageChange); window.removeEventListener('transactionUpdated', () => {});
window.removeEventListener('budgetDataUpdated', handleStorageChange); window.removeEventListener('budgetDataUpdated', () => {});
window.removeEventListener('categoryBudgetsUpdated', handleStorageChange); window.removeEventListener('categoryBudgetsUpdated', () => {});
window.removeEventListener('storage', handleStorageChange);
console.log('Analytics 페이지 언마운트: 데이터 감지 중지');
}; };
}, []); }, []);
// 실제 예산 및 지출 데이터 사용 // 실제 예산 및 지출 데이터 사용
const totalBudget = budgetData.monthly.targetAmount; const totalBudget = budgetData?.monthly?.targetAmount || 0;
const totalExpense = budgetData.monthly.spentAmount; const totalExpense = budgetData?.monthly?.spentAmount || 0;
const savings = Math.max(0, totalBudget - totalExpense); const savings = Math.max(0, totalBudget - totalExpense);
const savingsPercentage = totalBudget > 0 ? Math.round(savings / totalBudget * 100) : 0; const savingsPercentage = totalBudget > 0 ? Math.round(savings / totalBudget * 100) : 0;
@@ -72,17 +92,12 @@ const Analytics = () => {
// 월별 데이터 생성 // 월별 데이터 생성
useEffect(() => { useEffect(() => {
console.log('Analytics 페이지: 월별 데이터 생성'); console.log('Analytics 페이지: 월별 데이터 생성', { totalBudget, totalExpense });
// 현재 월 가져오기 // 현재 월 가져오기
const today = new Date(); const today = new Date();
const currentMonth = today.getMonth(); const currentMonth = today.getMonth();
if (totalBudget === 0 && totalExpense === 0) {
// 모든 데이터가 초기화된 상태라면 빈 배열 사용
setMonthlyData([]);
return;
}
// 최근 6개월 데이터 배열 생성 // 최근 6개월 데이터 배열 생성
const last6Months = []; const last6Months = [];
for (let i = 5; i >= 0; i--) { for (let i = 5; i >= 0; i--) {
@@ -101,7 +116,7 @@ const Analytics = () => {
setMonthlyData(last6Months); setMonthlyData(last6Months);
console.log('Analytics 페이지: 월별 데이터 생성 완료', last6Months); console.log('Analytics 페이지: 월별 데이터 생성 완료', last6Months);
}, [totalBudget, totalExpense]); }, [totalBudget, totalExpense, refreshTrigger]);
// 이전/다음 기간 이동 처리 // 이전/다음 기간 이동 처리
const handlePrevPeriod = () => { const handlePrevPeriod = () => {
@@ -112,15 +127,6 @@ const Analytics = () => {
console.log('다음 기간으로 이동'); console.log('다음 기간으로 이동');
}; };
// 디버깅을 위한 로그
useEffect(() => {
console.log('Analytics 페이지 렌더링:', {
totalBudget,
totalExpense,
categorySpending: categorySpending.length
});
});
return ( return (
<div className="min-h-screen bg-neuro-background pb-24"> <div className="min-h-screen bg-neuro-background pb-24">
<div className="max-w-md mx-auto px-6"> <div className="max-w-md mx-auto px-6">
@@ -148,7 +154,7 @@ const Analytics = () => {
<h2 className="text-lg font-semibold mb-3"> </h2> <h2 className="text-lg font-semibold mb-3"> </h2>
<MonthlyComparisonChart <MonthlyComparisonChart
monthlyData={monthlyData} monthlyData={monthlyData}
isEmpty={monthlyData.length === 0} isEmpty={monthlyData.length === 0 || (totalBudget === 0 && totalExpense === 0)}
/> />
</div> </div>

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import NavBar from '@/components/NavBar'; import NavBar from '@/components/NavBar';
import TransactionCard from '@/components/TransactionCard'; import TransactionCard from '@/components/TransactionCard';
import AddTransactionButton from '@/components/AddTransactionButton'; import AddTransactionButton from '@/components/AddTransactionButton';
@@ -23,6 +23,14 @@ const Transactions = () => {
} = useTransactions(); } = useTransactions();
const { budgetData } = useBudget(); const { budgetData } = useBudget();
const [isDataLoaded, setIsDataLoaded] = useState(false);
// 데이터 로드 상태 관리
useEffect(() => {
if (budgetData && !isLoading) {
setIsDataLoaded(true);
}
}, [budgetData, isLoading]);
// 트랜잭션을 날짜별로 그룹화 // 트랜잭션을 날짜별로 그룹화
const groupedTransactions: Record<string, typeof transactions> = {}; const groupedTransactions: Record<string, typeof transactions> = {};
@@ -35,6 +43,31 @@ const Transactions = () => {
groupedTransactions[datePart].push(transaction); 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 ( return (
<div className="min-h-screen bg-neuro-background pb-24"> <div className="min-h-screen bg-neuro-background pb-24">
<div className="max-w-md mx-auto px-6"> <div className="max-w-md mx-auto px-6">
@@ -81,7 +114,7 @@ const Transactions = () => {
<div className="neuro-card"> <div className="neuro-card">
<p className="text-sm text-gray-500 mb-1"> </p> <p className="text-sm text-gray-500 mb-1"> </p>
<p className="text-lg font-bold text-neuro-income"> <p className="text-lg font-bold text-neuro-income">
{formatCurrency(budgetData.monthly.targetAmount)} {formatCurrency(budgetData?.monthly?.targetAmount || 0)}
</p> </p>
</div> </div>
<div className="neuro-card"> <div className="neuro-card">