diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-29 13:31:40 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-29 13:31:40 +0000 |
| commit | 4614210aa9878922cfa1e424ce677ef893a1b6b2 (patch) | |
| tree | 5e7edcce05fbee207230af0a43ed08cd351d7c4f /components/permissions/role-selector.tsx | |
| parent | e41e3af4e72870d44a94b03e0f3246d6ccaaca48 (diff) | |
(대표님) 구매 권한설정, data room 등
Diffstat (limited to 'components/permissions/role-selector.tsx')
| -rw-r--r-- | components/permissions/role-selector.tsx | 227 |
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 |
