From d59fb97f7c511242de20f0fc31f6e16d76189452 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 16 Mar 2025 07:05:20 +0000 Subject: [PATCH] Fix budget update issues Addresses delayed notifications and data loss after budget updates and page transitions. --- src/components/BudgetInputCard.tsx | 17 +- src/components/CategoryBudgetInputs.tsx | 30 ++- .../budget/hooks/useBudgetDataState.ts | 104 +++++++--- .../budget/hooks/useCategoryBudgetState.ts | 64 +++++- src/contexts/budget/storage/budgetStorage.ts | 48 ++++- .../budget/storage/categoryStorage.ts | 65 +++++- .../budget/storage/transactionStorage.ts | 56 ++++- src/contexts/budget/useBudgetState.ts | 191 ++++++++++++------ src/pages/Index.tsx | 74 ++++++- 9 files changed, 523 insertions(+), 126 deletions(-) diff --git a/src/components/BudgetInputCard.tsx b/src/components/BudgetInputCard.tsx index 21fb52d..110489e 100644 --- a/src/components/BudgetInputCard.tsx +++ b/src/components/BudgetInputCard.tsx @@ -34,6 +34,7 @@ const BudgetInputCard: React.FC = ({ 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 = ({ 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); }; // 비어있으면 빈 문자열을, 그렇지 않으면 포맷팅된 문자열을 반환 diff --git a/src/components/CategoryBudgetInputs.tsx b/src/components/CategoryBudgetInputs.tsx index c03a5da..f2c4215 100644 --- a/src/components/CategoryBudgetInputs.tsx +++ b/src/components/CategoryBudgetInputs.tsx @@ -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; @@ -14,6 +15,7 @@ const CategoryBudgetInputs: React.FC = ({ handleCategoryInputChange }) => { const isMobile = useIsMobile(); + const previousBudgetsRef = useRef>({}); // Format number with commas for display const formatWithCommas = (value: number): string => { @@ -26,6 +28,12 @@ const CategoryBudgetInputs: React.FC = ({ // 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 = ({ }; }, []); + // 값이 변경될 때마다 토스트 메시지 표시 + 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 (
{EXPENSE_CATEGORIES.map(category => ( @@ -53,7 +79,7 @@ const CategoryBudgetInputs: React.FC = ({ 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`} />
))} diff --git a/src/contexts/budget/hooks/useBudgetDataState.ts b/src/contexts/budget/hooks/useBudgetDataState.ts index 0606520..3d497c7 100644 --- a/src/contexts/budget/hooks/useBudgetDataState.ts +++ b/src/contexts/budget/hooks/useBudgetDataState.ts @@ -16,14 +16,26 @@ import { export const useBudgetDataState = (transactions: any[]) => { const [budgetData, setBudgetData] = useState(loadBudgetDataFromStorage()); const [selectedTab, setSelectedTab] = useState("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 ) => { - 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" + }); + } }, []); // 예산 데이터 변경 시 로그 기록 diff --git a/src/contexts/budget/hooks/useCategoryBudgetState.ts b/src/contexts/budget/hooks/useCategoryBudgetState.ts index d77886b..498746b 100644 --- a/src/contexts/budget/hooks/useCategoryBudgetState.ts +++ b/src/contexts/budget/hooks/useCategoryBudgetState.ts @@ -11,14 +11,26 @@ export const useCategoryBudgetState = () => { const [categoryBudgets, setCategoryBudgets] = useState>( 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) => { - 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); + } }, []); // 카테고리 예산 변경 시 로그 기록 diff --git a/src/contexts/budget/storage/budgetStorage.ts b/src/contexts/budget/storage/budgetStorage.ts index a428b93..a4ef446 100644 --- a/src/contexts/budget/storage/budgetStorage.ts +++ b/src/contexts/budget/storage/budgetStorage.ts @@ -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" + }); } }; diff --git a/src/contexts/budget/storage/categoryStorage.ts b/src/contexts/budget/storage/categoryStorage.ts index a0d2ba3..cd236c2 100644 --- a/src/contexts/budget/storage/categoryStorage.ts +++ b/src/contexts/budget/storage/categoryStorage.ts @@ -1,17 +1,29 @@ import { DEFAULT_CATEGORY_BUDGETS } from '../budgetUtils'; +import { toast } from '@/components/ui/use-toast'; /** * 카테고리 예산 불러오기 */ export const loadCategoryBudgetsFromStorage = (): Record => { 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 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 { 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" + }); } }; diff --git a/src/contexts/budget/storage/transactionStorage.ts b/src/contexts/budget/storage/transactionStorage.ts index fd07d24..78b01cd 100644 --- a/src/contexts/budget/storage/transactionStorage.ts +++ b/src/contexts/budget/storage/transactionStorage.ts @@ -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" + }); } }; diff --git a/src/contexts/budget/useBudgetState.ts b/src/contexts/budget/useBudgetState.ts index 8303f93..e55fa40 100644 --- a/src/contexts/budget/useBudgetState.ts +++ b/src/contexts/budget/useBudgetState.ts @@ -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 ) => { - 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 = {}; + + // 비율에 따라 카테고리 예산 업데이트 + 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 = {}; - - 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]); diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 6cfdb2e..b189ae6 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -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 (