ERP-node/frontend/components/admin/MenuPermissionsTable.tsx

370 lines
13 KiB
TypeScript

"use client";
import React, { useState, useCallback, useEffect } 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 { Search, ChevronRight, ChevronDown } from "lucide-react";
import { RoleGroup, roleAPI } from "@/lib/api/role";
import { useAuth } from "@/hooks/useAuth";
interface MenuPermission {
menuObjid: number;
menuName: string;
menuPath?: string;
parentObjid?: number;
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<Set<number>>(new Set());
const [allMenus, setAllMenus] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
// 최고 관리자 여부 확인
const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN";
// 전체 메뉴 목록 로드
useEffect(() => {
// currentUser가 로드될 때까지 대기
if (!currentUser) {
console.log("⏳ [MenuPermissionsTable] currentUser 로드 대기 중...");
return;
}
const loadAllMenus = async () => {
// 최고 관리자: companyCode 없이 모든 메뉴 조회
// 회사 관리자: 자기 회사 메뉴만 조회
const targetCompanyCode = isSuperAdmin ? undefined : roleGroup.companyCode;
console.log("🔍 [MenuPermissionsTable] 전체 메뉴 로드 시작", {
currentUser: {
userId: currentUser.userId,
companyCode: currentUser.companyCode,
userType: currentUser.userType,
},
isSuperAdmin,
roleGroupCompanyCode: roleGroup.companyCode,
targetCompanyCode: 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]);
// 메뉴 권한 상태 (로컬 상태 관리)
const [menuPermissions, setMenuPermissions] = useState<Map<number, MenuPermission>>(new Map());
const [isInitialized, setIsInitialized] = useState(false);
// allMenus가 로드되면 초기 권한 상태 설정 (한 번만)
useEffect(() => {
if (allMenus.length > 0 && !isInitialized) {
const permissionsMap = new Map<number, MenuPermission>();
allMenus.forEach((menu) => {
// 기존 권한이 있으면 사용, 없으면 기본값
const existingPermission = permissions.find((p) => p.menuObjid === menu.objid);
permissionsMap.set(menu.objid, {
menuObjid: menu.objid,
menuName: menu.menuName,
menuPath: menu.menuUrl,
parentObjid: menu.parentObjid,
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]);
// 메뉴 트리 구조 생성 (menuPermissions에서)
const menuTree: MenuPermission[] = Array.from(menuPermissions.values());
// 메뉴 펼치기/접기 토글
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;
});
}, []);
// 권한 변경 핸들러
const handlePermissionChange = useCallback(
(menuObjid: number, permission: "createYn" | "readYn" | "updateYn" | "deleteYn", checked: boolean) => {
setMenuPermissions((prev) => {
const newMap = new Map(prev);
const menuPerm = newMap.get(menuObjid);
if (menuPerm) {
newMap.set(menuObjid, {
...menuPerm,
[permission]: checked ? "Y" : "N",
});
}
return newMap;
});
console.log("✅ 권한 변경:", { menuObjid, permission, checked });
},
[],
);
// 전체 선택/해제
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 });
},
[],
);
// 메뉴 행 렌더링
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;
return (
<React.Fragment key={menu.menuObjid}>
<TableRow className="hover:bg-muted/50 transition-colors">
{/* 메뉴명 */}
<TableCell className="h-16 text-sm" style={{ paddingLeft: `${paddingLeft + 16}px` }}>
<div className="flex items-center gap-2">
{hasChildren && (
<button onClick={() => toggleExpand(menu.menuObjid)} className="transition-transform">
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</button>
)}
<span className={`text-sm ${hasChildren ? "font-semibold" : "font-medium"}`}>{menu.menuName}</span>
</div>
</TableCell>
{/* 생성(Create) */}
<TableCell className="h-16 text-center text-sm">
<div className="flex justify-center">
<Checkbox
checked={menu.createYn === "Y"}
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "createYn", checked as boolean)}
/>
</div>
</TableCell>
{/* 조회(Read) */}
<TableCell className="h-16 text-center text-sm">
<div className="flex justify-center">
<Checkbox
checked={menu.readYn === "Y"}
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "readYn", checked as boolean)}
/>
</div>
</TableCell>
{/* 수정(Update) */}
<TableCell className="h-16 text-center text-sm">
<div className="flex justify-center">
<Checkbox
checked={menu.updateYn === "Y"}
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "updateYn", checked as boolean)}
/>
</div>
</TableCell>
{/* 삭제(Delete) */}
<TableCell className="h-16 text-center text-sm">
<div className="flex justify-center">
<Checkbox
checked={menu.deleteYn === "Y"}
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "deleteYn", checked as boolean)}
/>
</div>
</TableCell>
</TableRow>
{/* 자식 메뉴 렌더링 */}
{hasChildren && isExpanded && menu.children!.map((child) => renderMenuRow(child, level + 1))}
</React.Fragment>
);
};
return (
<div className="space-y-4">
{/* 검색 */}
<div className="flex items-center gap-4">
<div className="relative flex-1 sm:max-w-[400px]">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
placeholder="메뉴 검색..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="h-10 pl-10 text-sm"
/>
</div>
</div>
{/* 데스크톱 테이블 */}
<div className="bg-card hidden shadow-sm lg:block">
<Table>
<TableHeader>
<TableRow className="bg-muted/50 hover:bg-muted/50">
<TableHead className="h-12 w-[40%] text-sm font-semibold"></TableHead>
<TableHead className="h-12 w-[15%] text-center text-sm font-semibold">
<div className="flex flex-col items-center gap-1">
<span> (C)</span>
<Checkbox
onCheckedChange={(checked) => handleSelectAll("createYn", checked as boolean)}
className="mt-1"
/>
</div>
</TableHead>
<TableHead className="h-12 w-[15%] text-center text-sm font-semibold">
<div className="flex flex-col items-center gap-1">
<span> (R)</span>
<Checkbox
onCheckedChange={(checked) => handleSelectAll("readYn", checked as boolean)}
className="mt-1"
/>
</div>
</TableHead>
<TableHead className="h-12 w-[15%] text-center text-sm font-semibold">
<div className="flex flex-col items-center gap-1">
<span> (U)</span>
<Checkbox
onCheckedChange={(checked) => handleSelectAll("updateYn", checked as boolean)}
className="mt-1"
/>
</div>
</TableHead>
<TableHead className="h-12 w-[15%] text-center text-sm font-semibold">
<div className="flex flex-col items-center gap-1">
<span> (D)</span>
<Checkbox
onCheckedChange={(checked) => handleSelectAll("deleteYn", checked as boolean)}
className="mt-1"
/>
</div>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>{menuTree.map((menu) => renderMenuRow(menu))}</TableBody>
</Table>
</div>
{/* 모바일 카드 뷰 */}
<div className="grid gap-4 lg:hidden">
{menuTree.map((menu) => (
<div key={menu.menuObjid} className="bg-card p-4 shadow-sm">
<h3 className="mb-3 text-base font-semibold">{menu.menuName}</h3>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground"> (C)</span>
<Checkbox
checked={menu.createYn === "Y"}
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "createYn", checked as boolean)}
/>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground"> (R)</span>
<Checkbox
checked={menu.readYn === "Y"}
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "readYn", checked as boolean)}
/>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground"> (U)</span>
<Checkbox
checked={menu.updateYn === "Y"}
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "updateYn", checked as boolean)}
/>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground"> (D)</span>
<Checkbox
checked={menu.deleteYn === "Y"}
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "deleteYn", checked as boolean)}
/>
</div>
</div>
</div>
))}
</div>
</div>
);
}