'use client'; import { useState, useMemo, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { format } from 'date-fns'; import { Shield, Plus, Pencil, Trash2, Settings, Eye, EyeOff, Users, Loader2, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { TableRow, TableCell } from '@/components/ui/table'; import { UniversalListPage, type UniversalListConfig, type TableColumn, type StatCard, type TabOption, } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { toast } from 'sonner'; import type { Role, RoleStats } from './types'; import { fetchRoles, fetchRoleStats, deleteRole } from './actions'; export function PermissionManagement() { const router = useRouter(); // ===== 상태 관리 ===== const [searchQuery, setSearchQuery] = useState(''); const [selectedItems, setSelectedItems] = useState>(new Set()); // 역할 데이터 const [roles, setRoles] = useState([]); const [stats, setStats] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // 삭제 확인 다이얼로그 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [roleToDelete, setRoleToDelete] = useState(null); const [isBulkDelete, setIsBulkDelete] = useState(false); const [isDeleting, setIsDeleting] = useState(false); // API에서 데이터 로드 const loadData = useCallback(async () => { setIsLoading(true); setError(null); try { const [rolesResult, statsResult] = await Promise.all([ fetchRoles({ size: 1000 }), // 전체 로드 후 클라이언트 필터링 fetchRoleStats(), ]); if (rolesResult.success && rolesResult.data) { setRoles(rolesResult.data.data); } else { setError(rolesResult.error || '역할 목록 조회 실패'); } if (statsResult.success && statsResult.data) { setStats(statsResult.data); } } catch (err) { setError(err instanceof Error ? err.message : '데이터 로드 실패'); } finally { setIsLoading(false); } }, []); // 초기 데이터 로드 useEffect(() => { loadData(); }, [loadData]); // ===== 탭 상태 ===== const [activeTab, setActiveTab] = useState('all'); // ===== 체크박스 핸들러 ===== 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 toggleSelectAll = useCallback(() => { if (selectedItems.size === filteredData.length && filteredData.length > 0) { setSelectedItems(new Set()); } else { setSelectedItems(new Set(filteredData.map(item => item.id.toString()))); } }, [selectedItems.size]); // ===== 필터링된 데이터 ===== const filteredData = useMemo(() => { let result = roles; // 탭 필터 if (activeTab === 'visible') { result = result.filter(item => !item.is_hidden); } else if (activeTab === 'hidden') { result = result.filter(item => item.is_hidden); } // 검색 필터 if (searchQuery) { result = result.filter(item => item.name.toLowerCase().includes(searchQuery.toLowerCase()) || (item.description?.toLowerCase().includes(searchQuery.toLowerCase())) ); } return result; }, [roles, searchQuery, activeTab]); // ===== 핸들러 ===== const handleAdd = () => { router.push('/settings/permissions/new'); }; const handleEdit = (role: Role, e?: React.MouseEvent) => { e?.stopPropagation(); router.push(`/settings/permissions/${role.id}`); }; const handleViewDetail = (role: Role) => { router.push(`/settings/permissions/${role.id}`); }; const handleDelete = (role: Role, e?: React.MouseEvent) => { e?.stopPropagation(); setRoleToDelete(role); setIsBulkDelete(false); setDeleteDialogOpen(true); }; const handleBulkDelete = () => { if (selectedItems.size === 0) return; setIsBulkDelete(true); setDeleteDialogOpen(true); }; const confirmDelete = async () => { setIsDeleting(true); try { if (isBulkDelete) { // 일괄 삭제 const deletePromises = Array.from(selectedItems).map(id => deleteRole(parseInt(id))); const results = await Promise.all(deletePromises); const failedCount = results.filter(r => !r.success).length; if (failedCount > 0) { toast.error(`${failedCount}개 역할 삭제 실패`); } else { toast.success(`${selectedItems.size}개 역할 삭제 완료`); } setSelectedItems(new Set()); } else if (roleToDelete) { // 단일 삭제 const result = await deleteRole(roleToDelete.id); if (result.success) { toast.success(`"${roleToDelete.name}" 역할 삭제 완료`); } else { toast.error(result.error || '역할 삭제 실패'); } } // 데이터 새로고침 await loadData(); } catch (err) { toast.error(err instanceof Error ? err.message : '삭제 중 오류 발생'); } finally { setIsDeleting(false); setDeleteDialogOpen(false); setRoleToDelete(null); } }; // ===== 날짜 포맷 ===== const formatDate = (dateStr: string) => { return format(new Date(dateStr), 'yyyy-MM-dd'); }; // ===== 탭 설정 ===== const tabs: TabOption[] = useMemo(() => { const visibleCount = roles.filter(r => !r.is_hidden).length; const hiddenCount = roles.filter(r => r.is_hidden).length; return [ { value: 'all', label: '전체', count: roles.length, color: 'blue' }, { value: 'visible', label: '공개', count: visibleCount, color: 'green' }, { value: 'hidden', label: '숨김', count: hiddenCount, color: 'gray' }, ]; }, [roles]); // ===== 통계 카드 ===== const statCards: StatCard[] = useMemo(() => { return [ { label: '전체 역할', value: stats?.total_roles ?? roles.length, icon: Shield, iconColor: 'text-blue-500', }, { label: '공개', value: stats?.visible_roles ?? roles.filter(r => !r.is_hidden).length, icon: Eye, iconColor: 'text-green-500', }, { label: '숨김', value: stats?.hidden_roles ?? roles.filter(r => r.is_hidden).length, icon: EyeOff, iconColor: 'text-gray-500', }, { label: '사용 중', value: stats?.roles_with_users ?? 0, icon: Users, iconColor: 'text-purple-500', }, ]; }, [roles, stats]); // ===== 테이블 컬럼 ===== const tableColumns: TableColumn[] = useMemo(() => { const baseColumns: TableColumn[] = [ { key: 'index', label: '번호', className: 'text-center w-[80px]' }, { key: 'name', label: '역할', className: 'flex-1' }, { key: 'description', label: '설명', className: 'flex-1' }, { key: 'status', label: '상태', className: 'text-center w-[100px]' }, { key: 'createdAt', label: '등록일', className: 'text-center w-[120px]' }, ]; if (selectedItems.size > 0) { baseColumns.push({ key: 'action', label: '작업', className: 'text-center w-[150px]' }); } return baseColumns; }, [selectedItems.size]); // ===== 테이블 행 렌더링 ===== const renderTableRow = useCallback(( item: Role, index: number, globalIndex: number, handlers: { isSelected: boolean; onToggle: () => void; onRowClick?: () => void } ) => { const { isSelected, onToggle } = handlers; const hasSelection = selectedItems.size > 0; return ( handleViewDetail(item)} > e.stopPropagation()}> {globalIndex} {item.name} {item.description || '-'} {item.is_hidden ? '숨김' : '공개'} {formatDate(item.created_at)} {hasSelection && ( e.stopPropagation()}>
)}
); }, [selectedItems, toggleSelection]); // ===== 모바일 카드 렌더링 ===== const renderMobileCard = useCallback(( item: Role, index: number, globalIndex: number, handlers: { isSelected: boolean; onToggle: () => void; onRowClick?: () => void } ) => { const { isSelected, onToggle } = handlers; return ( {item.is_hidden ? '숨김' : '공개'} } isSelected={isSelected} onToggleSelection={onToggle} infoGrid={
} actions={
} /> ); }, []); // ===== 헤더 액션 ===== const renderHeaderActions = useCallback(({ selectedItems: selItems }: { onCreate?: () => void; selectedItems: Set; onClearSelection: () => void; onRefresh: () => void; }) => (
{selItems.size > 0 && ( )}
), [handleBulkDelete, handleAdd]); // ===== 로딩/에러 상태 ===== if (isLoading) { return ; } if (error) { return (

{error}

); } // ===== UniversalListPage 설정 ===== const permissionConfig: UniversalListConfig = { title: '권한관리', description: '역할 기반 권한을 관리합니다', icon: Shield, basePath: '/settings/permissions', idField: 'id', actions: { getList: async () => ({ success: true, data: roles, totalCount: roles.length, }), }, columns: tableColumns, tabs: tabs, defaultTab: activeTab, stats: statCards, searchPlaceholder: '역할명, 설명 검색...', itemsPerPage: 20, clientSideFiltering: true, searchFilter: (item, searchValue) => { return ( item.name.toLowerCase().includes(searchValue.toLowerCase()) || (item.description?.toLowerCase().includes(searchValue.toLowerCase()) ?? false) ); }, tabFilter: (item, tabValue) => { if (tabValue === 'all') return true; if (tabValue === 'visible') return !item.is_hidden; if (tabValue === 'hidden') return item.is_hidden; return true; }, headerActions: renderHeaderActions, renderTableRow, renderMobileCard, renderDialogs: () => ( 역할 삭제 {isBulkDelete ? `선택한 ${selectedItems.size}개의 역할을 삭제하시겠습니까?` : `"${roleToDelete?.name}" 역할을 삭제하시겠습니까?` }
이 역할을 사용 중인 사원이 있으면 해당 사원의 역할이 초기화됩니다.
취소 {isDeleting ? ( <> 삭제 중... ) : ( '삭제' )}
), }; // ===== 목록 화면 ===== return ( config={permissionConfig} initialData={roles} initialTotalCount={roles.length} externalSelection={{ selectedItems, setSelectedItems, }} externalTab={{ activeTab, setActiveTab, }} externalSearch={{ searchValue: searchQuery, setSearchValue: setSearchQuery, }} /> ); }