diff --git a/src/contexts/budget/budgetUtils.ts b/src/contexts/budget/budgetUtils.ts index fb7858f..6458d24 100644 --- a/src/contexts/budget/budgetUtils.ts +++ b/src/contexts/budget/budgetUtils.ts @@ -1,232 +1,24 @@ -import { BudgetData, BudgetPeriod, CategoryBudget, Transaction } from './types'; -import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons'; +// 분리된 유틸리티 파일들에서 함수와 상수를 재내보내기 +export { + DEFAULT_CATEGORY_BUDGETS, + DEFAULT_MONTHLY_BUDGET, + getInitialBudgetData +} from './utils/constants'; -// 기본 데이터 상수 (기본값을 0으로 설정) -export const DEFAULT_CATEGORY_BUDGETS: Record = { - 음식: 0, - 쇼핑: 0, - 교통: 0, - 기타: 0 -}; +export { + calculateCategorySpending +} from './utils/categoryUtils'; -export const DEFAULT_MONTHLY_BUDGET = 0; +export { + calculateUpdatedBudgetData +} from './utils/budgetCalculation'; -// 카테고리별 지출 계산 -export const calculateCategorySpending = ( - transactions: Transaction[], - categoryBudgets: Record -): CategoryBudget[] => { - const expenseTransactions = transactions.filter(t => t.type === 'expense'); - const categorySpending: Record = {}; - - // 모든 카테고리에 대해 초기값 0 설정 - Object.keys(categoryBudgets).forEach(category => { - // 정의된 카테고리만 유지 - if (EXPENSE_CATEGORIES.includes(category)) { - categorySpending[category] = 0; - } - }); - - // 지원되는 카테고리가 없을 경우 기본값 설정 - if (Object.keys(categorySpending).length === 0) { - EXPENSE_CATEGORIES.forEach(category => { - categorySpending[category] = 0; - }); - } - - expenseTransactions.forEach(t => { - if (t.category in categorySpending) { - categorySpending[t.category] += t.amount; - } else if (EXPENSE_CATEGORIES.includes(t.category)) { - // 지원되는 카테고리이지만 초기화되지 않은 경우 - categorySpending[t.category] = t.amount; - } else if (t.category === '교통비') { - // 예전 카테고리명 '교통비'를 '교통'으로 매핑 - categorySpending['교통'] = (categorySpending['교통'] || 0) + t.amount; - } else { - // 지원되지 않는 카테고리는 '기타'로 집계 - categorySpending['기타'] = (categorySpending['기타'] || 0) + t.amount; - } - }); - - return EXPENSE_CATEGORIES.map(category => ({ - title: category, - current: categorySpending[category] || 0, - total: categoryBudgets[category] || 0 - })); -}; +export { + calculateSpentAmounts +} from './utils/spendingCalculation'; -// 예산 데이터 업데이트 계산 - 수정된 함수 -export const calculateUpdatedBudgetData = ( - prevBudgetData: BudgetData, - type: BudgetPeriod, - amount: number -): BudgetData => { - console.log(`예산 업데이트 계산 시작: 타입=${type}, 금액=${amount}`); - - // 값이 없거나 유효하지 않은 경우 로깅 - if (!prevBudgetData) { - console.error('이전 예산 데이터가 없습니다. 기본값 사용.'); - prevBudgetData = getInitialBudgetData(); - } - - // 선택된 타입에 따라 다른 타입의 예산도 자동으로 계산 - let monthlyAmount: number, weeklyAmount: number, dailyAmount: number; - - if (type === 'monthly') { - // 월간 예산이 직접 입력된 경우 - monthlyAmount = amount; - // 월 30일 기준 (실제 사용 시 값 확인) - dailyAmount = Math.round(monthlyAmount / 30); - // 월 4.3주 기준 (실제 사용 시 값 확인) - weeklyAmount = Math.round(monthlyAmount / 4.3); - } else if (type === 'weekly') { - // 주간 예산이 직접 입력된 경우 - weeklyAmount = amount; - monthlyAmount = Math.round(weeklyAmount * 4.3); - dailyAmount = Math.round(weeklyAmount / 7); - } else { // 'daily' - // 일일 예산이 직접 입력된 경우 - dailyAmount = amount; - weeklyAmount = Math.round(dailyAmount * 7); - monthlyAmount = Math.round(dailyAmount * 30); - } - - // 모든 금액이 최소한 0 이상이 되도록 보장 - monthlyAmount = Math.max(0, monthlyAmount); - weeklyAmount = Math.max(0, weeklyAmount); - dailyAmount = Math.max(0, dailyAmount); - - console.log(`최종 예산 계산 결과: 월간=${monthlyAmount}원, 주간=${weeklyAmount}원, 일일=${dailyAmount}원`); - - // 로그에 이전 예산 데이터 출력 - console.log("이전 예산 데이터:", JSON.stringify(prevBudgetData)); - - // 이전 지출 데이터 보존 - const dailySpent = prevBudgetData.daily?.spentAmount || 0; - const weeklySpent = prevBudgetData.weekly?.spentAmount || 0; - const monthlySpent = prevBudgetData.monthly?.spentAmount || 0; - - // 새 예산 데이터 생성 (spentAmount는 이전 값 유지) - const updatedBudgetData = { - daily: { - targetAmount: dailyAmount, - spentAmount: dailySpent, - remainingAmount: Math.max(0, dailyAmount - dailySpent) - }, - weekly: { - targetAmount: weeklyAmount, - spentAmount: weeklySpent, - remainingAmount: Math.max(0, weeklyAmount - weeklySpent) - }, - monthly: { - targetAmount: monthlyAmount, - spentAmount: monthlySpent, - remainingAmount: Math.max(0, monthlyAmount - monthlySpent) - } - }; - - console.log("새 예산 데이터:", JSON.stringify(updatedBudgetData)); - - return updatedBudgetData; -}; - -// 지출액 계산 (일일, 주간, 월간) - 문제 수정 -export const calculateSpentAmounts = ( - transactions: Transaction[], - prevBudgetData: BudgetData -): BudgetData => { - console.log("지출액 계산 시작, 트랜잭션 수:", transactions.length); - - // 지출 거래 필터링 - const expenseTransactions = transactions.filter(t => t.type === 'expense'); - - // 오늘 지출 계산 - const todayExpenses = expenseTransactions.filter(t => { - if (t.date.includes('오늘')) return true; - return false; - }); - const dailySpent = todayExpenses.reduce((sum, t) => sum + t.amount, 0); - - // 이번 주 지출 계산 (단순화된 버전) - const weeklyExpenses = expenseTransactions.filter(t => { - if (t.date.includes('오늘') || t.date.includes('어제') || t.date.includes('이번주')) return true; - return true; - }); - const weeklySpent = weeklyExpenses.reduce((sum, t) => sum + t.amount, 0); - - // 이번 달 총 지출 계산 - const monthlySpent = expenseTransactions.reduce((sum, t) => sum + t.amount, 0); - - // 기존 예산 목표 유지 (없으면 기본값 0) - const dailyTarget = prevBudgetData?.daily?.targetAmount || 0; - const weeklyTarget = prevBudgetData?.weekly?.targetAmount || 0; - const monthlyTarget = prevBudgetData?.monthly?.targetAmount || 0; - - // 예산 데이터 업데이트 - const updatedBudget = { - daily: { - targetAmount: dailyTarget, - spentAmount: dailySpent, - remainingAmount: Math.max(0, dailyTarget - dailySpent) - }, - weekly: { - targetAmount: weeklyTarget, - spentAmount: weeklySpent, - remainingAmount: Math.max(0, weeklyTarget - weeklySpent) - }, - monthly: { - targetAmount: monthlyTarget, - spentAmount: monthlySpent, - remainingAmount: Math.max(0, monthlyTarget - monthlySpent) - } - }; - - console.log("지출액 계산 결과:", updatedBudget); - - return updatedBudget; -}; - -// 초기 예산 데이터 생성 -export const getInitialBudgetData = (): BudgetData => { - return { - daily: { - targetAmount: 0, - spentAmount: 0, - remainingAmount: 0 - }, - weekly: { - targetAmount: 0, - spentAmount: 0, - remainingAmount: 0 - }, - monthly: { - targetAmount: 0, - spentAmount: 0, - remainingAmount: 0 - } - }; -}; - -// 스토리지에서 안전하게 예산 데이터 가져오기 -export const safelyLoadBudgetData = (defaultData: BudgetData = getInitialBudgetData()): BudgetData => { - try { - const budgetDataStr = localStorage.getItem('budgetData'); - if (budgetDataStr) { - const parsed = JSON.parse(budgetDataStr); - - // 데이터 구조 검증 (daily, weekly, monthly 키 존재 확인) - if (parsed && parsed.daily && parsed.weekly && parsed.monthly) { - return parsed; - } else { - console.warn('저장된 예산 데이터 구조가 유효하지 않습니다. 기본값 사용.'); - } - } - } catch (error) { - console.error('예산 데이터 로드 오류:', error); - } - - // 오류 발생 또는 데이터 없음 시 기본값 반환 - return defaultData; -}; +export { + safelyLoadBudgetData, + safeStorage +} from './utils/storageUtils'; diff --git a/src/contexts/budget/storage/budgetStorage.ts b/src/contexts/budget/storage/budgetStorage.ts index 8a9463c..ee9719c 100644 --- a/src/contexts/budget/storage/budgetStorage.ts +++ b/src/contexts/budget/storage/budgetStorage.ts @@ -1,50 +1,17 @@ import { BudgetData } from '../types'; -import { getInitialBudgetData } from '../budgetUtils'; -import { toast } from '@/hooks/useToast.wrapper'; // 래퍼 사용 +import { getInitialBudgetData, safelyLoadBudgetData, safeStorage } from '../budgetUtils'; +import { toast } from '@/hooks/useToast.wrapper'; /** * 예산 데이터 불러오기 */ export const loadBudgetDataFromStorage = (): BudgetData => { try { - const storedBudgetData = localStorage.getItem('budgetData'); - if (storedBudgetData) { - try { - const parsed = JSON.parse(storedBudgetData); - console.log('예산 데이터 로드 완료', parsed); - - // 데이터 유효성 검사 추가 - if (!parsed || !parsed.monthly || !parsed.daily || !parsed.weekly) { - console.warn('불완전한 예산 데이터, 백업에서 복구 시도'); - throw new Error('잘못된 형식의 예산 데이터'); - } - - return parsed; - } catch (error) { - console.error('예산 데이터 파싱 오류:', error); - - // 백업에서 복구 시도 - const backupData = localStorage.getItem('budgetData_backup'); - if (backupData) { - try { - const parsed = JSON.parse(backupData); - console.log('백업에서 예산 데이터 복구 완료', parsed); - - // 백업 데이터 유효성 검사 - if (!parsed || !parsed.monthly || !parsed.daily || !parsed.weekly) { - console.warn('백업 예산 데이터도 불완전함, 새 데이터 생성'); - throw new Error('백업 데이터도 잘못된 형식'); - } - - localStorage.setItem('budgetData', backupData); // 메인 스토리지에 복구 - return parsed; - } catch (backupError) { - console.error('백업 데이터 복구 실패:', backupError); - } - } - } - } + // 새로운 safelyLoadBudgetData 함수 사용 + const budgetData = safelyLoadBudgetData(); + console.log('예산 데이터 로드 완료', budgetData); + return budgetData; } catch (error) { console.error('예산 데이터 로드 중 오류:', error); } @@ -73,9 +40,8 @@ export const saveBudgetDataToStorage = (budgetData: BudgetData): void => { // 이전 예산과 비교하여 변경 여부 확인 let hasChanged = true; try { - const oldDataString = localStorage.getItem('budgetData'); - if (oldDataString) { - const oldData = JSON.parse(oldDataString); + const oldData = safeStorage.get('budgetData'); + if (oldData) { // 월간 예산이 동일하면 변경되지 않은 것으로 판단 hasChanged = oldData.monthly.targetAmount !== budgetData.monthly.targetAmount; } @@ -84,12 +50,12 @@ export const saveBudgetDataToStorage = (budgetData: BudgetData): void => { } // 로컬 스토리지에 저장 - localStorage.setItem('budgetData', dataString); + safeStorage.set('budgetData', budgetData); console.log('예산 데이터 저장 완료', budgetData); // 중요: 즉시 자동 백업 (데이터 손실 방지) - localStorage.setItem('budgetData_backup', dataString); - localStorage.setItem('lastBudgetSaveTime', new Date().toISOString()); + safeStorage.set('budgetData_backup', budgetData); + safeStorage.set('lastBudgetSaveTime', new Date().toISOString()); // 이벤트 발생 (단일 이벤트로 통합) const event = new CustomEvent('budgetChanged', { @@ -121,14 +87,13 @@ export const saveBudgetDataToStorage = (budgetData: BudgetData): void => { */ export const clearAllBudgetData = (): void => { try { - localStorage.removeItem('budgetData'); - localStorage.removeItem('budgetData_backup'); + safeStorage.remove('budgetData'); + safeStorage.remove('budgetData_backup'); // 기본값으로 재설정 const initialData = getInitialBudgetData(); - const dataString = JSON.stringify(initialData); - localStorage.setItem('budgetData', dataString); - localStorage.setItem('budgetData_backup', dataString); + safeStorage.set('budgetData', initialData); + safeStorage.set('budgetData_backup', initialData); console.log('예산 데이터가 초기화되었습니다.'); @@ -136,7 +101,7 @@ export const clearAllBudgetData = (): void => { window.dispatchEvent(new Event('budgetDataUpdated')); window.dispatchEvent(new StorageEvent('storage', { key: 'budgetData', - newValue: dataString + newValue: JSON.stringify(initialData) })); // 토스트 알림 (사용자가 직접 초기화한 경우만) diff --git a/src/contexts/budget/utils/budgetCalculation.ts b/src/contexts/budget/utils/budgetCalculation.ts new file mode 100644 index 0000000..64a0947 --- /dev/null +++ b/src/contexts/budget/utils/budgetCalculation.ts @@ -0,0 +1,78 @@ + +import { BudgetData, BudgetPeriod } from '../types'; +import { getInitialBudgetData } from './constants'; + +// 예산 데이터 업데이트 계산 +export const calculateUpdatedBudgetData = ( + prevBudgetData: BudgetData, + type: BudgetPeriod, + amount: number +): BudgetData => { + console.log(`예산 업데이트 계산 시작: 타입=${type}, 금액=${amount}`); + + // 값이 없거나 유효하지 않은 경우 로깅 + if (!prevBudgetData) { + console.error('이전 예산 데이터가 없습니다. 기본값 사용.'); + prevBudgetData = getInitialBudgetData(); + } + + // 선택된 타입에 따라 다른 타입의 예산도 자동으로 계산 + let monthlyAmount: number, weeklyAmount: number, dailyAmount: number; + + if (type === 'monthly') { + // 월간 예산이 직접 입력된 경우 + monthlyAmount = amount; + // 월 30일 기준 (실제 사용 시 값 확인) + dailyAmount = Math.round(monthlyAmount / 30); + // 월 4.3주 기준 (실제 사용 시 값 확인) + weeklyAmount = Math.round(monthlyAmount / 4.3); + } else if (type === 'weekly') { + // 주간 예산이 직접 입력된 경우 + weeklyAmount = amount; + monthlyAmount = Math.round(weeklyAmount * 4.3); + dailyAmount = Math.round(weeklyAmount / 7); + } else { // 'daily' + // 일일 예산이 직접 입력된 경우 + dailyAmount = amount; + weeklyAmount = Math.round(dailyAmount * 7); + monthlyAmount = Math.round(dailyAmount * 30); + } + + // 모든 금액이 최소한 0 이상이 되도록 보장 + monthlyAmount = Math.max(0, monthlyAmount); + weeklyAmount = Math.max(0, weeklyAmount); + dailyAmount = Math.max(0, dailyAmount); + + console.log(`최종 예산 계산 결과: 월간=${monthlyAmount}원, 주간=${weeklyAmount}원, 일일=${dailyAmount}원`); + + // 로그에 이전 예산 데이터 출력 + console.log("이전 예산 데이터:", JSON.stringify(prevBudgetData)); + + // 이전 지출 데이터 보존 + const dailySpent = prevBudgetData.daily?.spentAmount || 0; + const weeklySpent = prevBudgetData.weekly?.spentAmount || 0; + const monthlySpent = prevBudgetData.monthly?.spentAmount || 0; + + // 새 예산 데이터 생성 (spentAmount는 이전 값 유지) + const updatedBudgetData = { + daily: { + targetAmount: dailyAmount, + spentAmount: dailySpent, + remainingAmount: Math.max(0, dailyAmount - dailySpent) + }, + weekly: { + targetAmount: weeklyAmount, + spentAmount: weeklySpent, + remainingAmount: Math.max(0, weeklyAmount - weeklySpent) + }, + monthly: { + targetAmount: monthlyAmount, + spentAmount: monthlySpent, + remainingAmount: Math.max(0, monthlyAmount - monthlySpent) + } + }; + + console.log("새 예산 데이터:", JSON.stringify(updatedBudgetData)); + + return updatedBudgetData; +}; diff --git a/src/contexts/budget/utils/categoryUtils.ts b/src/contexts/budget/utils/categoryUtils.ts new file mode 100644 index 0000000..4b9ba01 --- /dev/null +++ b/src/contexts/budget/utils/categoryUtils.ts @@ -0,0 +1,48 @@ + +import { CategoryBudget, Transaction } from '../types'; +import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons'; + +// 카테고리별 지출 계산 +export const calculateCategorySpending = ( + transactions: Transaction[], + categoryBudgets: Record +): CategoryBudget[] => { + const expenseTransactions = transactions.filter(t => t.type === 'expense'); + const categorySpending: Record = {}; + + // 모든 카테고리에 대해 초기값 0 설정 + Object.keys(categoryBudgets).forEach(category => { + // 정의된 카테고리만 유지 + if (EXPENSE_CATEGORIES.includes(category)) { + categorySpending[category] = 0; + } + }); + + // 지원되는 카테고리가 없을 경우 기본값 설정 + if (Object.keys(categorySpending).length === 0) { + EXPENSE_CATEGORIES.forEach(category => { + categorySpending[category] = 0; + }); + } + + expenseTransactions.forEach(t => { + if (t.category in categorySpending) { + categorySpending[t.category] += t.amount; + } else if (EXPENSE_CATEGORIES.includes(t.category)) { + // 지원되는 카테고리이지만 초기화되지 않은 경우 + categorySpending[t.category] = t.amount; + } else if (t.category === '교통비') { + // 예전 카테고리명 '교통비'를 '교통'으로 매핑 + categorySpending['교통'] = (categorySpending['교통'] || 0) + t.amount; + } else { + // 지원되지 않는 카테고리는 '기타'로 집계 + categorySpending['기타'] = (categorySpending['기타'] || 0) + t.amount; + } + }); + + return EXPENSE_CATEGORIES.map(category => ({ + title: category, + current: categorySpending[category] || 0, + total: categoryBudgets[category] || 0 + })); +}; diff --git a/src/contexts/budget/utils/constants.ts b/src/contexts/budget/utils/constants.ts new file mode 100644 index 0000000..ce4dd42 --- /dev/null +++ b/src/contexts/budget/utils/constants.ts @@ -0,0 +1,33 @@ + +import { BudgetData } from '../types'; + +// 기본 데이터 상수 (기본값을 0으로 설정) +export const DEFAULT_CATEGORY_BUDGETS: Record = { + 음식: 0, + 쇼핑: 0, + 교통: 0, + 기타: 0 +}; + +export const DEFAULT_MONTHLY_BUDGET = 0; + +// 초기 예산 데이터 생성 +export const getInitialBudgetData = (): BudgetData => { + return { + daily: { + targetAmount: 0, + spentAmount: 0, + remainingAmount: 0 + }, + weekly: { + targetAmount: 0, + spentAmount: 0, + remainingAmount: 0 + }, + monthly: { + targetAmount: 0, + spentAmount: 0, + remainingAmount: 0 + } + }; +}; diff --git a/src/contexts/budget/utils/spendingCalculation.ts b/src/contexts/budget/utils/spendingCalculation.ts new file mode 100644 index 0000000..9fde989 --- /dev/null +++ b/src/contexts/budget/utils/spendingCalculation.ts @@ -0,0 +1,58 @@ + +import { BudgetData, Transaction } from '../types'; + +// 지출액 계산 (일일, 주간, 월간) +export const calculateSpentAmounts = ( + transactions: Transaction[], + prevBudgetData: BudgetData +): BudgetData => { + console.log("지출액 계산 시작, 트랜잭션 수:", transactions.length); + + // 지출 거래 필터링 + const expenseTransactions = transactions.filter(t => t.type === 'expense'); + + // 오늘 지출 계산 + const todayExpenses = expenseTransactions.filter(t => { + if (t.date.includes('오늘')) return true; + return false; + }); + const dailySpent = todayExpenses.reduce((sum, t) => sum + t.amount, 0); + + // 이번 주 지출 계산 (단순화된 버전) + const weeklyExpenses = expenseTransactions.filter(t => { + if (t.date.includes('오늘') || t.date.includes('어제') || t.date.includes('이번주')) return true; + return true; + }); + const weeklySpent = weeklyExpenses.reduce((sum, t) => sum + t.amount, 0); + + // 이번 달 총 지출 계산 + const monthlySpent = expenseTransactions.reduce((sum, t) => sum + t.amount, 0); + + // 기존 예산 목표 유지 (없으면 기본값 0) + const dailyTarget = prevBudgetData?.daily?.targetAmount || 0; + const weeklyTarget = prevBudgetData?.weekly?.targetAmount || 0; + const monthlyTarget = prevBudgetData?.monthly?.targetAmount || 0; + + // 예산 데이터 업데이트 + const updatedBudget = { + daily: { + targetAmount: dailyTarget, + spentAmount: dailySpent, + remainingAmount: Math.max(0, dailyTarget - dailySpent) + }, + weekly: { + targetAmount: weeklyTarget, + spentAmount: weeklySpent, + remainingAmount: Math.max(0, weeklyTarget - weeklySpent) + }, + monthly: { + targetAmount: monthlyTarget, + spentAmount: monthlySpent, + remainingAmount: Math.max(0, monthlyTarget - monthlySpent) + } + }; + + console.log("지출액 계산 결과:", updatedBudget); + + return updatedBudget; +}; diff --git a/src/contexts/budget/utils/storageUtils.ts b/src/contexts/budget/utils/storageUtils.ts new file mode 100644 index 0000000..066a2a9 --- /dev/null +++ b/src/contexts/budget/utils/storageUtils.ts @@ -0,0 +1,65 @@ + +import { BudgetData } from '../types'; +import { getInitialBudgetData } from './constants'; +import { toast } from '@/hooks/useToast.wrapper'; + +// 스토리지에서 안전하게 예산 데이터 가져오기 +export const safelyLoadBudgetData = (defaultData: BudgetData = getInitialBudgetData()): BudgetData => { + try { + const budgetDataStr = localStorage.getItem('budgetData'); + if (budgetDataStr) { + const parsed = JSON.parse(budgetDataStr); + + // 데이터 구조 검증 (daily, weekly, monthly 키 존재 확인) + if (parsed && parsed.daily && parsed.weekly && parsed.monthly) { + return parsed; + } else { + console.warn('저장된 예산 데이터 구조가 유효하지 않습니다. 기본값 사용.'); + } + } + } catch (error) { + console.error('예산 데이터 로드 오류:', error); + } + + // 오류 발생 또는 데이터 없음 시 기본값 반환 + return defaultData; +}; + +// 안전한 스토리지 접근 +export const safeStorage = { + get: (key: string, defaultValue: any = null): any => { + try { + const value = localStorage.getItem(key); + if (value === null) return defaultValue; + return JSON.parse(value); + } catch (error) { + console.error(`스토리지 읽기 오류 (${key}):`, error); + return defaultValue; + } + }, + + set: (key: string, value: any): boolean => { + try { + localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (error) { + console.error(`스토리지 쓰기 오류 (${key}):`, error); + toast({ + title: "저장 오류", + description: "데이터를 저장하는 중 문제가 발생했습니다.", + variant: "destructive" + }); + return false; + } + }, + + remove: (key: string): boolean => { + try { + localStorage.removeItem(key); + return true; + } catch (error) { + console.error(`스토리지 삭제 오류 (${key}):`, error); + return false; + } + } +};