Fix budget update issues
Addresses delayed notifications and data loss after budget updates and page transitions.
This commit is contained in:
@@ -34,6 +34,7 @@ const BudgetInputCard: React.FC<BudgetGoalProps> = ({
|
||||
return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
};
|
||||
|
||||
// 초기값 변경시 입력 필드 값 업데이트
|
||||
useEffect(() => {
|
||||
setBudgetInputs({
|
||||
daily: initialBudgets.daily > 0 ? initialBudgets.daily.toString() : '',
|
||||
@@ -53,9 +54,21 @@ const BudgetInputCard: React.FC<BudgetGoalProps> = ({
|
||||
|
||||
const handleSave = () => {
|
||||
const amount = parseInt(budgetInputs[selectedTab].replace(/,/g, ''), 10) || 0;
|
||||
onSave(selectedTab, amount);
|
||||
// Close the collapsible after saving
|
||||
if (amount <= 0) {
|
||||
return; // 0 이하의 금액은 저장하지 않음
|
||||
}
|
||||
|
||||
// 즉시 입력 필드를 업데이트하여 사용자에게 피드백 제공
|
||||
setBudgetInputs(prev => ({
|
||||
...prev,
|
||||
[selectedTab]: amount.toString()
|
||||
}));
|
||||
|
||||
// 즉시 콜랩시블을 닫아 사용자에게 완료 피드백 제공
|
||||
setIsOpen(false);
|
||||
|
||||
// 예산 저장
|
||||
onSave(selectedTab, amount);
|
||||
};
|
||||
|
||||
// 비어있으면 빈 문자열을, 그렇지 않으면 포맷팅된 문자열을 반환
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { toast } from '@/components/ui/use-toast';
|
||||
|
||||
interface CategoryBudgetInputsProps {
|
||||
categoryBudgets: Record<string, number>;
|
||||
@@ -14,6 +15,7 @@ const CategoryBudgetInputs: React.FC<CategoryBudgetInputsProps> = ({
|
||||
handleCategoryInputChange
|
||||
}) => {
|
||||
const isMobile = useIsMobile();
|
||||
const previousBudgetsRef = useRef<Record<string, number>>({});
|
||||
|
||||
// Format number with commas for display
|
||||
const formatWithCommas = (value: number): string => {
|
||||
@@ -26,6 +28,12 @@ const CategoryBudgetInputs: React.FC<CategoryBudgetInputsProps> = ({
|
||||
// Remove all non-numeric characters before passing to parent handler
|
||||
const numericValue = e.target.value.replace(/[^0-9]/g, '');
|
||||
handleCategoryInputChange(numericValue, category);
|
||||
|
||||
// 사용자에게 시각적 피드백 제공
|
||||
e.target.classList.add('border-green-500');
|
||||
setTimeout(() => {
|
||||
e.target.classList.remove('border-green-500');
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// 컴포넌트가 마운트될 때 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 (
|
||||
<div className="space-y-2 w-full">
|
||||
{EXPENSE_CATEGORIES.map(category => (
|
||||
@@ -53,7 +79,7 @@ const CategoryBudgetInputs: React.FC<CategoryBudgetInputsProps> = ({
|
||||
value={formatWithCommas(categoryBudgets[category] || 0)}
|
||||
onChange={(e) => handleInput(e, category)}
|
||||
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>
|
||||
))}
|
||||
|
||||
@@ -16,14 +16,26 @@ import {
|
||||
export const useBudgetDataState = (transactions: any[]) => {
|
||||
const [budgetData, setBudgetData] = useState<BudgetData>(loadBudgetDataFromStorage());
|
||||
const [selectedTab, setSelectedTab] = useState<BudgetPeriod>("daily");
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
// 초기 로드 및 이벤트 리스너 설정
|
||||
useEffect(() => {
|
||||
const loadBudget = () => {
|
||||
console.log('예산 데이터 로드 시도 중...');
|
||||
const loadedData = loadBudgetDataFromStorage();
|
||||
console.log('예산 데이터 로드됨:', loadedData);
|
||||
setBudgetData(loadedData);
|
||||
try {
|
||||
console.log('예산 데이터 로드 시도 중...');
|
||||
const loadedData = loadBudgetDataFromStorage();
|
||||
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('storage', handleBudgetUpdate);
|
||||
window.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
console.log('페이지 보임: 예산 데이터 새로고침');
|
||||
loadBudget();
|
||||
}
|
||||
});
|
||||
window.addEventListener('focus', () => {
|
||||
console.log('창 포커스: 예산 데이터 새로고침');
|
||||
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 () => {
|
||||
window.removeEventListener('budgetDataUpdated', () => handleBudgetUpdate());
|
||||
window.removeEventListener('storage', handleBudgetUpdate);
|
||||
window.removeEventListener('visibilitychange', () => {});
|
||||
window.removeEventListener('focus', () => loadBudget());
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, []);
|
||||
}, [isInitialized]);
|
||||
|
||||
// 트랜잭션 변경 시 지출 금액 업데이트
|
||||
useEffect(() => {
|
||||
if (transactions.length > 0) {
|
||||
console.log('트랜잭션 변경으로 인한 예산 데이터 업데이트. 트랜잭션 수:', transactions.length);
|
||||
// 지출 금액 업데이트
|
||||
const updatedBudgetData = calculateSpentAmounts(transactions, budgetData);
|
||||
|
||||
// 상태 및 스토리지 모두 업데이트
|
||||
setBudgetData(updatedBudgetData);
|
||||
saveBudgetDataToStorage(updatedBudgetData);
|
||||
try {
|
||||
// 지출 금액 업데이트
|
||||
const updatedBudgetData = calculateSpentAmounts(transactions, budgetData);
|
||||
|
||||
// 상태 및 스토리지 모두 업데이트
|
||||
setBudgetData(updatedBudgetData);
|
||||
saveBudgetDataToStorage(updatedBudgetData);
|
||||
|
||||
// 저장 시간 업데이트
|
||||
localStorage.setItem('lastBudgetSaveTime', new Date().toISOString());
|
||||
} catch (error) {
|
||||
console.error('예산 데이터 업데이트 중 오류:', error);
|
||||
}
|
||||
}
|
||||
}, [transactions]);
|
||||
}, [transactions, budgetData]);
|
||||
|
||||
// 예산 목표 업데이트 함수
|
||||
const handleBudgetGoalUpdate = useCallback((
|
||||
@@ -70,26 +108,42 @@ export const useBudgetDataState = (transactions: any[]) => {
|
||||
amount: number,
|
||||
newCategoryBudgets?: Record<string, number>
|
||||
) => {
|
||||
console.log(`예산 목표 업데이트: ${type}, 금액: ${amount}`);
|
||||
// 월간 예산 직접 업데이트 (카테고리 예산이 없는 경우)
|
||||
if (!newCategoryBudgets) {
|
||||
const updatedBudgetData = calculateUpdatedBudgetData(budgetData, type, amount);
|
||||
console.log('새 예산 데이터:', updatedBudgetData);
|
||||
setBudgetData(updatedBudgetData);
|
||||
saveBudgetDataToStorage(updatedBudgetData);
|
||||
|
||||
try {
|
||||
console.log(`예산 목표 업데이트: ${type}, 금액: ${amount}`);
|
||||
// 월간 예산 직접 업데이트 (카테고리 예산이 없는 경우)
|
||||
if (!newCategoryBudgets) {
|
||||
const updatedBudgetData = calculateUpdatedBudgetData(budgetData, type, amount);
|
||||
console.log('새 예산 데이터:', updatedBudgetData);
|
||||
setBudgetData(updatedBudgetData);
|
||||
saveBudgetDataToStorage(updatedBudgetData);
|
||||
|
||||
// 저장 시간 업데이트
|
||||
localStorage.setItem('lastBudgetSaveTime', new Date().toISOString());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('예산 목표 업데이트 중 오류:', error);
|
||||
toast({
|
||||
title: "목표 업데이트 완료",
|
||||
description: `${type === 'daily' ? '일일' : type === 'weekly' ? '주간' : '월간'} 목표가 ${amount.toLocaleString()}원으로 설정되었습니다.`
|
||||
title: "예산 업데이트 실패",
|
||||
description: "예산 목표를 업데이트하는데 문제가 발생했습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
}, [budgetData]);
|
||||
|
||||
// 예산 데이터 초기화 함수
|
||||
const resetBudgetData = useCallback(() => {
|
||||
console.log('예산 데이터 초기화');
|
||||
clearAllBudgetData();
|
||||
setBudgetData(loadBudgetDataFromStorage());
|
||||
try {
|
||||
console.log('예산 데이터 초기화');
|
||||
clearAllBudgetData();
|
||||
setBudgetData(loadBudgetDataFromStorage());
|
||||
} catch (error) {
|
||||
console.error('예산 데이터 초기화 중 오류:', error);
|
||||
toast({
|
||||
title: "예산 초기화 실패",
|
||||
description: "예산 데이터를 초기화하는데 문제가 발생했습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 예산 데이터 변경 시 로그 기록
|
||||
|
||||
@@ -11,14 +11,26 @@ export const useCategoryBudgetState = () => {
|
||||
const [categoryBudgets, setCategoryBudgets] = useState<Record<string, number>>(
|
||||
loadCategoryBudgetsFromStorage()
|
||||
);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
// 초기 로드 및 이벤트 리스너 설정
|
||||
useEffect(() => {
|
||||
const loadCategories = () => {
|
||||
console.log('카테고리 예산 로드 시도 중...');
|
||||
const loaded = loadCategoryBudgetsFromStorage();
|
||||
console.log('카테고리 예산 로드됨:', loaded);
|
||||
setCategoryBudgets(loaded);
|
||||
try {
|
||||
console.log('카테고리 예산 로드 시도 중...');
|
||||
const loaded = loadCategoryBudgetsFromStorage();
|
||||
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('storage', handleCategoryUpdate);
|
||||
window.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
console.log('페이지 보임: 카테고리 예산 새로고침');
|
||||
loadCategories();
|
||||
}
|
||||
});
|
||||
window.addEventListener('focus', () => {
|
||||
console.log('창 포커스: 카테고리 예산 새로고침');
|
||||
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 () => {
|
||||
window.removeEventListener('categoryBudgetsUpdated', () => handleCategoryUpdate());
|
||||
window.removeEventListener('storage', handleCategoryUpdate);
|
||||
window.removeEventListener('visibilitychange', () => {});
|
||||
window.removeEventListener('focus', () => loadCategories());
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, []);
|
||||
}, [isInitialized]);
|
||||
|
||||
// 카테고리 예산 업데이트 함수
|
||||
const updateCategoryBudgets = useCallback((newCategoryBudgets: Record<string, number>) => {
|
||||
console.log('카테고리 예산 업데이트:', newCategoryBudgets);
|
||||
setCategoryBudgets(newCategoryBudgets);
|
||||
saveCategoryBudgetsToStorage(newCategoryBudgets);
|
||||
try {
|
||||
console.log('카테고리 예산 업데이트:', newCategoryBudgets);
|
||||
setCategoryBudgets(newCategoryBudgets);
|
||||
saveCategoryBudgetsToStorage(newCategoryBudgets);
|
||||
|
||||
// 저장 시간 업데이트
|
||||
localStorage.setItem('lastCategoryBudgetSaveTime', new Date().toISOString());
|
||||
} catch (error) {
|
||||
console.error('카테고리 예산 업데이트 중 오류:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 카테고리 예산 초기화 함수
|
||||
const resetCategoryBudgets = useCallback(() => {
|
||||
console.log('카테고리 예산 초기화');
|
||||
clearAllCategoryBudgets();
|
||||
setCategoryBudgets(loadCategoryBudgetsFromStorage());
|
||||
try {
|
||||
console.log('카테고리 예산 초기화');
|
||||
clearAllCategoryBudgets();
|
||||
setCategoryBudgets(loadCategoryBudgetsFromStorage());
|
||||
} catch (error) {
|
||||
console.error('카테고리 예산 초기화 중 오류:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 카테고리 예산 변경 시 로그 기록
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
import { BudgetData } from '../types';
|
||||
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);
|
||||
console.log('예산 데이터 저장 완료', budgetData);
|
||||
|
||||
// 중요: 즉시 자동 백업 (데이터 손실 방지)
|
||||
localStorage.setItem('budgetData_backup', dataString);
|
||||
|
||||
// 스토리지 이벤트 수동 트리거 (동일 창에서도 감지하기 위함)
|
||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||
window.dispatchEvent(new StorageEvent('storage', {
|
||||
key: 'budgetData',
|
||||
newValue: dataString
|
||||
}));
|
||||
try {
|
||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||
window.dispatchEvent(new StorageEvent('storage', {
|
||||
key: 'budgetData',
|
||||
newValue: dataString
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('이벤트 발생 오류:', e);
|
||||
}
|
||||
|
||||
// toast 알림은 즉시 표시
|
||||
if (budgetData.monthly.targetAmount > 0) {
|
||||
toast({
|
||||
title: "예산 저장 완료",
|
||||
description: `월 예산이 ${budgetData.monthly.targetAmount.toLocaleString()}원으로 설정되었습니다.`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('예산 데이터 저장 오류:', error);
|
||||
|
||||
// 오류 발생 시 토스트 알림
|
||||
toast({
|
||||
title: "예산 저장 실패",
|
||||
description: "예산 데이터를 저장하는데 문제가 발생했습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,10 +76,14 @@ export const saveBudgetDataToStorage = (budgetData: BudgetData): void => {
|
||||
export const clearAllBudgetData = (): void => {
|
||||
try {
|
||||
localStorage.removeItem('budgetData');
|
||||
localStorage.removeItem('budgetData_backup');
|
||||
|
||||
// 기본값으로 재설정
|
||||
const initialData = getInitialBudgetData();
|
||||
const dataString = JSON.stringify(initialData);
|
||||
localStorage.setItem('budgetData', dataString);
|
||||
localStorage.setItem('budgetData_backup', dataString);
|
||||
|
||||
console.log('예산 데이터가 초기화되었습니다.');
|
||||
|
||||
// 스토리지 이벤트 수동 트리거
|
||||
@@ -65,7 +92,18 @@ export const clearAllBudgetData = (): void => {
|
||||
key: 'budgetData',
|
||||
newValue: dataString
|
||||
}));
|
||||
|
||||
// 토스트 알림
|
||||
toast({
|
||||
title: "예산 초기화",
|
||||
description: "모든, 예산 데이터가 초기화되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('예산 데이터 삭제 오류:', error);
|
||||
toast({
|
||||
title: "초기화 실패",
|
||||
description: "예산 데이터를 초기화하는데 문제가 발생했습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
|
||||
import { DEFAULT_CATEGORY_BUDGETS } from '../budgetUtils';
|
||||
import { toast } from '@/components/ui/use-toast';
|
||||
|
||||
/**
|
||||
* 카테고리 예산 불러오기
|
||||
*/
|
||||
export const loadCategoryBudgetsFromStorage = (): Record<string, number> => {
|
||||
try {
|
||||
// 메인 스토리지에서 시도
|
||||
const storedCategoryBudgets = localStorage.getItem('categoryBudgets');
|
||||
if (storedCategoryBudgets) {
|
||||
const parsed = JSON.parse(storedCategoryBudgets);
|
||||
console.log('카테고리 예산 로드 완료:', 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) {
|
||||
console.error('카테고리 예산 데이터 파싱 오류:', error);
|
||||
}
|
||||
@@ -32,16 +44,42 @@ export const saveCategoryBudgetsToStorage = (categoryBudgets: Record<string, num
|
||||
|
||||
// 로컬 스토리지에 저장
|
||||
localStorage.setItem('categoryBudgets', dataString);
|
||||
// 백업 저장
|
||||
localStorage.setItem('categoryBudgets_backup', dataString);
|
||||
|
||||
console.log('카테고리 예산 저장 완료:', categoryBudgets);
|
||||
|
||||
// 스토리지 이벤트 수동 트리거 (동일 창에서도 감지하기 위함)
|
||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||
window.dispatchEvent(new StorageEvent('storage', {
|
||||
key: 'categoryBudgets',
|
||||
newValue: dataString
|
||||
}));
|
||||
try {
|
||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||
window.dispatchEvent(new StorageEvent('storage', {
|
||||
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) {
|
||||
console.error('카테고리 예산 저장 오류:', error);
|
||||
|
||||
// 오류 발생 시 토스트 알림
|
||||
toast({
|
||||
title: "카테고리 예산 저장 실패",
|
||||
description: "카테고리 예산을 저장하는데 문제가 발생했습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -51,9 +89,13 @@ export const saveCategoryBudgetsToStorage = (categoryBudgets: Record<string, num
|
||||
export const clearAllCategoryBudgets = (): void => {
|
||||
try {
|
||||
localStorage.removeItem('categoryBudgets');
|
||||
localStorage.removeItem('categoryBudgets_backup');
|
||||
|
||||
// 기본값으로 재설정
|
||||
const dataString = JSON.stringify(DEFAULT_CATEGORY_BUDGETS);
|
||||
localStorage.setItem('categoryBudgets', dataString);
|
||||
localStorage.setItem('categoryBudgets_backup', dataString);
|
||||
|
||||
console.log('카테고리 예산이 초기화되었습니다.');
|
||||
|
||||
// 이벤트 발생
|
||||
@@ -62,7 +104,20 @@ export const clearAllCategoryBudgets = (): void => {
|
||||
key: 'categoryBudgets',
|
||||
newValue: dataString
|
||||
}));
|
||||
|
||||
// 토스트 알림
|
||||
toast({
|
||||
title: "카테고리 예산 초기화",
|
||||
description: "모든 카테고리 예산이 기본값으로 초기화되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('카테고리 예산 삭제 오류:', error);
|
||||
|
||||
// 오류 발생 시 토스트 알림
|
||||
toast({
|
||||
title: "초기화 실패",
|
||||
description: "카테고리 예산을 초기화하는데 문제가 발생했습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
|
||||
import { Transaction } from '../types';
|
||||
import { toast } from '@/components/ui/use-toast';
|
||||
|
||||
/**
|
||||
* 로컬 스토리지에서 트랜잭션 불러오기
|
||||
*/
|
||||
export const loadTransactionsFromStorage = (): Transaction[] => {
|
||||
try {
|
||||
// 메인 스토리지에서 먼저 시도
|
||||
const storedTransactions = localStorage.getItem('transactions');
|
||||
if (storedTransactions) {
|
||||
const parsedData = JSON.parse(storedTransactions);
|
||||
console.log('트랜잭션 로드 완료, 항목 수:', parsedData.length);
|
||||
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) {
|
||||
console.error('트랜잭션 데이터 파싱 오류:', error);
|
||||
}
|
||||
@@ -28,16 +40,33 @@ export const saveTransactionsToStorage = (transactions: Transaction[]): void =>
|
||||
|
||||
// 로컬 스토리지에 저장
|
||||
localStorage.setItem('transactions', dataString);
|
||||
// 백업 저장
|
||||
localStorage.setItem('transactions_backup', dataString);
|
||||
|
||||
console.log('트랜잭션 저장 완료, 항목 수:', transactions.length);
|
||||
|
||||
// 스토리지 이벤트 수동 트리거 (동일 창에서도 감지하기 위함)
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
window.dispatchEvent(new StorageEvent('storage', {
|
||||
key: 'transactions',
|
||||
newValue: dataString
|
||||
}));
|
||||
try {
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
window.dispatchEvent(new StorageEvent('storage', {
|
||||
key: 'transactions',
|
||||
newValue: dataString
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('이벤트 발생 오류:', e);
|
||||
}
|
||||
|
||||
// 마지막 저장 시간 기록 (데이터 검증용)
|
||||
localStorage.setItem('lastTransactionSaveTime', new Date().toISOString());
|
||||
} catch (error) {
|
||||
console.error('트랜잭션 저장 오류:', error);
|
||||
|
||||
// 오류 발생 시 토스트 알림
|
||||
toast({
|
||||
title: "지출 저장 실패",
|
||||
description: "지출 데이터를 저장하는데 문제가 발생했습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -47,9 +76,13 @@ export const saveTransactionsToStorage = (transactions: Transaction[]): void =>
|
||||
export const clearAllTransactions = (): void => {
|
||||
try {
|
||||
localStorage.removeItem('transactions');
|
||||
localStorage.removeItem('transactions_backup');
|
||||
|
||||
// 빈 배열을 저장하여 확실히 초기화
|
||||
const emptyData = JSON.stringify([]);
|
||||
localStorage.setItem('transactions', emptyData);
|
||||
localStorage.setItem('transactions_backup', emptyData);
|
||||
|
||||
console.log('모든 트랜잭션이 삭제되었습니다.');
|
||||
|
||||
// 스토리지 이벤트 수동 트리거
|
||||
@@ -58,7 +91,20 @@ export const clearAllTransactions = (): void => {
|
||||
key: 'transactions',
|
||||
newValue: emptyData
|
||||
}));
|
||||
|
||||
// 토스트 알림
|
||||
toast({
|
||||
title: "지출 내역 초기화",
|
||||
description: "모든 지출 내역이 삭제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('트랜잭션 삭제 오류:', error);
|
||||
|
||||
// 오류 발생 시 토스트 알림
|
||||
toast({
|
||||
title: "초기화 실패",
|
||||
description: "지출 내역을 초기화하는데 문제가 발생했습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,51 +40,98 @@ export const useBudgetState = () => {
|
||||
console.log('- 예산 데이터:', budgetData);
|
||||
console.log('- 카테고리 예산:', categoryBudgets);
|
||||
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]);
|
||||
|
||||
// 카테고리별 예산 및 지출 계산
|
||||
useEffect(() => {
|
||||
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
|
||||
}
|
||||
};
|
||||
try {
|
||||
const totalMonthlyBudget = Object.values(categoryBudgets).reduce((sum, value) => sum + value, 0);
|
||||
console.log('카테고리 예산 합계:', totalMonthlyBudget);
|
||||
|
||||
// 로컬 상태 업데이트
|
||||
handleBudgetGoalUpdate('monthly', totalMonthlyBudget);
|
||||
console.log('예산 데이터 자동 업데이트:', updatedBudgetData);
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
// 로컬 상태 업데이트
|
||||
handleBudgetGoalUpdate('monthly', totalMonthlyBudget);
|
||||
console.log('예산 데이터 자동 업데이트:', updatedBudgetData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('카테고리 예산 계산 중 오류:', error);
|
||||
}
|
||||
}, [categoryBudgets, handleBudgetGoalUpdate, budgetData]);
|
||||
|
||||
// 모든 데이터 리셋 함수
|
||||
const resetBudgetData = useCallback(() => {
|
||||
console.log('BudgetContext에서 데이터 리셋 시작');
|
||||
|
||||
// 로컬 스토리지 초기화
|
||||
resetTransactions();
|
||||
resetCategoryBudgets();
|
||||
resetBudgetDataInternal();
|
||||
|
||||
console.log('BudgetContext에서 데이터 리셋 완료');
|
||||
try {
|
||||
console.log('BudgetContext에서 데이터 리셋 시작');
|
||||
|
||||
// 로컬 스토리지 초기화
|
||||
resetTransactions();
|
||||
resetCategoryBudgets();
|
||||
resetBudgetDataInternal();
|
||||
|
||||
console.log('BudgetContext에서 데이터 리셋 완료');
|
||||
|
||||
// 토스트 알림
|
||||
toast({
|
||||
title: "모든 데이터 초기화",
|
||||
description: "예산과 지출 내역이 모두 초기화되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('데이터 초기화 중 오류:', error);
|
||||
toast({
|
||||
title: "초기화 실패",
|
||||
description: "데이터를 초기화하는 중 오류가 발생했습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
}, [resetTransactions, resetCategoryBudgets, resetBudgetDataInternal]);
|
||||
|
||||
// 확장된 예산 목표 업데이트 함수
|
||||
@@ -93,37 +140,55 @@ export const useBudgetState = () => {
|
||||
amount: number,
|
||||
newCategoryBudgets?: Record<string, number>
|
||||
) => {
|
||||
console.log(`확장된 예산 목표 업데이트 호출: ${type}, 금액: ${amount}`);
|
||||
|
||||
// 카테고리 예산이 직접 업데이트된 경우
|
||||
if (newCategoryBudgets) {
|
||||
console.log('카테고리 예산 직접 업데이트:', newCategoryBudgets);
|
||||
updateCategoryBudgets(newCategoryBudgets);
|
||||
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<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({
|
||||
title: "카테고리 예산 업데이트 완료",
|
||||
description: "카테고리별 예산이 저장되었습니다."
|
||||
title: "예산 업데이트 실패",
|
||||
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]);
|
||||
|
||||
|
||||
@@ -45,9 +45,41 @@ const Index = () => {
|
||||
console.log('예산 데이터:', budgetData);
|
||||
|
||||
// 수동으로 이벤트 발생시켜 데이터 갱신
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||
try {
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
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 = () => {
|
||||
console.log('창이 포커스를 얻음 - 데이터 새로고침');
|
||||
// 이벤트 발생시켜 데이터 새로고침
|
||||
window.dispatchEvent(new Event('storage'));
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// 포커스 이벤트
|
||||
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 (
|
||||
|
||||
Reference in New Issue
Block a user