336 lines
12 KiB
TypeScript
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}
|
||
|
|
/>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|