"use client"; import React, { useState, useCallback, useEffect, useMemo } from "react"; import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Search, ChevronRight, ChevronDown, ChevronsDown, ChevronsUp, BookOpen, Shield, Eye, CheckSquare, Building2 } from "lucide-react"; import { RoleGroup, roleAPI } from "@/lib/api/role"; import { useAuth } from "@/hooks/useAuth"; import { cn } from "@/lib/utils"; interface MenuPermission { menuObjid: number; menuName: string; menuPath?: string; parentObjid?: number; companyCode?: string; createYn: string; readYn: string; updateYn: string; deleteYn: string; children?: MenuPermission[]; } interface MenuPermissionsTableProps { permissions: any[]; onPermissionsChange: (permissions: any[]) => void; roleGroup: RoleGroup; } /** * 메뉴 권한 설정 테이블 * * 기능: * - 메뉴 트리 구조 표시 * - CRUD 권한 체크박스 * - 전체 선택/해제 * - 검색 기능 */ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGroup }: MenuPermissionsTableProps) { const { user: currentUser } = useAuth(); const [searchText, setSearchText] = useState(""); const [expandedMenus, setExpandedMenus] = useState>(new Set()); const [allMenus, setAllMenus] = useState([]); const [isLoading, setIsLoading] = useState(true); // 최고 관리자 전용: 회사별 필터 const [companyFilter, setCompanyFilter] = useState("all"); // 초기값: 모든 메뉴 const [companyInfo, setCompanyInfo] = useState<{ code: string; name: string } | null>(null); // 메뉴 타입 필터 (관리자/사용자) const [menuTypeFilter, setMenuTypeFilter] = useState("all"); // 최고 관리자 여부 확인 const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN"; // 회사 정보 가져오기 useEffect(() => { const fetchCompanyInfo = async () => { if (roleGroup.companyCode && roleGroup.companyCode !== "*") { try { const { companyAPI } = await import("@/lib/api/company"); const company = await companyAPI.getInfo(roleGroup.companyCode); setCompanyInfo({ code: company.companyCode, name: company.companyName, }); } catch (error) { console.error("회사 정보 로드 실패", error); } } }; fetchCompanyInfo(); }, [roleGroup.companyCode]); // 전체 메뉴 목록 로드 useEffect(() => { // currentUser가 로드될 때까지 대기 if (!currentUser) { console.log("⏳ [MenuPermissionsTable] currentUser 로드 대기 중..."); return; } const loadAllMenus = async () => { let targetCompanyCode: string | undefined; if (isSuperAdmin) { // 최고 관리자: 권한그룹의 회사 코드로 조회 (해당 회사 + 공통 메뉴 모두 반환) targetCompanyCode = roleGroup.companyCode; } else { // 회사 관리자: 자기 회사 메뉴만 (공통 메뉴 제외) targetCompanyCode = roleGroup.companyCode; } console.log("🔍 [MenuPermissionsTable] 전체 메뉴 로드 시작", { currentUser: { userId: currentUser.userId, companyCode: currentUser.companyCode, userType: currentUser.userType, }, isSuperAdmin, roleGroupCompanyCode: roleGroup.companyCode, companyFilter, targetCompanyCode, }); try { setIsLoading(true); const response = await roleAPI.getAllMenus(targetCompanyCode); console.log("✅ [MenuPermissionsTable] 전체 메뉴 로드 응답", { success: response.success, count: response.data?.length, data: response.data, }); if (response.success && response.data) { setAllMenus(response.data); console.log("✅ [MenuPermissionsTable] 메뉴 상태 업데이트 완료", { count: response.data.length, }); } } catch (error) { console.error("❌ [MenuPermissionsTable] 메뉴 로드 실패", error); } finally { setIsLoading(false); } }; loadAllMenus(); }, [currentUser, isSuperAdmin, roleGroup.companyCode, companyFilter]); // 메뉴 권한 상태 (로컬 상태 관리) const [menuPermissions, setMenuPermissions] = useState>(new Map()); const [isInitialized, setIsInitialized] = useState(false); // allMenus가 로드되면 초기 권한 상태 설정 (한 번만) useEffect(() => { if (allMenus.length > 0 && !isInitialized) { const permissionsMap = new Map(); allMenus.forEach((menu) => { // 기존 권한이 있으면 사용, 없으면 기본값 const existingPermission = permissions.find((p) => p.menuObjid === menu.objid); // objid를 숫자로 변환하여 저장 const menuObjid = Number(menu.objid); const parentObjid = menu.parentObjid ? Number(menu.parentObjid) : 0; permissionsMap.set(menuObjid, { menuObjid, menuName: menu.menuName, menuPath: menu.menuUrl, parentObjid, companyCode: menu.companyCode, createYn: existingPermission?.createYn || "N", readYn: existingPermission?.readYn || "N", updateYn: existingPermission?.updateYn || "N", deleteYn: existingPermission?.deleteYn || "N", }); }); setMenuPermissions(permissionsMap); setIsInitialized(true); console.log("✅ [MenuPermissionsTable] 권한 상태 초기화", { count: permissionsMap.size, }); } }, [allMenus, permissions, isInitialized]); // menuPermissions가 변경되면 부모에 전달 (초기화 이후에만) useEffect(() => { if (isInitialized && menuPermissions.size > 0) { const updatedPermissions = Array.from(menuPermissions.values()); onPermissionsChange(updatedPermissions); } }, [menuPermissions, isInitialized, onPermissionsChange]); // 메뉴 트리 구조 생성 및 필터링 const buildMenuTree = useCallback((menus: MenuPermission[]): MenuPermission[] => { const menuMap = new Map(); const rootMenus: MenuPermission[] = []; // 먼저 모든 메뉴를 Map에 저장 menus.forEach((menu) => { menuMap.set(menu.menuObjid, { ...menu, children: [] }); }); // 부모-자식 관계 구성 menuMap.forEach((menu) => { // parentObjid를 숫자로 변환하여 비교 const parentId = Number(menu.parentObjid); if (!menu.parentObjid || parentId === 0 || isNaN(parentId)) { rootMenus.push(menu); } else { const parent = menuMap.get(parentId); if (parent) { parent.children = parent.children || []; parent.children.push(menu); } else { // 부모를 찾을 수 없으면 최상위로 처리 console.warn("⚠️ 부모 메뉴를 찾을 수 없음", { menuObjid: menu.menuObjid, menuName: menu.menuName, parentObjid: menu.parentObjid, parentId, }); rootMenus.push(menu); } } }); return rootMenus; }, []); // 검색 필터링 const filterMenus = useCallback( (menus: MenuPermission[], searchText: string): MenuPermission[] => { if (!searchText.trim()) { return menus; } const search = searchText.toLowerCase(); const filtered: MenuPermission[] = []; const matchesSearch = (menu: MenuPermission): boolean => { return menu.menuName.toLowerCase().includes(search); }; const filterRecursive = (menu: MenuPermission): MenuPermission | null => { const matches = matchesSearch(menu); const filteredChildren = (menu.children || []) .map((child) => filterRecursive(child)) .filter((child): child is MenuPermission => child !== null); if (matches || filteredChildren.length > 0) { return { ...menu, children: filteredChildren, }; } return null; }; menus.forEach((menu) => { const result = filterRecursive(menu); if (result) { filtered.push(result); } }); return filtered; }, [], ); // 메뉴 트리 구조 생성 (menuPermissions에서) const menuTree = useMemo(() => { let allMenusArray = Array.from(menuPermissions.values()); // 회사 필터링 (최고 관리자 전용) if (isSuperAdmin && companyFilter !== "all") { // 특정 회사 또는 공통 메뉴만 선택한 경우 allMenusArray = allMenusArray.filter(menu => menu.companyCode === companyFilter); } // 메뉴 타입 필터링 (관리자/사용자) if (menuTypeFilter !== "all") { const targetMenuType = menuTypeFilter; // "0" (관리자) 또는 "1" (사용자) allMenusArray = allMenusArray.filter(menu => { // 백엔드에서 받은 menuType을 비교 (allMenus에서 가져와야 함) const originalMenu = allMenus.find(m => Number(m.objid) === menu.menuObjid); return originalMenu && String(originalMenu.menuType) === targetMenuType; }); } console.log("🌲 [MenuTree] 트리 생성 시작", { allMenusCount: allMenusArray.length, searchText, menuTypeFilter, sampleMenu: allMenusArray[0], }); const tree = buildMenuTree(allMenusArray); console.log("🌲 [MenuTree] 빌드 완료", { rootMenusCount: tree.length, rootMenus: tree.slice(0, 5).map(m => ({ objid: m.menuObjid, name: m.menuName, parentObjid: m.parentObjid })), }); const filtered = filterMenus(tree, searchText); console.log("🌲 [MenuTree] 필터링 완료", { filteredCount: filtered.length, filtered: filtered.slice(0, 5).map(m => ({ objid: m.menuObjid, name: m.menuName })), }); return filtered; }, [menuPermissions, searchText, menuTypeFilter, allMenus, isSuperAdmin, companyFilter, buildMenuTree, filterMenus]); // 통계 계산 const statistics = useMemo(() => { let totalMenus = 0; let menusWithPermissions = 0; menuPermissions.forEach((menu) => { totalMenus++; if (menu.createYn === "Y" || menu.readYn === "Y" || menu.updateYn === "Y" || menu.deleteYn === "Y") { menusWithPermissions++; } }); return { totalMenus, menusWithPermissions }; }, [menuPermissions]); // 전체 펼치기/접기 const expandAll = useCallback(() => { const allIds = new Set(); const collectIds = (menu: MenuPermission) => { if (menu.children && menu.children.length > 0) { allIds.add(menu.menuObjid); menu.children.forEach(collectIds); } }; menuTree.forEach(collectIds); setExpandedMenus(allIds); }, [menuTree]); const collapseAll = useCallback(() => { setExpandedMenus(new Set()); }, []); // 메뉴 펼치기/접기 토글 const toggleExpand = useCallback((menuObjid: number) => { setExpandedMenus((prev) => { const newSet = new Set(prev); if (newSet.has(menuObjid)) { newSet.delete(menuObjid); } else { newSet.add(menuObjid); } return newSet; }); }, []); // 하위 메뉴 ID 수집 const collectChildIds = useCallback((menu: MenuPermission): number[] => { const ids = [menu.menuObjid]; if (menu.children) { menu.children.forEach((child) => { ids.push(...collectChildIds(child)); }); } return ids; }, []); // 권한 변경 핸들러 (하위 메뉴 포함) const handlePermissionChange = useCallback( (menuObjid: number, permission: "createYn" | "readYn" | "updateYn" | "deleteYn", checked: boolean, applyToChildren = true) => { setMenuPermissions((prev) => { const newMap = new Map(prev); const menuPerm = newMap.get(menuObjid); if (!menuPerm) return newMap; // 현재 메뉴 권한 변경 newMap.set(menuObjid, { ...menuPerm, [permission]: checked ? "Y" : "N", }); // 하위 메뉴에도 적용 (옵션) if (applyToChildren && menuPerm.children && menuPerm.children.length > 0) { const childIds = collectChildIds(menuPerm); childIds.forEach((childId) => { const childPerm = newMap.get(childId); if (childPerm) { newMap.set(childId, { ...childPerm, [permission]: checked ? "Y" : "N", }); } }); } return newMap; }); console.log("✅ 권한 변경:", { menuObjid, permission, checked, applyToChildren }); }, [collectChildIds], ); // 전체 선택/해제 const handleSelectAll = useCallback( (permission: "createYn" | "readYn" | "updateYn" | "deleteYn", checked: boolean) => { setMenuPermissions((prev) => { const newMap = new Map(prev); newMap.forEach((menuPerm, menuObjid) => { newMap.set(menuObjid, { ...menuPerm, [permission]: checked ? "Y" : "N", }); }); return newMap; }); console.log("✅ 전체 선택:", { permission, checked }); }, [], ); // 필터링된 메뉴 ID 수집 (재귀) const collectFilteredMenuIds = useCallback((menus: MenuPermission[]): Set => { const ids = new Set(); const traverse = (menu: MenuPermission) => { ids.add(menu.menuObjid); if (menu.children) { menu.children.forEach(traverse); } }; menus.forEach(traverse); return ids; }, []); // 빠른 권한 설정 프리셋 (필터링된 메뉴만) const applyPreset = useCallback((preset: "read-only" | "full" | "none") => { // 현재 필터링된 메뉴 ID 수집 const filteredIds = collectFilteredMenuIds(menuTree); console.log("🎯 프리셋 적용 대상:", { preset, filteredCount: filteredIds.size, filteredIds: Array.from(filteredIds), }); setMenuPermissions((prev) => { const newMap = new Map(prev); // 필터링된 메뉴만 권한 변경 filteredIds.forEach((menuObjid) => { const menuPerm = newMap.get(menuObjid); if (!menuPerm) return; switch (preset) { case "read-only": newMap.set(menuObjid, { ...menuPerm, createYn: "N", readYn: "Y", updateYn: "N", deleteYn: "N", }); break; case "full": newMap.set(menuObjid, { ...menuPerm, createYn: "Y", readYn: "Y", updateYn: "Y", deleteYn: "Y", }); break; case "none": newMap.set(menuObjid, { ...menuPerm, createYn: "N", readYn: "N", updateYn: "N", deleteYn: "N", }); break; } }); return newMap; }); console.log("✅ 프리셋 적용 완료:", { preset, count: filteredIds.size }); }, [menuTree, collectFilteredMenuIds]); // 회사 코드에서 회사명 가져오기 const getCompanyLabel = useCallback((code: string) => { if (code === "*") { return "공통"; } // 현재 권한그룹의 회사 코드와 일치하면 회사명 표시 if (companyInfo && code === companyInfo.code) { return companyInfo.name; } // 그 외에는 회사 코드 표시 return code; }, [companyInfo]); // 메뉴 행 렌더링 const renderMenuRow = (menu: MenuPermission, level: number = 0) => { const hasChildren = menu.children && menu.children.length > 0; const isExpanded = expandedMenus.has(menu.menuObjid); const paddingLeft = level * 24; const hasAnyPermission = menu.createYn === "Y" || menu.readYn === "Y" || menu.updateYn === "Y" || menu.deleteYn === "Y"; return ( {/* 메뉴명 */}
{hasChildren && ( )} {!hasChildren &&
} {menu.menuName} {menu.companyCode && ( {getCompanyLabel(menu.companyCode)} )}
{/* 생성(Create) */}
handlePermissionChange(menu.menuObjid, "createYn", checked as boolean)} />
{/* 조회(Read) */}
handlePermissionChange(menu.menuObjid, "readYn", checked as boolean)} />
{/* 수정(Update) */}
handlePermissionChange(menu.menuObjid, "updateYn", checked as boolean)} />
{/* 삭제(Delete) */}
handlePermissionChange(menu.menuObjid, "deleteYn", checked as boolean)} />
{/* 자식 메뉴 렌더링 */} {hasChildren && isExpanded && menu.children!.map((child) => renderMenuRow(child, level + 1))} ); }; return (
{/* 필터 영역 (통합) */}
{/* 왼쪽: 필터들 */}
{/* 최고 관리자 전용: 회사 필터 */} {isSuperAdmin && ( <>
회사:
{/* 구분선 */}
)} {/* 메뉴 타입 필터 (모든 사용자) */}
타입:
{/* 오른쪽: 메뉴 개수 */}
({statistics.totalMenus}개 메뉴)
{/* 통계 및 빠른 액션 */}
전체: {statistics.totalMenus}
권한 있음: {statistics.menusWithPermissions}
{/* 검색 및 트리 제어 */}
setSearchText(e.target.value)} className="h-9 pl-10 text-sm" />
{/* 안내 메시지 */} {searchText && menuTree.length === 0 && (

"{searchText}"에 대한 검색 결과가 없습니다.

)} {!searchText && menuTree.length === 0 && (

메뉴 데이터가 없습니다.

)} {/* 데스크톱 테이블 */} {menuTree.length > 0 && (
메뉴
생성 (C) handleSelectAll("createYn", checked as boolean)} className="data-[state=checked]:bg-green-600" />
조회 (R) handleSelectAll("readYn", checked as boolean)} className="data-[state=checked]:bg-blue-600" />
수정 (U) handleSelectAll("updateYn", checked as boolean)} className="data-[state=checked]:bg-amber-600" />
삭제 (D) handleSelectAll("deleteYn", checked as boolean)} className="data-[state=checked]:bg-red-600" />
{isLoading ? (

메뉴 권한 로딩 중...

) : ( menuTree.map((menu) => renderMenuRow(menu)) )}
)} {/* 모바일 카드 뷰 */} {menuTree.length > 0 && (
{menuTree.map((menu) => { const hasAnyPermission = menu.createYn === "Y" || menu.readYn === "Y" || menu.updateYn === "Y" || menu.deleteYn === "Y"; return (

{menu.menuName}

{menu.companyCode && ( {getCompanyLabel(menu.companyCode)} )}
생성 (C) handlePermissionChange(menu.menuObjid, "createYn", checked as boolean)} className="data-[state=checked]:bg-green-600" />
조회 (R) handlePermissionChange(menu.menuObjid, "readYn", checked as boolean)} className="data-[state=checked]:bg-blue-600" />
수정 (U) handlePermissionChange(menu.menuObjid, "updateYn", checked as boolean)} className="data-[state=checked]:bg-amber-600" />
삭제 (D) handlePermissionChange(menu.menuObjid, "deleteYn", checked as boolean)} className="data-[state=checked]:bg-red-600" />
); })}
)}
); }