feat: Implement budget edit functionality
When the "Edit Budget" button is clicked, display a popup similar to the expense input form for budget modification.
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } 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';
|
||||
import BudgetDialog from './budget/BudgetDialog';
|
||||
|
||||
interface BudgetData {
|
||||
targetAmount: number;
|
||||
@@ -26,10 +26,11 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
|
||||
calculatePercentage,
|
||||
onSaveBudget
|
||||
}) => {
|
||||
const [showBudgetDialog, setShowBudgetDialog] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const {
|
||||
categoryBudgets,
|
||||
showBudgetInput,
|
||||
toggleBudgetInput,
|
||||
handleCategoryInputChange,
|
||||
handleSaveCategoryBudgets,
|
||||
isBudgetSet,
|
||||
@@ -48,6 +49,23 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
|
||||
onSaveBudget
|
||||
});
|
||||
|
||||
const handleOpenDialog = () => {
|
||||
setShowBudgetDialog(true);
|
||||
};
|
||||
|
||||
const handleSaveBudget = () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
handleSaveCategoryBudgets();
|
||||
} finally {
|
||||
// 성공 또는 실패 여부와 관계없이 제출 상태 해제 및 다이얼로그 닫기
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false);
|
||||
setShowBudgetDialog(false);
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
// 월간 예산 모드 로깅
|
||||
React.useEffect(() => {
|
||||
console.log('BudgetTabContent 렌더링: 월간 예산');
|
||||
@@ -81,16 +99,17 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
|
||||
<BudgetInputButton
|
||||
isBudgetSet={isBudgetSet}
|
||||
budgetButtonText={budgetButtonText}
|
||||
toggleBudgetInput={toggleBudgetInput}
|
||||
toggleBudgetInput={handleOpenDialog}
|
||||
/>
|
||||
|
||||
<BudgetInputForm
|
||||
showBudgetInput={showBudgetInput}
|
||||
<BudgetDialog
|
||||
open={showBudgetDialog}
|
||||
onOpenChange={setShowBudgetDialog}
|
||||
categoryBudgets={categoryBudgets}
|
||||
handleCategoryInputChange={handleCategoryInputChange}
|
||||
handleSaveCategoryBudgets={handleSaveCategoryBudgets}
|
||||
handleSaveCategoryBudgets={handleSaveBudget}
|
||||
calculateTotalBudget={calculateTotalBudget}
|
||||
formatCurrency={formatCurrency}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
95
src/components/budget/BudgetDialog.tsx
Normal file
95
src/components/budget/BudgetDialog.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription
|
||||
} from '@/components/ui/dialog';
|
||||
import CategoryBudgetInputs from '../CategoryBudgetInputs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Check } from 'lucide-react';
|
||||
import { formatCurrency } from '@/utils/formatters';
|
||||
|
||||
interface BudgetDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
categoryBudgets: Record<string, number>;
|
||||
handleCategoryInputChange: (value: string, category: string) => void;
|
||||
handleSaveCategoryBudgets: () => void;
|
||||
calculateTotalBudget: () => number;
|
||||
isSubmitting?: boolean;
|
||||
}
|
||||
|
||||
const BudgetDialog: React.FC<BudgetDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
categoryBudgets,
|
||||
handleCategoryInputChange,
|
||||
handleSaveCategoryBudgets,
|
||||
calculateTotalBudget,
|
||||
isSubmitting = false
|
||||
}) => {
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSaveCategoryBudgets();
|
||||
};
|
||||
|
||||
const formattedTotal = formatCurrency(calculateTotalBudget());
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(newOpen) => {
|
||||
if (isSubmitting && !newOpen) return;
|
||||
onOpenChange(newOpen);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="w-[90%] max-w-sm mx-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>예산 설정</DialogTitle>
|
||||
<DialogDescription>
|
||||
카테고리별로 월간 예산을 설정하세요. 일일, 주간 예산은 자동으로 계산됩니다.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<CategoryBudgetInputs
|
||||
categoryBudgets={categoryBudgets}
|
||||
handleCategoryInputChange={handleCategoryInputChange}
|
||||
/>
|
||||
|
||||
<div className="border-t border-gray-300 pt-3 mt-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="font-medium text-sm">월간 총 예산:</h3>
|
||||
<p className="font-bold text-neuro-income text-base">{formattedTotal}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-neuro-income hover:bg-neuro-income/90 text-white"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Check size={18} className="mr-1" />
|
||||
저장하기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default BudgetDialog;
|
||||
@@ -1,68 +1,9 @@
|
||||
|
||||
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;
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSaveCategoryBudgets();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="neuro-card p-4">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<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
|
||||
type="submit"
|
||||
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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
// 이 컴포넌트는 더 이상 사용되지 않으며 BudgetDialog로 대체되었습니다
|
||||
const BudgetInputForm = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export default BudgetInputForm;
|
||||
|
||||
@@ -16,8 +16,6 @@ interface UseBudgetTabContentProps {
|
||||
|
||||
interface UseBudgetTabContentReturn {
|
||||
categoryBudgets: Record<string, number>;
|
||||
showBudgetInput: boolean;
|
||||
toggleBudgetInput: () => void;
|
||||
handleCategoryInputChange: (value: string, category: string) => void;
|
||||
handleSaveCategoryBudgets: () => void;
|
||||
isBudgetSet: boolean;
|
||||
@@ -38,7 +36,6 @@ export const useBudgetTabContent = ({
|
||||
onSaveBudget
|
||||
}: UseBudgetTabContentProps): UseBudgetTabContentReturn => {
|
||||
const [categoryBudgets, setCategoryBudgets] = useState<Record<string, number>>({});
|
||||
const [showBudgetInput, setShowBudgetInput] = useState(false);
|
||||
const spentAmount = data.spentAmount;
|
||||
const targetAmount = data.targetAmount;
|
||||
|
||||
@@ -55,7 +52,7 @@ export const useBudgetTabContent = ({
|
||||
|
||||
window.addEventListener('budgetDataUpdated', handleBudgetDataUpdated);
|
||||
return () => window.removeEventListener('budgetDataUpdated', handleBudgetDataUpdated);
|
||||
}, [showBudgetInput, targetAmount]);
|
||||
}, [targetAmount]);
|
||||
|
||||
// 예산 설정 여부 확인 - 데이터 targetAmount가 실제로 0보다 큰지 확인
|
||||
const isBudgetSet = targetAmount > 0;
|
||||
@@ -112,7 +109,6 @@ export const useBudgetTabContent = ({
|
||||
if (totalBudget > 0) {
|
||||
// 명시적으로 월간 예산으로 설정 - 항상 월간 예산만 저장
|
||||
onSaveBudget(totalBudget, updatedCategoryBudgets);
|
||||
setShowBudgetInput(false);
|
||||
|
||||
// 이벤트 발생 추가 (데이터 저장 후 즉시 UI 업데이트를 위해)
|
||||
setTimeout(() => {
|
||||
@@ -126,7 +122,6 @@ export const useBudgetTabContent = ({
|
||||
|
||||
// 기존 카테고리 예산 불러오기
|
||||
useEffect(() => {
|
||||
if (showBudgetInput) {
|
||||
// 로컬 스토리지에서 카테고리 예산 불러오기
|
||||
try {
|
||||
const storedCategoryBudgets = localStorage.getItem('categoryBudgets');
|
||||
@@ -138,14 +133,7 @@ export const useBudgetTabContent = ({
|
||||
} catch (error) {
|
||||
console.error('카테고리 예산 불러오기 오류:', error);
|
||||
}
|
||||
}
|
||||
}, [showBudgetInput]);
|
||||
|
||||
// 예산 버튼 클릭 핸들러 - 토글 기능 추가
|
||||
const toggleBudgetInput = () => {
|
||||
console.log('예산 입력 폼 토글. 현재 상태:', showBudgetInput, '예산 설정 여부:', isBudgetSet);
|
||||
setShowBudgetInput(prev => !prev);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 예산 여부에 따른 텍스트 결정
|
||||
const budgetButtonText = isBudgetSet ? "예산 수정하기" : "예산 입력하기";
|
||||
@@ -155,8 +143,6 @@ export const useBudgetTabContent = ({
|
||||
|
||||
return {
|
||||
categoryBudgets,
|
||||
showBudgetInput,
|
||||
toggleBudgetInput,
|
||||
handleCategoryInputChange,
|
||||
handleSaveCategoryBudgets,
|
||||
isBudgetSet,
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
|
||||
/**
|
||||
* 금액 포맷 유틸리티
|
||||
*/
|
||||
|
||||
// 금액을 한국 원화 형식으로 포맷팅
|
||||
export const formatCurrency = (amount: number): string => {
|
||||
return new Intl.NumberFormat('ko-KR', {
|
||||
style: 'currency',
|
||||
currency: 'KRW',
|
||||
maximumFractionDigits: 0
|
||||
}).format(amount);
|
||||
return amount.toLocaleString('ko-KR') + '원';
|
||||
};
|
||||
|
||||
export const calculatePercentage = (spent: number, target: number): number => {
|
||||
// 타겟이 0이면 0%를 반환하도록 수정
|
||||
if (target === 0 || isNaN(target)) return 0;
|
||||
return Math.min(Math.round(spent / target * 100), 100);
|
||||
// 숫자 문자열에서 쉼표 제거
|
||||
export const removeCommas = (value: string): string => {
|
||||
return value.replace(/,/g, '');
|
||||
};
|
||||
|
||||
// 숫자 문자열에 쉼표 추가
|
||||
export const addCommas = (value: string): string => {
|
||||
// 숫자 이외의 문자는 제거
|
||||
const numericValue = value.replace(/[^\d]/g, '');
|
||||
return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
};
|
||||
|
||||
// 금액 입력 값 포맷팅 (입력 필드용)
|
||||
export const formatWithCommas = (value: string): string => {
|
||||
// 기존 쉼표 제거 후 다시 포맷팅
|
||||
return addCommas(removeCommas(value));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user