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>
This commit is contained in:
500
src/stores/budgetStore.ts
Normal file
500
src/stores/budgetStore.ts
Normal file
@@ -0,0 +1,500 @@
|
||||
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 };
|
||||
};
|
||||
Reference in New Issue
Block a user