Fix budget discrepancies
The budget amounts displayed on the home page were inconsistent with those shown on the transactions and analytics pages. This commit addresses this issue.
This commit is contained in:
@@ -4,56 +4,38 @@ import ExpenseChart from '@/components/ExpenseChart';
|
|||||||
import AddTransactionButton from '@/components/AddTransactionButton';
|
import AddTransactionButton from '@/components/AddTransactionButton';
|
||||||
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
||||||
import { ChevronLeft, ChevronRight, Wallet, CreditCard, PiggyBank } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, Wallet, CreditCard, PiggyBank } from 'lucide-react';
|
||||||
|
import { useBudget } from '@/contexts/BudgetContext';
|
||||||
|
import { formatCurrency } from '@/utils/formatters';
|
||||||
|
import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons';
|
||||||
|
|
||||||
const Analytics = () => {
|
const Analytics = () => {
|
||||||
const [selectedPeriod, setSelectedPeriod] = useState('이번 달');
|
const [selectedPeriod, setSelectedPeriod] = useState('이번 달');
|
||||||
|
const { budgetData, getCategorySpending, transactions } = useBudget();
|
||||||
|
|
||||||
|
// 월간 예산 및 지출 데이터 가져오기
|
||||||
|
const totalBudget = budgetData.monthly.targetAmount;
|
||||||
|
const totalExpense = budgetData.monthly.spentAmount;
|
||||||
|
const savings = Math.max(0, totalBudget - totalExpense);
|
||||||
|
const savingsPercentage = totalBudget > 0 ? Math.round(savings / totalBudget * 100) : 0;
|
||||||
|
|
||||||
|
// 카테고리별 지출 데이터 생성
|
||||||
|
const categorySpending = getCategorySpending();
|
||||||
|
const expenseData = categorySpending.map(category => ({
|
||||||
|
name: category.title,
|
||||||
|
value: category.current,
|
||||||
|
color: category.title === '식비' ? '#81c784' :
|
||||||
|
category.title === '생활비' ? '#AED581' : '#2E7D32'
|
||||||
|
}));
|
||||||
|
|
||||||
// Updated expense categories with green color scheme
|
// 최근 6개월 데이터 (샘플 데이터와 현재 월 실제 데이터 결합)
|
||||||
const expenseData = [{
|
const monthlyData = [
|
||||||
name: '식비',
|
{ name: '3월', budget: totalBudget, expense: totalBudget * 0.7 },
|
||||||
value: 350000,
|
{ name: '4월', budget: totalBudget, expense: totalBudget * 0.65 },
|
||||||
color: '#81c784' // Green (matching neuro-income)
|
{ name: '5월', budget: totalBudget, expense: totalBudget * 0.7 },
|
||||||
}, {
|
{ name: '6월', budget: totalBudget, expense: totalBudget * 0.55 },
|
||||||
name: '생활비',
|
{ name: '7월', budget: totalBudget, expense: totalBudget * 0.6 },
|
||||||
value: 650000,
|
{ name: '8월', budget: totalBudget, expense: totalExpense } // 현재 달은 실제 데이터
|
||||||
color: '#AED581' // Light green
|
];
|
||||||
}, {
|
|
||||||
name: '교통비',
|
|
||||||
value: 175000,
|
|
||||||
color: '#2E7D32' // Dark green (replacing yellow)
|
|
||||||
}];
|
|
||||||
|
|
||||||
// Sample data for the monthly comparison - renamed income to budget
|
|
||||||
const monthlyData = [{
|
|
||||||
name: '3월',
|
|
||||||
budget: 2400000,
|
|
||||||
expense: 1800000
|
|
||||||
}, {
|
|
||||||
name: '4월',
|
|
||||||
budget: 2300000,
|
|
||||||
expense: 1700000
|
|
||||||
}, {
|
|
||||||
name: '5월',
|
|
||||||
budget: 2700000,
|
|
||||||
expense: 1900000
|
|
||||||
}, {
|
|
||||||
name: '6월',
|
|
||||||
budget: 2200000,
|
|
||||||
expense: 1500000
|
|
||||||
}, {
|
|
||||||
name: '7월',
|
|
||||||
budget: 2500000,
|
|
||||||
expense: 1650000
|
|
||||||
}, {
|
|
||||||
name: '8월',
|
|
||||||
budget: 2550000,
|
|
||||||
expense: 1740000
|
|
||||||
}];
|
|
||||||
|
|
||||||
// Updated variable names to match new terminology
|
|
||||||
const totalBudget = 2550000;
|
|
||||||
const totalExpense = 1740000;
|
|
||||||
const savings = totalBudget - totalExpense;
|
|
||||||
const savingsPercentage = Math.round(savings / totalBudget * 100);
|
|
||||||
|
|
||||||
// Custom formatter for Y-axis that removes currency symbol and uses K format
|
// Custom formatter for Y-axis that removes currency symbol and uses K format
|
||||||
const formatYAxisTick = (value: number) => {
|
const formatYAxisTick = (value: number) => {
|
||||||
@@ -63,15 +45,13 @@ const Analytics = () => {
|
|||||||
// Custom formatter for tooltip that keeps the original formatting with currency symbol
|
// Custom formatter for tooltip that keeps the original formatting with currency symbol
|
||||||
const formatTooltip = (value: number | string) => {
|
const formatTooltip = (value: number | string) => {
|
||||||
if (typeof value === 'number') {
|
if (typeof value === 'number') {
|
||||||
return new Intl.NumberFormat('ko-KR', {
|
return formatCurrency(value);
|
||||||
style: 'currency',
|
|
||||||
currency: 'KRW',
|
|
||||||
maximumFractionDigits: 0
|
|
||||||
}).format(value);
|
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
return <div className="min-h-screen bg-neuro-background pb-24">
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-neuro-background pb-24">
|
||||||
<div className="max-w-md mx-auto px-6">
|
<div className="max-w-md mx-auto px-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="py-8">
|
<header className="py-8">
|
||||||
@@ -100,11 +80,7 @@ const Analytics = () => {
|
|||||||
<p className="text-gray-500 text-base">예산</p>
|
<p className="text-gray-500 text-base">예산</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-bold text-neuro-income">
|
<p className="text-sm font-bold text-neuro-income">
|
||||||
{new Intl.NumberFormat('ko-KR', {
|
{formatCurrency(totalBudget)}
|
||||||
style: 'currency',
|
|
||||||
currency: 'KRW',
|
|
||||||
maximumFractionDigits: 0
|
|
||||||
}).format(totalBudget)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="neuro-card">
|
<div className="neuro-card">
|
||||||
@@ -113,11 +89,7 @@ const Analytics = () => {
|
|||||||
<p className="text-gray-500 font-medium text-base">지출</p>
|
<p className="text-gray-500 font-medium text-base">지출</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-bold text-neuro-income">
|
<p className="text-sm font-bold text-neuro-income">
|
||||||
{new Intl.NumberFormat('ko-KR', {
|
{formatCurrency(totalExpense)}
|
||||||
style: 'currency',
|
|
||||||
currency: 'KRW',
|
|
||||||
maximumFractionDigits: 0
|
|
||||||
}).format(totalExpense)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="neuro-card">
|
<div className="neuro-card">
|
||||||
@@ -138,13 +110,13 @@ const Analytics = () => {
|
|||||||
<div className="neuro-card h-72">
|
<div className="neuro-card h-72">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={monthlyData} margin={{
|
<BarChart data={monthlyData} margin={{
|
||||||
top: 20,
|
top: 20,
|
||||||
right: 10,
|
right: 10,
|
||||||
left: -10,
|
left: -10,
|
||||||
bottom: 5
|
bottom: 5
|
||||||
}} style={{
|
}} style={{
|
||||||
fontSize: '11px'
|
fontSize: '11px'
|
||||||
}}>
|
}}>
|
||||||
<XAxis dataKey="name" />
|
<XAxis dataKey="name" />
|
||||||
<YAxis tickFormatter={formatYAxisTick} />
|
<YAxis tickFormatter={formatYAxisTick} />
|
||||||
<Tooltip formatter={formatTooltip} />
|
<Tooltip formatter={formatTooltip} />
|
||||||
@@ -164,32 +136,32 @@ const Analytics = () => {
|
|||||||
<h2 className="text-lg font-semibold mb-3 mt-6">주요 지출 카테고리</h2>
|
<h2 className="text-lg font-semibold mb-3 mt-6">주요 지출 카테고리</h2>
|
||||||
<div className="neuro-card mb-6">
|
<div className="neuro-card mb-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{expenseData.map((category, index) => <div key={category.name} className="flex items-center justify-between">
|
{expenseData.map((category, index) => (
|
||||||
|
<div key={category.name} className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-6 h-6 rounded-full" style={{
|
<div className="w-6 h-6 rounded-full" style={{
|
||||||
backgroundColor: category.color
|
backgroundColor: category.color
|
||||||
}}></div>
|
}}></div>
|
||||||
<span>{category.name}</span>
|
<span>{category.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{new Intl.NumberFormat('ko-KR', {
|
{formatCurrency(category.value)}
|
||||||
style: 'currency',
|
|
||||||
currency: 'KRW',
|
|
||||||
maximumFractionDigits: 0
|
|
||||||
}).format(category.value)}
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
{Math.round(category.value / totalExpense * 100)}%
|
{totalExpense > 0 ? Math.round(category.value / totalExpense * 100) : 0}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>)}
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddTransactionButton />
|
<AddTransactionButton />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
</div>;
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
export default Analytics;
|
|
||||||
|
export default Analytics;
|
||||||
|
|||||||
@@ -1,45 +1,59 @@
|
|||||||
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import NavBar from '@/components/NavBar';
|
import NavBar from '@/components/NavBar';
|
||||||
import TransactionCard from '@/components/TransactionCard';
|
import TransactionCard from '@/components/TransactionCard';
|
||||||
import AddTransactionButton from '@/components/AddTransactionButton';
|
import AddTransactionButton from '@/components/AddTransactionButton';
|
||||||
import { Calendar, Search, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
|
import { Calendar, Search, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
|
||||||
import { useTransactions } from '@/hooks/useTransactions';
|
|
||||||
import { formatCurrency } from '@/utils/formatters';
|
import { formatCurrency } from '@/utils/formatters';
|
||||||
|
import { useBudget } from '@/contexts/BudgetContext';
|
||||||
|
import { MONTHS_KR, getCurrentMonth, getPrevMonth, getNextMonth } from '@/utils/dateUtils';
|
||||||
|
|
||||||
const Transactions = () => {
|
const Transactions = () => {
|
||||||
const {
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
transactions,
|
const [selectedMonth, setSelectedMonth] = useState(getCurrentMonth());
|
||||||
isLoading,
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
error,
|
const [filteredTransactions, setFilteredTransactions] = useState<any[]>([]);
|
||||||
totalBudget,
|
|
||||||
selectedMonth,
|
const {
|
||||||
searchQuery,
|
transactions,
|
||||||
setSearchQuery,
|
budgetData,
|
||||||
handlePrevMonth,
|
updateTransaction
|
||||||
handleNextMonth,
|
} = useBudget();
|
||||||
updateTransaction,
|
|
||||||
totalExpenses,
|
|
||||||
refreshTransactions
|
|
||||||
} = useTransactions();
|
|
||||||
|
|
||||||
// 페이지 진입/초점 시 새로고침
|
// 월 변경 처리
|
||||||
|
const handlePrevMonth = () => {
|
||||||
|
setSelectedMonth(getPrevMonth(selectedMonth));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextMonth = () => {
|
||||||
|
setSelectedMonth(getNextMonth(selectedMonth));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 트랜잭션 필터링
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFocus = () => {
|
setIsLoading(true);
|
||||||
refreshTransactions();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('focus', handleFocus);
|
// 해당 월의 트랜잭션만 필터링
|
||||||
|
let filtered = transactions.filter(t => {
|
||||||
|
return t.date.includes(selectedMonth);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
// 검색어로 필터링
|
||||||
window.removeEventListener('focus', handleFocus);
|
if (searchQuery.trim()) {
|
||||||
};
|
filtered = filtered.filter(t =>
|
||||||
}, [refreshTransactions]);
|
t.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
t.category.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredTransactions(filtered);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [transactions, selectedMonth, searchQuery]);
|
||||||
|
|
||||||
// 트랜잭션을 날짜별로 그룹화
|
// 트랜잭션을 날짜별로 그룹화
|
||||||
const groupedTransactions: Record<string, typeof transactions> = {};
|
const groupedTransactions: Record<string, typeof filteredTransactions> = {};
|
||||||
|
|
||||||
transactions.forEach(transaction => {
|
filteredTransactions.forEach(transaction => {
|
||||||
const datePart = transaction.date.split(',')[0];
|
const datePart = transaction.date.split(',')[0];
|
||||||
if (!groupedTransactions[datePart]) {
|
if (!groupedTransactions[datePart]) {
|
||||||
groupedTransactions[datePart] = [];
|
groupedTransactions[datePart] = [];
|
||||||
@@ -47,6 +61,11 @@ const Transactions = () => {
|
|||||||
groupedTransactions[datePart].push(transaction);
|
groupedTransactions[datePart].push(transaction);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 총 지출 계산
|
||||||
|
const totalExpenses = filteredTransactions
|
||||||
|
.filter(t => t.type === 'expense')
|
||||||
|
.reduce((sum, t) => sum + t.amount, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-neuro-background pb-24">
|
<div className="min-h-screen bg-neuro-background pb-24">
|
||||||
<div className="max-w-md mx-auto px-6">
|
<div className="max-w-md mx-auto px-6">
|
||||||
@@ -93,7 +112,7 @@ const Transactions = () => {
|
|||||||
<div className="neuro-card">
|
<div className="neuro-card">
|
||||||
<p className="text-sm text-gray-500 mb-1">총 예산</p>
|
<p className="text-sm text-gray-500 mb-1">총 예산</p>
|
||||||
<p className="text-lg font-bold text-neuro-income">
|
<p className="text-lg font-bold text-neuro-income">
|
||||||
{formatCurrency(totalBudget)}
|
{formatCurrency(budgetData.monthly.targetAmount)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="neuro-card">
|
<div className="neuro-card">
|
||||||
@@ -113,21 +132,8 @@ const Transactions = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error State */}
|
|
||||||
{error && (
|
|
||||||
<div className="neuro-card p-4 text-red-500 mb-6">
|
|
||||||
<p>{error}</p>
|
|
||||||
<button
|
|
||||||
className="text-neuro-income mt-2"
|
|
||||||
onClick={refreshTransactions}
|
|
||||||
>
|
|
||||||
다시 시도
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Empty State */}
|
{/* Empty State */}
|
||||||
{!isLoading && !error && transactions.length === 0 && (
|
{!isLoading && filteredTransactions.length === 0 && (
|
||||||
<div className="text-center py-10">
|
<div className="text-center py-10">
|
||||||
<p className="text-gray-500 mb-3">
|
<p className="text-gray-500 mb-3">
|
||||||
{searchQuery.trim()
|
{searchQuery.trim()
|
||||||
@@ -146,7 +152,7 @@ const Transactions = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Transactions By Date */}
|
{/* Transactions By Date */}
|
||||||
{!isLoading && !error && (
|
{!isLoading && filteredTransactions.length > 0 && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{Object.entries(groupedTransactions).map(([date, transactions]) => (
|
{Object.entries(groupedTransactions).map(([date, transactions]) => (
|
||||||
<div key={date}>
|
<div key={date}>
|
||||||
|
|||||||
Reference in New Issue
Block a user