'use client'; /** * 지출 예상 내역서 - UniversalListPage 마이그레이션 * * 기존 IntegratedListTemplateV2 → UniversalListPage config 기반으로 변환 * - 월별 그룹핑 테이블 (헤더, 소계, 합계 행 포함) * - 등록/수정 폼 다이얼로그 * - 예상 지급일 변경 다이얼로그 * - 일괄 삭제 다이얼로그 * - 외부 선택 상태 관리 (데이터 행만 선택 가능) */ import { useState, useMemo, useCallback, useTransition, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { format } from 'date-fns'; import { toast } from 'sonner'; import { Receipt, Calendar as CalendarIcon, FileText, Plus, Pencil, Trash2, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Calendar } from '@/components/ui/calendar'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { TableRow, TableCell } from '@/components/ui/table'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { UniversalListPage, type UniversalListConfig, type SelectionHandlers, type RowClickHandlers, type StatCard, } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import type { ExpectedExpenseRecord, TransactionType, PaymentStatus, ApprovalStatus, SortOption, } from './types'; import { TRANSACTION_TYPE_LABELS, PAYMENT_STATUS_LABELS, APPROVAL_STATUS_LABELS, SORT_OPTIONS, } from './types'; import { deleteExpectedExpense, deleteExpectedExpenses, updateExpectedPaymentDate, createExpectedExpense, updateExpectedExpense, getClients, getBankAccounts, } from './actions'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { CurrencyInput } from '@/components/ui/currency-input'; import { TRANSACTION_TYPE_FILTER_OPTIONS, PAYMENT_STATUS_FILTER_OPTIONS, ACCOUNT_SUBJECT_OPTIONS, } from './types'; // ===== 테이블 행 타입 (데이터 + 그룹 헤더 + 소계) ===== type RowType = 'data' | 'monthHeader' | 'monthSubtotal' | 'totalExpense' | 'expectedBalance' | 'finalBalance'; interface TableRowData extends ExpectedExpenseRecord { rowType: RowType; monthLabel?: string; subtotalAmount?: number; dataIndex?: number; // 데이터 행 순번 (헤더/소계 제외) } // ===== 컴포넌트 Props ===== interface ExpectedExpenseManagementProps { initialData: ExpectedExpenseRecord[]; pagination: { currentPage: number; lastPage: number; perPage: number; total: number; }; } // 월 추출 함수 const getMonthKey = (dateStr: string): string => { const date = new Date(dateStr); return format(date, 'yyyy/MM'); }; // 월 레이블 함수 const getMonthLabel = (monthKey: string): string => { const [year, month] = monthKey.split('/'); return `${year}년 ${parseInt(month)}월`; }; export function ExpectedExpenseManagement({ initialData, pagination: initialPagination, }: ExpectedExpenseManagementProps) { const router = useRouter(); const [isPending, startTransition] = useTransition(); // ===== 상태 관리 ===== const [searchQuery, setSearchQuery] = useState(''); const [vendorFilter, setVendorFilter] = useState('all'); const [sortOption, setSortOption] = useState('latest'); const [selectedItems, setSelectedItems] = useState>(new Set()); const [currentPage, setCurrentPage] = useState(initialPagination.currentPage); const itemsPerPage = initialPagination.perPage; // 예상 지급일 변경 다이얼로그 const [showDateChangeDialog, setShowDateChangeDialog] = useState(false); const [newExpectedDate, setNewExpectedDate] = useState(undefined); // 삭제 다이얼로그 const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [deleteTargetId, setDeleteTargetId] = useState(null); // 일괄삭제 다이얼로그 const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false); // 등록/수정 폼 다이얼로그 const [showFormDialog, setShowFormDialog] = useState(false); const [editingItem, setEditingItem] = useState(null); const [formData, setFormData] = useState>({ expectedPaymentDate: format(new Date(), 'yyyy-MM-dd'), settlementDate: '', transactionType: 'purchase', amount: 0, vendorId: '', vendorName: '', bankAccount: '', accountSubject: '', paymentStatus: 'pending', note: '', }); const [formExpectedDate, setFormExpectedDate] = useState(new Date()); // 거래처/계좌 옵션 const [clientOptions, setClientOptions] = useState<{ id: string; name: string }[]>([]); const [bankAccountOptions, setBankAccountOptions] = useState<{ id: string; bankName: string; accountName: string; accountNumber: string }[]>([]); // 날짜 범위 상태 (현재 월 기준) const now = new Date(); const [startDate, setStartDate] = useState(format(new Date(now.getFullYear(), now.getMonth(), 1), 'yyyy-MM-dd')); const [endDate, setEndDate] = useState(format(new Date(now.getFullYear(), now.getMonth() + 3, 0), 'yyyy-MM-dd')); // API에서 받아온 데이터 const [data, setData] = useState(initialData); // ===== 거래처/계좌 옵션 로드 ===== useEffect(() => { const loadOptions = async () => { const [clientsResult, accountsResult] = await Promise.all([ getClients(), getBankAccounts(), ]); if (clientsResult.success) setClientOptions(clientsResult.data); if (accountsResult.success) setBankAccountOptions(accountsResult.data); }; loadOptions(); }, []); // ===== 폼 초기화 ===== const resetForm = useCallback(() => { setFormData({ expectedPaymentDate: format(new Date(), 'yyyy-MM-dd'), settlementDate: '', transactionType: 'purchase', amount: 0, vendorId: '', vendorName: '', bankAccount: '', accountSubject: '', paymentStatus: 'pending', note: '', }); setFormExpectedDate(new Date()); setEditingItem(null); }, []); // ===== 등록 다이얼로그 열기 ===== const handleOpenCreateDialog = useCallback(() => { resetForm(); setShowFormDialog(true); }, [resetForm]); // ===== 수정 다이얼로그 열기 ===== const handleOpenEditDialog = useCallback((item: ExpectedExpenseRecord) => { setEditingItem(item); setFormData({ expectedPaymentDate: item.expectedPaymentDate, settlementDate: item.settlementDate, transactionType: item.transactionType, amount: item.amount, vendorId: item.vendorId, vendorName: item.vendorName, bankAccount: item.bankAccount, accountSubject: item.accountSubject, paymentStatus: item.paymentStatus, note: item.note, }); setFormExpectedDate(item.expectedPaymentDate ? new Date(item.expectedPaymentDate) : new Date()); setShowFormDialog(true); }, []); // ===== 폼 제출 (등록/수정) ===== const handleFormSubmit = useCallback(async () => { if (!formData.expectedPaymentDate || !formData.amount) { toast.error('예상 지급일과 지출금액은 필수입니다.'); return; } startTransition(async () => { if (editingItem) { // 수정 const result = await updateExpectedExpense(editingItem.id, formData); if (result.success && result.data) { setData(prev => prev.map(item => item.id === editingItem.id ? result.data! : item)); toast.success('미지급비용이 수정되었습니다.'); setShowFormDialog(false); resetForm(); } else { toast.error(result.error || '수정에 실패했습니다.'); } } else { // 등록 const result = await createExpectedExpense(formData); if (result.success && result.data) { setData(prev => [result.data!, ...prev]); toast.success('미지급비용이 등록되었습니다.'); setShowFormDialog(false); resetForm(); } else { toast.error(result.error || '등록에 실패했습니다.'); } } }); }, [formData, editingItem, resetForm]); // ===== 일괄 삭제 핸들러 ===== const handleBulkDelete = useCallback(async () => { if (selectedItems.size === 0) return; const selectedIds = Array.from(selectedItems); startTransition(async () => { const result = await deleteExpectedExpenses(selectedIds); if (result.success) { setData(prev => prev.filter(item => !selectedItems.has(item.id))); setSelectedItems(new Set()); toast.success(`${result.deletedCount || selectedIds.length}건이 삭제되었습니다.`); } else { toast.error(result.error || '일괄 삭제에 실패했습니다.'); } setShowBulkDeleteDialog(false); }); }, [selectedItems]); // ===== 체크박스 핸들러 ===== const toggleSelection = useCallback((id: string) => { setSelectedItems(prev => { const newSet = new Set(prev); if (newSet.has(id)) newSet.delete(id); else newSet.add(id); return newSet; }); }, []); // ===== 거래처 필터 옵션 (데이터에서 동적 추출) ===== const vendorFilterOptions = useMemo(() => { const vendors = [...new Set(data.map(item => item.vendorName).filter(Boolean))]; return [ { value: 'all', label: '전체' }, ...vendors.map(vendor => ({ value: vendor, label: vendor })) ]; }, [data]); // ===== 필터링된 원본 데이터 ===== const filteredRawData = useMemo(() => { let result = data.filter(item => item.vendorName.includes(searchQuery) || item.accountSubject.includes(searchQuery) || item.note.includes(searchQuery) ); // 거래처 필터 if (vendorFilter !== 'all') { result = result.filter(item => item.vendorName === vendorFilter); } // 정렬 적용 switch (sortOption) { case 'latest': result.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); break; case 'oldest': result.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); break; default: result.sort((a, b) => new Date(a.expectedPaymentDate).getTime() - new Date(b.expectedPaymentDate).getTime()); } return result; }, [data, searchQuery, vendorFilter, sortOption]); // ===== 월별 그룹핑된 테이블 데이터 (헤더 + 데이터 + 소계 포함) ===== const tableData = useMemo((): TableRowData[] => { const groups: { [monthKey: string]: ExpectedExpenseRecord[] } = {}; filteredRawData.forEach(item => { const monthKey = getMonthKey(item.expectedPaymentDate); if (!groups[monthKey]) { groups[monthKey] = []; } groups[monthKey].push(item); }); const sortedMonths = Object.keys(groups).sort(); const result: TableRowData[] = []; let totalExpense = 0; let dataIndex = 0; // 데이터 행 순번 추적 (SSR/CSR 일치를 위해 useMemo 내에서 계산) sortedMonths.forEach(monthKey => { const monthItems = groups[monthKey]; const monthSubtotal = monthItems.reduce((sum, item) => sum + item.amount, 0); totalExpense += monthSubtotal; // 월 헤더 추가 result.push({ id: `header-${monthKey}`, rowType: 'monthHeader', monthLabel: getMonthLabel(monthKey), expectedPaymentDate: '', settlementDate: '', transactionType: 'other', amount: 0, vendorId: '', vendorName: '', bankAccount: '', accountSubject: '', paymentStatus: 'pending', approvalStatus: 'none', note: '', createdAt: '', updatedAt: '', }); // 데이터 행들 추가 (순번 포함) monthItems.forEach(item => { dataIndex++; result.push({ ...item, rowType: 'data', dataIndex, // 미리 계산된 순번 }); }); // 월별 소계 추가 result.push({ id: `subtotal-${monthKey}`, rowType: 'monthSubtotal', monthLabel: getMonthLabel(monthKey), subtotalAmount: monthSubtotal, expectedPaymentDate: '', settlementDate: '', transactionType: 'other', amount: monthSubtotal, vendorId: '', vendorName: '', bankAccount: '', accountSubject: '', paymentStatus: 'pending', approvalStatus: 'none', note: '', createdAt: '', updatedAt: '', }); }); // 전체 합계 행들 추가 const expectedBalance = 10000000; const finalBalance = expectedBalance - totalExpense; if (filteredRawData.length > 0) { result.push({ id: 'total-expense', rowType: 'totalExpense', subtotalAmount: totalExpense, expectedPaymentDate: '', settlementDate: '', transactionType: 'other', amount: totalExpense, vendorId: '', vendorName: '', bankAccount: '', accountSubject: '', paymentStatus: 'pending', approvalStatus: 'none', note: '', createdAt: '', updatedAt: '', }); result.push({ id: 'expected-balance', rowType: 'expectedBalance', subtotalAmount: expectedBalance, expectedPaymentDate: '', settlementDate: '', transactionType: 'other', amount: expectedBalance, vendorId: '', vendorName: '', bankAccount: '', accountSubject: '', paymentStatus: 'pending', approvalStatus: 'none', note: '', createdAt: '', updatedAt: '', }); result.push({ id: 'final-balance', rowType: 'finalBalance', subtotalAmount: finalBalance, expectedPaymentDate: '', settlementDate: '', transactionType: 'other', amount: finalBalance, vendorId: '', vendorName: '', bankAccount: '', accountSubject: '', paymentStatus: 'pending', approvalStatus: 'none', note: '', createdAt: '', updatedAt: '', }); } return result; }, [filteredRawData]); const totalPages = initialPagination.lastPage || Math.ceil(tableData.length / itemsPerPage); // ===== 전체 선택 핸들러 (데이터 행만) ===== const toggleSelectAll = useCallback(() => { const dataRows = filteredRawData; if (selectedItems.size === dataRows.length && dataRows.length > 0) { setSelectedItems(new Set()); } else { setSelectedItems(new Set(dataRows.map(item => item.id))); } }, [selectedItems.size, filteredRawData]); // ===== 액션 핸들러 ===== // 상세페이지 없음 - 행 클릭 시 이동하지 않음 const handleDeleteClick = useCallback((id: string) => { setDeleteTargetId(id); setShowDeleteDialog(true); }, []); const handleConfirmDelete = useCallback(async () => { if (!deleteTargetId) return; startTransition(async () => { const result = await deleteExpectedExpense(deleteTargetId); if (result.success) { setData(prev => prev.filter(item => item.id !== deleteTargetId)); setSelectedItems(prev => { const newSet = new Set(prev); newSet.delete(deleteTargetId); return newSet; }); toast.success('미지급비용이 삭제되었습니다.'); } else { toast.error(result.error || '삭제에 실패했습니다.'); } setShowDeleteDialog(false); setDeleteTargetId(null); }); }, [deleteTargetId]); // ===== 예상 지급일 변경 핸들러 ===== const handleOpenDateChangeDialog = useCallback(() => { if (selectedItems.size === 0) return; setNewExpectedDate(undefined); setShowDateChangeDialog(true); }, [selectedItems.size]); const handleConfirmDateChange = useCallback(async () => { if (!newExpectedDate || selectedItems.size === 0) return; const newDateStr = format(newExpectedDate, 'yyyy-MM-dd'); const selectedIds = Array.from(selectedItems); startTransition(async () => { const result = await updateExpectedPaymentDate(selectedIds, newDateStr); if (result.success) { setData(prev => prev.map(item => selectedItems.has(item.id) ? { ...item, expectedPaymentDate: newDateStr } : item )); toast.success(`${result.updatedCount || selectedIds.length}건의 예상 지급일이 변경되었습니다.`); setSelectedItems(new Set()); } else { toast.error(result.error || '예상 지급일 변경에 실패했습니다.'); } setShowDateChangeDialog(false); setNewExpectedDate(undefined); }); }, [newExpectedDate, selectedItems]); // ===== 선택된 항목 요약 ===== const selectedItemsSummary = useMemo(() => { const selectedData = data.filter(item => selectedItems.has(item.id)); if (selectedData.length === 0) return { label: '', totalAmount: 0 }; const firstItem = selectedData[0]; const remainingCount = selectedData.length - 1; const label = remainingCount > 0 ? `${firstItem.vendorName} 외 ${remainingCount}` : firstItem.vendorName; const totalAmount = selectedData.reduce((sum, item) => sum + item.amount, 0); return { label, totalAmount }; }, [data, selectedItems]); // ===== 테이블 컬럼 ===== const tableColumns = useMemo(() => [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, { key: 'expectedPaymentDate', label: '예상 지급일' }, { key: 'accountSubject', label: '항목' }, { key: 'amount', label: '지출금액', className: 'text-right' }, { key: 'vendorName', label: '거래처' }, { key: 'bankAccount', label: '계좌' }, { key: 'approvalStatus', label: '전자결재', className: 'text-center' }, { key: 'actions', label: '작업', className: 'w-[100px] text-center' }, ], []); // ===== 전자결재 상태 Badge 스타일 ===== const getApprovalStatusBadge = (status: ApprovalStatus) => { const styles = { none: 'border-gray-300 text-gray-600 bg-gray-50', pending: 'border-orange-300 text-orange-600 bg-orange-50', approved: 'border-green-300 text-green-600 bg-green-50', rejected: 'border-red-300 text-red-600 bg-red-50', }; return ( {APPROVAL_STATUS_LABELS[status]} ); }; // ===== 테이블 행 렌더링 ===== // 컬럼 순서: 체크박스 + 번호 + 예상 지급일 + 항목 + 지출금액 + 거래처 + 계좌 + 전자결재 + 작업 const renderTableRow = useCallback(( item: TableRowData, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => { // 월 헤더 행 (9개 컬럼) if (item.rowType === 'monthHeader') { return ( {item.monthLabel} ); } // 월별 소계 행 if (item.rowType === 'monthSubtotal') { return ( {item.monthLabel} 소계 {item.subtotalAmount?.toLocaleString()} ); } // 지출 합계 행 if (item.rowType === 'totalExpense') { return ( 지출 합계 {item.subtotalAmount?.toLocaleString()} ); } // 예상 잔액 행 if (item.rowType === 'expectedBalance') { return ( 예상 잔액 {item.subtotalAmount?.toLocaleString()} ); } // 최종 잔액 행 if (item.rowType === 'finalBalance') { return ( 최종 잔액 {item.subtotalAmount?.toLocaleString()}원 ); } // 일반 데이터 행 const isSelected = selectedItems.has(item.id); return ( toggleSelection(item.id)} /> {item.dataIndex} {item.expectedPaymentDate} {item.accountSubject} {item.amount.toLocaleString()} {item.vendorName} {item.bankAccount} {getApprovalStatusBadge(item.approvalStatus)}
); }, [selectedItems, toggleSelection, handleOpenEditDialog, handleDeleteClick]); // ===== 모바일 카드 렌더링 ===== const renderMobileCard = useCallback(( item: TableRowData, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => { const isSelected = selectedItems.has(item.id); const onToggle = () => toggleSelection(item.id); // 헤더/소계/합계 행은 모바일에서 다르게 표시 if (item.rowType !== 'data') { if (item.rowType === 'monthHeader') { return (
{item.monthLabel}
); } if (item.rowType === 'monthSubtotal') { return (
{item.monthLabel} 소계 {item.subtotalAmount?.toLocaleString()}원
); } if (item.rowType === 'totalExpense') { return (
지출 합계 {item.subtotalAmount?.toLocaleString()}원
); } if (item.rowType === 'expectedBalance') { return (
예상 잔액 {item.subtotalAmount?.toLocaleString()}원
); } if (item.rowType === 'finalBalance') { return (
최종 잔액 {item.subtotalAmount?.toLocaleString()}원
); } return null; } return ( {TRANSACTION_TYPE_LABELS[item.transactionType]} {PAYMENT_STATUS_LABELS[item.paymentStatus]} {getApprovalStatusBadge(item.approvalStatus)} } isSelected={isSelected} onToggleSelection={onToggle} infoGrid={
} /> ); }, []); // ===== 선택된 항목 데이터 가져오기 ===== const getSelectedItemsData = useCallback(() => { return data.filter(item => selectedItems.has(item.id)); }, [data, selectedItems]); // ===== 전자결재 페이지로 이동 (선택 데이터 전달) ===== const handleElectronicApproval = useCallback(() => { const selectedData = getSelectedItemsData(); // 선택된 항목 ID들을 쿼리 파라미터로 전달 const selectedIds = selectedData.map(item => item.id).join(','); router.push(`/ko/approval/draft/new?type=expected-expense&items=${encodeURIComponent(selectedIds)}`); }, [getSelectedItemsData, router]); // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ // 페이지 기본 정보 title: '지출 예상 내역서', description: '지출 예상 내역을 등록하고 조회합니다', icon: Receipt, basePath: '/accounting/expected-expense', // ID 추출 idField: 'id', // API 액션 actions: { getList: async () => { return { success: true, data: tableData, totalCount: initialPagination.total || filteredRawData.length, }; }, }, // 테이블 컬럼 columns: tableColumns, // 클라이언트 사이드 필터링 (이미 tableData에서 필터링 완료) clientSideFiltering: false, itemsPerPage, // 검색 searchPlaceholder: '거래처, 계정과목, 적요 검색...', onSearchChange: setSearchQuery, // 행 번호 숨기기 (커스텀 번호 사용) showRowNumber: false, // 날짜 범위 선택기 dateRangeSelector: { enabled: true, startDate, endDate, onStartDateChange: setStartDate, onEndDateChange: setEndDate, }, // 모바일 필터 설정 filterConfig: [ { key: 'transactionType', label: '거래유형', type: 'single', options: TRANSACTION_TYPE_FILTER_OPTIONS.filter(o => o.value !== 'all'), }, { key: 'paymentStatus', label: '지급상태', type: 'single', options: PAYMENT_STATUS_FILTER_OPTIONS.filter(o => o.value !== 'all'), }, { key: 'sortBy', label: '정렬', type: 'single', options: SORT_OPTIONS, }, ], initialFilters: { transactionType: 'all', paymentStatus: 'all', sortBy: sortOption, }, filterTitle: '예상비용 필터', // 테이블 헤더 액션 (거래처/정렬 필터) tableHeaderActions: () => (
{/* 거래처 필터 */} {/* 정렬 필터 (최신순/등록순) */}
), // 테이블 앞 컨텐츠 (액션 버튼) beforeTableContent: (
{/* 등록 버튼 */} {/* 예상 지급일 변경 버튼 - 1개 이상 선택 시 활성화 */} {/* 전자결재 버튼 - 1개 이상 선택 시 활성화 */} {/* 일괄삭제 버튼 - 1개 이상 선택 시 활성화 */}
), // Stats 카드 computeStats: (): StatCard[] => { const totalExpense = filteredRawData.reduce((sum, d) => sum + d.amount, 0); const expectedBalance = 10000000; return [ { label: '지출 합계', value: `${totalExpense.toLocaleString()}원`, icon: Receipt, iconColor: 'text-red-500' }, { label: '예상 잔액', value: `${expectedBalance.toLocaleString()}원`, icon: Receipt, iconColor: 'text-blue-500' }, ]; }, // 테이블 행 렌더링 renderTableRow, // 모바일 카드 렌더링 renderMobileCard, }), [ tableData, tableColumns, filteredRawData, initialPagination, itemsPerPage, startDate, endDate, vendorFilter, vendorFilterOptions, sortOption, selectedItems, handleOpenCreateDialog, handleOpenDateChangeDialog, handleElectronicApproval, renderTableRow, renderMobileCard, ] ); return ( <> item.id, }} /> {/* 삭제 확인 다이얼로그 */} 지출 예상 내역 삭제 이 지출 예상 내역을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다. 취소 삭제 {/* 예상 지급일 변경 다이얼로그 */} 예상 지급일 변경
{/* 선택된 항목 리스트 */}
{data.filter(item => selectedItems.has(item.id)).map((item, idx) => (
{item.vendorName} {item.accountSubject} • {item.expectedPaymentDate}
{item.amount.toLocaleString()}원
))}
{/* 합계 */}
합계 {selectedItemsSummary.totalAmount.toLocaleString()}원
{/* 예상 지급일 선택 */}
{/* 버튼 영역 */}
{/* 등록/수정 폼 다이얼로그 */} {editingItem ? '미지급비용 수정' : '미지급비용 등록'}
{/* 예상 지급일 */}
{ setFormExpectedDate(date); if (date) { setFormData(prev => ({ ...prev, expectedPaymentDate: format(date, 'yyyy-MM-dd') })); } }} />
{/* 거래유형 */}
{/* 거래처 / 금액 */}
setFormData(prev => ({ ...prev, amount: value ?? 0 }))} placeholder="금액 입력" />
{/* 계좌 / 계정과목 */}
{/* 결제상태 */}
{/* 비고 */}