'use client'; /** * 공정 목록 - UniversalListPage 마이그레이션 * * 기존 IntegratedListTemplateV2 → UniversalListPage config 기반으로 변환 * - 클라이언트 사이드 필터링 (탭별, 검색) * - 상태 토글 기능 * - 삭제 다이얼로그 */ import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { useDateRange } from '@/hooks'; import { Wrench, Plus, GripVertical } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow, TableHead } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { UniversalListPage, type UniversalListConfig, type SelectionHandlers, type RowClickHandlers, type ListParams, } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import { toast } from 'sonner'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import type { Process } from '@/types/process'; import { getProcessList, deleteProcess, deleteProcesses, toggleProcessActive, getProcessStats, reorderProcesses } from './actions'; interface ProcessListClientProps { initialData?: Process[]; initialStats?: { total: number; active: number; inactive: number }; } export default function ProcessListClient({ initialData = [], initialStats }: ProcessListClientProps) { const router = useRouter(); // ===== 상태 ===== const [allProcesses, setAllProcesses] = useState(initialData); const [stats, setStats] = useState(initialStats ?? { total: 0, active: 0, inactive: 0 }); const [isLoading, setIsLoading] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteTargetId, setDeleteTargetId] = useState(null); // 날짜 범위 상태 const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear'); // 검색어 상태 const [searchQuery, setSearchQuery] = useState(''); // 드래그&드롭 순서 변경 상태 const [isOrderChanged, setIsOrderChanged] = useState(false); const dragProcessIdRef = useRef(null); const dragNodeRef = useRef(null); const allProcessesRef = useRef(allProcesses); allProcessesRef.current = allProcesses; // ===== 데이터 로드 ===== const loadData = useCallback(async () => { setIsLoading(true); try { const [listResult, statsResult] = await Promise.all([ getProcessList({ size: 1000 }), getProcessStats(), ]); if (listResult.success && listResult.data) { setAllProcesses(listResult.data.items); } if (statsResult.success && statsResult.data) { setStats({ total: statsResult.data.total, active: statsResult.data.active, inactive: statsResult.data.inactive, }); } } catch { toast.error('데이터 로드에 실패했습니다.'); } finally { setIsLoading(false); } }, []); // 초기 데이터가 없으면 로드 useEffect(() => { if (initialData.length === 0) { loadData(); } }, [initialData.length, loadData]); // ===== 핸들러 ===== const handleRowClick = useCallback((process: Process) => { router.push(`/ko/master-data/process-management/${process.id}?mode=view`); }, [router]); const handleCreate = useCallback(() => { router.push('/ko/master-data/process-management?mode=new'); }, [router]); const handleEdit = useCallback((process: Process) => { router.push(`/ko/master-data/process-management/${process.id}?mode=edit`); }, [router]); const handleDeleteClick = useCallback((processId: string) => { setDeleteTargetId(processId); setDeleteDialogOpen(true); }, []); const handleDeleteConfirm = useCallback(async () => { if (!deleteTargetId) return; setIsLoading(true); try { const result = await deleteProcess(deleteTargetId); if (result.success) { toast.success('공정이 삭제되었습니다.'); const deletedProcess = allProcesses.find((p) => p.id === deleteTargetId); setAllProcesses((prev) => prev.filter((p) => p.id !== deleteTargetId)); setStats((prev) => ({ ...prev, total: prev.total - 1, active: prev.active - (deletedProcess?.status === '사용중' ? 1 : 0), inactive: prev.inactive - (deletedProcess?.status === '미사용' ? 1 : 0), })); } else { toast.error(result.error || '삭제에 실패했습니다.'); } } catch { toast.error('삭제 중 오류가 발생했습니다.'); } finally { setIsLoading(false); setDeleteDialogOpen(false); setDeleteTargetId(null); } }, [deleteTargetId, allProcesses]); const handleBulkDelete = useCallback(async (selectedIds: string[]) => { if (selectedIds.length === 0) { toast.warning('삭제할 항목을 선택해주세요.'); return; } setIsLoading(true); try { const result = await deleteProcesses(selectedIds); if (result.success) { toast.success(`${result.deletedCount}개 항목이 삭제되었습니다.`); await loadData(); } else { toast.error(result.error || '일괄 삭제에 실패했습니다.'); } } catch { toast.error('일괄 삭제 중 오류가 발생했습니다.'); } finally { setIsLoading(false); } }, [loadData]); const handleToggleStatus = useCallback(async (processId: string) => { setIsLoading(true); try { const result = await toggleProcessActive(processId); if (result.success && result.data) { toast.success('상태가 변경되었습니다.'); setAllProcesses((prev) => prev.map((p) => (p.id === processId ? result.data! : p))); const oldProcess = allProcesses.find((p) => p.id === processId); if (oldProcess) { const wasActive = oldProcess.status === '사용중'; setStats((prev) => ({ ...prev, active: wasActive ? prev.active - 1 : prev.active + 1, inactive: wasActive ? prev.inactive + 1 : prev.inactive - 1, })); } } else { toast.error(result.error || '상태 변경에 실패했습니다.'); } } catch { toast.error('상태 변경 중 오류가 발생했습니다.'); } finally { setIsLoading(false); } }, [allProcesses]); // ===== 드래그&드롭 순서 변경 ===== const handleDragStart = useCallback((e: React.DragEvent, processId: string) => { dragProcessIdRef.current = processId; dragNodeRef.current = e.currentTarget; e.dataTransfer.effectAllowed = 'move'; requestAnimationFrame(() => { if (dragNodeRef.current) { dragNodeRef.current.style.opacity = '0.4'; } }); }, []); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; e.currentTarget.classList.add('border-t-2', 'border-t-primary'); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { const related = e.relatedTarget as Node; if (!e.currentTarget.contains(related)) { e.currentTarget.classList.remove('border-t-2', 'border-t-primary'); } }, []); const handleDragEnd = useCallback(() => { if (dragNodeRef.current) { dragNodeRef.current.style.opacity = '1'; } dragProcessIdRef.current = null; dragNodeRef.current = null; }, []); const handleProcessDrop = useCallback((e: React.DragEvent, dropProcessId: string) => { e.preventDefault(); e.currentTarget.classList.remove('border-t-2', 'border-t-primary'); const dragId = dragProcessIdRef.current; if (!dragId || dragId === dropProcessId) { handleDragEnd(); return; } setAllProcesses((prev) => { const updated = [...prev]; const dragIdx = updated.findIndex(p => p.id === dragId); const dropIdx = updated.findIndex(p => p.id === dropProcessId); if (dragIdx === -1 || dropIdx === -1) return prev; const [moved] = updated.splice(dragIdx, 1); updated.splice(dropIdx, 0, moved); return updated; }); setIsOrderChanged(true); handleDragEnd(); }, [handleDragEnd]); const handleSaveOrder = useCallback(async () => { setIsLoading(true); try { const orderData = allProcessesRef.current.map((p, idx) => ({ id: p.id, order: idx + 1 })); const result = await reorderProcesses(orderData); if (result.success) { toast.success('순서가 저장되었습니다.'); setIsOrderChanged(false); } else { toast.error(result.error || '순서 저장에 실패했습니다.'); } } catch { toast.error('순서 저장 중 오류가 발생했습니다.'); } finally { setIsLoading(false); } }, []); // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ // 페이지 기본 정보 title: '공정 목록', icon: Wrench, basePath: '/master-data/process-management', // ID 추출 idField: 'id', // API 액션 actions: { getList: async (params?: ListParams) => { try { const [listResult, statsResult] = await Promise.all([ getProcessList({ size: 1000 }), getProcessStats(), ]); if (listResult.success && listResult.data) { setAllProcesses(listResult.data.items); if (statsResult.success && statsResult.data) { setStats({ total: statsResult.data.total, active: statsResult.data.active, inactive: statsResult.data.inactive, }); } return { success: true, data: listResult.data.items, totalCount: listResult.data.items.length, totalPages: 1, }; } return { success: false, error: '데이터 로드에 실패했습니다.' }; } catch { return { success: false, error: '서버 오류가 발생했습니다.' }; } }, deleteItem: async (id: string) => { const result = await deleteProcess(id); return { success: result.success, error: result.error }; }, deleteBulk: async (ids: string[]) => { const result = await deleteProcesses(ids); return { success: result.success, error: result.error }; }, }, // 테이블 컬럼 (showCheckbox: false + renderCustomTableHeader로 수동 관리) showCheckbox: false, columns: [ { key: 'drag', label: '', className: 'w-[40px]' }, { key: 'checkbox', label: '', className: 'w-[50px]' }, { key: 'no', label: '번호', className: 'w-[60px] text-center' }, { key: 'processCode', label: '공정번호', className: 'w-[120px]' }, { key: 'processName', label: '공정명', className: 'min-w-[200px]' }, { key: 'department', label: '담당부서', className: 'w-[120px]' }, { key: 'items', label: '품목', className: 'w-[80px] text-center' }, { key: 'inspection', label: '중간검사', className: 'w-[90px] text-center' }, { key: 'workLog', label: '작업일지', className: 'w-[90px] text-center' }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, ], // 커스텀 테이블 헤더 (드래그 → 전체선택 체크박스 → 번호 → 데이터 순) renderCustomTableHeader: ({ displayData, selectedItems, onToggleSelectAll }) => ( <> 0 && selectedItems.size === displayData.length} onCheckedChange={onToggleSelectAll} /> 번호 공정번호 공정명 담당부서 품목 중간검사 작업일지 상태 ), // 클라이언트 사이드 필터링 clientSideFiltering: true, itemsPerPage: 20, // 검색창 (공통 컴포넌트에서 자동 생성) hideSearch: true, searchValue: searchQuery, onSearchChange: setSearchQuery, // 날짜 범위 선택기 dateRangeSelector: { enabled: true, showPresets: true, startDate, endDate, onStartDateChange: setStartDate, onEndDateChange: setEndDate, }, // 필터 설정 (상태 필터) filterConfig: [ { key: 'status', label: '상태', type: 'single' as const, options: [ { value: '사용중', label: '사용' }, { value: '미사용', label: '미사용' }, ], allOptionLabel: '전체', }, ], initialFilters: { status: '' }, // 커스텀 필터 함수 (상태 필터) customFilterFn: (items: Process[], filterValues: Record) => { const statusFilter = filterValues.status as string; if (!statusFilter) return items; return items.filter(item => item.status === statusFilter); }, // 검색 필터 searchFilter: (item, searchValue) => { if (!searchValue || !searchValue.trim()) return true; const search = searchValue.toLowerCase().trim(); return ( (item.processCode || '').toLowerCase().includes(search) || (item.processName || '').toLowerCase().includes(search) || (item.department || '').toLowerCase().includes(search) ); }, // 순서 변경 저장 버튼 headerActions: () => ( ), // 등록 버튼 (공통 컴포넌트에서 오른쪽에 렌더링) createButton: { label: '공정 등록', onClick: handleCreate, icon: Plus, }, // 일괄 삭제 핸들러 onBulkDelete: handleBulkDelete, // 테이블 행 렌더링 renderTableRow: ( process: Process, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => { const itemCount = process.classificationRules .filter(r => r.registrationType === 'individual') .reduce((sum, r) => sum + (r.items?.length || 0), 0); return ( handleDragStart(e, process.id)} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDragEnd={handleDragEnd} onDrop={(e) => handleProcessDrop(e, process.id)} className={`cursor-pointer hover:bg-muted/50 ${handlers.isSelected ? 'bg-blue-50' : ''}`} onClick={() => handleRowClick(process)} > e.stopPropagation()} > e.stopPropagation()}> {globalIndex} {process.processCode} {process.processName} {process.department} {itemCount > 0 ? itemCount : '-'} {process.needsInspection ? '사용' : '미사용'} {process.needsWorkLog ? '사용' : '미사용'} e.stopPropagation()}> handleToggleStatus(process.id)} > {process.status === '사용중' ? '사용' : '미사용'} ); }, // 모바일 카드 렌더링 renderMobileCard: ( process: Process, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => { const itemCount = process.classificationRules .filter(r => r.registrationType === 'individual') .reduce((sum, r) => sum + (r.items?.length || 0), 0); return ( handleRowClick(process)} headerBadges={ <> {globalIndex} {process.processCode} } title={process.processName} statusBadge={ { e.stopPropagation(); handleToggleStatus(process.id); }} > {process.status === '사용중' ? '사용' : '미사용'} } infoGrid={
0 ? `${itemCount}개` : '-'} />
} /> ); }, }), [handleCreate, handleRowClick, handleToggleStatus, handleBulkDelete, startDate, endDate, searchQuery, isOrderChanged, handleSaveOrder, handleDragStart, handleDragOver, handleDragLeave, handleDragEnd, handleProcessDrop] ); return ( <> {/* 삭제 확인 다이얼로그 */} ); }