Refactor BudgetTabContent component

Splits the BudgetTabContent component into smaller, more manageable components to improve code organization and maintainability. The UI and logic are separated into distinct components while preserving the original functionality.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-22 10:58:21 +00:00
parent ab86d9b5f9
commit d5ac14793b
7 changed files with 420 additions and 192 deletions

View File

@@ -1,16 +1,18 @@
import React, { useState, useEffect } from 'react';
import { CirclePlus, Save, Check } from 'lucide-react';
import BudgetInputCard from './BudgetInputCard';
import { Button } from '@/components/ui/button';
import CategoryBudgetInputs from './CategoryBudgetInputs';
import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons';
import React from 'react';
import { useBudgetTabContent } from '@/hooks/budget/useBudgetTabContent';
import BudgetHeader from './budget/BudgetHeader';
import BudgetProgressBar from './budget/BudgetProgressBar';
import BudgetStatusDisplay from './budget/BudgetStatusDisplay';
import BudgetInputButton from './budget/BudgetInputButton';
import BudgetInputForm from './budget/BudgetInputForm';
interface BudgetData {
targetAmount: number;
spentAmount: number;
remainingAmount: number;
}
interface BudgetTabContentProps {
data: BudgetData;
formatCurrency: (amount: number) => string;
@@ -24,202 +26,66 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
calculatePercentage,
onSaveBudget
}) => {
const [categoryBudgets, setCategoryBudgets] = useState<Record<string, number>>({});
const [showBudgetInput, setShowBudgetInput] = useState(false);
const spentAmount = data.spentAmount;
const targetAmount = data.targetAmount;
// 로그 추가 - 받은 데이터 확인
useEffect(() => {
console.log(`BudgetTabContent 수신 데이터:`, data);
}, [data]);
// 전역 예산 데이터가 변경되었을 때 로컬 상태 갱신
useEffect(() => {
const handleBudgetDataUpdated = () => {
console.log(`BudgetTabContent: 전역 예산 데이터 이벤트 감지, 현재 targetAmount=${targetAmount}`);
// 입력 폼이 열려있고 예산이 설정된 경우 폼 닫기
if (showBudgetInput && targetAmount > 0) {
console.log('예산이 설정되어 입력 폼을 닫습니다.');
setShowBudgetInput(false);
}
};
window.addEventListener('budgetDataUpdated', handleBudgetDataUpdated);
return () => window.removeEventListener('budgetDataUpdated', handleBudgetDataUpdated);
}, [showBudgetInput, targetAmount]);
// 예산 설정 여부 확인 - 데이터 targetAmount가 실제로 0보다 큰지 확인
const isBudgetSet = targetAmount > 0;
useEffect(() => {
if (isBudgetSet && showBudgetInput) {
console.log('예산이 설정되었으므로 입력 폼을 닫습니다.');
setShowBudgetInput(false);
}
}, [isBudgetSet, showBudgetInput]);
// 실제 백분율 계산 (초과해도 실제 퍼센트로 표시)
const actualPercentage = targetAmount > 0 ? Math.round((spentAmount / targetAmount) * 100) : 0;
const percentage = Math.min(actualPercentage, 100); // 대시보드 표시용으로는 100% 제한
// 예산 초과 여부 계산
const isOverBudget = spentAmount > targetAmount && targetAmount > 0;
// 예산이 얼마 남지 않은 경우 (10% 미만)
const isLowBudget = targetAmount > 0 && actualPercentage >= 90 && actualPercentage < 100;
// 프로그레스 바 색상 결정
const progressBarColor = isOverBudget ? 'bg-red-500' : isLowBudget ? 'bg-yellow-400' : 'bg-neuro-income';
// 남은 예산 또는 초과 예산 텍스트 및 금액
const budgetStatusText = isOverBudget ? '예산 초과: ' : '남은 예산: ';
const budgetAmount = isOverBudget ? formatCurrency(Math.abs(targetAmount - spentAmount)) : formatCurrency(Math.max(0, targetAmount - spentAmount));
const handleCategoryInputChange = (value: string, category: string) => {
const numValue = parseInt(value.replace(/,/g, ''), 10) || 0;
setCategoryBudgets(prev => ({
...prev,
[category]: numValue
}));
};
// 카테고리별 예산 합계 계산
const calculateTotalBudget = () => {
// 모든 EXPENSE_CATEGORIES에 있는 카테고리 포함해서 합계 계산
let total = 0;
EXPENSE_CATEGORIES.forEach(category => {
total += categoryBudgets[category] || 0;
});
console.log('카테고리 예산 총합:', total, categoryBudgets);
return total;
};
// 카테고리 예산 저장
const handleSaveCategoryBudgets = () => {
// 카테고리 예산 기본값 설정 - 모든 카테고리 포함
const updatedCategoryBudgets: Record<string, number> = {};
EXPENSE_CATEGORIES.forEach(category => {
updatedCategoryBudgets[category] = categoryBudgets[category] || 0;
});
const totalBudget = calculateTotalBudget();
console.log('카테고리 예산 저장 및 총 예산 설정:', totalBudget, updatedCategoryBudgets);
// 총액이 0이 아닐 때만 저장 처리
if (totalBudget > 0) {
onSaveBudget(totalBudget, updatedCategoryBudgets);
setShowBudgetInput(false);
// 이벤트 발생 추가 (데이터 저장 후 즉시 UI 업데이트를 위해)
setTimeout(() => {
window.dispatchEvent(new Event('budgetDataUpdated'));
}, 200);
} else {
alert('예산을 입력해주세요.');
}
};
// 기존 카테고리 예산 불러오기
useEffect(() => {
if (showBudgetInput) {
// 로컬 스토리지에서 카테고리 예산 불러오기
try {
const storedCategoryBudgets = localStorage.getItem('categoryBudgets');
if (storedCategoryBudgets) {
const parsedBudgets = JSON.parse(storedCategoryBudgets);
console.log('저장된 카테고리 예산 불러옴:', parsedBudgets);
setCategoryBudgets(parsedBudgets);
}
} catch (error) {
console.error('카테고리 예산 불러오기 오류:', error);
}
}
}, [showBudgetInput]);
// 예산 버튼 클릭 핸들러 - 토글 기능 추가
const toggleBudgetInput = () => {
console.log('예산 입력 폼 토글. 현재 상태:', showBudgetInput, '예산 설정 여부:', isBudgetSet);
setShowBudgetInput(prev => !prev);
};
// 예산 여부에 따른 텍스트 결정
const budgetButtonText = isBudgetSet ? "예산 수정하기" : "예산 입력하기";
// 화면에 표시할 내용 - 디버깅을 위한 로그 추가
console.log(`BudgetTabContent 렌더링: targetAmount=${targetAmount}, isBudgetSet=${isBudgetSet}, 표시될 화면:`, isBudgetSet ? "예산 진행 상황" : "예산 입력하기 버튼");
const {
categoryBudgets,
showBudgetInput,
toggleBudgetInput,
handleCategoryInputChange,
handleSaveCategoryBudgets,
isBudgetSet,
actualPercentage,
percentage,
isOverBudget,
isLowBudget,
progressBarColor,
budgetStatusText,
budgetAmount,
budgetButtonText,
calculateTotalBudget
} = useBudgetTabContent({
data,
calculatePercentage,
onSaveBudget
});
return (
<div>
{isBudgetSet ? (
<>
<div className="flex justify-between items-center mb-3">
<div className="text-base font-bold">{formatCurrency(spentAmount)}</div>
<div className="text-sm text-gray-500">/ {formatCurrency(targetAmount)}</div>
</div>
<BudgetHeader
spentAmount={data.spentAmount}
targetAmount={data.targetAmount}
formatCurrency={formatCurrency}
/>
<div className="w-full h-2 neuro-pressed overflow-hidden mb-3">
<div
className={`h-full ${progressBarColor} transition-all duration-700 ease-out`}
style={{
width: `${percentage}%`,
}}
/>
</div>
<BudgetProgressBar
percentage={percentage}
progressBarColor={progressBarColor}
/>
<div className="flex justify-between items-center">
<div className={`text-sm font-medium ${isOverBudget ? 'text-red-500' : 'text-gray-500'}`}>
{budgetStatusText}{budgetAmount}
</div>
<div className="text-sm font-medium text-gray-500">
{actualPercentage}%
</div>
</div>
<div className="mt-6">
<button
onClick={toggleBudgetInput}
className="text-neuro-income hover:underline flex items-center text-base font-semibold group"
>
<CirclePlus size={26} className="mr-2 text-neuro-income transition-transform group-hover:scale-110" />
<span className="text-base font-semibold">{budgetButtonText}</span>
</button>
</div>
<BudgetStatusDisplay
budgetStatusText={budgetStatusText}
budgetAmount={budgetAmount}
actualPercentage={actualPercentage}
isOverBudget={isOverBudget}
/>
</>
) : (
<div className="py-4 text-center">
<div className="text-gray-400 mb-4"> </div>
<Button onClick={toggleBudgetInput} variant="default" className="bg-neuro-income hover:bg-neuro-income/90 animate-pulse shadow-lg">
<CirclePlus className="mr-2" size={24} />
<span className="animate-pulse">{budgetButtonText}</span>
</Button>
</div>
)}
) : null}
{showBudgetInput && (
<div className="mt-4">
<div className="neuro-card p-4">
<div>
<h3 className="text-base font-medium mb-3"> </h3>
<p className="text-sm text-gray-500 mb-4"> . , .</p>
<CategoryBudgetInputs categoryBudgets={categoryBudgets} handleCategoryInputChange={handleCategoryInputChange} />
<BudgetInputButton
isBudgetSet={isBudgetSet}
budgetButtonText={budgetButtonText}
toggleBudgetInput={toggleBudgetInput}
/>
<div className="mt-4 border-t border-gray-300 pt-3">
<div className="flex justify-between items-center mb-4">
<h3 className="font-medium text-sm px-[34px]"> :</h3>
<p className="font-bold text-neuro-income text-base px-[10px]">{formatCurrency(calculateTotalBudget())}</p>
</div>
<div className="flex justify-end">
<Button onClick={handleSaveCategoryBudgets} size="sm" className="bg-neuro-income hover:bg-neuro-income/90 text-white mx-[6px]">
<Check size={18} className="mr-1" />
</Button>
</div>
</div>
</div>
</div>
</div>
)}
<BudgetInputForm
showBudgetInput={showBudgetInput}
categoryBudgets={categoryBudgets}
handleCategoryInputChange={handleCategoryInputChange}
handleSaveCategoryBudgets={handleSaveCategoryBudgets}
calculateTotalBudget={calculateTotalBudget}
formatCurrency={formatCurrency}
/>
</div>
);
};