import { create } from "zustand"; import { devtools, persist } from "zustand/middleware"; import { v4 as uuidv4 } from "uuid"; import { Transaction, BudgetData, BudgetPeriod, BudgetPeriodData, CategoryBudget, PaymentMethodStats, } from "@/contexts/budget/types"; import { getInitialBudgetData } from "@/contexts/budget/utils/constants"; import { EXPENSE_CATEGORIES } from "@/constants/categoryIcons"; import { syncLogger } from "@/utils/logger"; import type { PaymentMethod } from "@/types/common"; // 상수 정의 const CATEGORIES = EXPENSE_CATEGORIES; const DEFAULT_BUDGET_DATA = getInitialBudgetData(); const PAYMENT_METHODS: PaymentMethod[] = [ "신용카드", "현금", "체크카드", "간편결제", ]; /** * Zustand 예산 스토어 상태 타입 */ interface BudgetState { // 상태 transactions: Transaction[]; categoryBudgets: Record; budgetData: BudgetData; selectedTab: BudgetPeriod; // 트랜잭션 관리 액션 addTransaction: (transaction: Omit) => void; updateTransaction: (updatedTransaction: Transaction) => void; deleteTransaction: (id: string) => void; setTransactions: (transactions: Transaction[]) => void; // 예산 관리 액션 handleBudgetGoalUpdate: ( type: BudgetPeriod, amount: number, newCategoryBudgets?: Record ) => void; setCategoryBudgets: (budgets: Record) => void; updateCategoryBudget: (category: string, amount: number) => void; // UI 상태 액션 setSelectedTab: (tab: BudgetPeriod) => void; // 계산 및 분석 함수 getCategorySpending: () => CategoryBudget[]; getPaymentMethodStats: () => PaymentMethodStats[]; calculateBudgetData: () => BudgetData; // 데이터 초기화 resetBudgetData: () => void; // 내부 헬퍼 함수 recalculateBudgetData: () => void; persistToLocalStorage: () => void; } /** * 날짜 범위 계산 헬퍼 함수들 */ const getDateRanges = () => { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); // 일일 범위 (오늘) const dailyStart = today; const dailyEnd = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1); // 주간 범위 (이번 주 월요일부터 일요일까지) const dayOfWeek = today.getDay(); const weekStart = new Date(today); weekStart.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1)); const weekEnd = new Date(weekStart.getTime() + 7 * 24 * 60 * 60 * 1000 - 1); // 월간 범위 (이번 달 1일부터 마지막 날까지) const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); const monthEnd = new Date( today.getFullYear(), today.getMonth() + 1, 0, 23, 59, 59, 999 ); const _ranges = { daily: { start: dailyStart, end: dailyEnd }, weekly: { start: weekStart, end: weekEnd }, monthly: { start: monthStart, end: monthEnd }, }; return _ranges; }; /** * 트랜잭션 필터링 헬퍼 */ const filterTransactionsByPeriod = ( transactions: Transaction[], period: BudgetPeriod ): Transaction[] => { const _ranges = getDateRanges(); const { start, end } = _ranges[period]; return transactions.filter((transaction) => { const transactionDate = new Date(transaction.date); return transactionDate >= start && transactionDate <= end; }); }; /** * 예산 스토어 * * Context API의 복잡한 예산 관리를 Zustand로 단순화 * - 트랜잭션 CRUD 작업 * - 예산 목표 설정 및 추적 * - 카테고리별 지출 분석 * - 결제 방법별 통계 * - localStorage 영속성 */ export const useBudgetStore = create()( devtools( persist( (set, get) => ({ // 초기 상태 transactions: [], categoryBudgets: {}, budgetData: DEFAULT_BUDGET_DATA, selectedTab: "monthly" as BudgetPeriod, // 트랜잭션 추가 addTransaction: (transactionData: Omit) => { const newTransaction: Transaction = { ...transactionData, id: uuidv4(), localTimestamp: new Date().toISOString(), }; set( (state) => ({ transactions: [...state.transactions, newTransaction], }), false, "addTransaction" ); // 예산 데이터 재계산 및 이벤트 발생 get().recalculateBudgetData(); get().persistToLocalStorage(); window.dispatchEvent(new Event("budgetDataUpdated")); syncLogger.info("트랜잭션 추가됨", { id: newTransaction.id, amount: newTransaction.amount, category: newTransaction.category, }); }, // 트랜잭션 업데이트 updateTransaction: (updatedTransaction: Transaction) => { set( (state) => ({ transactions: state.transactions.map((transaction) => transaction.id === updatedTransaction.id ? { ...updatedTransaction, localTimestamp: new Date().toISOString(), } : transaction ), }), false, "updateTransaction" ); get().recalculateBudgetData(); get().persistToLocalStorage(); window.dispatchEvent(new Event("budgetDataUpdated")); syncLogger.info("트랜잭션 업데이트됨", { id: updatedTransaction.id, amount: updatedTransaction.amount, }); }, // 트랜잭션 삭제 deleteTransaction: (id: string) => { set( (state) => ({ transactions: state.transactions.filter( (transaction) => transaction.id !== id ), }), false, "deleteTransaction" ); get().recalculateBudgetData(); get().persistToLocalStorage(); window.dispatchEvent(new Event("budgetDataUpdated")); syncLogger.info("트랜잭션 삭제됨", { id }); }, // 트랜잭션 목록 설정 (동기화용) setTransactions: (transactions: Transaction[]) => { set({ transactions }, false, "setTransactions"); get().recalculateBudgetData(); get().persistToLocalStorage(); }, // 예산 목표 업데이트 handleBudgetGoalUpdate: ( type: BudgetPeriod, amount: number, newCategoryBudgets?: Record ) => { set( (state) => { const updatedBudgetData = { ...state.budgetData }; updatedBudgetData[type] = { ...updatedBudgetData[type], targetAmount: amount, }; const updatedState: Partial = { budgetData: updatedBudgetData, }; if (newCategoryBudgets) { updatedState.categoryBudgets = newCategoryBudgets; } return updatedState; }, false, "handleBudgetGoalUpdate" ); get().recalculateBudgetData(); get().persistToLocalStorage(); window.dispatchEvent(new Event("budgetDataUpdated")); syncLogger.info("예산 목표 업데이트됨", { type, amount }); }, // 카테고리 예산 설정 setCategoryBudgets: (budgets: Record) => { set({ categoryBudgets: budgets }, false, "setCategoryBudgets"); get().persistToLocalStorage(); }, // 개별 카테고리 예산 업데이트 updateCategoryBudget: (category: string, amount: number) => { set( (state) => ({ categoryBudgets: { ...state.categoryBudgets, [category]: amount, }, }), false, "updateCategoryBudget" ); get().persistToLocalStorage(); }, // 선택된 탭 변경 setSelectedTab: (tab: BudgetPeriod) => { set({ selectedTab: tab }, false, "setSelectedTab"); }, // 카테고리별 지출 계산 getCategorySpending: (): CategoryBudget[] => { const { transactions, categoryBudgets, selectedTab } = get(); const filteredTransactions = filterTransactionsByPeriod( transactions, selectedTab ); return CATEGORIES.map((category) => { const spent = filteredTransactions .filter((t) => t.category === category && t.type === "expense") .reduce((sum, t) => sum + t.amount, 0); const budget = categoryBudgets[category] || 0; return { title: category, current: spent, total: budget, }; }); }, // 결제 방법별 통계 계산 getPaymentMethodStats: (): PaymentMethodStats[] => { const { transactions, selectedTab } = get(); const filteredTransactions = filterTransactionsByPeriod( transactions, selectedTab ); const expenseTransactions = filteredTransactions.filter( (t) => t.type === "expense" ); const totalAmount = expenseTransactions.reduce( (sum, t) => sum + t.amount, 0 ); if (totalAmount === 0) { return PAYMENT_METHODS.map((method) => ({ method, amount: 0, percentage: 0, })); } return PAYMENT_METHODS.map((method) => { const amount = expenseTransactions .filter((t) => t.paymentMethod === method) .reduce((sum, t) => sum + t.amount, 0); return { method, amount, percentage: Math.round((amount / totalAmount) * 100), }; }); }, // 예산 데이터 계산 calculateBudgetData: (): BudgetData => { const { transactions } = get(); const _ranges = getDateRanges(); const calculatePeriodData = ( period: BudgetPeriod ): BudgetPeriodData => { const periodTransactions = filterTransactionsByPeriod( transactions, period ); const expenses = periodTransactions.filter( (t) => t.type === "expense" ); const spentAmount = expenses.reduce((sum, t) => sum + t.amount, 0); const currentBudget = get().budgetData[period]; const targetAmount = currentBudget?.targetAmount || 0; const remainingAmount = Math.max(0, targetAmount - spentAmount); return { targetAmount, spentAmount, remainingAmount, }; }; return { daily: calculatePeriodData("daily"), weekly: calculatePeriodData("weekly"), monthly: calculatePeriodData("monthly"), }; }, // 예산 데이터 재계산 (내부 헬퍼) recalculateBudgetData: () => { const newBudgetData = get().calculateBudgetData(); set({ budgetData: newBudgetData }, false, "recalculateBudgetData"); }, // localStorage 저장 (내부 헬퍼) persistToLocalStorage: () => { const { transactions, categoryBudgets, budgetData } = get(); try { localStorage.setItem( "budget-store-transactions", JSON.stringify(transactions) ); localStorage.setItem( "budget-store-categoryBudgets", JSON.stringify(categoryBudgets) ); localStorage.setItem( "budget-store-budgetData", JSON.stringify(budgetData) ); } catch (error) { syncLogger.error("localStorage 저장 실패", error); } }, // 데이터 초기화 resetBudgetData: () => { set( { transactions: [], categoryBudgets: {}, budgetData: DEFAULT_BUDGET_DATA, selectedTab: "monthly" as BudgetPeriod, }, false, "resetBudgetData" ); // localStorage 초기화 try { localStorage.removeItem("budget-store-transactions"); localStorage.removeItem("budget-store-categoryBudgets"); localStorage.removeItem("budget-store-budgetData"); } catch (error) { syncLogger.error("localStorage 초기화 실패", error); } window.dispatchEvent(new Event("budgetDataUpdated")); syncLogger.info("예산 데이터 초기화됨"); }, }), { name: "budget-store", // localStorage 키 partialize: (state) => ({ // localStorage에 저장할 상태만 선택 transactions: state.transactions, categoryBudgets: state.categoryBudgets, budgetData: state.budgetData, selectedTab: state.selectedTab, }), } ), { name: "budget-store", // DevTools 이름 } ) ); // 컴포넌트에서 사용할 편의 훅들 export const useBudget = () => { const { transactions, categoryBudgets, budgetData, selectedTab, addTransaction, updateTransaction, deleteTransaction, handleBudgetGoalUpdate, setSelectedTab, getCategorySpending, getPaymentMethodStats, resetBudgetData, } = useBudgetStore(); return { transactions, categoryBudgets, budgetData, selectedTab, addTransaction, updateTransaction, deleteTransaction, handleBudgetGoalUpdate, setSelectedTab, getCategorySpending, getPaymentMethodStats, resetBudgetData, }; }; // 트랜잭션만 필요한 경우의 경량 훅 export const useTransactions = () => { const { transactions, addTransaction, updateTransaction, deleteTransaction } = useBudgetStore(); return { transactions, addTransaction, updateTransaction, deleteTransaction }; }; // 예산 데이터만 필요한 경우의 경량 훅 export const useBudgetData = () => { const { budgetData, selectedTab, setSelectedTab, handleBudgetGoalUpdate } = useBudgetStore(); return { budgetData, selectedTab, setSelectedTab, handleBudgetGoalUpdate }; }; // 분석 데이터만 필요한 경우의 경량 훅 export const useBudgetAnalytics = () => { const { getCategorySpending, getPaymentMethodStats } = useBudgetStore(); return { getCategorySpending, getPaymentMethodStats }; };