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:
@@ -1,16 +1,18 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import { CirclePlus, Save, Check } from 'lucide-react';
|
import { useBudgetTabContent } from '@/hooks/budget/useBudgetTabContent';
|
||||||
import BudgetInputCard from './BudgetInputCard';
|
import BudgetHeader from './budget/BudgetHeader';
|
||||||
import { Button } from '@/components/ui/button';
|
import BudgetProgressBar from './budget/BudgetProgressBar';
|
||||||
import CategoryBudgetInputs from './CategoryBudgetInputs';
|
import BudgetStatusDisplay from './budget/BudgetStatusDisplay';
|
||||||
import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons';
|
import BudgetInputButton from './budget/BudgetInputButton';
|
||||||
|
import BudgetInputForm from './budget/BudgetInputForm';
|
||||||
|
|
||||||
interface BudgetData {
|
interface BudgetData {
|
||||||
targetAmount: number;
|
targetAmount: number;
|
||||||
spentAmount: number;
|
spentAmount: number;
|
||||||
remainingAmount: number;
|
remainingAmount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BudgetTabContentProps {
|
interface BudgetTabContentProps {
|
||||||
data: BudgetData;
|
data: BudgetData;
|
||||||
formatCurrency: (amount: number) => string;
|
formatCurrency: (amount: number) => string;
|
||||||
@@ -24,202 +26,66 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
|
|||||||
calculatePercentage,
|
calculatePercentage,
|
||||||
onSaveBudget
|
onSaveBudget
|
||||||
}) => {
|
}) => {
|
||||||
const [categoryBudgets, setCategoryBudgets] = useState<Record<string, number>>({});
|
const {
|
||||||
const [showBudgetInput, setShowBudgetInput] = useState(false);
|
categoryBudgets,
|
||||||
const spentAmount = data.spentAmount;
|
showBudgetInput,
|
||||||
const targetAmount = data.targetAmount;
|
toggleBudgetInput,
|
||||||
|
handleCategoryInputChange,
|
||||||
// 로그 추가 - 받은 데이터 확인
|
handleSaveCategoryBudgets,
|
||||||
useEffect(() => {
|
isBudgetSet,
|
||||||
console.log(`BudgetTabContent 수신 데이터:`, data);
|
actualPercentage,
|
||||||
}, [data]);
|
percentage,
|
||||||
|
isOverBudget,
|
||||||
// 전역 예산 데이터가 변경되었을 때 로컬 상태 갱신
|
isLowBudget,
|
||||||
useEffect(() => {
|
progressBarColor,
|
||||||
const handleBudgetDataUpdated = () => {
|
budgetStatusText,
|
||||||
console.log(`BudgetTabContent: 전역 예산 데이터 이벤트 감지, 현재 targetAmount=${targetAmount}`);
|
budgetAmount,
|
||||||
// 입력 폼이 열려있고 예산이 설정된 경우 폼 닫기
|
budgetButtonText,
|
||||||
if (showBudgetInput && targetAmount > 0) {
|
calculateTotalBudget
|
||||||
console.log('예산이 설정되어 입력 폼을 닫습니다.');
|
} = useBudgetTabContent({
|
||||||
setShowBudgetInput(false);
|
data,
|
||||||
}
|
calculatePercentage,
|
||||||
};
|
onSaveBudget
|
||||||
|
});
|
||||||
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 ? "예산 진행 상황" : "예산 입력하기 버튼");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isBudgetSet ? (
|
{isBudgetSet ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-between items-center mb-3">
|
<BudgetHeader
|
||||||
<div className="text-base font-bold">{formatCurrency(spentAmount)}</div>
|
spentAmount={data.spentAmount}
|
||||||
<div className="text-sm text-gray-500">/ {formatCurrency(targetAmount)}</div>
|
targetAmount={data.targetAmount}
|
||||||
</div>
|
formatCurrency={formatCurrency}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="w-full h-2 neuro-pressed overflow-hidden mb-3">
|
<BudgetProgressBar
|
||||||
<div
|
percentage={percentage}
|
||||||
className={`h-full ${progressBarColor} transition-all duration-700 ease-out`}
|
progressBarColor={progressBarColor}
|
||||||
style={{
|
/>
|
||||||
width: `${percentage}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center">
|
<BudgetStatusDisplay
|
||||||
<div className={`text-sm font-medium ${isOverBudget ? 'text-red-500' : 'text-gray-500'}`}>
|
budgetStatusText={budgetStatusText}
|
||||||
{budgetStatusText}{budgetAmount}
|
budgetAmount={budgetAmount}
|
||||||
</div>
|
actualPercentage={actualPercentage}
|
||||||
<div className="text-sm font-medium text-gray-500">
|
isOverBudget={isOverBudget}
|
||||||
{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>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : null}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showBudgetInput && (
|
<BudgetInputButton
|
||||||
<div className="mt-4">
|
isBudgetSet={isBudgetSet}
|
||||||
<div className="neuro-card p-4">
|
budgetButtonText={budgetButtonText}
|
||||||
<div>
|
toggleBudgetInput={toggleBudgetInput}
|
||||||
<h3 className="text-base font-medium mb-3">카테고리별 월간 예산 설정</h3>
|
/>
|
||||||
<p className="text-sm text-gray-500 mb-4">카테고리별로 월간 예산을 설정하세요. 일일, 주간 예산은 자동으로 계산됩니다.</p>
|
|
||||||
<CategoryBudgetInputs categoryBudgets={categoryBudgets} handleCategoryInputChange={handleCategoryInputChange} />
|
|
||||||
|
|
||||||
<div className="mt-4 border-t border-gray-300 pt-3">
|
<BudgetInputForm
|
||||||
<div className="flex justify-between items-center mb-4">
|
showBudgetInput={showBudgetInput}
|
||||||
<h3 className="font-medium text-sm px-[34px]">월간 총 예산:</h3>
|
categoryBudgets={categoryBudgets}
|
||||||
<p className="font-bold text-neuro-income text-base px-[10px]">{formatCurrency(calculateTotalBudget())}</p>
|
handleCategoryInputChange={handleCategoryInputChange}
|
||||||
</div>
|
handleSaveCategoryBudgets={handleSaveCategoryBudgets}
|
||||||
|
calculateTotalBudget={calculateTotalBudget}
|
||||||
<div className="flex justify-end">
|
formatCurrency={formatCurrency}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
23
src/components/budget/BudgetHeader.tsx
Normal file
23
src/components/budget/BudgetHeader.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface BudgetHeaderProps {
|
||||||
|
spentAmount: number;
|
||||||
|
targetAmount: number;
|
||||||
|
formatCurrency: (amount: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BudgetHeader: React.FC<BudgetHeaderProps> = ({
|
||||||
|
spentAmount,
|
||||||
|
targetAmount,
|
||||||
|
formatCurrency
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BudgetHeader;
|
||||||
42
src/components/budget/BudgetInputButton.tsx
Normal file
42
src/components/budget/BudgetInputButton.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { CirclePlus } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
interface BudgetInputButtonProps {
|
||||||
|
isBudgetSet: boolean;
|
||||||
|
budgetButtonText: string;
|
||||||
|
toggleBudgetInput: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BudgetInputButton: React.FC<BudgetInputButtonProps> = ({
|
||||||
|
isBudgetSet,
|
||||||
|
budgetButtonText,
|
||||||
|
toggleBudgetInput
|
||||||
|
}) => {
|
||||||
|
if (isBudgetSet) {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BudgetInputButton;
|
||||||
60
src/components/budget/BudgetInputForm.tsx
Normal file
60
src/components/budget/BudgetInputForm.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import CategoryBudgetInputs from '../CategoryBudgetInputs';
|
||||||
|
|
||||||
|
interface BudgetInputFormProps {
|
||||||
|
showBudgetInput: boolean;
|
||||||
|
categoryBudgets: Record<string, number>;
|
||||||
|
handleCategoryInputChange: (value: string, category: string) => void;
|
||||||
|
handleSaveCategoryBudgets: () => void;
|
||||||
|
calculateTotalBudget: () => number;
|
||||||
|
formatCurrency: (amount: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BudgetInputForm: React.FC<BudgetInputFormProps> = ({
|
||||||
|
showBudgetInput,
|
||||||
|
categoryBudgets,
|
||||||
|
handleCategoryInputChange,
|
||||||
|
handleSaveCategoryBudgets,
|
||||||
|
calculateTotalBudget,
|
||||||
|
formatCurrency
|
||||||
|
}) => {
|
||||||
|
if (!showBudgetInput) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BudgetInputForm;
|
||||||
25
src/components/budget/BudgetProgressBar.tsx
Normal file
25
src/components/budget/BudgetProgressBar.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface BudgetProgressBarProps {
|
||||||
|
percentage: number;
|
||||||
|
progressBarColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BudgetProgressBar: React.FC<BudgetProgressBarProps> = ({
|
||||||
|
percentage,
|
||||||
|
progressBarColor
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BudgetProgressBar;
|
||||||
29
src/components/budget/BudgetStatusDisplay.tsx
Normal file
29
src/components/budget/BudgetStatusDisplay.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface BudgetStatusDisplayProps {
|
||||||
|
budgetStatusText: string;
|
||||||
|
budgetAmount: string;
|
||||||
|
actualPercentage: number;
|
||||||
|
isOverBudget: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BudgetStatusDisplay: React.FC<BudgetStatusDisplayProps> = ({
|
||||||
|
budgetStatusText,
|
||||||
|
budgetAmount,
|
||||||
|
actualPercentage,
|
||||||
|
isOverBudget
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BudgetStatusDisplay;
|
||||||
183
src/hooks/budget/useBudgetTabContent.ts
Normal file
183
src/hooks/budget/useBudgetTabContent.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons';
|
||||||
|
|
||||||
|
interface BudgetData {
|
||||||
|
targetAmount: number;
|
||||||
|
spentAmount: number;
|
||||||
|
remainingAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseBudgetTabContentProps {
|
||||||
|
data: BudgetData;
|
||||||
|
calculatePercentage: (spent: number, target: number) => number;
|
||||||
|
onSaveBudget: (amount: number, categoryBudgets?: Record<string, number>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseBudgetTabContentReturn {
|
||||||
|
categoryBudgets: Record<string, number>;
|
||||||
|
showBudgetInput: boolean;
|
||||||
|
toggleBudgetInput: () => void;
|
||||||
|
handleCategoryInputChange: (value: string, category: string) => void;
|
||||||
|
handleSaveCategoryBudgets: () => void;
|
||||||
|
isBudgetSet: boolean;
|
||||||
|
actualPercentage: number;
|
||||||
|
percentage: number;
|
||||||
|
isOverBudget: boolean;
|
||||||
|
isLowBudget: boolean;
|
||||||
|
progressBarColor: string;
|
||||||
|
budgetStatusText: string;
|
||||||
|
budgetAmount: string;
|
||||||
|
budgetButtonText: string;
|
||||||
|
calculateTotalBudget: () => number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBudgetTabContent = ({
|
||||||
|
data,
|
||||||
|
calculatePercentage,
|
||||||
|
onSaveBudget
|
||||||
|
}: UseBudgetTabContentProps): UseBudgetTabContentReturn => {
|
||||||
|
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 ?
|
||||||
|
Math.abs(targetAmount - spentAmount).toLocaleString() :
|
||||||
|
Math.max(0, targetAmount - spentAmount).toLocaleString();
|
||||||
|
|
||||||
|
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 ? "예산 진행 상황" : "예산 입력하기 버튼");
|
||||||
|
|
||||||
|
return {
|
||||||
|
categoryBudgets,
|
||||||
|
showBudgetInput,
|
||||||
|
toggleBudgetInput,
|
||||||
|
handleCategoryInputChange,
|
||||||
|
handleSaveCategoryBudgets,
|
||||||
|
isBudgetSet,
|
||||||
|
actualPercentage,
|
||||||
|
percentage,
|
||||||
|
isOverBudget,
|
||||||
|
isLowBudget,
|
||||||
|
progressBarColor,
|
||||||
|
budgetStatusText,
|
||||||
|
budgetAmount,
|
||||||
|
budgetButtonText,
|
||||||
|
calculateTotalBudget
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user