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:
@@ -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;
|
||||
|
||||
67
src/components/analytics/PaymentMethodChart.tsx
Normal file
67
src/components/analytics/PaymentMethodChart.tsx
Normal 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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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, // 지출 방법 업데이트
|
||||
};
|
||||
|
||||
// 컨텍스트를 통해 트랜잭션 업데이트
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user