summaryrefslogtreecommitdiff
path: root/lib/users/access-control/assign-domain-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/users/access-control/assign-domain-dialog.tsx')
-rw-r--r--lib/users/access-control/assign-domain-dialog.tsx253
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