'use client'; /** * 매입관리 - UniversalListPage 마이그레이션 * * 기존 IntegratedListTemplateV2 → UniversalListPage config 기반으로 변환 * - 클라이언트 사이드 필터링 (검색, 필터, 정렬) * - Stats 카드 (단순 표시, 클릭 없음) * - beforeTableContent (계정과목명 Select + 저장 버튼) * - tableHeaderActions (4개 인라인 필터: 거래처, 매입유형, 발행여부, 정렬) * - tableFooter (합계 행) * - Switch 토글 (세금계산서 수취) * - 커스텀 Dialog (계정과목명 저장) - UniversalListPage 외부 유지 * - deleteConfirmMessage로 삭제 다이얼로그 처리 */ import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { useDateRange } from '@/hooks'; import { toast } from 'sonner'; import { Receipt, Save, Search, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; // Badge, getPresetStyle removed (매입유형/연결문서 컬럼 삭제) import { Switch } from '@/components/ui/switch'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; 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, type FilterFieldConfig, } from '@/components/templates/UniversalListPage'; import { MobileCard } from '@/components/organisms/MobileCard'; import type { PurchaseRecord } from './types'; import { SORT_OPTIONS, TAX_INVOICE_RECEIVED_FILTER_OPTIONS, ACCOUNT_SUBJECT_SELECTOR_OPTIONS, } from './types'; import { getPurchases, togglePurchaseTaxInvoice, deletePurchase } from './actions'; import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import { formatNumber } from '@/lib/utils/amount'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'no', label: 'No.', className: 'w-[60px] text-center' }, { key: 'purchaseNo', label: '매입번호', sortable: true }, { key: 'purchaseDate', label: '매입일', sortable: true }, { key: 'vendorName', label: '거래처', sortable: true }, { key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true }, { key: 'vat', label: '부가세', className: 'text-right', sortable: true }, { key: 'totalAmount', label: '합계금액', className: 'text-right', sortable: true }, { key: 'taxInvoice', label: '세금계산서 수취 확인', className: 'text-center' }, ]; export function PurchaseManagement() { const router = useRouter(); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear'); const [purchaseData, setPurchaseData] = useState([]); const [isLoading, setIsLoading] = useState(true); const isInitialLoadDone = useRef(false); const [searchQuery, setSearchQuery] = useState(''); // 통합 필터 상태 (filterConfig 기반) const [filterValues, setFilterValues] = useState>({ vendor: 'all', taxInvoiceReceived: 'all', sort: 'latest', }); // 계정과목명 저장 다이얼로그 const [selectedAccountSubject, setSelectedAccountSubject] = useState('unset'); const [showSaveDialog, setShowSaveDialog] = useState(false); const [selectedItemsForSave, setSelectedItemsForSave] = useState>(new Set()); // ===== API 데이터 로드 ===== useEffect(() => { const loadData = async () => { if (!isInitialLoadDone.current) { setIsLoading(true); } try { const result = await getPurchases({ startDate, endDate, perPage: 100, }); if (result.success) { setPurchaseData(result.data); } else { toast.error(result.error || '매입 목록 조회에 실패했습니다.'); setPurchaseData([]); } } catch { toast.error('매입 목록 조회 중 오류가 발생했습니다.'); setPurchaseData([]); } finally { setIsLoading(false); isInitialLoadDone.current = true; } }; loadData(); }, [startDate, endDate]); // ===== 통계 계산 ===== const stats = useMemo(() => { const totalPurchaseAmount = purchaseData.reduce((sum, d) => sum + d.totalAmount, 0); const currentMonth = new Date().getMonth(); const currentYear = new Date().getFullYear(); const monthlyAmount = purchaseData .filter(d => { const date = new Date(d.purchaseDate); return date.getMonth() === currentMonth && date.getFullYear() === currentYear; }) .reduce((sum, d) => sum + d.totalAmount, 0); const taxInvoicePendingCount = purchaseData.filter(d => !d.taxInvoiceReceived).length; return { totalPurchaseAmount, monthlyAmount, taxInvoicePendingCount }; }, [purchaseData]); // ===== 거래처 목록 (필터용) ===== const vendorOptions = useMemo(() => { const uniqueVendors = [...new Set(purchaseData.map(d => d.vendorName).filter(v => v && v.trim() !== ''))]; return uniqueVendors.map(v => ({ value: v, label: v })); }, [purchaseData]); // ===== filterConfig 정의 (PC/모바일 자동 분기) ===== const filterConfig: FilterFieldConfig[] = useMemo(() => [ { key: 'vendor', label: '거래처', type: 'single', options: vendorOptions, allOptionLabel: '거래처 전체', }, { key: 'taxInvoiceReceived', label: '세금계산서 수취여부', type: 'single', options: TAX_INVOICE_RECEIVED_FILTER_OPTIONS.filter(o => o.value !== 'all'), allOptionLabel: '전체', }, { key: 'sort', label: '정렬', type: 'single', options: SORT_OPTIONS, allOptionLabel: '최신순', }, ], [vendorOptions]); // 필터 변경 핸들러 const handleFilterChange = useCallback((key: string, value: string | string[]) => { setFilterValues(prev => ({ ...prev, [key]: value })); }, []); // 필터 초기화 핸들러 const handleFilterReset = useCallback(() => { setFilterValues({ vendor: 'all', taxInvoiceReceived: 'all', sort: 'latest', }); }, []); // ===== 핸들러 ===== const handleRowClick = useCallback((item: PurchaseRecord) => { router.push(`/ko/accounting/purchase/${item.id}?mode=view`); }, [router]); // 토글 핸들러 const handleTaxInvoiceToggle = useCallback(async (itemId: string, checked: boolean) => { setPurchaseData(prev => prev.map(item => item.id === itemId ? { ...item, taxInvoiceReceived: checked } : item )); const result = await togglePurchaseTaxInvoice(itemId, checked); if (!result.success) { setPurchaseData(prev => prev.map(item => item.id === itemId ? { ...item, taxInvoiceReceived: !checked } : item )); toast.error(result.error || '세금계산서 수취 상태 변경에 실패했습니다.'); } }, []); // 계정과목명 저장 핸들러 const handleSaveAccountSubject = useCallback((selectedItems: Set) => { if (selectedItems.size === 0) { toast.warning('변경할 매입 항목을 선택해주세요.'); return; } setSelectedItemsForSave(selectedItems); setShowSaveDialog(true); }, []); const handleConfirmSaveAccountSubject = useCallback(() => { // TODO: API 호출로 저장 toast.success('계정과목명이 변경되었습니다.'); setShowSaveDialog(false); setSelectedItemsForSave(new Set()); }, [selectedAccountSubject]); // ===== 테이블 합계 계산 ===== const tableTotals = useMemo(() => { const totalSupplyAmount = purchaseData.reduce((sum, item) => sum + item.supplyAmount, 0); const totalVat = purchaseData.reduce((sum, item) => sum + item.vat, 0); const totalAmount = purchaseData.reduce((sum, item) => sum + item.totalAmount, 0); return { totalSupplyAmount, totalVat, totalAmount }; }, [purchaseData]); // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ // 페이지 기본 정보 title: '매입관리', description: '매입 내역을 등록하고 관리합니다', icon: Receipt, basePath: '/accounting/purchase', // ID 추출 idField: 'id', // API 액션 actions: { getList: async () => { return { success: true, data: purchaseData, totalCount: purchaseData.length, }; }, deleteItem: async (id: string) => { const result = await deletePurchase(id); if (result.success) { invalidateDashboard('purchase'); setPurchaseData(prev => prev.filter(item => item.id !== id)); toast.success('매입이 삭제되었습니다.'); } return { success: result.success, error: result.error }; }, }, // 테이블 컬럼 columns: tableColumns, // 클라이언트 사이드 필터링 clientSideFiltering: true, itemsPerPage: 20, // 데이터 변경 콜백 onDataChange: (data) => setPurchaseData(data), // 검색 필터 searchPlaceholder: '매입번호, 거래처명 검색...', searchFilter: (item, searchValue) => { const search = searchValue.toLowerCase(); return ( item.purchaseNo.toLowerCase().includes(search) || item.vendorName.toLowerCase().includes(search) ); }, // 커스텀 필터 함수 (filterValues 파라미터 사용) customFilterFn: (items, fv) => { if (!items || items.length === 0) return items; return items.filter((item) => { // 검색어 필터 if (searchQuery) { const search = searchQuery.toLowerCase(); const matchesSearch = item.purchaseNo.toLowerCase().includes(search) || item.vendorName.toLowerCase().includes(search); if (!matchesSearch) return false; } const vendorVal = fv.vendor as string; const taxInvoiceReceivedVal = fv.taxInvoiceReceived as string; // 거래처 필터 if (vendorVal !== 'all' && item.vendorName !== vendorVal) { return false; } // 세금계산서 수취여부 필터 if (taxInvoiceReceivedVal === 'received' && !item.taxInvoiceReceived) { return false; } if (taxInvoiceReceivedVal === 'notReceived' && item.taxInvoiceReceived) { return false; } return true; }); }, // 커스텀 정렬 함수 (filterValues 파라미터 사용) customSortFn: (items, fv) => { const sorted = [...items]; const sortVal = fv.sort as string; switch (sortVal) { case 'oldest': sorted.sort((a, b) => new Date(a.purchaseDate).getTime() - new Date(b.purchaseDate).getTime()); break; case 'amountHigh': sorted.sort((a, b) => b.totalAmount - a.totalAmount); break; case 'amountLow': sorted.sort((a, b) => a.totalAmount - b.totalAmount); break; default: // latest sorted.sort((a, b) => new Date(b.purchaseDate).getTime() - new Date(a.purchaseDate).getTime()); break; } return sorted; }, // 검색창 (공통 컴포넌트에서 자동 생성) hideSearch: true, searchValue: searchQuery, onSearchChange: setSearchQuery, // 공통 헤더 옵션 dateRangeSelector: { enabled: true, showPresets: true, startDate, endDate, onStartDateChange: setStartDate, onEndDateChange: setEndDate, }, // 헤더 액션: 계정과목명 Select + 저장 버튼 headerActions: ({ selectedItems }) => (
계정과목명
), // 통합 필터 시스템 (PC: 인라인, 모바일: 바텀시트 자동 분기) filterConfig, initialFilters: filterValues, filterTitle: '매입 필터', // Stats 카드 computeStats: (): StatCard[] => [ { label: '총매입', value: `${formatNumber(stats.totalPurchaseAmount)}원`, icon: Receipt, iconColor: 'text-blue-500' }, { label: '당월 매입', value: `${formatNumber(stats.monthlyAmount)}원`, icon: Receipt, iconColor: 'text-green-500' }, { label: '세금계산서 수취 미확인', value: `${stats.taxInvoicePendingCount}건`, icon: Receipt, iconColor: 'text-red-500' }, ], // 테이블 하단 합계 행 tableFooter: ( 합계 {formatNumber(tableTotals.totalSupplyAmount)} {formatNumber(tableTotals.totalVat)} {formatNumber(tableTotals.totalAmount)} ), // 삭제 확인 메시지 deleteConfirmMessage: { title: '매입 삭제', description: '이 매입 항목을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.', }, // 테이블 행 렌더링 renderTableRow: ( item: PurchaseRecord, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => ( handleRowClick(item)} > e.stopPropagation()}> {globalIndex} {item.purchaseNo} {item.purchaseDate} {item.vendorName} {formatNumber(item.supplyAmount)} {formatNumber(item.vat)} {formatNumber(item.totalAmount)} e.stopPropagation()}>
handleTaxInvoiceToggle(item.id, checked)} className="data-[state=checked]:bg-orange-500" /> {item.taxInvoiceReceived && 수취}
), // 모바일 카드 렌더링 renderMobileCard: ( item: PurchaseRecord, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => ( handleRowClick(item)} details={[ { label: '매입일', value: item.purchaseDate }, { label: '공급가액', value: `${formatNumber(item.supplyAmount)}원` }, { label: '합계금액', value: `${formatNumber(item.totalAmount)}원` }, ]} /> ), }), [ purchaseData, startDate, endDate, stats, filterConfig, filterValues, selectedAccountSubject, tableTotals, searchQuery, handleRowClick, handleTaxInvoiceToggle, handleSaveAccountSubject, ] ); return ( <> {/* 계정과목명 저장 확인 다이얼로그 */} 계정과목명 변경 {selectedItemsForSave.size}개의 매입유형을{' '} {ACCOUNT_SUBJECT_SELECTOR_OPTIONS.find(o => o.value === selectedAccountSubject)?.label} (으)로 모두 변경하시겠습니까? ); }