370 lines
13 KiB
TypeScript
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>
|
|
<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>
|
|
);
|
|
}
|