'use client'; /** * 점검표 목록 - UniversalListPage 기반 * * 공정 목록(ProcessListClient)과 동일한 패턴: * - 클라이언트 사이드 필터링 (상태별) * - 드래그&드롭 순서 변경 * - 상태 토글 */ import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { useDateRange } from '@/hooks'; import { ClipboardList, 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 { Checklist } from '@/types/checklist'; import { getChecklistList, deleteChecklist, deleteChecklists, toggleChecklistStatus, getChecklistStats, reorderChecklists, } from './actions'; export default function ChecklistListClient() { const router = useRouter(); // ===== 상태 ===== const [allChecklists, setAllChecklists] = useState([]); const [, setStats] = useState({ 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 dragIdRef = useRef(null); const dragNodeRef = useRef(null); const allChecklistsRef = useRef(allChecklists); allChecklistsRef.current = allChecklists; // ===== 데이터 로드 ===== const loadData = useCallback(async () => { setIsLoading(true); try { const [listResult, statsResult] = await Promise.all([ getChecklistList(), getChecklistStats(), ]); if (listResult.success && listResult.data) { setAllChecklists(listResult.data.items); } if (statsResult.success && statsResult.data) { setStats(statsResult.data); } } catch { toast.error('데이터 로드에 실패했습니다.'); } finally { setIsLoading(false); } }, []); useEffect(() => { loadData(); }, [loadData]); // ===== 핸들러 ===== const handleRowClick = useCallback( (checklist: Checklist) => { router.push(`/ko/master-data/checklist-management/${checklist.id}?mode=view`); }, [router] ); const handleCreate = useCallback(() => { router.push('/ko/master-data/checklist-management?mode=new'); }, [router]); const handleDeleteConfirm = useCallback(async () => { if (!deleteTargetId) return; setIsLoading(true); try { const result = await deleteChecklist(deleteTargetId); if (result.success) { toast.success('점검표가 삭제되었습니다.'); setAllChecklists((prev) => prev.filter((c) => c.id !== deleteTargetId)); } else { toast.error(result.error || '삭제에 실패했습니다.'); } } catch { toast.error('삭제 중 오류가 발생했습니다.'); } finally { setIsLoading(false); setDeleteDialogOpen(false); setDeleteTargetId(null); } }, [deleteTargetId]); const handleBulkDelete = useCallback( async (selectedIds: string[]) => { if (selectedIds.length === 0) { toast.warning('삭제할 항목을 선택해주세요.'); return; } setIsLoading(true); try { const result = await deleteChecklists(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 (checklistId: string) => { setIsLoading(true); try { const result = await toggleChecklistStatus(checklistId); if (result.success && result.data) { toast.success('상태가 변경되었습니다.'); setAllChecklists((prev) => prev.map((c) => (c.id === checklistId ? result.data! : c)) ); } else { toast.error(result.error || '상태 변경에 실패했습니다.'); } } catch { toast.error('상태 변경 중 오류가 발생했습니다.'); } finally { setIsLoading(false); } }, [] ); // ===== 드래그&드롭 ===== const handleDragStart = useCallback( (e: React.DragEvent, id: string) => { dragIdRef.current = id; 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'; dragIdRef.current = null; dragNodeRef.current = null; }, []); const handleDrop = useCallback( (e: React.DragEvent, dropId: string) => { e.preventDefault(); e.currentTarget.classList.remove('border-t-2', 'border-t-primary'); const dragId = dragIdRef.current; if (!dragId || dragId === dropId) { handleDragEnd(); return; } setAllChecklists((prev) => { const updated = [...prev]; const dragIdx = updated.findIndex((c) => c.id === dragId); const dropIdx = updated.findIndex((c) => c.id === dropId); 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 = allChecklistsRef.current.map((c, idx) => ({ id: c.id, order: idx + 1, })); const result = await reorderChecklists(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: ClipboardList, basePath: '/master-data/checklist-management', idField: 'id', actions: { getList: async (_params?: ListParams) => { try { const [listResult, statsResult] = await Promise.all([ getChecklistList(), getChecklistStats(), ]); if (listResult.success && listResult.data) { setAllChecklists(listResult.data.items); if (statsResult.success && statsResult.data) { setStats(statsResult.data); } 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 deleteChecklist(id); return { success: result.success, error: result.error }; }, deleteBulk: async (ids: string[]) => { const result = await deleteChecklists(ids); return { success: result.success, error: result.error }; }, }, // 테이블 컬럼 (showCheckbox: false → 수동 관리) showCheckbox: false, columns: [ { key: 'drag', label: '', className: 'w-[40px]' }, { key: 'checkbox', label: '', className: 'w-[50px]' }, { key: 'no', label: 'No.', className: 'w-[60px] text-center' }, { key: 'checklistCode', label: '점검표 번호', className: 'w-[120px]', copyable: true }, { key: 'checklistName', label: '점검표', className: 'min-w-[200px]', copyable: true }, { key: 'items', label: '항목', className: 'w-[80px] text-center' }, { key: 'documents', label: '문서', className: 'w-[80px] text-center' }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, ], // 커스텀 테이블 헤더 (드래그 → 전체선택 체크박스 → No. → 데이터 순) renderCustomTableHeader: ({ displayData, selectedItems, onToggleSelectAll }) => ( <> 0 && selectedItems.size === displayData.length} onCheckedChange={onToggleSelectAll} /> No. 점검표 번호 점검표 항목 문서 상태 ), 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: Checklist[], 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.checklistCode || '').toLowerCase().includes(search) || (item.checklistName || '').toLowerCase().includes(search) ); }, headerActions: () => ( ), createButton: { label: '점검표 등록', onClick: handleCreate, icon: Plus, }, onBulkDelete: handleBulkDelete, renderTableRow: ( checklist: Checklist, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => ( handleDragStart(e, checklist.id)} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDragEnd={handleDragEnd} onDrop={(e) => handleDrop(e, checklist.id)} className={`cursor-pointer hover:bg-muted/50 ${handlers.isSelected ? 'bg-blue-50' : ''}`} onClick={() => handleRowClick(checklist)} > e.stopPropagation()} > e.stopPropagation()}> {globalIndex} {checklist.checklistCode} {checklist.checklistName} {checklist.itemCount} {checklist.documentCount} e.stopPropagation()} > handleToggleStatus(checklist.id)} > {checklist.status} ), renderMobileCard: ( checklist: Checklist, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => ( handleRowClick(checklist)} headerBadges={ <> {globalIndex} {checklist.checklistCode} } title={checklist.checklistName} statusBadge={ { e.stopPropagation(); handleToggleStatus(checklist.id); }} > {checklist.status} } infoGrid={
} /> ), }), [ handleCreate, handleRowClick, handleToggleStatus, handleBulkDelete, startDate, endDate, searchQuery, isOrderChanged, handleSaveOrder, handleDragStart, handleDragOver, handleDragLeave, handleDragEnd, handleDrop, ] ); return ( <> ); }