diff --git a/src/app/[locale]/(protected)/accounting/condolence-expenses/page.tsx b/src/app/[locale]/(protected)/accounting/condolence-expenses/page.tsx new file mode 100644 index 00000000..9577948e --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/condolence-expenses/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { CondolenceExpenseList } from '@/components/accounting/condolence-expenses/CondolenceExpenseList'; + +export default function CondolenceExpensesPage() { + return ; +} diff --git a/src/components/accounting/condolence-expenses/CondolenceExpenseForm.tsx b/src/components/accounting/condolence-expenses/CondolenceExpenseForm.tsx new file mode 100644 index 00000000..09501add --- /dev/null +++ b/src/components/accounting/condolence-expenses/CondolenceExpenseForm.tsx @@ -0,0 +1,334 @@ +'use client'; + +/** + * 경조사비 등록/수정 모달 (Dialog) + * + * - 부조금/선물 토글 (체크 해제 시 필드 숨김 + 값 초기화) + * - 총금액 자동 계산 (부조금액 + 선물금액) + */ + +import { useState, useEffect, useMemo, useCallback } from 'react'; +import { Input } from '@/components/ui/input'; +import { DatePicker } from '@/components/ui/date-picker'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button'; +import { NumberInput } from '@/components/ui/number-input'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { toast } from 'sonner'; +import { Save, Loader2 } from 'lucide-react'; +import { formatAmount } from '@/lib/utils/amount'; +import { createCondolenceExpense, updateCondolenceExpense } from './actions'; +import { + CATEGORY_OPTIONS, + CASH_METHOD_OPTIONS, + INITIAL_FORM_DATA, + type CondolenceExpense, + type CondolenceExpenseFormData, + type CondolenceCategory, + type CashMethod, +} from './types'; + +interface CondolenceExpenseFormModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + editItem: CondolenceExpense | null; + onSuccess: () => void; +} + +export function CondolenceExpenseFormModal({ + open, + onOpenChange, + editItem, + onSuccess, +}: CondolenceExpenseFormModalProps) { + const isEdit = !!editItem; + const [form, setForm] = useState({ ...INITIAL_FORM_DATA }); + const [isSaving, setIsSaving] = useState(false); + + // 수정 시 데이터 로드 + useEffect(() => { + if (editItem) { + setForm({ + event_date: editItem.event_date || '', + expense_date: editItem.expense_date || '', + partner_name: editItem.partner_name, + description: editItem.description || '', + category: editItem.category, + has_cash: editItem.has_cash, + cash_method: editItem.cash_method || '', + cash_amount: editItem.cash_amount || 0, + has_gift: editItem.has_gift, + gift_type: editItem.gift_type || '', + gift_amount: editItem.gift_amount || 0, + memo: editItem.memo || '', + }); + } else { + setForm({ ...INITIAL_FORM_DATA }); + } + }, [editItem, open]); + + // 총금액 자동 계산 + const totalAmount = useMemo(() => { + const cash = form.has_cash ? Number(form.cash_amount || 0) : 0; + const gift = form.has_gift ? Number(form.gift_amount || 0) : 0; + return cash + gift; + }, [form.has_cash, form.cash_amount, form.has_gift, form.gift_amount]); + + // 필드 변경 + const handleChange = useCallback( + (key: K, value: CondolenceExpenseFormData[K]) => { + setForm((prev) => { + const next = { ...prev, [key]: value }; + // 부조금 해제 시 초기화 + if (key === 'has_cash' && !value) { + next.cash_method = ''; + next.cash_amount = 0; + } + // 선물 해제 시 초기화 + if (key === 'has_gift' && !value) { + next.gift_type = ''; + next.gift_amount = 0; + } + return next; + }); + }, + [] + ); + + // 저장 + const handleSubmit = useCallback(async () => { + if (!form.partner_name.trim()) { + toast.error('거래처명/대상자를 입력하세요.'); + return; + } + if (!form.category) { + toast.error('구분을 선택하세요.'); + return; + } + + setIsSaving(true); + try { + const payload: Record = { + event_date: form.event_date || undefined, + expense_date: form.expense_date || undefined, + partner_name: form.partner_name, + description: form.description || undefined, + category: form.category, + has_cash: form.has_cash, + cash_method: form.has_cash ? form.cash_method || undefined : undefined, + cash_amount: form.has_cash ? Number(form.cash_amount || 0) : 0, + has_gift: form.has_gift, + gift_type: form.has_gift ? form.gift_type || undefined : undefined, + gift_amount: form.has_gift ? Number(form.gift_amount || 0) : 0, + memo: form.memo || undefined, + }; + // undefined 제거 + Object.keys(payload).forEach((k) => { + if (payload[k] === undefined) delete payload[k]; + }); + + const result = isEdit + ? await updateCondolenceExpense(editItem!.id, payload) + : await createCondolenceExpense(payload); + + if (result.success) { + toast.success(isEdit ? '수정되었습니다.' : '등록되었습니다.'); + onSuccess(); + } else { + toast.error(result.error || '저장 실패'); + } + } catch { + toast.error('저장 중 오류 발생'); + } finally { + setIsSaving(false); + } + }, [form, isEdit, editItem, onSuccess]); + + return ( + + + + {isEdit ? '경조사비 수정' : '경조사비 등록'} + + +
+ {/* 날짜 */} +
+
+ + handleChange('event_date', d)} + /> +
+
+ + handleChange('expense_date', d)} + /> +
+
+ + {/* 기본 정보 */} +
+ + handleChange('partner_name', e.target.value)} + placeholder="거래처명 또는 대상자" + /> +
+ +
+ + handleChange('description', e.target.value)} + placeholder="예: 김과장 결혼축의금" + /> +
+ +
+ + +
+ + {/* 부조금 섹션 */} +
+
+ handleChange('has_cash', !!v)} + /> + +
+ {form.has_cash && ( +
+
+ + +
+
+ + handleChange('cash_amount', v ?? 0)} + min={0} + useComma + /> +
+
+ )} +
+ + {/* 선물 섹션 */} +
+
+ handleChange('has_gift', !!v)} + /> + +
+ {form.has_gift && ( +
+
+ + handleChange('gift_type', e.target.value)} + placeholder="예: 화환" + /> +
+
+ + handleChange('gift_amount', v ?? 0)} + min={0} + useComma + /> +
+
+ )} +
+ + {/* 총금액 */} +
+ + {formatAmount(totalAmount)} +
+ + {/* 비고 */} +
+ +