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

336 lines
12 KiB
TypeScript

"use client";
import React, { useState, useCallback, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Plus, Edit, Trash2, Users, Menu, Filter, X } from "lucide-react";
import { roleAPI, RoleGroup } from "@/lib/api/role";
import { useAuth } from "@/hooks/useAuth";
import { AlertCircle } from "lucide-react";
import { RoleFormModal } from "./RoleFormModal";
import { RoleDeleteModal } from "./RoleDeleteModal";
import { useRouter } from "next/navigation";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { companyAPI } from "@/lib/api/company";
/**
* 권한 그룹 관리 메인 컴포넌트
*
* 기능:
* - 권한 그룹 목록 조회 (회사별)
* - 권한 그룹 생성/수정/삭제
* - 상세 페이지로 이동 (멤버 관리 + 메뉴 권한 설정)
*/
export function RoleManagement() {
const { user: currentUser } = useAuth();
const router = useRouter();
// 회사 관리자 또는 최고 관리자 여부
const isAdmin =
(currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN") ||
currentUser?.userType === "COMPANY_ADMIN";
const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN";
// 상태 관리
const [roleGroups, setRoleGroups] = useState<RoleGroup[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 회사 필터 (최고 관리자 전용)
const [companies, setCompanies] = useState<Array<{ company_code: string; company_name: string }>>([]);
const [selectedCompany, setSelectedCompany] = useState<string>("all");
// 모달 상태
const [formModal, setFormModal] = useState({
isOpen: false,
editingRole: null as RoleGroup | null,
});
const [deleteModal, setDeleteModal] = useState({
isOpen: false,
role: null as RoleGroup | null,
});
// 회사 목록 로드 (최고 관리자만)
const loadCompanies = useCallback(async () => {
if (!isSuperAdmin) return;
try {
const companies = await companyAPI.getList();
setCompanies(companies);
} catch (error) {
console.error("회사 목록 로드 오류:", error);
}
}, [isSuperAdmin]);
// 데이터 로드
const loadRoleGroups = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
// 최고 관리자: selectedCompany에 따라 필터링 (all이면 전체 조회)
// 회사 관리자: 자기 회사만 조회
const companyFilter =
isSuperAdmin && selectedCompany !== "all"
? selectedCompany
: isSuperAdmin
? undefined
: currentUser?.companyCode;
console.log("권한 그룹 목록 조회:", { isSuperAdmin, selectedCompany, companyFilter });
const response = await roleAPI.getList({
companyCode: companyFilter,
});
if (response.success && response.data) {
setRoleGroups(response.data);
console.log("권한 그룹 조회 성공:", response.data.length, "개");
} else {
setError(response.message || "권한 그룹 목록을 불러오는데 실패했습니다.");
}
} catch (err) {
console.error("권한 그룹 목록 로드 오류:", err);
setError("권한 그룹 목록을 불러오는 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
}, [isSuperAdmin, selectedCompany, currentUser?.companyCode]);
useEffect(() => {
if (isAdmin) {
if (isSuperAdmin) {
loadCompanies(); // 최고 관리자는 회사 목록 먼저 로드
}
loadRoleGroups();
} else {
setIsLoading(false);
}
}, [isAdmin, isSuperAdmin, loadRoleGroups, loadCompanies]);
// 권한 그룹 생성 핸들러
const handleCreateRole = useCallback(() => {
setFormModal({ isOpen: true, editingRole: null });
}, []);
// 권한 그룹 수정 핸들러
const handleEditRole = useCallback((role: RoleGroup) => {
setFormModal({ isOpen: true, editingRole: role });
}, []);
// 권한 그룹 삭제 핸들러
const handleDeleteRole = useCallback((role: RoleGroup) => {
setDeleteModal({ isOpen: true, role });
}, []);
// 폼 모달 닫기
const handleFormModalClose = useCallback(() => {
setFormModal({ isOpen: false, editingRole: null });
}, []);
// 삭제 모달 닫기
const handleDeleteModalClose = useCallback(() => {
setDeleteModal({ isOpen: false, role: null });
}, []);
// 모달 성공 후 새로고침
const handleModalSuccess = useCallback(() => {
loadRoleGroups();
}, [loadRoleGroups]);
// 상세 페이지로 이동
const handleViewDetail = useCallback(
(role: RoleGroup) => {
router.push(`/admin/roles/${role.objid}`);
},
[router],
);
// 관리자가 아니면 접근 제한
if (!isAdmin) {
return (
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border p-6 shadow-sm">
<AlertCircle className="text-destructive mb-4 h-12 w-12" />
<h3 className="mb-2 text-lg font-semibold"> </h3>
<p className="text-muted-foreground mb-4 text-center text-sm">
.
</p>
<Button variant="outline" onClick={() => window.history.back()}>
</Button>
</div>
);
}
return (
<>
{/* 에러 메시지 */}
{error && (
<div className="border-destructive/50 bg-destructive/10 rounded-lg border p-4">
<div className="flex items-center justify-between">
<p className="text-destructive text-sm font-semibold"> </p>
<button
onClick={() => setError(null)}
className="text-destructive hover:text-destructive/80 transition-colors"
aria-label="에러 메시지 닫기"
>
</button>
</div>
<p className="text-destructive/80 mt-1.5 text-sm">{error}</p>
</div>
)}
{/* 액션 버튼 영역 */}
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="flex items-center gap-4">
<h2 className="text-xl font-semibold"> ({roleGroups.length})</h2>
{/* 최고 관리자 전용: 회사 필터 */}
{isSuperAdmin && (
<div className="flex items-center gap-2">
<Filter className="text-muted-foreground h-4 w-4" />
<Select value={selectedCompany} onValueChange={(value) => setSelectedCompany(value)}>
<SelectTrigger className="h-10 w-[200px]">
<SelectValue placeholder="회사 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{companies.map((company) => (
<SelectItem key={company.company_code} value={company.company_code}>
{company.company_name}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedCompany !== "all" && (
<Button variant="ghost" size="sm" onClick={() => setSelectedCompany("all")} className="h-8 w-8 p-0">
<X className="h-4 w-4" />
</Button>
)}
</div>
)}
</div>
<Button onClick={handleCreateRole} className="gap-2">
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 권한 그룹 목록 */}
{isLoading ? (
<div className="bg-card rounded-lg border p-12 shadow-sm">
<div className="flex flex-col items-center justify-center gap-4">
<div className="border-primary h-8 w-8 animate-spin rounded-full border-4 border-t-transparent"></div>
<p className="text-muted-foreground text-sm"> ...</p>
</div>
</div>
) : roleGroups.length === 0 ? (
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border shadow-sm">
<div className="flex flex-col items-center gap-2 text-center">
<p className="text-muted-foreground text-sm"> .</p>
<p className="text-muted-foreground text-xs"> .</p>
</div>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{roleGroups.map((role) => (
<div key={role.objid} className="bg-card rounded-lg border shadow-sm transition-colors">
{/* 헤더 (클릭 시 상세 페이지) */}
<div
className="hover:bg-muted/50 cursor-pointer p-4 transition-colors"
onClick={() => handleViewDetail(role)}
>
<div className="mb-4 flex items-start justify-between">
<div className="flex-1">
<h3 className="text-base font-semibold">{role.authName}</h3>
<p className="text-muted-foreground mt-1 font-mono text-sm">{role.authCode}</p>
</div>
<span
className={`rounded-full px-2 py-1 text-xs font-medium ${
role.status === "active" ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"
}`}
>
{role.status === "active" ? "활성" : "비활성"}
</span>
</div>
{/* 정보 */}
<div className="space-y-2 border-t pt-4">
{/* 최고 관리자는 회사명 표시 */}
{isSuperAdmin && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium">
{companies.find((c) => c.company_code === role.companyCode)?.company_name || role.companyCode}
</span>
</div>
)}
<div className="flex justify-between text-sm">
<span className="text-muted-foreground flex items-center gap-1">
<Users className="h-3 w-3" />
</span>
<span className="font-medium">{role.memberCount || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground flex items-center gap-1">
<Menu className="h-3 w-3" />
</span>
<span className="font-medium">{role.menuCount || 0}</span>
</div>
</div>
</div>
{/* 액션 버튼 */}
<div className="flex gap-2 border-t p-3">
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleEditRole(role);
}}
className="flex-1 gap-1 text-xs"
>
<Edit className="h-3 w-3" />
</Button>
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleDeleteRole(role);
}}
className="text-destructive hover:bg-destructive hover:text-destructive-foreground gap-1 text-xs"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
))}
</div>
)}
{/* 모달들 */}
<RoleFormModal
isOpen={formModal.isOpen}
onClose={handleFormModalClose}
onSuccess={handleModalSuccess}
editingRole={formModal.editingRole}
/>
<RoleDeleteModal
isOpen={deleteModal.isOpen}
onClose={handleDeleteModalClose}
onSuccess={handleModalSuccess}
role={deleteModal.role}
/>
</>
);
}