Fix budget update issues

Addresses delayed notifications and data loss after budget updates and page transitions.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-16 07:05:20 +00:00
parent 61b00cfdcb
commit d59fb97f7c
9 changed files with 523 additions and 126 deletions

View File

@@ -34,6 +34,7 @@ const BudgetInputCard: React.FC<BudgetGoalProps> = ({
return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, ','); return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}; };
// 초기값 변경시 입력 필드 값 업데이트
useEffect(() => { useEffect(() => {
setBudgetInputs({ setBudgetInputs({
daily: initialBudgets.daily > 0 ? initialBudgets.daily.toString() : '', daily: initialBudgets.daily > 0 ? initialBudgets.daily.toString() : '',
@@ -53,9 +54,21 @@ const BudgetInputCard: React.FC<BudgetGoalProps> = ({
const handleSave = () => { const handleSave = () => {
const amount = parseInt(budgetInputs[selectedTab].replace(/,/g, ''), 10) || 0; const amount = parseInt(budgetInputs[selectedTab].replace(/,/g, ''), 10) || 0;
onSave(selectedTab, amount); if (amount <= 0) {
// Close the collapsible after saving return; // 0 이하의 금액은 저장하지 않음
}
// 즉시 입력 필드를 업데이트하여 사용자에게 피드백 제공
setBudgetInputs(prev => ({
...prev,
[selectedTab]: amount.toString()
}));
// 즉시 콜랩시블을 닫아 사용자에게 완료 피드백 제공
setIsOpen(false); setIsOpen(false);
// 예산 저장
onSave(selectedTab, amount);
}; };
// 비어있으면 빈 문자열을, 그렇지 않으면 포맷팅된 문자열을 반환 // 비어있으면 빈 문자열을, 그렇지 않으면 포맷팅된 문자열을 반환

View File

@@ -1,8 +1,9 @@
import React, { useEffect } from 'react'; import React, { useEffect, useRef } from 'react';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons'; import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons';
import { useIsMobile } from '@/hooks/use-mobile'; import { useIsMobile } from '@/hooks/use-mobile';
import { toast } from '@/components/ui/use-toast';
interface CategoryBudgetInputsProps { interface CategoryBudgetInputsProps {
categoryBudgets: Record<string, number>; categoryBudgets: Record<string, number>;
@@ -14,6 +15,7 @@ const CategoryBudgetInputs: React.FC<CategoryBudgetInputsProps> = ({
handleCategoryInputChange handleCategoryInputChange
}) => { }) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const previousBudgetsRef = useRef<Record<string, number>>({});
// Format number with commas for display // Format number with commas for display
const formatWithCommas = (value: number): string => { const formatWithCommas = (value: number): string => {
@@ -26,6 +28,12 @@ const CategoryBudgetInputs: React.FC<CategoryBudgetInputsProps> = ({
// Remove all non-numeric characters before passing to parent handler // Remove all non-numeric characters before passing to parent handler
const numericValue = e.target.value.replace(/[^0-9]/g, ''); const numericValue = e.target.value.replace(/[^0-9]/g, '');
handleCategoryInputChange(numericValue, category); handleCategoryInputChange(numericValue, category);
// 사용자에게 시각적 피드백 제공
e.target.classList.add('border-green-500');
setTimeout(() => {
e.target.classList.remove('border-green-500');
}, 300);
}; };
// 컴포넌트가 마운트될 때 categoryBudgets가 로컬 스토리지에서 다시 로드되도록 이벤트 리스너 설정 // 컴포넌트가 마운트될 때 categoryBudgets가 로컬 스토리지에서 다시 로드되도록 이벤트 리스너 설정
@@ -44,6 +52,24 @@ const CategoryBudgetInputs: React.FC<CategoryBudgetInputsProps> = ({
}; };
}, []); }, []);
// 값이 변경될 때마다 토스트 메시지 표시
useEffect(() => {
const hasChanges = Object.keys(categoryBudgets).some(
category => categoryBudgets[category] !== previousBudgetsRef.current[category]
);
const totalBudget = Object.values(categoryBudgets).reduce((sum, val) => sum + val, 0);
const previousTotal = Object.values(previousBudgetsRef.current).reduce((sum, val) => sum + val, 0);
// 이전 값과 다르고, 총 예산이 있는 경우 토스트 표시
if (hasChanges && totalBudget > 0 && totalBudget !== previousTotal) {
// 토스트 메시지는 storage에서 처리
}
// 현재 값을 이전 값으로 업데이트
previousBudgetsRef.current = { ...categoryBudgets };
}, [categoryBudgets]);
return ( return (
<div className="space-y-2 w-full"> <div className="space-y-2 w-full">
{EXPENSE_CATEGORIES.map(category => ( {EXPENSE_CATEGORIES.map(category => (
@@ -53,7 +79,7 @@ const CategoryBudgetInputs: React.FC<CategoryBudgetInputsProps> = ({
value={formatWithCommas(categoryBudgets[category] || 0)} value={formatWithCommas(categoryBudgets[category] || 0)}
onChange={(e) => handleInput(e, category)} onChange={(e) => handleInput(e, category)}
placeholder="예산 입력" placeholder="예산 입력"
className={`neuro-pressed ${isMobile ? 'w-[150px]' : 'max-w-[150px]'} text-xs`} className={`neuro-pressed transition-colors duration-300 ${isMobile ? 'w-[150px]' : 'max-w-[150px]'} text-xs`}
/> />
</div> </div>
))} ))}

View File

@@ -16,14 +16,26 @@ import {
export const useBudgetDataState = (transactions: any[]) => { export const useBudgetDataState = (transactions: any[]) => {
const [budgetData, setBudgetData] = useState<BudgetData>(loadBudgetDataFromStorage()); const [budgetData, setBudgetData] = useState<BudgetData>(loadBudgetDataFromStorage());
const [selectedTab, setSelectedTab] = useState<BudgetPeriod>("daily"); const [selectedTab, setSelectedTab] = useState<BudgetPeriod>("daily");
const [isInitialized, setIsInitialized] = useState(false);
// 초기 로드 및 이벤트 리스너 설정 // 초기 로드 및 이벤트 리스너 설정
useEffect(() => { useEffect(() => {
const loadBudget = () => { const loadBudget = () => {
console.log('예산 데이터 로드 시도 중...'); try {
const loadedData = loadBudgetDataFromStorage(); console.log('예산 데이터 로드 시도 중...');
console.log('예산 데이터 로드됨:', loadedData); const loadedData = loadBudgetDataFromStorage();
setBudgetData(loadedData); console.log('예산 데이터 로드됨:', loadedData);
setBudgetData(loadedData);
// 최근 데이터 로드 시간 기록
localStorage.setItem('lastBudgetDataLoadTime', new Date().toISOString());
if (!isInitialized) {
setIsInitialized(true);
}
} catch (error) {
console.error('예산 데이터 로드 중 오류:', error);
}
}; };
// 초기 로드 // 초기 로드
@@ -39,30 +51,56 @@ export const useBudgetDataState = (transactions: any[]) => {
window.addEventListener('budgetDataUpdated', () => handleBudgetUpdate()); window.addEventListener('budgetDataUpdated', () => handleBudgetUpdate());
window.addEventListener('storage', handleBudgetUpdate); window.addEventListener('storage', handleBudgetUpdate);
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
console.log('페이지 보임: 예산 데이터 새로고침');
loadBudget();
}
});
window.addEventListener('focus', () => { window.addEventListener('focus', () => {
console.log('창 포커스: 예산 데이터 새로고침'); console.log('창 포커스: 예산 데이터 새로고침');
loadBudget(); loadBudget();
}); });
// 주기적 데이터 검사 (1초마다)
const intervalId = setInterval(() => {
const lastSaveTime = localStorage.getItem('lastBudgetSaveTime');
const lastLoadTime = localStorage.getItem('lastBudgetDataLoadTime');
if (lastSaveTime && lastLoadTime && new Date(lastSaveTime) > new Date(lastLoadTime)) {
console.log('새로운 저장 감지됨, 데이터 다시 로드...');
loadBudget();
}
}, 1000);
return () => { return () => {
window.removeEventListener('budgetDataUpdated', () => handleBudgetUpdate()); window.removeEventListener('budgetDataUpdated', () => handleBudgetUpdate());
window.removeEventListener('storage', handleBudgetUpdate); window.removeEventListener('storage', handleBudgetUpdate);
window.removeEventListener('visibilitychange', () => {});
window.removeEventListener('focus', () => loadBudget()); window.removeEventListener('focus', () => loadBudget());
clearInterval(intervalId);
}; };
}, []); }, [isInitialized]);
// 트랜잭션 변경 시 지출 금액 업데이트 // 트랜잭션 변경 시 지출 금액 업데이트
useEffect(() => { useEffect(() => {
if (transactions.length > 0) { if (transactions.length > 0) {
console.log('트랜잭션 변경으로 인한 예산 데이터 업데이트. 트랜잭션 수:', transactions.length); console.log('트랜잭션 변경으로 인한 예산 데이터 업데이트. 트랜잭션 수:', transactions.length);
// 지출 금액 업데이트 try {
const updatedBudgetData = calculateSpentAmounts(transactions, budgetData); // 지출 금액 업데이트
const updatedBudgetData = calculateSpentAmounts(transactions, budgetData);
// 상태 및 스토리지 모두 업데이트
setBudgetData(updatedBudgetData); // 상태 및 스토리지 모두 업데이트
saveBudgetDataToStorage(updatedBudgetData); setBudgetData(updatedBudgetData);
saveBudgetDataToStorage(updatedBudgetData);
// 저장 시간 업데이트
localStorage.setItem('lastBudgetSaveTime', new Date().toISOString());
} catch (error) {
console.error('예산 데이터 업데이트 중 오류:', error);
}
} }
}, [transactions]); }, [transactions, budgetData]);
// 예산 목표 업데이트 함수 // 예산 목표 업데이트 함수
const handleBudgetGoalUpdate = useCallback(( const handleBudgetGoalUpdate = useCallback((
@@ -70,26 +108,42 @@ export const useBudgetDataState = (transactions: any[]) => {
amount: number, amount: number,
newCategoryBudgets?: Record<string, number> newCategoryBudgets?: Record<string, number>
) => { ) => {
console.log(`예산 목표 업데이트: ${type}, 금액: ${amount}`); try {
// 월간 예산 직접 업데이트 (카테고리 예산이 없는 경우) console.log(`예산 목표 업데이트: ${type}, 금액: ${amount}`);
if (!newCategoryBudgets) { // 월간 예산 직접 업데이트 (카테고리 예산이 없는 경우)
const updatedBudgetData = calculateUpdatedBudgetData(budgetData, type, amount); if (!newCategoryBudgets) {
console.log('새 예산 데이터:', updatedBudgetData); const updatedBudgetData = calculateUpdatedBudgetData(budgetData, type, amount);
setBudgetData(updatedBudgetData); console.log('새 예산 데이터:', updatedBudgetData);
saveBudgetDataToStorage(updatedBudgetData); setBudgetData(updatedBudgetData);
saveBudgetDataToStorage(updatedBudgetData);
// 저장 시간 업데이트
localStorage.setItem('lastBudgetSaveTime', new Date().toISOString());
}
} catch (error) {
console.error('예산 목표 업데이트 중 오류:', error);
toast({ toast({
title: "목표 업데이트 완료", title: "예산 업데이트 실패",
description: `${type === 'daily' ? '일일' : type === 'weekly' ? '주간' : '월간'} 목표가 ${amount.toLocaleString()}원으로 설정되었습니다.` description: "예산 목표를 업데이트하는데 문제가 발생했습니다.",
variant: "destructive"
}); });
} }
}, [budgetData]); }, [budgetData]);
// 예산 데이터 초기화 함수 // 예산 데이터 초기화 함수
const resetBudgetData = useCallback(() => { const resetBudgetData = useCallback(() => {
console.log('예산 데이터 초기화'); try {
clearAllBudgetData(); console.log('예산 데이터 초기화');
setBudgetData(loadBudgetDataFromStorage()); clearAllBudgetData();
setBudgetData(loadBudgetDataFromStorage());
} catch (error) {
console.error('예산 데이터 초기화 중 오류:', error);
toast({
title: "예산 초기화 실패",
description: "예산 데이터를 초기화하는데 문제가 발생했습니다.",
variant: "destructive"
});
}
}, []); }, []);
// 예산 데이터 변경 시 로그 기록 // 예산 데이터 변경 시 로그 기록

View File

@@ -11,14 +11,26 @@ export const useCategoryBudgetState = () => {
const [categoryBudgets, setCategoryBudgets] = useState<Record<string, number>>( const [categoryBudgets, setCategoryBudgets] = useState<Record<string, number>>(
loadCategoryBudgetsFromStorage() loadCategoryBudgetsFromStorage()
); );
const [isInitialized, setIsInitialized] = useState(false);
// 초기 로드 및 이벤트 리스너 설정 // 초기 로드 및 이벤트 리스너 설정
useEffect(() => { useEffect(() => {
const loadCategories = () => { const loadCategories = () => {
console.log('카테고리 예산 로드 시도 중...'); try {
const loaded = loadCategoryBudgetsFromStorage(); console.log('카테고리 예산 로드 시도 중...');
console.log('카테고리 예산 로드됨:', loaded); const loaded = loadCategoryBudgetsFromStorage();
setCategoryBudgets(loaded); console.log('카테고리 예산 로드됨:', loaded);
setCategoryBudgets(loaded);
// 최근 데이터 로드 시간 기록
localStorage.setItem('lastCategoryBudgetLoadTime', new Date().toISOString());
if (!isInitialized) {
setIsInitialized(true);
}
} catch (error) {
console.error('카테고리 예산 로드 중 오류:', error);
}
}; };
// 초기 로드 // 초기 로드
@@ -34,30 +46,60 @@ export const useCategoryBudgetState = () => {
window.addEventListener('categoryBudgetsUpdated', () => handleCategoryUpdate()); window.addEventListener('categoryBudgetsUpdated', () => handleCategoryUpdate());
window.addEventListener('storage', handleCategoryUpdate); window.addEventListener('storage', handleCategoryUpdate);
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
console.log('페이지 보임: 카테고리 예산 새로고침');
loadCategories();
}
});
window.addEventListener('focus', () => { window.addEventListener('focus', () => {
console.log('창 포커스: 카테고리 예산 새로고침'); console.log('창 포커스: 카테고리 예산 새로고침');
loadCategories(); loadCategories();
}); });
// 주기적 데이터 검사
const intervalId = setInterval(() => {
const lastSaveTime = localStorage.getItem('lastCategoryBudgetSaveTime');
const lastLoadTime = localStorage.getItem('lastCategoryBudgetLoadTime');
if (lastSaveTime && lastLoadTime && new Date(lastSaveTime) > new Date(lastLoadTime)) {
console.log('새로운 카테고리 저장 감지됨, 데이터 다시 로드...');
loadCategories();
}
}, 1000);
return () => { return () => {
window.removeEventListener('categoryBudgetsUpdated', () => handleCategoryUpdate()); window.removeEventListener('categoryBudgetsUpdated', () => handleCategoryUpdate());
window.removeEventListener('storage', handleCategoryUpdate); window.removeEventListener('storage', handleCategoryUpdate);
window.removeEventListener('visibilitychange', () => {});
window.removeEventListener('focus', () => loadCategories()); window.removeEventListener('focus', () => loadCategories());
clearInterval(intervalId);
}; };
}, []); }, [isInitialized]);
// 카테고리 예산 업데이트 함수 // 카테고리 예산 업데이트 함수
const updateCategoryBudgets = useCallback((newCategoryBudgets: Record<string, number>) => { const updateCategoryBudgets = useCallback((newCategoryBudgets: Record<string, number>) => {
console.log('카테고리 예산 업데이트:', newCategoryBudgets); try {
setCategoryBudgets(newCategoryBudgets); console.log('카테고리 예산 업데이트:', newCategoryBudgets);
saveCategoryBudgetsToStorage(newCategoryBudgets); setCategoryBudgets(newCategoryBudgets);
saveCategoryBudgetsToStorage(newCategoryBudgets);
// 저장 시간 업데이트
localStorage.setItem('lastCategoryBudgetSaveTime', new Date().toISOString());
} catch (error) {
console.error('카테고리 예산 업데이트 중 오류:', error);
}
}, []); }, []);
// 카테고리 예산 초기화 함수 // 카테고리 예산 초기화 함수
const resetCategoryBudgets = useCallback(() => { const resetCategoryBudgets = useCallback(() => {
console.log('카테고리 예산 초기화'); try {
clearAllCategoryBudgets(); console.log('카테고리 예산 초기화');
setCategoryBudgets(loadCategoryBudgetsFromStorage()); clearAllCategoryBudgets();
setCategoryBudgets(loadCategoryBudgetsFromStorage());
} catch (error) {
console.error('카테고리 예산 초기화 중 오류:', error);
}
}, []); }, []);
// 카테고리 예산 변경 시 로그 기록 // 카테고리 예산 변경 시 로그 기록

View File

@@ -1,6 +1,7 @@
import { BudgetData } from '../types'; import { BudgetData } from '../types';
import { getInitialBudgetData } from '../budgetUtils'; import { getInitialBudgetData } from '../budgetUtils';
import { toast } from '@/components/ui/use-toast';
/** /**
* 예산 데이터 불러오기 * 예산 데이터 불러오기
@@ -36,14 +37,36 @@ export const saveBudgetDataToStorage = (budgetData: BudgetData): void => {
localStorage.setItem('budgetData', dataString); localStorage.setItem('budgetData', dataString);
console.log('예산 데이터 저장 완료', budgetData); console.log('예산 데이터 저장 완료', budgetData);
// 중요: 즉시 자동 백업 (데이터 손실 방지)
localStorage.setItem('budgetData_backup', dataString);
// 스토리지 이벤트 수동 트리거 (동일 창에서도 감지하기 위함) // 스토리지 이벤트 수동 트리거 (동일 창에서도 감지하기 위함)
window.dispatchEvent(new Event('budgetDataUpdated')); try {
window.dispatchEvent(new StorageEvent('storage', { window.dispatchEvent(new Event('budgetDataUpdated'));
key: 'budgetData', window.dispatchEvent(new StorageEvent('storage', {
newValue: dataString key: 'budgetData',
})); newValue: dataString
}));
} catch (e) {
console.error('이벤트 발생 오류:', e);
}
// toast 알림은 즉시 표시
if (budgetData.monthly.targetAmount > 0) {
toast({
title: "예산 저장 완료",
description: `월 예산이 ${budgetData.monthly.targetAmount.toLocaleString()}원으로 설정되었습니다.`,
});
}
} catch (error) { } catch (error) {
console.error('예산 데이터 저장 오류:', error); console.error('예산 데이터 저장 오류:', error);
// 오류 발생 시 토스트 알림
toast({
title: "예산 저장 실패",
description: "예산 데이터를 저장하는데 문제가 발생했습니다.",
variant: "destructive"
});
} }
}; };
@@ -53,10 +76,14 @@ export const saveBudgetDataToStorage = (budgetData: BudgetData): void => {
export const clearAllBudgetData = (): void => { export const clearAllBudgetData = (): void => {
try { try {
localStorage.removeItem('budgetData'); localStorage.removeItem('budgetData');
localStorage.removeItem('budgetData_backup');
// 기본값으로 재설정 // 기본값으로 재설정
const initialData = getInitialBudgetData(); const initialData = getInitialBudgetData();
const dataString = JSON.stringify(initialData); const dataString = JSON.stringify(initialData);
localStorage.setItem('budgetData', dataString); localStorage.setItem('budgetData', dataString);
localStorage.setItem('budgetData_backup', dataString);
console.log('예산 데이터가 초기화되었습니다.'); console.log('예산 데이터가 초기화되었습니다.');
// 스토리지 이벤트 수동 트리거 // 스토리지 이벤트 수동 트리거
@@ -65,7 +92,18 @@ export const clearAllBudgetData = (): void => {
key: 'budgetData', key: 'budgetData',
newValue: dataString newValue: dataString
})); }));
// 토스트 알림
toast({
title: "예산 초기화",
description: "모든, 예산 데이터가 초기화되었습니다.",
});
} catch (error) { } catch (error) {
console.error('예산 데이터 삭제 오류:', error); console.error('예산 데이터 삭제 오류:', error);
toast({
title: "초기화 실패",
description: "예산 데이터를 초기화하는데 문제가 발생했습니다.",
variant: "destructive"
});
} }
}; };

View File

@@ -1,17 +1,29 @@
import { DEFAULT_CATEGORY_BUDGETS } from '../budgetUtils'; import { DEFAULT_CATEGORY_BUDGETS } from '../budgetUtils';
import { toast } from '@/components/ui/use-toast';
/** /**
* 카테고리 예산 불러오기 * 카테고리 예산 불러오기
*/ */
export const loadCategoryBudgetsFromStorage = (): Record<string, number> => { export const loadCategoryBudgetsFromStorage = (): Record<string, number> => {
try { try {
// 메인 스토리지에서 시도
const storedCategoryBudgets = localStorage.getItem('categoryBudgets'); const storedCategoryBudgets = localStorage.getItem('categoryBudgets');
if (storedCategoryBudgets) { if (storedCategoryBudgets) {
const parsed = JSON.parse(storedCategoryBudgets); const parsed = JSON.parse(storedCategoryBudgets);
console.log('카테고리 예산 로드 완료:', parsed); console.log('카테고리 예산 로드 완료:', parsed);
return parsed; return parsed;
} }
// 백업에서 시도
const backupCategoryBudgets = localStorage.getItem('categoryBudgets_backup');
if (backupCategoryBudgets) {
const parsedBackup = JSON.parse(backupCategoryBudgets);
console.log('백업에서 카테고리 예산 복구:', parsedBackup);
// 메인 스토리지도 복구
localStorage.setItem('categoryBudgets', backupCategoryBudgets);
return parsedBackup;
}
} catch (error) { } catch (error) {
console.error('카테고리 예산 데이터 파싱 오류:', error); console.error('카테고리 예산 데이터 파싱 오류:', error);
} }
@@ -32,16 +44,42 @@ export const saveCategoryBudgetsToStorage = (categoryBudgets: Record<string, num
// 로컬 스토리지에 저장 // 로컬 스토리지에 저장
localStorage.setItem('categoryBudgets', dataString); localStorage.setItem('categoryBudgets', dataString);
// 백업 저장
localStorage.setItem('categoryBudgets_backup', dataString);
console.log('카테고리 예산 저장 완료:', categoryBudgets); console.log('카테고리 예산 저장 완료:', categoryBudgets);
// 스토리지 이벤트 수동 트리거 (동일 창에서도 감지하기 위함) // 스토리지 이벤트 수동 트리거 (동일 창에서도 감지하기 위함)
window.dispatchEvent(new Event('categoryBudgetsUpdated')); try {
window.dispatchEvent(new StorageEvent('storage', { window.dispatchEvent(new Event('categoryBudgetsUpdated'));
key: 'categoryBudgets', window.dispatchEvent(new StorageEvent('storage', {
newValue: dataString key: 'categoryBudgets',
})); newValue: dataString
}));
} catch (e) {
console.error('이벤트 발생 오류:', e);
}
// 마지막 저장 시간 기록 (데이터 검증용)
localStorage.setItem('lastCategoryBudgetSaveTime', new Date().toISOString());
// 토스트 알림
const totalBudget = Object.values(categoryBudgets).reduce((sum, val) => sum + val, 0);
if (totalBudget > 0) {
toast({
title: "카테고리 예산 저장 완료",
description: `카테고리별 예산 총 ${totalBudget.toLocaleString()}원이 설정되었습니다.`,
});
}
} catch (error) { } catch (error) {
console.error('카테고리 예산 저장 오류:', error); console.error('카테고리 예산 저장 오류:', error);
// 오류 발생 시 토스트 알림
toast({
title: "카테고리 예산 저장 실패",
description: "카테고리 예산을 저장하는데 문제가 발생했습니다.",
variant: "destructive"
});
} }
}; };
@@ -51,9 +89,13 @@ export const saveCategoryBudgetsToStorage = (categoryBudgets: Record<string, num
export const clearAllCategoryBudgets = (): void => { export const clearAllCategoryBudgets = (): void => {
try { try {
localStorage.removeItem('categoryBudgets'); localStorage.removeItem('categoryBudgets');
localStorage.removeItem('categoryBudgets_backup');
// 기본값으로 재설정 // 기본값으로 재설정
const dataString = JSON.stringify(DEFAULT_CATEGORY_BUDGETS); const dataString = JSON.stringify(DEFAULT_CATEGORY_BUDGETS);
localStorage.setItem('categoryBudgets', dataString); localStorage.setItem('categoryBudgets', dataString);
localStorage.setItem('categoryBudgets_backup', dataString);
console.log('카테고리 예산이 초기화되었습니다.'); console.log('카테고리 예산이 초기화되었습니다.');
// 이벤트 발생 // 이벤트 발생
@@ -62,7 +104,20 @@ export const clearAllCategoryBudgets = (): void => {
key: 'categoryBudgets', key: 'categoryBudgets',
newValue: dataString newValue: dataString
})); }));
// 토스트 알림
toast({
title: "카테고리 예산 초기화",
description: "모든 카테고리 예산이 기본값으로 초기화되었습니다.",
});
} catch (error) { } catch (error) {
console.error('카테고리 예산 삭제 오류:', error); console.error('카테고리 예산 삭제 오류:', error);
// 오류 발생 시 토스트 알림
toast({
title: "초기화 실패",
description: "카테고리 예산을 초기화하는데 문제가 발생했습니다.",
variant: "destructive"
});
} }
}; };

View File

@@ -1,17 +1,29 @@
import { Transaction } from '../types'; import { Transaction } from '../types';
import { toast } from '@/components/ui/use-toast';
/** /**
* 로컬 스토리지에서 트랜잭션 불러오기 * 로컬 스토리지에서 트랜잭션 불러오기
*/ */
export const loadTransactionsFromStorage = (): Transaction[] => { export const loadTransactionsFromStorage = (): Transaction[] => {
try { try {
// 메인 스토리지에서 먼저 시도
const storedTransactions = localStorage.getItem('transactions'); const storedTransactions = localStorage.getItem('transactions');
if (storedTransactions) { if (storedTransactions) {
const parsedData = JSON.parse(storedTransactions); const parsedData = JSON.parse(storedTransactions);
console.log('트랜잭션 로드 완료, 항목 수:', parsedData.length); console.log('트랜잭션 로드 완료, 항목 수:', parsedData.length);
return parsedData; return parsedData;
} }
// 백업에서 시도
const backupTransactions = localStorage.getItem('transactions_backup');
if (backupTransactions) {
const parsedBackup = JSON.parse(backupTransactions);
console.log('백업에서 트랜잭션 복구, 항목 수:', parsedBackup.length);
// 메인 스토리지도 복구
localStorage.setItem('transactions', backupTransactions);
return parsedBackup;
}
} catch (error) { } catch (error) {
console.error('트랜잭션 데이터 파싱 오류:', error); console.error('트랜잭션 데이터 파싱 오류:', error);
} }
@@ -28,16 +40,33 @@ export const saveTransactionsToStorage = (transactions: Transaction[]): void =>
// 로컬 스토리지에 저장 // 로컬 스토리지에 저장
localStorage.setItem('transactions', dataString); localStorage.setItem('transactions', dataString);
// 백업 저장
localStorage.setItem('transactions_backup', dataString);
console.log('트랜잭션 저장 완료, 항목 수:', transactions.length); console.log('트랜잭션 저장 완료, 항목 수:', transactions.length);
// 스토리지 이벤트 수동 트리거 (동일 창에서도 감지하기 위함) // 스토리지 이벤트 수동 트리거 (동일 창에서도 감지하기 위함)
window.dispatchEvent(new Event('transactionUpdated')); try {
window.dispatchEvent(new StorageEvent('storage', { window.dispatchEvent(new Event('transactionUpdated'));
key: 'transactions', window.dispatchEvent(new StorageEvent('storage', {
newValue: dataString key: 'transactions',
})); newValue: dataString
}));
} catch (e) {
console.error('이벤트 발생 오류:', e);
}
// 마지막 저장 시간 기록 (데이터 검증용)
localStorage.setItem('lastTransactionSaveTime', new Date().toISOString());
} catch (error) { } catch (error) {
console.error('트랜잭션 저장 오류:', error); console.error('트랜잭션 저장 오류:', error);
// 오류 발생 시 토스트 알림
toast({
title: "지출 저장 실패",
description: "지출 데이터를 저장하는데 문제가 발생했습니다.",
variant: "destructive"
});
} }
}; };
@@ -47,9 +76,13 @@ export const saveTransactionsToStorage = (transactions: Transaction[]): void =>
export const clearAllTransactions = (): void => { export const clearAllTransactions = (): void => {
try { try {
localStorage.removeItem('transactions'); localStorage.removeItem('transactions');
localStorage.removeItem('transactions_backup');
// 빈 배열을 저장하여 확실히 초기화 // 빈 배열을 저장하여 확실히 초기화
const emptyData = JSON.stringify([]); const emptyData = JSON.stringify([]);
localStorage.setItem('transactions', emptyData); localStorage.setItem('transactions', emptyData);
localStorage.setItem('transactions_backup', emptyData);
console.log('모든 트랜잭션이 삭제되었습니다.'); console.log('모든 트랜잭션이 삭제되었습니다.');
// 스토리지 이벤트 수동 트리거 // 스토리지 이벤트 수동 트리거
@@ -58,7 +91,20 @@ export const clearAllTransactions = (): void => {
key: 'transactions', key: 'transactions',
newValue: emptyData newValue: emptyData
})); }));
// 토스트 알림
toast({
title: "지출 내역 초기화",
description: "모든 지출 내역이 삭제되었습니다.",
});
} catch (error) { } catch (error) {
console.error('트랜잭션 삭제 오류:', error); console.error('트랜잭션 삭제 오류:', error);
// 오류 발생 시 토스트 알림
toast({
title: "초기화 실패",
description: "지출 내역을 초기화하는데 문제가 발생했습니다.",
variant: "destructive"
});
} }
}; };

View File

@@ -40,51 +40,98 @@ export const useBudgetState = () => {
console.log('- 예산 데이터:', budgetData); console.log('- 예산 데이터:', budgetData);
console.log('- 카테고리 예산:', categoryBudgets); console.log('- 카테고리 예산:', categoryBudgets);
console.log('- 트랜잭션 수:', transactions.length); console.log('- 트랜잭션 수:', transactions.length);
// 데이터 손실 방지를 위한 타이머 설정
const saveTimer = setInterval(() => {
// 저장된 데이터 유효성 검사 및 백업
try {
const storedBudgetData = localStorage.getItem('budgetData');
const storedCategoryBudgets = localStorage.getItem('categoryBudgets');
const storedTransactions = localStorage.getItem('transactions');
if (storedBudgetData) {
localStorage.setItem('budgetData_backup_auto', storedBudgetData);
}
if (storedCategoryBudgets) {
localStorage.setItem('categoryBudgets_backup_auto', storedCategoryBudgets);
}
if (storedTransactions) {
localStorage.setItem('transactions_backup_auto', storedTransactions);
}
} catch (error) {
console.error('자동 백업 중 오류:', error);
}
}, 5000); // 5초마다 백업
return () => {
clearInterval(saveTimer);
};
}, [budgetData, categoryBudgets, transactions]); }, [budgetData, categoryBudgets, transactions]);
// 카테고리별 예산 및 지출 계산 // 카테고리별 예산 및 지출 계산
useEffect(() => { useEffect(() => {
const totalMonthlyBudget = Object.values(categoryBudgets).reduce((sum, value) => sum + value, 0); try {
console.log('카테고리 예산 합계:', totalMonthlyBudget); const totalMonthlyBudget = Object.values(categoryBudgets).reduce((sum, value) => sum + value, 0);
console.log('카테고리 예산 합계:', totalMonthlyBudget);
if (totalMonthlyBudget > 0) {
const totalDailyBudget = Math.round(totalMonthlyBudget / 30);
const totalWeeklyBudget = Math.round(totalMonthlyBudget / 4.3);
const updatedBudgetData = {
daily: {
targetAmount: totalDailyBudget,
spentAmount: budgetData.daily.spentAmount,
remainingAmount: totalDailyBudget - budgetData.daily.spentAmount
},
weekly: {
targetAmount: totalWeeklyBudget,
spentAmount: budgetData.weekly.spentAmount,
remainingAmount: totalWeeklyBudget - budgetData.weekly.spentAmount
},
monthly: {
targetAmount: totalMonthlyBudget,
spentAmount: budgetData.monthly.spentAmount,
remainingAmount: totalMonthlyBudget - budgetData.monthly.spentAmount
}
};
// 로컬 상태 업데이트 if (totalMonthlyBudget > 0) {
handleBudgetGoalUpdate('monthly', totalMonthlyBudget); const totalDailyBudget = Math.round(totalMonthlyBudget / 30);
console.log('예산 데이터 자동 업데이트:', updatedBudgetData); const totalWeeklyBudget = Math.round(totalMonthlyBudget / 4.3);
const updatedBudgetData = {
daily: {
targetAmount: totalDailyBudget,
spentAmount: budgetData.daily.spentAmount,
remainingAmount: totalDailyBudget - budgetData.daily.spentAmount
},
weekly: {
targetAmount: totalWeeklyBudget,
spentAmount: budgetData.weekly.spentAmount,
remainingAmount: totalWeeklyBudget - budgetData.weekly.spentAmount
},
monthly: {
targetAmount: totalMonthlyBudget,
spentAmount: budgetData.monthly.spentAmount,
remainingAmount: totalMonthlyBudget - budgetData.monthly.spentAmount
}
};
// 로컬 상태 업데이트
handleBudgetGoalUpdate('monthly', totalMonthlyBudget);
console.log('예산 데이터 자동 업데이트:', updatedBudgetData);
}
} catch (error) {
console.error('카테고리 예산 계산 중 오류:', error);
} }
}, [categoryBudgets, handleBudgetGoalUpdate, budgetData]); }, [categoryBudgets, handleBudgetGoalUpdate, budgetData]);
// 모든 데이터 리셋 함수 // 모든 데이터 리셋 함수
const resetBudgetData = useCallback(() => { const resetBudgetData = useCallback(() => {
console.log('BudgetContext에서 데이터 리셋 시작'); try {
console.log('BudgetContext에서 데이터 리셋 시작');
// 로컬 스토리지 초기화
resetTransactions(); // 로컬 스토리지 초기화
resetCategoryBudgets(); resetTransactions();
resetBudgetDataInternal(); resetCategoryBudgets();
resetBudgetDataInternal();
console.log('BudgetContext에서 데이터 리셋 완료');
console.log('BudgetContext에서 데이터 리셋 완료');
// 토스트 알림
toast({
title: "모든 데이터 초기화",
description: "예산과 지출 내역이 모두 초기화되었습니다.",
});
} catch (error) {
console.error('데이터 초기화 중 오류:', error);
toast({
title: "초기화 실패",
description: "데이터를 초기화하는 중 오류가 발생했습니다.",
variant: "destructive"
});
}
}, [resetTransactions, resetCategoryBudgets, resetBudgetDataInternal]); }, [resetTransactions, resetCategoryBudgets, resetBudgetDataInternal]);
// 확장된 예산 목표 업데이트 함수 // 확장된 예산 목표 업데이트 함수
@@ -93,37 +140,55 @@ export const useBudgetState = () => {
amount: number, amount: number,
newCategoryBudgets?: Record<string, number> newCategoryBudgets?: Record<string, number>
) => { ) => {
console.log(`확장된 예산 목표 업데이트 호출: ${type}, 금액: ${amount}`); try {
console.log(`확장된 예산 목표 업데이트 호출: ${type}, 금액: ${amount}`);
// 카테고리 예산이 직접 업데이트된 경우
if (newCategoryBudgets) {
console.log('카테고리 예산 직접 업데이트:', newCategoryBudgets);
updateCategoryBudgets(newCategoryBudgets);
// 카테고리 예산이 직접 업데이트된 경우
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);
}
} catch (error) {
console.error('예산 목표 업데이트 중 오류:', error);
toast({ toast({
title: "카테고리 예산 업데이트 완료", title: "예산 업데이트 실패",
description: "카테고리별 예산이 저장되었습니다." description: "예산 목표를 업데이트하는 중 오류가 발생했습니다.",
variant: "destructive"
}); });
return; // 카테고리 예산이 변경되면 useEffect에서 자동으로 budgetData가 업데이트됩니다
}
// 월간 예산을 업데이트하고 일일, 주간도 자동 계산
if (type === 'monthly') {
console.log('월간 예산 업데이트:', amount);
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);
});
console.log('업데이트된 카테고리 예산:', updatedCategoryBudgets);
updateCategoryBudgets(updatedCategoryBudgets);
} else {
// 일일이나 주간 예산이 직접 업데이트되는 경우
console.log(`${type} 예산 직접 업데이트:`, amount);
handleBudgetGoalUpdate(type, amount);
} }
}, [budgetData, categoryBudgets, handleBudgetGoalUpdate, updateCategoryBudgets]); }, [budgetData, categoryBudgets, handleBudgetGoalUpdate, updateCategoryBudgets]);

View File

@@ -45,9 +45,41 @@ const Index = () => {
console.log('예산 데이터:', budgetData); console.log('예산 데이터:', budgetData);
// 수동으로 이벤트 발생시켜 데이터 갱신 // 수동으로 이벤트 발생시켜 데이터 갱신
window.dispatchEvent(new Event('transactionUpdated')); try {
window.dispatchEvent(new Event('budgetDataUpdated')); window.dispatchEvent(new Event('transactionUpdated'));
window.dispatchEvent(new Event('categoryBudgetsUpdated')); window.dispatchEvent(new Event('budgetDataUpdated'));
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
} catch (e) {
console.error('이벤트 발생 오류:', e);
}
// 백업된 데이터 복구 확인
try {
const budgetBackup = localStorage.getItem('budgetData_backup');
const categoryBackup = localStorage.getItem('categoryBudgets_backup');
const transactionBackup = localStorage.getItem('transactions_backup');
// 메인 데이터가 없지만 백업은 있는 경우 복구
if (!localStorage.getItem('budgetData') && budgetBackup) {
console.log('예산 데이터 백업에서 복구');
localStorage.setItem('budgetData', budgetBackup);
window.dispatchEvent(new Event('budgetDataUpdated'));
}
if (!localStorage.getItem('categoryBudgets') && categoryBackup) {
console.log('카테고리 예산 백업에서 복구');
localStorage.setItem('categoryBudgets', categoryBackup);
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
}
if (!localStorage.getItem('transactions') && transactionBackup) {
console.log('트랜잭션 백업에서 복구');
localStorage.setItem('transactions', transactionBackup);
window.dispatchEvent(new Event('transactionUpdated'));
}
} catch (error) {
console.error('백업 복구 시도 중 오류:', error);
}
}, []); }, []);
// 앱이 포커스를 얻었을 때 데이터를 새로고침 // 앱이 포커스를 얻었을 때 데이터를 새로고침
@@ -55,14 +87,40 @@ const Index = () => {
const handleFocus = () => { const handleFocus = () => {
console.log('창이 포커스를 얻음 - 데이터 새로고침'); console.log('창이 포커스를 얻음 - 데이터 새로고침');
// 이벤트 발생시켜 데이터 새로고침 // 이벤트 발생시켜 데이터 새로고침
window.dispatchEvent(new Event('storage')); try {
window.dispatchEvent(new Event('transactionUpdated')); window.dispatchEvent(new Event('storage'));
window.dispatchEvent(new Event('budgetDataUpdated')); window.dispatchEvent(new Event('transactionUpdated'));
window.dispatchEvent(new Event('categoryBudgetsUpdated')); window.dispatchEvent(new Event('budgetDataUpdated'));
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
} catch (e) {
console.error('이벤트 발생 오류:', e);
}
}; };
// 포커스 이벤트
window.addEventListener('focus', handleFocus); window.addEventListener('focus', handleFocus);
return () => window.removeEventListener('focus', handleFocus);
// 가시성 변경 이벤트 (백그라운드에서 전경으로 돌아올 때)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
console.log('페이지가 다시 보임 - 데이터 새로고침');
handleFocus();
}
});
// 정기적인 데이터 새로고침 (10초마다)
const refreshInterval = setInterval(() => {
if (document.visibilityState === 'visible') {
console.log('정기 새로고침 - 데이터 업데이트');
handleFocus();
}
}, 10000);
return () => {
window.removeEventListener('focus', handleFocus);
document.removeEventListener('visibilitychange', () => {});
clearInterval(refreshInterval);
};
}, []); }, []);
return ( return (