summaryrefslogtreecommitdiff
path: root/components/permissions/role-selector.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/permissions/role-selector.tsx')
-rw-r--r--components/permissions/role-selector.tsx227
1 files changed, 227 insertions, 0 deletions
diff --git a/components/permissions/role-selector.tsx b/components/permissions/role-selector.tsx
new file mode 100644
index 00000000..5f62ca51
--- /dev/null
+++ b/components/permissions/role-selector.tsx
@@ -0,0 +1,227 @@
+// components/permissions/role-selector.tsx
+
+"use client"
+
+import * as React from "react"
+import { Check, ChevronsUpDown, Shield, Users } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
+import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command"
+import { Badge } from "@/components/ui/badge"
+import { cn } from "@/lib/utils"
+import { getRoles } from "@/lib/permissions/service"
+
+export interface Role {
+ id: number;
+ name: string;
+ domain: string;
+ description?: string;
+ userCount?: number;
+}
+
+interface RoleSelectorProps {
+ selectedRoleId?: number | null;
+ onRoleSelect: (role: Role) => void;
+ placeholder?: string;
+ domain?: string; // 특정 도메인의 역할만 필터링
+ className?: string;
+}
+
+export function RoleSelector({
+ selectedRoleId,
+ onRoleSelect,
+ placeholder = "역할 선택...",
+ domain,
+ className
+}: RoleSelectorProps) {
+ const [open, setOpen] = React.useState(false)
+ const [searchTerm, setSearchTerm] = React.useState("")
+ const [roles, setRoles] = React.useState<Role[]>([])
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [selectedRole, setSelectedRole] = React.useState<Role | null>(null)
+
+ // 역할 데이터 로드
+ React.useEffect(() => {
+ async function loadRoles() {
+ setIsLoading(true);
+ try {
+ const allRoles = await getRoles();
+
+ // domain이 지정된 경우 해당 도메인만 필터링
+ const filteredByDomain = domain
+ ? allRoles.filter((r: Role) => r.domain === domain)
+ : allRoles;
+
+ console.log(`Loaded ${filteredByDomain.length} roles${domain ? ` for domain: ${domain}` : ''}`);
+ setRoles(filteredByDomain);
+
+ // 초기 선택된 역할이 있으면 설정
+ if (selectedRoleId) {
+ const selected = filteredByDomain.find((r: Role) => r.id === selectedRoleId);
+ if (selected) {
+ setSelectedRole(selected);
+ }
+ }
+ } catch (error) {
+ console.error("역할 목록 로드 오류:", error);
+ setRoles([]);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ loadRoles();
+ }, [selectedRoleId, domain]);
+
+ // 클라이언트 측에서 검색어로 필터링
+ const filteredRoles = React.useMemo(() => {
+ if (!searchTerm.trim()) return roles;
+
+ const lowerSearch = searchTerm.toLowerCase();
+ return roles.filter(
+ role =>
+ role.name.toLowerCase().includes(lowerSearch) ||
+ role.domain.toLowerCase().includes(lowerSearch) ||
+ (role.description && role.description.toLowerCase().includes(lowerSearch))
+ );
+ }, [roles, searchTerm]);
+
+ // 역할 선택 처리
+ const handleSelectRole = (role: Role) => {
+ setSelectedRole(role);
+ onRoleSelect(role);
+ setOpen(false);
+ };
+
+ // 도메인별 색상 결정
+ const getDomainColor = (domain: string) => {
+ switch(domain?.toLowerCase()) {
+ case 'evcp':
+ return 'default';
+ case 'partners':
+ return 'secondary';
+ case 'admin':
+ return 'destructive';
+ default:
+ return 'outline';
+ }
+ };
+
+ // 도메인별 그룹화
+ const groupedRoles = React.useMemo(() => {
+ return filteredRoles.reduce((acc, role) => {
+ const key = role.domain || 'other';
+ if (!acc[key]) acc[key] = [];
+ acc[key].push(role);
+ return acc;
+ }, {} as Record<string, Role[]>);
+ }, [filteredRoles]);
+
+ return (
+ <Popover open={open} onOpenChange={setOpen}>
+ <PopoverTrigger asChild>
+ <Button
+ variant="outline"
+ role="combobox"
+ aria-expanded={open}
+ className={cn("w-full justify-between", className)}
+ disabled={isLoading}
+ >
+ {isLoading ? (
+ <span className="text-muted-foreground">역할 로딩 중...</span>
+ ) : selectedRole ? (
+ <div className="flex items-center gap-2 w-full">
+ <Shield className="h-4 w-4 text-muted-foreground" />
+ <span className="truncate">{selectedRole.name}</span>
+ <Badge
+ variant={getDomainColor(selectedRole.domain)}
+ className="ml-auto text-xs"
+ >
+ {selectedRole.domain}
+ </Badge>
+ {selectedRole.userCount !== undefined && (
+ <Badge variant="outline" className="text-xs">
+ {selectedRole.userCount}명
+ </Badge>
+ )}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">{placeholder}</span>
+ )}
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-[400px] p-0">
+ <Command>
+ <CommandInput
+ placeholder="역할명, 도메인으로 검색..."
+ onValueChange={setSearchTerm}
+ />
+ <CommandList
+ className="max-h-[300px]"
+ onWheel={(e) => {
+ e.stopPropagation();
+ const target = e.currentTarget;
+ target.scrollTop += e.deltaY;
+ }}
+ >
+ {isLoading ? (
+ <div className="py-6 text-center text-sm text-muted-foreground">
+ 역할 목록 로딩 중...
+ </div>
+ ) : Object.keys(groupedRoles).length === 0 ? (
+ <CommandEmpty>
+ {searchTerm
+ ? "검색 결과가 없습니다"
+ : domain
+ ? `${domain} 도메인에 역할이 없습니다`
+ : "역할이 없습니다"}
+ </CommandEmpty>
+ ) : (
+ Object.entries(groupedRoles).map(([groupDomain, groupRoles]) => (
+ <CommandGroup key={groupDomain} heading={groupDomain.toUpperCase()}>
+ {groupRoles.map((role) => (
+ <CommandItem
+ key={role.id}
+ value={`${role.name} ${role.domain} ${role.description || ''}`}
+ onSelect={() => handleSelectRole(role)}
+ className="cursor-pointer"
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ selectedRole?.id === role.id
+ ? "opacity-100"
+ : "opacity-0"
+ )}
+ />
+ <div className="flex items-center gap-2 flex-1">
+ <Shield className="h-4 w-4 text-muted-foreground" />
+ <div className="flex-1">
+ <div className="font-medium">{role.name}</div>
+ {role.description && (
+ <div className="text-xs text-muted-foreground truncate">
+ {role.description}
+ </div>
+ )}
+ </div>
+ <div className="flex items-center gap-1">
+ {role.userCount !== undefined && (
+ <Badge variant="outline" className="text-xs">
+ <Users className="h-3 w-3 mr-1" />
+ {role.userCount}
+ </Badge>
+ )}
+ </div>
+ </div>
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ ))
+ )}
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ );
+} \ No newline at end of file