Implement budget input per category

Implement budget input fields for each category in the monthly budget settings, and automatically calculate and populate daily, weekly, and monthly budgets based on the sum of the category inputs.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-15 04:02:43 +00:00
parent 2626e35924
commit a5fac97a95
2 changed files with 160 additions and 32 deletions

View File

@@ -1,14 +1,23 @@
import React, { useState } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
import { Check, ChevronDown, ChevronUp, Calculator } from 'lucide-react';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
interface BudgetData {
targetAmount: number;
spentAmount: number;
remainingAmount: number;
}
interface CategoryBudget {
식비: number;
생활비: number;
교통비: number;
}
interface BudgetProgressCardProps {
budgetData: {
daily: BudgetData;
@@ -21,6 +30,7 @@ interface BudgetProgressCardProps {
calculatePercentage: (spent: number, target: number) => number;
onSaveBudget: (type: 'daily' | 'weekly' | 'monthly', amount: number) => void;
}
const BudgetProgressCard: React.FC<BudgetProgressCardProps> = ({
budgetData,
selectedTab,
@@ -59,12 +69,14 @@ const BudgetProgressCard: React.FC<BudgetProgressCardProps> = ({
</Tabs>
</div>;
};
interface BudgetTabContentProps {
data: BudgetData;
formatCurrency: (amount: number) => string;
calculatePercentage: (spent: number, target: number) => number;
onSaveBudget: (amount: number) => void;
}
const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
data,
formatCurrency,
@@ -74,14 +86,33 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
const percentage = calculatePercentage(data.spentAmount, data.targetAmount);
const [isOpen, setIsOpen] = useState(false);
const [budgetInput, setBudgetInput] = useState(data.targetAmount.toString());
const [categoryBudgets, setCategoryBudgets] = useState<CategoryBudget>({
식비: Math.round(data.targetAmount * 0.4),
생활비: Math.round(data.targetAmount * 0.4),
교통비: Math.round(data.targetAmount * 0.2)
});
const handleInputChange = (value: string) => {
// Remove all non-numeric characters
const numericValue = value.replace(/[^0-9]/g, '');
setBudgetInput(numericValue);
};
const handleCategoryInputChange = (value: string, category: keyof CategoryBudget) => {
// Remove all non-numeric characters
const numericValue = value.replace(/[^0-9]/g, '');
setCategoryBudgets(prev => ({
...prev,
[category]: parseInt(numericValue) || 0
}));
};
const handleSave = () => {
const amount = parseInt(budgetInput, 10) || 0;
onSaveBudget(amount);
// Calculate total from all categories
const totalAmount = Object.values(categoryBudgets).reduce((sum, value) => sum + value, 0);
onSaveBudget(totalAmount);
setIsOpen(false);
};
@@ -89,6 +120,7 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
const formatWithCommas = (amount: string) => {
return amount.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
return <div className="space-y-4">
<div>
<div className="flex items-center justify-between mb-2">
@@ -117,21 +149,60 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
<div className="pt-2 border-t border-gray-100">
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger className="flex items-center justify-between w-full px-1 text-left py-[10px]">
<span className="text-sm font-medium text-gray-600"> </span>
<span className="text-sm font-medium text-gray-600"> </span>
{isOpen ? <ChevronUp size={16} className="text-gray-500" /> : <ChevronDown size={16} className="text-gray-500" />}
</CollapsibleTrigger>
<CollapsibleContent className="pt-2">
<div className="flex items-center space-x-2">
<Input value={budgetInput} onChange={e => handleInputChange(e.target.value)} placeholder="목표 금액 입력" className="neuro-pressed" />
<Button onClick={handleSave} size="icon" className="neuro-flat text-slate-50 bg-slate-400 hover:bg-slate-300">
<Check size={18} />
<CollapsibleContent className="pt-2 space-y-3">
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm text-gray-600"></label>
<Input
value={categoryBudgets..toString()}
onChange={e => handleCategoryInputChange(e.target.value, '식비')}
className="neuro-pressed max-w-[150px]"
/>
</div>
<div className="flex items-center justify-between">
<label className="text-sm text-gray-600"></label>
<Input
value={categoryBudgets..toString()}
onChange={e => handleCategoryInputChange(e.target.value, '생활비')}
className="neuro-pressed max-w-[150px]"
/>
</div>
<div className="flex items-center justify-between">
<label className="text-sm text-gray-600"></label>
<Input
value={categoryBudgets..toString()}
onChange={e => handleCategoryInputChange(e.target.value, '교통비')}
className="neuro-pressed max-w-[150px]"
/>
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
<div className="flex items-center gap-1">
<Calculator size={16} className="text-gray-500" />
<span className="text-sm font-medium"> :</span>
</div>
<span className="font-semibold">{formatCurrency(Object.values(categoryBudgets).reduce((sum, value) => sum + value, 0))}</span>
</div>
<div className="flex justify-end">
<Button onClick={handleSave} size="sm" className="neuro-flat text-slate-50 bg-slate-400 hover:bg-slate-300">
<Check size={16} className="mr-1" />
</Button>
</div>
<p className="text-xs text-gray-500 mt-1 text-center py-[6px]"> , .</p>
<p className="text-xs text-gray-500 text-center py-[6px]"> , .</p>
</CollapsibleContent>
</Collapsible>
</div>
</div>;
};
export default BudgetProgressCard;
export default BudgetProgressCard;

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import NavBar from '@/components/NavBar';
import AddTransactionButton from '@/components/AddTransactionButton';
import Header from '@/components/Header';
@@ -37,45 +37,102 @@ const Index = () => {
type: 'income'
}];
// 예산 데이터 - 실제 앱에서는 백엔드에서 가져와야 함
// 카테고리별 예산
const [categoryBudgets, setCategoryBudgets] = useState({
식비: 400000,
생활비: 600000,
교통비: 200000
});
// 예산 데이터 - 월간 예산을 기반으로 일일, 주간 계산
const [budgetData, setBudgetData] = useState({
daily: {
targetAmount: 30000,
targetAmount: 0,
spentAmount: 15000,
remainingAmount: 15000
remainingAmount: 0
},
weekly: {
targetAmount: 200000,
targetAmount: 0,
spentAmount: 120000,
remainingAmount: 80000
remainingAmount: 0
},
monthly: {
targetAmount: 1200000,
targetAmount: 0,
spentAmount: 750000,
remainingAmount: 450000
remainingAmount: 0
}
});
// Updated to only use the three specified categories
// 초기 로드 및 카테고리 예산 변경 시 전체 예산 업데이트
useEffect(() => {
const totalMonthlyBudget = Object.values(categoryBudgets).reduce((sum, value) => sum + value, 0);
const totalDailyBudget = Math.round(totalMonthlyBudget / 30); // 월간을 일일로 변환
const totalWeeklyBudget = Math.round(totalMonthlyBudget / 4.3); // 월간을 주간으로 변환
setBudgetData({
daily: {
targetAmount: totalDailyBudget,
spentAmount: budgetData.daily.spentAmount,
remainingAmount: totalDailyBudget - budgetData.daily.spentAmount
},
weekly: {
targetAmount: totalWeeklyBudget,
spentAmount: budgetData.weekly.spentAmount,
remainingAmount: totalWeeklyBudget - budgetData.weekly.spentAmount
},
monthly: {
targetAmount: totalMonthlyBudget,
spentAmount: budgetData.monthly.spentAmount,
remainingAmount: totalMonthlyBudget - budgetData.monthly.spentAmount
}
});
}, [categoryBudgets]);
// 카테고리 업데이트
const categories = [
{ title: '식비', current: 240000, total: 400000 },
{ title: '생활비', current: 350000, total: 600000 },
{ title: '교통비', current: 190000, total: 200000 },
{ title: '식비', current: budgetData.monthly.spentAmount * 0.32, total: categoryBudgets.식비 },
{ title: '생활비', current: budgetData.monthly.spentAmount * 0.47, total: categoryBudgets.생활비 },
{ title: '교통비', current: budgetData.monthly.spentAmount * 0.21, total: categoryBudgets.교통비 },
];
// 예산 목표 업데이트 함수
const handleBudgetGoalUpdate = (type: 'daily' | 'weekly' | 'monthly', amount: number) => {
setBudgetData(prev => {
const remainingAmount = Math.max(0, amount - prev[type].spentAmount);
return {
...prev,
[type]: {
...prev[type],
// 월간 예산을 업데이트하고 일일, 주간도 자동 계산
if (type === 'monthly') {
const dailyAmount = Math.round(amount / 30);
const weeklyAmount = Math.round(amount / 4.3);
setBudgetData(prev => ({
daily: {
targetAmount: dailyAmount,
spentAmount: prev.daily.spentAmount,
remainingAmount: Math.max(0, dailyAmount - prev.daily.spentAmount)
},
weekly: {
targetAmount: weeklyAmount,
spentAmount: prev.weekly.spentAmount,
remainingAmount: Math.max(0, weeklyAmount - prev.weekly.spentAmount)
},
monthly: {
targetAmount: amount,
remainingAmount: remainingAmount
spentAmount: prev.monthly.spentAmount,
remainingAmount: Math.max(0, amount - prev.monthly.spentAmount)
}
};
});
}));
} else {
// 일일이나 주간 예산이 직접 업데이트되는 경우
setBudgetData(prev => {
const remainingAmount = Math.max(0, amount - prev[type].spentAmount);
return {
...prev,
[type]: {
...prev[type],
targetAmount: amount,
remainingAmount: remainingAmount
}
};
});
}
toast({
title: "목표 업데이트 완료",