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 { useBudgetTabContent } from '@/hooks/budget/useBudgetTabContent';
|
||||||
import BudgetHeader from './budget/BudgetHeader';
|
import BudgetHeader from './budget/BudgetHeader';
|
||||||
import BudgetProgressBar from './budget/BudgetProgressBar';
|
import BudgetProgressBar from './budget/BudgetProgressBar';
|
||||||
import BudgetStatusDisplay from './budget/BudgetStatusDisplay';
|
import BudgetStatusDisplay from './budget/BudgetStatusDisplay';
|
||||||
import BudgetInputButton from './budget/BudgetInputButton';
|
import BudgetInputButton from './budget/BudgetInputButton';
|
||||||
import BudgetInputForm from './budget/BudgetInputForm';
|
import BudgetDialog from './budget/BudgetDialog';
|
||||||
|
|
||||||
interface BudgetData {
|
interface BudgetData {
|
||||||
targetAmount: number;
|
targetAmount: number;
|
||||||
@@ -26,10 +26,11 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
|
|||||||
calculatePercentage,
|
calculatePercentage,
|
||||||
onSaveBudget
|
onSaveBudget
|
||||||
}) => {
|
}) => {
|
||||||
|
const [showBudgetDialog, setShowBudgetDialog] = useState(false);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
categoryBudgets,
|
categoryBudgets,
|
||||||
showBudgetInput,
|
|
||||||
toggleBudgetInput,
|
|
||||||
handleCategoryInputChange,
|
handleCategoryInputChange,
|
||||||
handleSaveCategoryBudgets,
|
handleSaveCategoryBudgets,
|
||||||
isBudgetSet,
|
isBudgetSet,
|
||||||
@@ -48,6 +49,23 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
|
|||||||
onSaveBudget
|
onSaveBudget
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleOpenDialog = () => {
|
||||||
|
setShowBudgetDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveBudget = () => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
handleSaveCategoryBudgets();
|
||||||
|
} finally {
|
||||||
|
// 성공 또는 실패 여부와 관계없이 제출 상태 해제 및 다이얼로그 닫기
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setShowBudgetDialog(false);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 월간 예산 모드 로깅
|
// 월간 예산 모드 로깅
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log('BudgetTabContent 렌더링: 월간 예산');
|
console.log('BudgetTabContent 렌더링: 월간 예산');
|
||||||
@@ -81,16 +99,17 @@ const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
|
|||||||
<BudgetInputButton
|
<BudgetInputButton
|
||||||
isBudgetSet={isBudgetSet}
|
isBudgetSet={isBudgetSet}
|
||||||
budgetButtonText={budgetButtonText}
|
budgetButtonText={budgetButtonText}
|
||||||
toggleBudgetInput={toggleBudgetInput}
|
toggleBudgetInput={handleOpenDialog}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BudgetInputForm
|
<BudgetDialog
|
||||||
showBudgetInput={showBudgetInput}
|
open={showBudgetDialog}
|
||||||
|
onOpenChange={setShowBudgetDialog}
|
||||||
categoryBudgets={categoryBudgets}
|
categoryBudgets={categoryBudgets}
|
||||||
handleCategoryInputChange={handleCategoryInputChange}
|
handleCategoryInputChange={handleCategoryInputChange}
|
||||||
handleSaveCategoryBudgets={handleSaveCategoryBudgets}
|
handleSaveCategoryBudgets={handleSaveBudget}
|
||||||
calculateTotalBudget={calculateTotalBudget}
|
calculateTotalBudget={calculateTotalBudget}
|
||||||
formatCurrency={formatCurrency}
|
isSubmitting={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 React from 'react';
|
||||||
import { Check } from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import CategoryBudgetInputs from '../CategoryBudgetInputs';
|
|
||||||
|
|
||||||
interface BudgetInputFormProps {
|
// 이 컴포넌트는 더 이상 사용되지 않으며 BudgetDialog로 대체되었습니다
|
||||||
showBudgetInput: boolean;
|
const BudgetInputForm = () => {
|
||||||
categoryBudgets: Record<string, number>;
|
return null;
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BudgetInputForm;
|
export default BudgetInputForm;
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ interface UseBudgetTabContentProps {
|
|||||||
|
|
||||||
interface UseBudgetTabContentReturn {
|
interface UseBudgetTabContentReturn {
|
||||||
categoryBudgets: Record<string, number>;
|
categoryBudgets: Record<string, number>;
|
||||||
showBudgetInput: boolean;
|
|
||||||
toggleBudgetInput: () => void;
|
|
||||||
handleCategoryInputChange: (value: string, category: string) => void;
|
handleCategoryInputChange: (value: string, category: string) => void;
|
||||||
handleSaveCategoryBudgets: () => void;
|
handleSaveCategoryBudgets: () => void;
|
||||||
isBudgetSet: boolean;
|
isBudgetSet: boolean;
|
||||||
@@ -38,7 +36,6 @@ export const useBudgetTabContent = ({
|
|||||||
onSaveBudget
|
onSaveBudget
|
||||||
}: UseBudgetTabContentProps): UseBudgetTabContentReturn => {
|
}: UseBudgetTabContentProps): UseBudgetTabContentReturn => {
|
||||||
const [categoryBudgets, setCategoryBudgets] = useState<Record<string, number>>({});
|
const [categoryBudgets, setCategoryBudgets] = useState<Record<string, number>>({});
|
||||||
const [showBudgetInput, setShowBudgetInput] = useState(false);
|
|
||||||
const spentAmount = data.spentAmount;
|
const spentAmount = data.spentAmount;
|
||||||
const targetAmount = data.targetAmount;
|
const targetAmount = data.targetAmount;
|
||||||
|
|
||||||
@@ -55,7 +52,7 @@ export const useBudgetTabContent = ({
|
|||||||
|
|
||||||
window.addEventListener('budgetDataUpdated', handleBudgetDataUpdated);
|
window.addEventListener('budgetDataUpdated', handleBudgetDataUpdated);
|
||||||
return () => window.removeEventListener('budgetDataUpdated', handleBudgetDataUpdated);
|
return () => window.removeEventListener('budgetDataUpdated', handleBudgetDataUpdated);
|
||||||
}, [showBudgetInput, targetAmount]);
|
}, [targetAmount]);
|
||||||
|
|
||||||
// 예산 설정 여부 확인 - 데이터 targetAmount가 실제로 0보다 큰지 확인
|
// 예산 설정 여부 확인 - 데이터 targetAmount가 실제로 0보다 큰지 확인
|
||||||
const isBudgetSet = targetAmount > 0;
|
const isBudgetSet = targetAmount > 0;
|
||||||
@@ -112,7 +109,6 @@ export const useBudgetTabContent = ({
|
|||||||
if (totalBudget > 0) {
|
if (totalBudget > 0) {
|
||||||
// 명시적으로 월간 예산으로 설정 - 항상 월간 예산만 저장
|
// 명시적으로 월간 예산으로 설정 - 항상 월간 예산만 저장
|
||||||
onSaveBudget(totalBudget, updatedCategoryBudgets);
|
onSaveBudget(totalBudget, updatedCategoryBudgets);
|
||||||
setShowBudgetInput(false);
|
|
||||||
|
|
||||||
// 이벤트 발생 추가 (데이터 저장 후 즉시 UI 업데이트를 위해)
|
// 이벤트 발생 추가 (데이터 저장 후 즉시 UI 업데이트를 위해)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -126,26 +122,18 @@ export const useBudgetTabContent = ({
|
|||||||
|
|
||||||
// 기존 카테고리 예산 불러오기
|
// 기존 카테고리 예산 불러오기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showBudgetInput) {
|
// 로컬 스토리지에서 카테고리 예산 불러오기
|
||||||
// 로컬 스토리지에서 카테고리 예산 불러오기
|
try {
|
||||||
try {
|
const storedCategoryBudgets = localStorage.getItem('categoryBudgets');
|
||||||
const storedCategoryBudgets = localStorage.getItem('categoryBudgets');
|
if (storedCategoryBudgets) {
|
||||||
if (storedCategoryBudgets) {
|
const parsedBudgets = JSON.parse(storedCategoryBudgets);
|
||||||
const parsedBudgets = JSON.parse(storedCategoryBudgets);
|
console.log('저장된 카테고리 예산 불러옴:', parsedBudgets);
|
||||||
console.log('저장된 카테고리 예산 불러옴:', parsedBudgets);
|
setCategoryBudgets(parsedBudgets);
|
||||||
setCategoryBudgets(parsedBudgets);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('카테고리 예산 불러오기 오류:', error);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('카테고리 예산 불러오기 오류:', error);
|
||||||
}
|
}
|
||||||
}, [showBudgetInput]);
|
}, []);
|
||||||
|
|
||||||
// 예산 버튼 클릭 핸들러 - 토글 기능 추가
|
|
||||||
const toggleBudgetInput = () => {
|
|
||||||
console.log('예산 입력 폼 토글. 현재 상태:', showBudgetInput, '예산 설정 여부:', isBudgetSet);
|
|
||||||
setShowBudgetInput(prev => !prev);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 예산 여부에 따른 텍스트 결정
|
// 예산 여부에 따른 텍스트 결정
|
||||||
const budgetButtonText = isBudgetSet ? "예산 수정하기" : "예산 입력하기";
|
const budgetButtonText = isBudgetSet ? "예산 수정하기" : "예산 입력하기";
|
||||||
@@ -155,8 +143,6 @@ export const useBudgetTabContent = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
categoryBudgets,
|
categoryBudgets,
|
||||||
showBudgetInput,
|
|
||||||
toggleBudgetInput,
|
|
||||||
handleCategoryInputChange,
|
handleCategoryInputChange,
|
||||||
handleSaveCategoryBudgets,
|
handleSaveCategoryBudgets,
|
||||||
isBudgetSet,
|
isBudgetSet,
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* 금액 포맷 유틸리티
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 금액을 한국 원화 형식으로 포맷팅
|
||||||
export const formatCurrency = (amount: number): string => {
|
export const formatCurrency = (amount: number): string => {
|
||||||
return new Intl.NumberFormat('ko-KR', {
|
return amount.toLocaleString('ko-KR') + '원';
|
||||||
style: 'currency',
|
|
||||||
currency: 'KRW',
|
|
||||||
maximumFractionDigits: 0
|
|
||||||
}).format(amount);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calculatePercentage = (spent: number, target: number): number => {
|
// 숫자 문자열에서 쉼표 제거
|
||||||
// 타겟이 0이면 0%를 반환하도록 수정
|
export const removeCommas = (value: string): string => {
|
||||||
if (target === 0 || isNaN(target)) return 0;
|
return value.replace(/,/g, '');
|
||||||
return Math.min(Math.round(spent / target * 100), 100);
|
};
|
||||||
|
|
||||||
|
// 숫자 문자열에 쉼표 추가
|
||||||
|
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