Files
zellyy-finance/src/stores/budgetStore.ts
hansoo 4d9effce41 fix: ESLint 오류 수정 - 사용하지 않는 변수들에 underscore prefix 추가
- AddTransactionButton.tsx: useEffect import 제거
- BudgetProgressCard.tsx: localBudgetData를 _localBudgetData로 변경
- Header.tsx: isMobile을 _isMobile로 변경
- RecentTransactionsSection.tsx: isDeleting을 _isDeleting로 변경
- TransactionCard.tsx: cn import 제거
- ExpenseForm.tsx: useState import 제거
- cacheStrategies.ts: QueryClient, Transaction import 제거
- Analytics.tsx: Separator import 제거, 미사용 변수들에 underscore prefix 추가
- Index.tsx: useMemo import 제거
- Login.tsx: setLoginError를 _setLoginError로 변경
- Register.tsx: useEffect dependency 수정 및 useCallback 추가
- Settings.tsx: toast, handleClick에 underscore prefix 추가
- authStore.ts: setError, setAppwriteInitialized에 underscore prefix 추가
- budgetStore.ts: ranges를 _ranges로 변경
- BudgetProgressCard.test.tsx: waitFor import 제거

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 20:49:36 +09:00

501 lines
15 KiB
TypeScript

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<string, number>;
budgetData: BudgetData;
selectedTab: BudgetPeriod;
// 트랜잭션 관리 액션
addTransaction: (transaction: Omit<Transaction, "id">) => void;
updateTransaction: (updatedTransaction: Transaction) => void;
deleteTransaction: (id: string) => void;
setTransactions: (transactions: Transaction[]) => void;
// 예산 관리 액션
handleBudgetGoalUpdate: (
type: BudgetPeriod,
amount: number,
newCategoryBudgets?: Record<string, number>
) => void;
setCategoryBudgets: (budgets: Record<string, number>) => 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<BudgetState>()(
devtools(
persist(
(set, get) => ({
// 초기 상태
transactions: [],
categoryBudgets: {},
budgetData: DEFAULT_BUDGET_DATA,
selectedTab: "monthly" as BudgetPeriod,
// 트랜잭션 추가
addTransaction: (transactionData: Omit<Transaction, "id">) => {
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<string, number>
) => {
set(
(state) => {
const updatedBudgetData = { ...state.budgetData };
updatedBudgetData[type] = {
...updatedBudgetData[type],
targetAmount: amount,
};
const updatedState: Partial<BudgetState> = {
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<string, number>) => {
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 };
};