Adds a payment method selection (Credit Card, Cash) to the expense form and includes a line separator. Also requests to add a graph showing the proportion of credit card and cash usage in expense analytics, but this part is not implemented in this commit.
174 lines
6.3 KiB
TypeScript
174 lines
6.3 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
|
import { PlusIcon } from 'lucide-react';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
|
|
import { toast } from '@/hooks/useToast.wrapper'; // 래퍼 사용
|
|
import { useBudget } from '@/contexts/BudgetContext';
|
|
import { supabase } from '@/lib/supabase';
|
|
import { isSyncEnabled, setLastSyncTime, trySyncAllData } from '@/utils/syncUtils';
|
|
import ExpenseForm, { ExpenseFormValues } from './expenses/ExpenseForm';
|
|
import { Transaction } from '@/components/TransactionCard';
|
|
import { normalizeDate } from '@/utils/sync/transaction/dateUtils';
|
|
import useNotifications from '@/hooks/useNotifications';
|
|
|
|
const AddTransactionButton = () => {
|
|
const [showExpenseDialog, setShowExpenseDialog] = useState(false);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [showText, setShowText] = useState(true);
|
|
const { addTransaction } = useBudget();
|
|
const { addNotification } = useNotifications();
|
|
|
|
// 3초 후에 텍스트 숨기기
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
setShowText(false);
|
|
}, 3000);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, []);
|
|
|
|
// Format number with commas
|
|
const formatWithCommas = (value: string): string => {
|
|
// Remove commas first to avoid duplicates when typing
|
|
const numericValue = value.replace(/[^0-9]/g, '');
|
|
return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
};
|
|
|
|
const onSubmit = async (data: ExpenseFormValues) => {
|
|
// 중복 제출 방지
|
|
if (isSubmitting) return;
|
|
|
|
try {
|
|
setIsSubmitting(true);
|
|
|
|
// Remove commas before processing the amount
|
|
const numericAmount = data.amount.replace(/,/g, '');
|
|
|
|
// 현재 날짜와 시간을 가져옵니다
|
|
const now = new Date();
|
|
const formattedDate = `오늘, ${now.getHours()}:${now.getMinutes() < 10 ? '0' + now.getMinutes() : now.getMinutes()} ${now.getHours() >= 12 ? 'PM' : 'AM'}`;
|
|
|
|
const newExpense: Transaction = {
|
|
id: Date.now().toString(),
|
|
title: data.title,
|
|
amount: parseInt(numericAmount),
|
|
date: formattedDate,
|
|
category: data.category,
|
|
type: 'expense',
|
|
paymentMethod: data.paymentMethod // 추가된 필드
|
|
};
|
|
|
|
console.log('새 지출 추가:', newExpense);
|
|
|
|
// BudgetContext를 통해 지출 추가
|
|
addTransaction(newExpense);
|
|
|
|
try {
|
|
const { data: { user } } = await supabase.auth.getUser();
|
|
|
|
if (isSyncEnabled() && user) {
|
|
// ISO 형식으로 날짜 변환
|
|
const isoDate = normalizeDate(formattedDate);
|
|
|
|
const { error } = await supabase.from('transactions').insert({
|
|
user_id: user.id,
|
|
title: data.title,
|
|
amount: parseInt(numericAmount),
|
|
date: isoDate, // ISO 형식 사용
|
|
category: data.category,
|
|
type: 'expense',
|
|
transaction_id: newExpense.id,
|
|
payment_method: data.paymentMethod // Supabase에 필드 추가
|
|
});
|
|
|
|
if (error) throw error;
|
|
|
|
// 지출 추가 후 자동 동기화 실행
|
|
console.log('지출 추가 후 자동 동기화 시작');
|
|
const syncResult = await trySyncAllData(user.id);
|
|
|
|
if (syncResult.success) {
|
|
// 동기화 성공 시 마지막 동기화 시간 업데이트
|
|
const currentTime = new Date().toISOString();
|
|
console.log('자동 동기화 성공, 시간 업데이트:', currentTime);
|
|
setLastSyncTime(currentTime);
|
|
|
|
// 동기화 성공 알림 추가
|
|
addNotification(
|
|
'동기화 완료',
|
|
'방금 추가하신 지출 데이터가 클라우드에 동기화되었습니다.'
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Supabase에 지출 추가 실패:', error);
|
|
// 실패 시 알림 추가
|
|
addNotification(
|
|
'동기화 실패',
|
|
'지출 데이터 동기화 중 문제가 발생했습니다. 나중에 다시 시도됩니다.'
|
|
);
|
|
}
|
|
|
|
// 다이얼로그를 닫습니다
|
|
setShowExpenseDialog(false);
|
|
|
|
// 이벤트 발생 처리 - 단일 이벤트로 통합
|
|
window.dispatchEvent(new CustomEvent('transactionChanged', {
|
|
detail: { type: 'add', transaction: newExpense }
|
|
}));
|
|
|
|
// 토스트는 한 번만 표시 (지연 제거하여 래퍼에서 처리되도록)
|
|
toast({
|
|
title: "지출이 추가되었습니다",
|
|
description: `${data.title} 항목이 ${formatWithCommas(numericAmount)}원으로 등록되었습니다.`,
|
|
duration: 3000
|
|
});
|
|
} catch (error) {
|
|
console.error('지출 추가 중 오류 발생:', error);
|
|
toast({
|
|
title: "지출 추가 실패",
|
|
description: "지출을 추가하는 도중 오류가 발생했습니다.",
|
|
variant: "destructive",
|
|
duration: 4000
|
|
});
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="fixed bottom-24 right-6 z-20">
|
|
<button
|
|
className={`transition-all duration-300 bg-neuro-income shadow-neuro-flat hover:shadow-neuro-convex text-white ${
|
|
showText ? 'flex items-center gap-2 px-4 py-3 rounded-full animate-bounce-gentle' : 'p-4 rounded-full animate-pulse-subtle'
|
|
}`}
|
|
onClick={() => setShowExpenseDialog(true)}
|
|
aria-label="지출 추가"
|
|
disabled={isSubmitting}
|
|
>
|
|
<PlusIcon size={showText ? 20 : 24} className={showText ? "" : "animate-spin-slow"} />
|
|
{showText && <span className="mr-1 animate-fade-in">지출 입력</span>}
|
|
</button>
|
|
</div>
|
|
|
|
<Dialog open={showExpenseDialog} onOpenChange={(open) => {
|
|
if (!isSubmitting) setShowExpenseDialog(open);
|
|
}}>
|
|
<DialogContent className="w-[90%] max-w-sm mx-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>지출 입력</DialogTitle>
|
|
</DialogHeader>
|
|
<ExpenseForm
|
|
onSubmit={onSubmit}
|
|
onCancel={() => !isSubmitting && setShowExpenseDialog(false)}
|
|
isSubmitting={isSubmitting}
|
|
/>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default AddTransactionButton;
|