Add payment method selection

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.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-22 07:08:02 +00:00
parent 60ef765380
commit aa8381a823
12 changed files with 489 additions and 23 deletions

View File

@@ -1,3 +1,4 @@
import React, { useState, useEffect } from 'react';
import { PlusIcon } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
@@ -53,7 +54,8 @@ const AddTransactionButton = () => {
amount: parseInt(numericAmount),
date: formattedDate,
category: data.category,
type: 'expense'
type: 'expense',
paymentMethod: data.paymentMethod // 추가된 필드
};
console.log('새 지출 추가:', newExpense);
@@ -75,7 +77,8 @@ const AddTransactionButton = () => {
date: isoDate, // ISO 형식 사용
category: data.category,
type: 'expense',
transaction_id: newExpense.id
transaction_id: newExpense.id,
payment_method: data.paymentMethod // Supabase에 필드 추가
});
if (error) throw error;

View File

@@ -0,0 +1,67 @@
import React from 'react';
import { PieChart, Pie, Cell, ResponsiveContainer, Legend } from 'recharts';
interface PaymentMethodData {
method: string;
amount: number;
percentage: number;
}
interface PaymentMethodChartProps {
data: PaymentMethodData[];
isEmpty: boolean;
}
const COLORS = ['#9b87f5', '#6E59A5']; // 신용카드, 현금 색상
const PaymentMethodChart: React.FC<PaymentMethodChartProps> = ({ data, isEmpty }) => {
if (isEmpty) {
return (
<div className="neuro-card h-52 w-full flex items-center justify-center text-gray-400">
<p> </p>
</div>
);
}
const chartData = data.map(item => ({
name: item.method,
value: item.amount
}));
return (
<div className="neuro-card h-64 desktop-card">
<ResponsiveContainer width="100%" height="100%">
<PieChart margin={{ left: 30, right: 30, top: 20, bottom: 5 }}>
<Pie
data={chartData}
cx="50%"
cy="50%"
innerRadius={40}
outerRadius={60}
paddingAngle={5}
dataKey="value"
labelLine={true}
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
fontSize={12}
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Legend
verticalAlign="bottom"
align="center"
layout="horizontal"
formatter={(value) => {
const item = data.find(d => d.method === value);
return item ? `${value} (${item.percentage.toFixed(0)}%)` : value;
}}
/>
</PieChart>
</ResponsiveContainer>
</div>
);
};
export default PaymentMethodChart;

View File

@@ -8,11 +8,14 @@ import { Loader2 } from 'lucide-react';
import ExpenseCategorySelector from './ExpenseCategorySelector';
import { Badge } from '@/components/ui/badge';
import { getPersonalizedTitleSuggestions } from '@/utils/userTitlePreferences';
import { Separator } from '@/components/ui/separator';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
export interface ExpenseFormValues {
title: string;
amount: string;
category: string;
paymentMethod: '신용카드' | '현금';
}
interface ExpenseFormProps {
@@ -27,6 +30,7 @@ const ExpenseForm: React.FC<ExpenseFormProps> = ({ onSubmit, onCancel, isSubmitt
title: '',
amount: '',
category: '음식',
paymentMethod: '신용카드'
}
});
@@ -113,7 +117,7 @@ const ExpenseForm: React.FC<ExpenseFormProps> = ({ onSubmit, onCancel, isSubmitt
)}
/>
{/* 금액 필드를 마지막으로 배치 */}
{/* 금액 필드를 세 번째로 배치 */}
<FormField
control={form.control}
name="amount"
@@ -130,6 +134,36 @@ const ExpenseForm: React.FC<ExpenseFormProps> = ({ onSubmit, onCancel, isSubmitt
)}
/>
{/* 구분선 추가 */}
<Separator className="my-2" />
{/* 지출 방법 필드 추가 */}
<FormField
control={form.control}
name="paymentMethod"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex gap-4"
value={field.value}
disabled={isSubmitting}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="신용카드" id="credit-card" />
<label htmlFor="credit-card" className="cursor-pointer"></label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="현금" id="cash" />
<label htmlFor="cash" className="cursor-pointer"></label>
</div>
</RadioGroup>
</FormItem>
)}
/>
<div className="flex justify-end gap-2 pt-2">
<Button
type="button"

View File

@@ -7,12 +7,15 @@ import { z } from 'zod';
import { categoryIcons, EXPENSE_CATEGORIES } from '@/constants/categoryIcons';
import { Badge } from '@/components/ui/badge';
import { getPersonalizedTitleSuggestions } from '@/utils/userTitlePreferences';
import { Separator } from '@/components/ui/separator';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
// Form schema for validation - 카테고리를 4개로 확장
// Form schema for validation - 카테고리를 4개로 확장 및 지출 방법 추가
export const transactionFormSchema = z.object({
title: z.string().min(1, '제목을 입력해주세요'),
amount: z.string().min(1, '금액을 입력해주세요'),
category: z.enum(['음식', '쇼핑', '교통', '기타']),
paymentMethod: z.enum(['신용카드', '현금']).default('신용카드'),
});
export type TransactionFormValues = z.infer<typeof transactionFormSchema>;
@@ -117,7 +120,7 @@ const TransactionFormFields: React.FC<TransactionFormFieldsProps> = ({ form }) =
)}
/>
{/* 금액 필드를 마지막으로 배치 */}
{/* 금액 필드를 세 번째로 배치 */}
<FormField
control={form.control}
name="amount"
@@ -135,6 +138,38 @@ const TransactionFormFields: React.FC<TransactionFormFieldsProps> = ({ form }) =
</FormItem>
)}
/>
{/* 구분선 추가 */}
<Separator className="my-4" />
{/* 지출 방법 필드 추가 */}
<FormField
control={form.control}
name="paymentMethod"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex gap-4"
value={field.value}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="신용카드" id="credit-card" />
<label htmlFor="credit-card" className="cursor-pointer"></label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="현금" id="cash" />
<label htmlFor="cash" className="cursor-pointer"></label>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
);
};

View File

@@ -31,6 +31,7 @@ export const useTransactionEdit = (
title: transaction.title,
amount: formatWithCommas(transaction.amount.toString()),
category: mapCategoryToNew(transaction.category),
paymentMethod: transaction.paymentMethod || '신용카드', // 지출 방법 추가, 기본값은 신용카드
},
});
@@ -41,6 +42,7 @@ export const useTransactionEdit = (
title: transaction.title,
amount: formatWithCommas(transaction.amount.toString()),
category: mapCategoryToNew(transaction.category),
paymentMethod: transaction.paymentMethod || '신용카드', // 지출 방법 기본값
});
}
}, [open, transaction, form]);
@@ -61,6 +63,7 @@ export const useTransactionEdit = (
title: values.title,
amount: Number(cleanAmount),
category: values.category,
paymentMethod: values.paymentMethod, // 지출 방법 업데이트
};
// 컨텍스트를 통해 트랜잭션 업데이트

View File

@@ -1,3 +1,4 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
@@ -26,13 +27,13 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"aspect-square h-4 w-4 rounded-full border border-primary bg-background ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
<Circle className="h-2.5 w-2.5 fill-neuro-income text-neuro-income" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)