'use client'; /** * 입금관리 - UniversalListPage 마이그레이션 * * 기존 IntegratedListTemplateV2 → UniversalListPage config 기반으로 변환 * - 클라이언트 사이드 필터링 (검색, 필터, 정렬) * - Stats 카드 (단순 표시, 클릭 없음) * - beforeTableContent (계정과목명 Select + 저장 버튼 + 새로고침) * - tableHeaderActions (3개 인라인 필터: 거래처, 입금유형, 정렬) * - tableFooter (합계 행) * - 커스텀 Dialog (계정과목명 저장, 선택 필요 경고) - UniversalListPage 외부 유지 * - deleteConfirmMessage로 삭제 다이얼로그 처리 */ import { useState, useMemo, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { Banknote, Pencil, Plus, Save, Trash2, RefreshCw, Search, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, 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 { UniversalListPage, type UniversalListConfig, type SelectionHandlers, type RowClickHandlers, type StatCard, } from '@/components/templates/UniversalListPage'; import { MobileCard } from '@/components/organisms/MobileCard'; import type { DepositRecord, SortOption, } from './types'; import { SORT_OPTIONS, DEPOSIT_TYPE_LABELS, DEPOSIT_TYPE_FILTER_OPTIONS, ACCOUNT_SUBJECT_OPTIONS, } from './types'; import { deleteDeposit, updateDepositTypes, getDeposits } from './actions'; import { toast } from 'sonner'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'depositDate', label: '입금일' }, { key: 'accountName', label: '입금계좌' }, { key: 'depositorName', label: '입금자명' }, { key: 'depositAmount', label: '입금금액', className: 'text-right' }, { key: 'vendorName', label: '거래처' }, { key: 'note', label: '적요' }, { key: 'depositType', label: '입금유형', className: 'text-center' }, { key: 'actions', label: '작업', className: 'text-center w-[80px]' }, ]; // ===== 컴포넌트 Props ===== interface DepositManagementProps { initialData: DepositRecord[]; initialPagination: { currentPage: number; lastPage: number; perPage: number; total: number; }; } export function DepositManagement({ initialData, initialPagination }: DepositManagementProps) { const router = useRouter(); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [startDate, setStartDate] = useState('2025-09-01'); const [endDate, setEndDate] = useState('2025-09-03'); const [depositData, setDepositData] = useState(initialData); const [isRefreshing, setIsRefreshing] = useState(false); const [searchQuery, setSearchQuery] = useState(''); // 인라인 필터 상태 (tableHeaderActions에서 사용) const [vendorFilter, setVendorFilter] = useState('all'); const [depositTypeFilter, setDepositTypeFilter] = useState('all'); const [sortOption, setSortOption] = useState('latest'); // 계정과목명 저장 다이얼로그 const [selectedAccountSubject, setSelectedAccountSubject] = useState('unset'); const [showSaveDialog, setShowSaveDialog] = useState(false); const [selectedItemsForSave, setSelectedItemsForSave] = useState>(new Set()); // 선택 필요 경고 다이얼로그 const [showSelectWarningDialog, setShowSelectWarningDialog] = useState(false); // ===== 통계 계산 ===== const stats = useMemo(() => { const totalDeposit = depositData.reduce((sum, d) => sum + (d.depositAmount ?? 0), 0); const currentMonth = new Date().getMonth(); const currentYear = new Date().getFullYear(); const monthlyDeposit = depositData .filter(d => { const date = new Date(d.depositDate); return date.getMonth() === currentMonth && date.getFullYear() === currentYear; }) .reduce((sum, d) => sum + (d.depositAmount ?? 0), 0); const vendorUnsetCount = depositData.filter(d => !d.vendorName).length; const depositTypeUnsetCount = depositData.filter(d => d.depositType === 'unset').length; return { totalDeposit, monthlyDeposit, vendorUnsetCount, depositTypeUnsetCount }; }, [depositData]); // ===== 거래처 목록 (필터용) ===== const vendorOptions = useMemo(() => { const uniqueVendors = [...new Set(depositData.map(d => d.vendorName).filter(v => v))]; return [ { value: 'all', label: '전체' }, ...uniqueVendors.map(v => ({ value: v, label: v })) ]; }, [depositData]); // ===== 핸들러 ===== const handleRowClick = useCallback((item: DepositRecord) => { router.push(`/ko/accounting/deposits/${item.id}?mode=view`); }, [router]); const handleEdit = useCallback((item: DepositRecord) => { router.push(`/ko/accounting/deposits/${item.id}?mode=edit`); }, [router]); // 새로고침 핸들러 const handleRefresh = useCallback(async () => { setIsRefreshing(true); try { const result = await getDeposits({ perPage: 100, startDate, endDate, depositType: depositTypeFilter !== 'all' ? depositTypeFilter : undefined, }); if (result.success) { setDepositData(result.data); toast.success('데이터를 새로고침했습니다.'); } else { toast.error(result.error || '새로고침에 실패했습니다.'); } } catch { toast.error('새로고침 중 오류가 발생했습니다.'); } finally { setIsRefreshing(false); } }, [startDate, endDate, depositTypeFilter]); // 계정과목명 저장 핸들러 const handleSaveAccountSubject = useCallback((selectedItems: Set) => { if (selectedItems.size === 0) { setShowSelectWarningDialog(true); return; } setSelectedItemsForSave(selectedItems); setShowSaveDialog(true); }, []); const handleConfirmSaveAccountSubject = useCallback(async () => { const ids = Array.from(selectedItemsForSave); const result = await updateDepositTypes(ids, selectedAccountSubject); if (result.success) { toast.success('계정과목명이 저장되었습니다.'); setDepositData(prev => prev.map(item => selectedItemsForSave.has(item.id) ? { ...item, depositType: selectedAccountSubject as DepositRecord['depositType'] } : item )); setSelectedItemsForSave(new Set()); } else { toast.error(result.error || '계정과목명 저장에 실패했습니다.'); } setShowSaveDialog(false); }, [selectedAccountSubject, selectedItemsForSave]); // ===== 테이블 합계 계산 ===== const tableTotals = useMemo(() => { const totalAmount = depositData.reduce((sum, item) => sum + (item.depositAmount ?? 0), 0); return { totalAmount }; }, [depositData]); // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ // 페이지 기본 정보 title: '입금관리', description: '입금 내역을 등록합니다', icon: Banknote, basePath: '/accounting/deposits', // ID 추출 idField: 'id', // API 액션 actions: { getList: async () => { return { success: true, data: initialData, totalCount: initialData.length, }; }, deleteItem: async (id: string) => { const result = await deleteDeposit(id); if (result.success) { setDepositData(prev => prev.filter(item => item.id !== id)); toast.success('입금 내역이 삭제되었습니다.'); } return { success: result.success, error: result.error }; }, }, // 테이블 컬럼 columns: tableColumns, // 클라이언트 사이드 필터링 clientSideFiltering: true, itemsPerPage: 20, // 데이터 변경 콜백 onDataChange: (data) => setDepositData(data), // 검색 필터 searchPlaceholder: '입금자명, 계좌명, 적요, 거래처 검색...', searchFilter: (item, searchValue) => { const search = searchValue.toLowerCase(); return ( item.depositorName.toLowerCase().includes(search) || item.accountName.toLowerCase().includes(search) || (item.note?.toLowerCase().includes(search) || false) || (item.vendorName?.toLowerCase().includes(search) || false) ); }, // 커스텀 필터 함수 (인라인 필터 사용) customFilterFn: (items, filterValues) => { if (!items || items.length === 0) return items; return items.filter((item) => { // 검색어 필터 if (searchQuery) { const search = searchQuery.toLowerCase(); const matchesSearch = item.depositorName.toLowerCase().includes(search) || item.accountName.toLowerCase().includes(search) || (item.note?.toLowerCase().includes(search) || false) || (item.vendorName?.toLowerCase().includes(search) || false); if (!matchesSearch) return false; } // 거래처 필터 if (vendorFilter !== 'all' && item.vendorName !== vendorFilter) { return false; } // 입금유형 필터 if (depositTypeFilter !== 'all' && item.depositType !== depositTypeFilter) { return false; } return true; }); }, // 커스텀 정렬 함수 customSortFn: (items, filterValues) => { const sorted = [...items]; switch (sortOption) { case 'oldest': sorted.sort((a, b) => new Date(a.depositDate).getTime() - new Date(b.depositDate).getTime()); break; case 'amountHigh': sorted.sort((a, b) => (b.depositAmount ?? 0) - (a.depositAmount ?? 0)); break; case 'amountLow': sorted.sort((a, b) => (a.depositAmount ?? 0) - (b.depositAmount ?? 0)); break; default: // latest sorted.sort((a, b) => new Date(b.depositDate).getTime() - new Date(a.depositDate).getTime()); break; } return sorted; }, // 검색창 (공통 컴포넌트에서 자동 생성) hideSearch: true, searchValue: searchQuery, onSearchChange: setSearchQuery, searchPlaceholder: '입금자명, 계좌명, 적요, 거래처 검색...', // 공통 헤더 옵션 dateRangeSelector: { enabled: true, showPresets: true, startDate, endDate, onStartDateChange: setStartDate, onEndDateChange: setEndDate, }, // 모바일 필터 설정 filterConfig: [ { key: 'depositType', label: '입금유형', type: 'single', options: DEPOSIT_TYPE_FILTER_OPTIONS.filter(o => o.value !== 'all'), }, { key: 'sortBy', label: '정렬', type: 'single', options: SORT_OPTIONS, }, ], initialFilters: { depositType: depositTypeFilter, sortBy: sortOption, }, filterTitle: '입금 필터', // 헤더 액션: 계정과목명 Select + 저장 + 새로고침 headerActions: ({ selectedItems }) => (
계정과목명
), // 등록 버튼 createButton: { label: '입금등록', icon: Plus, onClick: () => router.push('/ko/accounting/deposits?mode=new'), }, // Stats 카드 computeStats: (): StatCard[] => [ { label: '총 입금', value: `${stats.totalDeposit.toLocaleString()}원`, icon: Banknote, iconColor: 'text-blue-500' }, { label: '당월 입금', value: `${stats.monthlyDeposit.toLocaleString()}원`, icon: Banknote, iconColor: 'text-green-500' }, { label: '거래처 미설정', value: `${stats.vendorUnsetCount}건`, icon: Banknote, iconColor: 'text-orange-500' }, { label: '입금유형 미설정', value: `${stats.depositTypeUnsetCount}건`, icon: Banknote, iconColor: 'text-red-500' }, ], // tableHeaderActions: 3개 인라인 필터 tableHeaderActions: (
{/* 거래처 필터 */} {/* 입금유형 필터 */} {/* 정렬 */}
), // 테이블 하단 합계 행 tableFooter: ( 합계 {tableTotals.totalAmount.toLocaleString()} ), // 삭제 확인 메시지 deleteConfirmMessage: { title: '입금 삭제', description: '이 입금 내역을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.', }, // 테이블 행 렌더링 renderTableRow: ( item: DepositRecord, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => { const isVendorUnset = !item.vendorName; const isDepositTypeUnset = item.depositType === 'unset'; return ( handleRowClick(item)} > e.stopPropagation()}> {item.depositDate} {item.accountName} {item.depositorName} {(item.depositAmount ?? 0).toLocaleString()} {item.vendorName || '미설정'} {item.note || '-'} {DEPOSIT_TYPE_LABELS[item.depositType]} e.stopPropagation()}> {handlers.isSelected && (
)}
); }, // 모바일 카드 렌더링 renderMobileCard: ( item: DepositRecord, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => ( handleRowClick(item)} details={[ { label: '입금일', value: item.depositDate }, { label: '입금액', value: `${(item.depositAmount ?? 0).toLocaleString()}원` }, { label: '거래처', value: item.vendorName || '-' }, ]} actions={ handlers.isSelected ? (
) : undefined } /> ), }), [ initialData, startDate, endDate, stats, vendorFilter, depositTypeFilter, sortOption, selectedAccountSubject, vendorOptions, tableTotals, isRefreshing, searchQuery, handleRowClick, handleEdit, handleRefresh, handleSaveAccountSubject, ] ); return ( <> {/* 계정과목명 저장 확인 다이얼로그 */} 계정과목명 변경 {selectedItemsForSave.size}개의 입금 유형을{' '} {ACCOUNT_SUBJECT_OPTIONS.find(o => o.value === selectedAccountSubject)?.label} (으)로 모두 변경하시겠습니까? {/* 선택 필요 알림 다이얼로그 */} 항목 선택 필요 변경할 입금 항목을 먼저 선택해주세요. setShowSelectWarningDialog(false)}> 확인 ); }