'use client'; import { useState, useMemo, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { format } from 'date-fns'; import { Receipt, Pencil, Save, Trash2, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { TableRow, TableCell } from '@/components/ui/table'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { IntegratedListTemplateV2, type TableColumn, type StatCard, } from '@/components/templates/IntegratedListTemplateV2'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard'; import type { PurchaseRecord, PurchaseType, SortOption, IssuanceFilter, } from './types'; import { SORT_OPTIONS, PURCHASE_TYPE_LABELS, PURCHASE_TYPE_FILTER_OPTIONS, ISSUANCE_FILTER_OPTIONS, ACCOUNT_SUBJECT_SELECTOR_OPTIONS, PURCHASE_STATUS_LABELS, PURCHASE_STATUS_COLORS, } from './types'; // ===== Mock 데이터 생성 (Hydration 오류 방지를 위해 고정값 사용) ===== const generateMockData = (): PurchaseRecord[] => { const vendors = ['삼성전자', 'LG전자', 'SK하이닉스', '현대자동차', '포스코', '네이버', '카카오', 'KT']; const purchaseTypes: PurchaseType[] = ['unset', 'raw_material', 'subsidiary_material', 'product', 'outsourcing', 'consumables', 'repair', 'transportation']; const statuses: ('pending' | 'completed' | 'cancelled')[] = ['pending', 'completed', 'cancelled']; const evidenceTypes: ('tax_invoice' | 'card' | 'cash_receipt' | 'receipt' | 'none')[] = ['tax_invoice', 'card', 'cash_receipt', 'receipt', 'none']; const banks = ['국민', '신한', '우리', '하나', 'IBK기업']; // 고정된 금액 배열 (Hydration 오류 방지) const fixedAmounts = [1500000, 2300000, 870000, 4500000, 1200000, 3200000, 980000, 5600000, 2100000, 1800000]; return Array.from({ length: 50 }, (_, i) => { const supplyAmount = fixedAmounts[i % fixedAmounts.length] + (i * 100000); const vat = Math.floor(supplyAmount * 0.1); const totalAmount = supplyAmount + vat; return { id: `purchase-${i + 1}`, purchaseNo: `PUR-2025-${String(i + 1).padStart(4, '0')}`, purchaseDate: format(new Date(2025, 11, (i % 28) + 1), 'yyyy-MM-dd'), vendorId: `vendor-${i % vendors.length}`, vendorName: vendors[i % vendors.length], supplyAmount, vat, totalAmount, purchaseType: purchaseTypes[i % purchaseTypes.length], evidenceType: evidenceTypes[i % evidenceTypes.length], status: statuses[i % statuses.length], sourceDocument: i % 3 === 0 ? { type: i % 2 === 0 ? 'proposal' : 'expense_report', documentNo: `DOC-2025-${String(i + 1).padStart(4, '0')}`, title: `${i % 2 === 0 ? '품의' : '지출'} 건 - ${vendors[i % vendors.length]}`, expectedCost: supplyAmount, } : undefined, withdrawalAccount: { bankName: banks[i % banks.length], accountNo: `${String(100 + i).padStart(3, '0')}-${String(10 + (i % 90)).padStart(2, '0')}-${String(100000 + i * 1000).padStart(6, '0')}`, accountAlias: '법인계좌', }, items: [ { id: `item-${i}-1`, itemName: `품목 ${i + 1}-1`, quantity: (i % 10) + 1, unitPrice: 50000 + (i * 1000), supplyPrice: supplyAmount * 0.6, vat: vat * 0.6, note: '비고', }, { id: `item-${i}-2`, itemName: `품목 ${i + 1}-2`, quantity: (i % 5) + 1, unitPrice: 30000 + (i * 500), supplyPrice: supplyAmount * 0.4, vat: vat * 0.4, }, ], taxInvoiceReceived: i % 2 === 0, createdAt: '2025-12-18T00:00:00.000Z', updatedAt: '2025-12-18T00:00:00.000Z', }; }); }; export function PurchaseManagement() { const router = useRouter(); // ===== 상태 관리 ===== const [searchQuery, setSearchQuery] = useState(''); const [sortOption, setSortOption] = useState('latest'); const [purchaseTypeFilter, setPurchaseTypeFilter] = useState('all'); const [vendorFilter, setVendorFilter] = useState('all'); const [issuanceFilter, setIssuanceFilter] = useState('all'); const [selectedItems, setSelectedItems] = useState>(new Set()); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 20; // 상단 계정과목명 선택 (저장용) const [selectedAccountSubject, setSelectedAccountSubject] = useState('unset'); // 계정과목명 저장 다이얼로그 const [showSaveDialog, setShowSaveDialog] = useState(false); // 삭제 다이얼로그 const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [deleteTargetId, setDeleteTargetId] = useState(null); // 날짜 범위 상태 const [startDate, setStartDate] = useState('2025-01-01'); const [endDate, setEndDate] = useState('2025-12-31'); // Mock 데이터 (토글 상태 변경을 위해 setData 추가) const [data, setData] = useState(generateMockData); // ===== 체크박스 핸들러 ===== 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 filteredData = useMemo(() => { let result = data.filter(item => item.vendorName.includes(searchQuery) || item.purchaseNo.includes(searchQuery) ); // 거래처 필터 if (vendorFilter !== 'all') { result = result.filter(item => item.vendorName === vendorFilter); } // 매입유형 필터 if (purchaseTypeFilter !== 'all') { result = result.filter(item => item.purchaseType === purchaseTypeFilter); } // 발행여부 필터 if (issuanceFilter === 'taxInvoicePending') { result = result.filter(item => !item.taxInvoiceReceived); } // 정렬 switch (sortOption) { case 'latest': result.sort((a, b) => new Date(b.purchaseDate).getTime() - new Date(a.purchaseDate).getTime()); break; case 'oldest': result.sort((a, b) => new Date(a.purchaseDate).getTime() - new Date(b.purchaseDate).getTime()); break; case 'amountHigh': result.sort((a, b) => b.totalAmount - a.totalAmount); break; case 'amountLow': result.sort((a, b) => a.totalAmount - b.totalAmount); break; } return result; }, [data, searchQuery, vendorFilter, purchaseTypeFilter, issuanceFilter, sortOption]); const paginatedData = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredData.slice(startIndex, startIndex + itemsPerPage); }, [filteredData, currentPage, itemsPerPage]); const totalPages = Math.ceil(filteredData.length / itemsPerPage); // ===== 전체 선택 핸들러 ===== const toggleSelectAll = useCallback(() => { if (selectedItems.size === filteredData.length && filteredData.length > 0) { setSelectedItems(new Set()); } else { setSelectedItems(new Set(filteredData.map(item => item.id))); } }, [selectedItems.size, filteredData]); // ===== 액션 핸들러 ===== const handleRowClick = useCallback((item: PurchaseRecord) => { router.push(`/ko/accounting/purchase/${item.id}`); }, [router]); // 개별 항목 삭제 핸들러 const handleDeleteClick = useCallback((id: string) => { setDeleteTargetId(id); setShowDeleteDialog(true); }, []); // 삭제 확정 핸들러 const handleConfirmDelete = useCallback(() => { if (deleteTargetId) { console.log('삭제:', deleteTargetId); setData(prev => prev.filter(item => item.id !== deleteTargetId)); setSelectedItems(prev => { const newSet = new Set(prev); newSet.delete(deleteTargetId); return newSet; }); } setShowDeleteDialog(false); setDeleteTargetId(null); }, [deleteTargetId]); // ===== 통계 카드 ===== const statCards: StatCard[] = useMemo(() => { const totalPurchaseAmount = data.reduce((sum, d) => sum + d.totalAmount, 0); // 당월 매입 (현재 월 기준) const currentMonth = new Date().getMonth(); const currentYear = new Date().getFullYear(); const monthlyAmount = data .filter(d => { const date = new Date(d.purchaseDate); return date.getMonth() === currentMonth && date.getFullYear() === currentYear; }) .reduce((sum, d) => sum + d.totalAmount, 0); // 매입유형 미설정 건수 const unsetTypeCount = data.filter(d => d.purchaseType === 'unset').length; // 세금계산서 수취 미확인 건수 const taxInvoicePendingCount = data.filter(d => !d.taxInvoiceReceived).length; return [ { label: '총 매입', value: `${totalPurchaseAmount.toLocaleString()}원`, icon: Receipt, iconColor: 'text-blue-500' }, { label: '당월 매입', value: `${monthlyAmount.toLocaleString()}원`, icon: Receipt, iconColor: 'text-green-500' }, { label: '매입유형 미설정', value: `${unsetTypeCount}건`, icon: Receipt, iconColor: 'text-orange-500' }, { label: '세금계산서 수취 미확인', value: `${taxInvoicePendingCount}건`, icon: Receipt, iconColor: 'text-red-500' }, ]; }, [data]); // ===== 테이블 컬럼 (스크린샷 기준) ===== // No., 매입번호, 매입일, 거래처, 공급가액, 부가세, 합계금액, 매입유형, 세금계산서 수취 확인(토글), 작업 const tableColumns: TableColumn[] = useMemo(() => [ { key: 'no', label: 'No.', className: 'w-[60px] text-center' }, { key: 'purchaseNo', label: '매입번호' }, { key: 'purchaseDate', label: '매입일' }, { key: 'vendorName', label: '거래처' }, { key: 'supplyAmount', label: '공급가액', className: 'text-right' }, { key: 'vat', label: '부가세', className: 'text-right' }, { key: 'totalAmount', label: '합계금액', className: 'text-right' }, { key: 'purchaseType', label: '매입유형', className: 'text-center' }, { key: 'taxInvoice', label: '세금계산서 수취 확인', className: 'text-center' }, { key: 'actions', label: '작업', className: 'text-center w-[100px]' }, ], []); // ===== 토글 핸들러 ===== const handleTaxInvoiceToggle = useCallback((itemId: string, checked: boolean) => { console.log('세금계산서 수취 확인 변경:', itemId, checked); setData(prev => prev.map(item => item.id === itemId ? { ...item, taxInvoiceReceived: checked } : item )); // TODO: API 호출로 상태 저장 }, []); // ===== 테이블 행 렌더링 (스크린샷 기준 컬럼) ===== const renderTableRow = useCallback((item: PurchaseRecord, index: number, globalIndex: number) => { const isSelected = selectedItems.has(item.id); const isUnsetType = item.purchaseType === 'unset'; return ( handleRowClick(item)} > e.stopPropagation()}> toggleSelection(item.id)} /> {/* No. */} {globalIndex} {/* 매입번호 */} {item.purchaseNo} {/* 매입일 */} {item.purchaseDate} {/* 거래처 */} {item.vendorName} {/* 공급가액 */} {item.supplyAmount.toLocaleString()} {/* 부가세 */} {item.vat.toLocaleString()} {/* 합계금액 */} {item.totalAmount.toLocaleString()} {/* 매입유형 - 미설정일 경우 빨간색 */} {PURCHASE_TYPE_LABELS[item.purchaseType]} {/* 세금계산서 수취 확인 (토글) */} e.stopPropagation()}>
handleTaxInvoiceToggle(item.id, checked)} className="data-[state=checked]:bg-orange-500" /> {item.taxInvoiceReceived && 수취}
{/* 작업 */} e.stopPropagation()}> {isSelected && (
)}
); }, [selectedItems, toggleSelection, handleRowClick, handleTaxInvoiceToggle, handleDeleteClick]); // ===== 모바일 카드 렌더링 ===== const renderMobileCard = useCallback(( item: PurchaseRecord, index: number, globalIndex: number, isSelected: boolean, onToggle: () => void ) => { return ( {PURCHASE_TYPE_LABELS[item.purchaseType]} {PURCHASE_STATUS_LABELS[item.status]} } isSelected={isSelected} onToggleSelection={onToggle} infoGrid={
} actions={ isSelected ? (
) : undefined } onClick={() => handleRowClick(item)} /> ); }, [handleRowClick, handleDeleteClick]); // ===== 헤더 액션 ===== const headerActions = ( ); // ===== 계정과목명 저장 핸들러 ===== const handleSaveAccountSubject = useCallback(() => { if (selectedItems.size === 0) { alert('변경할 매입 항목을 선택해주세요.'); return; } setShowSaveDialog(true); }, [selectedItems.size]); // 계정과목명 저장 확정 const handleConfirmSaveAccountSubject = useCallback(() => { console.log('계정과목명 저장:', selectedAccountSubject, '선택된 항목:', Array.from(selectedItems)); // TODO: API 호출로 저장 setShowSaveDialog(false); setSelectedItems(new Set()); }, [selectedAccountSubject, selectedItems]); // ===== 거래처 목록 (필터용) ===== const vendorOptions = useMemo(() => { const uniqueVendors = [...new Set(data.map(d => d.vendorName))]; return [ { value: 'all', label: '거래처 전체' }, ...uniqueVendors.map(v => ({ value: v, label: v })) ]; }, [data]); // ===== 테이블 헤더 액션 (4개 필터: 거래처, 매입유형, 발행여부, 정렬) ===== const tableHeaderActions = (
{/* 거래처 필터 */} {/* 매입유형 필터 */} {/* 발행여부 필터 */} {/* 정렬 */}
); // ===== 테이블 합계 계산 ===== const tableTotals = useMemo(() => { const totalSupplyAmount = filteredData.reduce((sum, item) => sum + item.supplyAmount, 0); const totalVat = filteredData.reduce((sum, item) => sum + item.vat, 0); const totalAmount = filteredData.reduce((sum, item) => sum + item.totalAmount, 0); return { totalSupplyAmount, totalVat, totalAmount }; }, [filteredData]); // ===== 테이블 하단 합계 행 ===== const tableFooter = ( 합계 {tableTotals.totalSupplyAmount.toLocaleString()} {tableTotals.totalVat.toLocaleString()} {tableTotals.totalAmount.toLocaleString()} ); // ===== 상단 계정과목명 + 저장 버튼 ===== const accountSubjectSelector = (
계정과목명
); return ( <> item.id} renderTableRow={renderTableRow} renderMobileCard={renderMobileCard} pagination={{ currentPage, totalPages, totalItems: filteredData.length, itemsPerPage, onPageChange: setCurrentPage, }} /> {/* 삭제 확인 다이얼로그 */} 매입 삭제 이 매입 항목을 삭제하시겠습니까?
삭제된 데이터는 복구할 수 없습니다.
setShowDeleteDialog(false)}> 취소 삭제
{/* 계정과목명 저장 확인 다이얼로그 */} 계정과목명 변경 {selectedItems.size}개의 매입유형을{' '} {ACCOUNT_SUBJECT_SELECTOR_OPTIONS.find(o => o.value === selectedAccountSubject)?.label} (으)로 모두 변경하시겠습니까? ); }