From 5b6313f16f508882a0ea67716b7dbaa1c6967f04 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 30 Jun 2025 08:28:13 +0000 Subject: (대표님) 20250630 16시 - 유저 도메인별 라우터 분리와 보안성검토 대응 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/users/access-control/assign-domain-dialog.tsx | 253 ++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 lib/users/access-control/assign-domain-dialog.tsx (limited to 'lib/users/access-control/assign-domain-dialog.tsx') 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("") + const [isLoading, setIsLoading] = React.useState(false) + + // 도메인별 사용자 그룹핑 + const usersByDomain = React.useMemo(() => { + const groups: Record = {} + 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 ( + + + + + + + + + + 사용자 도메인 할당 + + + 선택된 {users.length}명의 사용자에게 도메인을 할당합니다. + + + +
+ {/* 현재 사용자 도메인 분포 */} +
+

현재 도메인 분포

+
+ {Object.entries(usersByDomain).map(([domain, domainUsers]) => { + const domainInfo = domainOptions.find(opt => opt.value === domain) + return ( + + {domainInfo?.icon || "⚪"} + {domainInfo?.label || domain} ({domainUsers.length}명) + + ) + })} +
+
+ + + + {/* 사용자 목록 */} +
+

대상 사용자

+ +
+ {users.map((user, index) => ( +
+
+ {user.name} + ({user.email}) +
+ + {domainOptions.find(opt => opt.value === user.domain)?.label || user.domain} + +
+ ))} +
+
+
+ + + + {/* 도메인 선택 */} +
+

할당할 도메인

+ +
+ + {/* 선택된 도메인 미리보기 */} + {selectedDomainInfo && ( +
+
+ + 선택된 도메인 +
+
+ {selectedDomainInfo.icon} +
+
{selectedDomainInfo.label}
+
+ {selectedDomainInfo.description} +
+
+
+
+ )} +
+ + + + + +
+
+ ) +} \ No newline at end of file -- cgit v1.2.3