diff options
Diffstat (limited to 'lib/users/access-control/assign-domain-dialog.tsx')
| -rw-r--r-- | lib/users/access-control/assign-domain-dialog.tsx | 253 |
1 files changed, 253 insertions, 0 deletions
diff --git a/lib/users/access-control/assign-domain-dialog.tsx b/lib/users/access-control/assign-domain-dialog.tsx new file mode 100644 index 00000000..fda06b28 --- /dev/null +++ b/lib/users/access-control/assign-domain-dialog.tsx @@ -0,0 +1,253 @@ +// components/assign-domain-dialog.tsx +"use client" + +import * as React from "react" +import { User } from "@/db/schema/users" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" +import { Users, Loader2, CheckCircle } from "lucide-react" +import { toast } from "sonner" +import { assignUsersDomain } from "../service" + +interface AssignDomainDialogProps { + users: User[] +} + +// 도메인 옵션 정의 +const domainOptions = [ + { + value: "pending", + label: "승인 대기", + description: "신규 사용자 (기본 메뉴만)", + color: "yellow", + icon: "🟡" + }, + { + value: "evcp", + label: "전체 시스템", + description: "모든 메뉴 접근 (관리자급)", + color: "blue", + icon: "🔵" + }, + { + value: "procurement", + label: "구매관리팀", + description: "구매, 협력업체, 계약 관리", + color: "green", + icon: "🟢" + }, + { + value: "sales", + label: "기술영업팀", + description: "기술영업, 견적, 프로젝트 관리", + color: "purple", + icon: "🟣" + }, + { + value: "engineering", + label: "설계관리팀", + description: "설계, 기술평가, 문서 관리", + color: "orange", + icon: "🟠" + }, + { + value: "partners", + label: "협력업체", + description: "외부 협력업체용 기능", + color: "indigo", + icon: "🟦" + } +] + +export function AssignDomainDialog({ users }: AssignDomainDialogProps) { + const [open, setOpen] = React.useState(false) + const [selectedDomain, setSelectedDomain] = React.useState<string>("") + const [isLoading, setIsLoading] = React.useState(false) + + // 도메인별 사용자 그룹핑 + const usersByDomain = React.useMemo(() => { + const groups: Record<string, User[]> = {} + users.forEach(user => { + const domain = user.domain || "pending" + if (!groups[domain]) { + groups[domain] = [] + } + groups[domain].push(user) + }) + return groups + }, [users]) + + const handleAssign = async () => { + if (!selectedDomain) { + toast.error("도메인을 선택해주세요.") + return + } + + setIsLoading(true) + try { + const userIds = users.map(user => user.id) + const result = await assignUsersDomain(userIds, selectedDomain as any) + + if (result.success) { + toast.success(`${users.length}명의 사용자에게 ${selectedDomain} 도메인이 할당되었습니다.`) + setOpen(false) + setSelectedDomain("") + // 테이블 새로고침을 위해 router.refresh() 또는 revalidation 필요 + window.location.reload() // 간단한 방법 + } else { + toast.error(result.message || "도메인 할당 중 오류가 발생했습니다.") + } + } catch (error) { + console.error("도메인 할당 오류:", error) + toast.error("도메인 할당 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + const selectedDomainInfo = domainOptions.find(option => option.value === selectedDomain) + + return ( + <Dialog open={open} onOpenChange={setOpen}> + <DialogTrigger asChild> + <Button variant="outline" size="sm" className="gap-2"> + <Users className="size-4" /> + 도메인 할당 ({users.length}명) + </Button> + </DialogTrigger> + + <DialogContent className="max-w-2xl"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <Users className="size-5" /> + 사용자 도메인 할당 + </DialogTitle> + <DialogDescription> + 선택된 {users.length}명의 사용자에게 도메인을 할당합니다. + </DialogDescription> + </DialogHeader> + + <div className="space-y-6"> + {/* 현재 사용자 도메인 분포 */} + <div> + <h4 className="text-sm font-medium mb-3">현재 도메인 분포</h4> + <div className="flex flex-wrap gap-2"> + {Object.entries(usersByDomain).map(([domain, domainUsers]) => { + const domainInfo = domainOptions.find(opt => opt.value === domain) + return ( + <Badge key={domain} variant="outline" className="gap-1"> + <span>{domainInfo?.icon || "⚪"}</span> + {domainInfo?.label || domain} ({domainUsers.length}명) + </Badge> + ) + })} + </div> + </div> + + <Separator /> + + {/* 사용자 목록 */} + <div> + <h4 className="text-sm font-medium mb-3">대상 사용자</h4> + <ScrollArea className="h-32 w-full border rounded-md p-3"> + <div className="space-y-2"> + {users.map((user, index) => ( + <div key={user.id} className="flex items-center justify-between text-sm"> + <div> + <span className="font-medium">{user.name}</span> + <span className="text-muted-foreground ml-2">({user.email})</span> + </div> + <Badge variant="secondary" className="text-xs"> + {domainOptions.find(opt => opt.value === user.domain)?.label || user.domain} + </Badge> + </div> + ))} + </div> + </ScrollArea> + </div> + + <Separator /> + + {/* 도메인 선택 */} + <div> + <h4 className="text-sm font-medium mb-3">할당할 도메인</h4> + <Select value={selectedDomain} onValueChange={setSelectedDomain}> + <SelectTrigger> + <SelectValue placeholder="도메인을 선택하세요" /> + </SelectTrigger> + <SelectContent> + {domainOptions.map((option) => ( + <SelectItem key={option.value} value={option.value}> + <div className="flex items-start gap-3 py-1"> + <span className="text-lg">{option.icon}</span> + <div className="flex-1"> + <div className="font-medium">{option.label}</div> + <div className="text-xs text-muted-foreground"> + {option.description} + </div> + </div> + </div> + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 선택된 도메인 미리보기 */} + {selectedDomainInfo && ( + <div className="bg-gray-50 rounded-lg p-4"> + <div className="flex items-center gap-2 mb-2"> + <CheckCircle className="size-4 text-green-600" /> + <span className="font-medium">선택된 도메인</span> + </div> + <div className="flex items-center gap-3"> + <span className="text-2xl">{selectedDomainInfo.icon}</span> + <div> + <div className="font-medium">{selectedDomainInfo.label}</div> + <div className="text-sm text-muted-foreground"> + {selectedDomainInfo.description} + </div> + </div> + </div> + </div> + )} + </div> + + <DialogFooter className="gap-2"> + <Button + variant="outline" + onClick={() => setOpen(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button + onClick={handleAssign} + disabled={!selectedDomain || isLoading} + > + {isLoading && <Loader2 className="size-4 mr-2 animate-spin" />} + {users.length}명에게 도메인 할당 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
