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 React, { useState } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; 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'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
interface BudgetData { interface BudgetData {
targetAmount: number; targetAmount: number;
spentAmount: number; spentAmount: number;
remainingAmount: number; remainingAmount: number;
} }
interface CategoryBudget {
식비: number;
생활비: number;
교통비: number;
}
interface BudgetProgressCardProps { interface BudgetProgressCardProps {
budgetData: { budgetData: {
daily: BudgetData; daily: BudgetData;
@@ -21,6 +30,7 @@ interface BudgetProgressCardProps {
calculatePercentage: (spent: number, target: number) => number; calculatePercentage: (spent: number, target: number) => number;
onSaveBudget: (type: 'daily' | 'weekly' | 'monthly', amount: number) => void; onSaveBudget: (type: 'daily' | 'weekly' | 'monthly', amount: number) => void;
} }
const BudgetProgressCard: React.FC<BudgetProgressCardProps> = ({ const BudgetProgressCard: React.FC<BudgetProgressCardProps> = ({
budgetData, budgetData,
selectedTab, selectedTab,
@@ -59,12 +69,14 @@ const BudgetProgressCard: React.FC<BudgetProgressCardProps> = ({
</Tabs> </Tabs>
</div>; </div>;
}; };
interface BudgetTabContentProps { interface BudgetTabContentProps {
data: BudgetData; data: BudgetData;
formatCurrency: (amount: number) => string; formatCurrency: (amount: number) => string;
calculatePercentage: (spent: number, target: number) => number; calculatePercentage: (spent: number, target: number) => number;
onSaveBudget: (amount: number) => void; onSaveBudget: (amount: number) => void;
} }
const BudgetTabContent: React.FC<BudgetTabContentProps> = ({ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
data, data,
formatCurrency, formatCurrency,
@@ -74,14 +86,33 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
const percentage = calculatePercentage(data.spentAmount, data.targetAmount); const percentage = calculatePercentage(data.spentAmount, data.targetAmount);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [budgetInput, setBudgetInput] = useState(data.targetAmount.toString()); 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) => { const handleInputChange = (value: string) => {
// Remove all non-numeric characters // Remove all non-numeric characters
const numericValue = value.replace(/[^0-9]/g, ''); const numericValue = value.replace(/[^0-9]/g, '');
setBudgetInput(numericValue); 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 handleSave = () => {
const amount = parseInt(budgetInput, 10) || 0; // Calculate total from all categories
onSaveBudget(amount); const totalAmount = Object.values(categoryBudgets).reduce((sum, value) => sum + value, 0);
onSaveBudget(totalAmount);
setIsOpen(false); setIsOpen(false);
}; };
@@ -89,6 +120,7 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
const formatWithCommas = (amount: string) => { const formatWithCommas = (amount: string) => {
return amount.replace(/\B(?=(\d{3})+(?!\d))/g, ','); return amount.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}; };
return <div className="space-y-4"> return <div className="space-y-4">
<div> <div>
<div className="flex items-center justify-between mb-2"> <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"> <div className="pt-2 border-t border-gray-100">
<Collapsible open={isOpen} onOpenChange={setIsOpen}> <Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger className="flex items-center justify-between w-full px-1 text-left py-[10px]"> <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" />} {isOpen ? <ChevronUp size={16} className="text-gray-500" /> : <ChevronDown size={16} className="text-gray-500" />}
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="pt-2"> <CollapsibleContent className="pt-2 space-y-3">
<div className="flex items-center space-x-2"> <div className="space-y-2">
<Input value={budgetInput} onChange={e => handleInputChange(e.target.value)} placeholder="목표 금액 입력" className="neuro-pressed" /> <div className="flex items-center justify-between">
<Button onClick={handleSave} size="icon" className="neuro-flat text-slate-50 bg-slate-400 hover:bg-slate-300"> <label className="text-sm text-gray-600"></label>
<Check size={18} /> <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> </Button>
</div> </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> </CollapsibleContent>
</Collapsible> </Collapsible>
</div> </div>
</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 NavBar from '@/components/NavBar';
import AddTransactionButton from '@/components/AddTransactionButton'; import AddTransactionButton from '@/components/AddTransactionButton';
import Header from '@/components/Header'; import Header from '@/components/Header';
@@ -37,45 +37,102 @@ const Index = () => {
type: 'income' type: 'income'
}]; }];
// 예산 데이터 - 실제 앱에서는 백엔드에서 가져와야 함 // 카테고리별 예산
const [categoryBudgets, setCategoryBudgets] = useState({
식비: 400000,
생활비: 600000,
교통비: 200000
});
// 예산 데이터 - 월간 예산을 기반으로 일일, 주간 계산
const [budgetData, setBudgetData] = useState({ const [budgetData, setBudgetData] = useState({
daily: { daily: {
targetAmount: 30000, targetAmount: 0,
spentAmount: 15000, spentAmount: 15000,
remainingAmount: 15000 remainingAmount: 0
}, },
weekly: { weekly: {
targetAmount: 200000, targetAmount: 0,
spentAmount: 120000, spentAmount: 120000,
remainingAmount: 80000 remainingAmount: 0
}, },
monthly: { monthly: {
targetAmount: 1200000, targetAmount: 0,
spentAmount: 750000, 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 = [ const categories = [
{ title: '식비', current: 240000, total: 400000 }, { title: '식비', current: budgetData.monthly.spentAmount * 0.32, total: categoryBudgets.식비 },
{ title: '생활비', current: 350000, total: 600000 }, { title: '생활비', current: budgetData.monthly.spentAmount * 0.47, total: categoryBudgets.생활비 },
{ title: '교통비', current: 190000, total: 200000 }, { title: '교통비', current: budgetData.monthly.spentAmount * 0.21, total: categoryBudgets.교통비 },
]; ];
// 예산 목표 업데이트 함수 // 예산 목표 업데이트 함수
const handleBudgetGoalUpdate = (type: 'daily' | 'weekly' | 'monthly', amount: number) => { const handleBudgetGoalUpdate = (type: 'daily' | 'weekly' | 'monthly', amount: number) => {
setBudgetData(prev => { // 월간 예산을 업데이트하고 일일, 주간도 자동 계산
const remainingAmount = Math.max(0, amount - prev[type].spentAmount); if (type === 'monthly') {
return { const dailyAmount = Math.round(amount / 30);
...prev, const weeklyAmount = Math.round(amount / 4.3);
[type]: {
...prev[type], 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, 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({ toast({
title: "목표 업데이트 완료", title: "목표 업데이트 완료",