Update budget display and input

- Allow budget progress bar to exceed 100% when over budget.
- Streamline budget input by directly displaying category budgets.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-17 12:10:59 +00:00
parent f9fb5364bb
commit edeb9f8ffb
3 changed files with 64 additions and 57 deletions

View File

@@ -20,9 +20,15 @@ const BudgetCategoriesSection: React.FC<BudgetCategoriesSectionProps> = ({
{categories.map((category, index) => { {categories.map((category, index) => {
// 예산 초과 여부 확인 // 예산 초과 여부 확인
const isOverBudget = category.current > category.total && category.total > 0; const isOverBudget = category.current > category.total && category.total > 0;
// 실제 백분율 계산 (초과해도 실제 퍼센트로 표시)
const actualPercentage = category.total > 0
? Math.round((category.current / category.total) * 100)
: 0;
// 프로그레스 바용 퍼센트 (100%로 제한하지 않음)
const displayPercentage = actualPercentage;
// 예산이 얼마 남지 않은 경우 (10% 미만) // 예산이 얼마 남지 않은 경우 (10% 미만)
const percentage = Math.min(Math.round((category.current / (category.total || 1)) * 100), 100); const isLowBudget = category.total > 0 && actualPercentage >= 90 && actualPercentage < 100;
const isLowBudget = category.total > 0 && percentage >= 90 && percentage < 100;
// 프로그레스 바 색상 결정 // 프로그레스 바 색상 결정
const progressBarColor = isOverBudget const progressBarColor = isOverBudget
@@ -57,7 +63,7 @@ const BudgetCategoriesSection: React.FC<BudgetCategoriesSectionProps> = ({
<div className="relative h-3 neuro-pressed overflow-hidden"> <div className="relative h-3 neuro-pressed overflow-hidden">
<div <div
className={`absolute top-0 left-0 h-full transition-all duration-700 ease-out ${progressBarColor}`} className={`absolute top-0 left-0 h-full transition-all duration-700 ease-out ${progressBarColor}`}
style={{ width: `${percentage}%` }} style={{ width: `${Math.min(displayPercentage, 100)}%` }}
/> />
</div> </div>
@@ -66,7 +72,7 @@ const BudgetCategoriesSection: React.FC<BudgetCategoriesSectionProps> = ({
{budgetStatusText}{formatCurrency(budgetAmount)} {budgetStatusText}{formatCurrency(budgetAmount)}
</span> </span>
<span className="text-xs font-medium text-gray-500"> <span className="text-xs font-medium text-gray-500">
{percentage}% {displayPercentage}%
</span> </span>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,9 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Plus, Wallet } from 'lucide-react'; import { Plus, Wallet } from 'lucide-react';
import BudgetInputCard from './BudgetInputCard'; import BudgetInputCard from './BudgetInputCard';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import CategoryBudgetInputs from './CategoryBudgetInputs';
interface BudgetData { interface BudgetData {
targetAmount: number; targetAmount: number;
@@ -23,11 +24,16 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
calculatePercentage, calculatePercentage,
onSaveBudget onSaveBudget
}) => { }) => {
const [categoryBudgets, setCategoryBudgets] = useState<Record<string, number>>({});
const [showBudgetInput, setShowBudgetInput] = useState(false); const [showBudgetInput, setShowBudgetInput] = useState(false);
const spentAmount = data.spentAmount; const spentAmount = data.spentAmount;
const targetAmount = data.targetAmount; const targetAmount = data.targetAmount;
const percentage = calculatePercentage(spentAmount, targetAmount); // 실제 백분율 계산 (초과해도 실제 퍼센트로 표시)
const actualPercentage = targetAmount > 0
? Math.round((spentAmount / targetAmount) * 100)
: 0;
const percentage = actualPercentage;
const isFirstBudget = targetAmount === 0; const isFirstBudget = targetAmount === 0;
// 예산 초과 여부 계산 // 예산 초과 여부 계산
@@ -51,6 +57,14 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
? formatCurrency(Math.abs(targetAmount - spentAmount)) ? formatCurrency(Math.abs(targetAmount - spentAmount))
: formatCurrency(Math.max(0, targetAmount - spentAmount)); : formatCurrency(Math.max(0, targetAmount - spentAmount));
const handleCategoryInputChange = (value: string, category: string) => {
const numValue = parseInt(value, 10) || 0;
setCategoryBudgets(prev => ({
...prev,
[category]: numValue
}));
};
return ( return (
<div> <div>
{targetAmount > 0 ? ( {targetAmount > 0 ? (
@@ -63,7 +77,7 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
<div className="w-full h-2 neuro-pressed overflow-hidden mb-3"> <div className="w-full h-2 neuro-pressed overflow-hidden mb-3">
<div <div
className={`h-full ${progressBarColor} transition-all duration-700 ease-out`} className={`h-full ${progressBarColor} transition-all duration-700 ease-out`}
style={{ width: `${percentage}%` }} style={{ width: `${Math.min(percentage, 100)}%` }}
/> />
</div> </div>
@@ -78,7 +92,7 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
<div className="mt-6"> <div className="mt-6">
<button <button
onClick={() => setShowBudgetInput(!showBudgetInput)} onClick={() => setShowBudgetInput(true)}
className="text-neuro-income text-sm font-medium hover:underline flex items-center text-[15px]" className="text-neuro-income text-sm font-medium hover:underline flex items-center text-[15px]"
> >
<Plus size={16} className="mr-1" /> <Plus size={16} className="mr-1" />
@@ -101,18 +115,41 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
{showBudgetInput && ( {showBudgetInput && (
<div className="mt-4"> <div className="mt-4">
<BudgetInputCard <div className="neuro-card p-4">
initialBudgets={{ <div className="mb-4">
daily: isFirstBudget ? 0 : data.targetAmount, <h3 className="text-base font-medium mb-3"> </h3>
weekly: isFirstBudget ? 0 : data.targetAmount, <div className="flex items-center space-x-2">
monthly: isFirstBudget ? 0 : data.targetAmount <input
type="text"
className="w-full p-2 rounded neuro-pressed"
placeholder="예산 금액 입력"
value={targetAmount > 0 ? targetAmount : ''}
onChange={(e) => {
const value = e.target.value.replace(/[^0-9]/g, '');
if (value) {
const amount = parseInt(value, 10);
onSaveBudget(amount, categoryBudgets);
}
}} }}
onSave={(type, amount) => {
onSaveBudget(amount);
setShowBudgetInput(false);
}}
highlight={isFirstBudget}
/> />
<Button
onClick={() => setShowBudgetInput(false)}
size="icon"
className="bg-neuro-income hover:bg-neuro-income/90 text-white"
>
<Wallet size={18} />
</Button>
</div>
</div>
<div>
<h3 className="text-base font-medium mb-3"> </h3>
<CategoryBudgetInputs
categoryBudgets={categoryBudgets}
handleCategoryInputChange={handleCategoryInputChange}
/>
</div>
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -1,9 +1,8 @@
import React, { useEffect, useRef } from 'react'; import React from 'react';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons'; import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons';
import { useIsMobile } from '@/hooks/use-mobile'; import { useIsMobile } from '@/hooks/use-mobile';
import { toast } from '@/components/ui/use-toast';
interface CategoryBudgetInputsProps { interface CategoryBudgetInputsProps {
categoryBudgets: Record<string, number>; categoryBudgets: Record<string, number>;
@@ -15,7 +14,6 @@ const CategoryBudgetInputs: React.FC<CategoryBudgetInputsProps> = ({
handleCategoryInputChange handleCategoryInputChange
}) => { }) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const previousBudgetsRef = useRef<Record<string, number>>({});
// Format number with commas for display // Format number with commas for display
const formatWithCommas = (value: number): string => { const formatWithCommas = (value: number): string => {
@@ -36,40 +34,6 @@ const CategoryBudgetInputs: React.FC<CategoryBudgetInputsProps> = ({
}, 300); }, 300);
}; };
// 컴포넌트가 마운트될 때 categoryBudgets가 로컬 스토리지에서 다시 로드되도록 이벤트 리스너 설정
useEffect(() => {
const handleStorageChange = () => {
// 부모 컴포넌트에서 데이터가 업데이트되므로 별도 처리 필요 없음
console.log('카테고리 예산 데이터 변경 감지됨');
};
window.addEventListener('categoryBudgetsUpdated', handleStorageChange);
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('categoryBudgetsUpdated', handleStorageChange);
window.removeEventListener('storage', handleStorageChange);
};
}, []);
// 값이 변경될 때마다 토스트 메시지 표시
useEffect(() => {
const hasChanges = Object.keys(categoryBudgets).some(
category => categoryBudgets[category] !== previousBudgetsRef.current[category]
);
const totalBudget = Object.values(categoryBudgets).reduce((sum, val) => sum + val, 0);
const previousTotal = Object.values(previousBudgetsRef.current).reduce((sum, val) => sum + val, 0);
// 이전 값과 다르고, 총 예산이 있는 경우 토스트 표시
if (hasChanges && totalBudget > 0 && totalBudget !== previousTotal) {
// 토스트 메시지는 storage에서 처리
}
// 현재 값을 이전 값으로 업데이트
previousBudgetsRef.current = { ...categoryBudgets };
}, [categoryBudgets]);
return ( return (
<div className="space-y-2 w-full"> <div className="space-y-2 w-full">
{EXPENSE_CATEGORIES.map(category => ( {EXPENSE_CATEGORIES.map(category => (