import * as React from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Check, ChevronsUpDown, Loader, UserRoundPlus, AlertTriangle, Users, UserMinus } from "lucide-react" import { cn } from "@/lib/utils" import { toast } from "sonner" import { Alert, AlertDescription } from "@/components/ui/alert" import { Badge } from "@/components/ui/badge" import { Textarea } from "@/components/ui/textarea" import { Company } from "@/db/schema/companies" import { getAllCompanies } from "@/lib/admin-users/service" import { Popover, PopoverTrigger, PopoverContent, } from "@/components/ui/popover" import { Command, CommandInput, CommandList, CommandGroup, CommandItem, CommandEmpty, } from "@/components/ui/command" import { assignRolesToUsers, getAllRoleView, checkMultipleRegularEvaluationRolesAssigned } from "@/lib/roles/services" import { Role, RoleView } from "@/db/schema/users" import { type UserView } from "@/db/schema/users" import { type Row } from "@tanstack/react-table" import { createRoleAssignmentSchema, CreateRoleAssignmentSchema, createRoleSchema, CreateRoleSchema } from "@/lib/roles/validations" import { MultiSelect } from "@/components/ui/multi-select" interface AssignRoleDialogProps extends React.ComponentPropsWithoutRef { users: Row["original"][] roles: RoleView[] } // 역할 상태 타입 정의 type RoleAssignmentStatus = 'all' | 'some' | 'none' interface RoleAnalysis { roleId: string roleName: string status: RoleAssignmentStatus assignedUserCount: number totalUserCount: number } export function AssignRoleDialog({ users, roles }: AssignRoleDialogProps) { const [open, setOpen] = React.useState(false) const [isAddPending, startAddTransition] = React.useTransition() const [loading, setLoading] = React.useState(false) const [regularEvaluationAssigned, setRegularEvaluationAssigned] = React.useState<{[roleId: string]: boolean}>({}) const [isCheckingRegularEvaluation, setIsCheckingRegularEvaluation] = React.useState(false) // 메모이제이션된 필터링된 역할들 const partnersRoles = React.useMemo(() => roles.filter(v => v.domain === "partners"), [roles]) const evcpRoles = React.useMemo(() => roles.filter(v => v.domain === "evcp"), [roles]) // 메모이제이션된 evcp 사용자들 const evcpUsers = React.useMemo(() => users.filter(v => v.user_domain === "evcp"), [users]) // 선택된 사용자들의 역할 분석 const roleAnalysis = React.useMemo((): RoleAnalysis[] => { if (evcpUsers.length === 0) return [] const analysis = evcpRoles.map(role => { const assignedUsers = evcpUsers.filter(user => user.roles && user.roles.includes(role.name) ) const assignedUserCount = assignedUsers.length const totalUserCount = evcpUsers.length let status: RoleAssignmentStatus if (assignedUserCount === totalUserCount) { status = 'all' } else if (assignedUserCount > 0) { status = 'some' } else { status = 'none' } return { roleId: String(role.id), roleName: role.name, status, assignedUserCount, totalUserCount } }) console.log('Role analysis:', analysis) return analysis }, [evcpUsers, evcpRoles]) // 초기 선택된 역할들 (모든 사용자에게 할당된 역할들 + 일부에게 할당된 역할들) const initialSelectedRoles = React.useMemo(() => { const selected = roleAnalysis .filter(analysis => analysis.status === 'all' || analysis.status === 'some') .map(analysis => analysis.roleId) console.log('Initial selected roles:', selected) return selected }, [roleAnalysis]) const form = useForm({ resolver: zodResolver(createRoleAssignmentSchema), defaultValues: { evcpRoles: [], }, }) const handleDialogOpenChange = React.useCallback((nextOpen: boolean) => { if (!nextOpen) { // 다이얼로그가 닫힐 때 리셋 form.reset({ evcpRoles: [], }) setRegularEvaluationAssigned({}) } setOpen(nextOpen) }, [form]) // 선택된 evcpRoles 감시 - 메모이제이션 const selectedEvcpRoles = form.watch("evcpRoles") const memoizedSelectedEvcpRoles = React.useMemo(() => selectedEvcpRoles || [], [selectedEvcpRoles]) // 정기평가 role들 찾기 - 의존성 수정 const selectedRegularEvaluationRoles = React.useMemo(() => { return memoizedSelectedEvcpRoles.filter(roleId => { const role = evcpRoles.find(r => String(r.id) === roleId) return role && role.name.includes("정기평가") }) }, [memoizedSelectedEvcpRoles, evcpRoles]) // 정기평가 role 할당 상태 체크 (debounced) React.useEffect(() => { if (selectedRegularEvaluationRoles.length === 0) { setRegularEvaluationAssigned({}) return } const timeoutId = setTimeout(async () => { setIsCheckingRegularEvaluation(true) try { const roleIds = selectedRegularEvaluationRoles.map(roleId => Number(roleId)) const assignmentStatus = await checkMultipleRegularEvaluationRolesAssigned(roleIds) const stringKeyStatus: {[roleId: string]: boolean} = {} Object.entries(assignmentStatus).forEach(([roleId, isAssigned]) => { stringKeyStatus[roleId] = isAssigned }) setRegularEvaluationAssigned(stringKeyStatus) } catch (error) { console.error("정기평가 role 할당 상태 체크 실패:", error) toast.error("정기평가 role 상태 확인에 실패했습니다") } finally { setIsCheckingRegularEvaluation(false) } }, 500) return () => clearTimeout(timeoutId) }, [selectedRegularEvaluationRoles]) // 할당 불가능한 정기평가 role 확인 const blockedRegularEvaluationRoles = React.useMemo(() => { return selectedRegularEvaluationRoles.filter(roleId => regularEvaluationAssigned[roleId] === true ) }, [selectedRegularEvaluationRoles, regularEvaluationAssigned]) // 제출 가능 여부 const canSubmit = React.useMemo(() => blockedRegularEvaluationRoles.length === 0, [blockedRegularEvaluationRoles]) // MultiSelect options 메모이제이션 - 상태 정보와 함께 표시 const multiSelectOptions = React.useMemo(() => { return evcpRoles.map((role) => { const analysis = roleAnalysis.find(a => a.roleId === String(role.id)) let statusSuffix = '' if (analysis) { if (analysis.status === 'all') { statusSuffix = ` (모든 사용자 ${analysis.assignedUserCount}/${analysis.totalUserCount})` } else if (analysis.status === 'some') { statusSuffix = ` (일부 사용자 ${analysis.assignedUserCount}/${analysis.totalUserCount})` } } return { value: String(role.id), label: role.name + statusSuffix, disabled: role.name.includes("정기평가") && regularEvaluationAssigned[String(role.id)] === true } }) }, [evcpRoles, roleAnalysis, regularEvaluationAssigned]) const onSubmit = React.useCallback(async (data: CreateRoleAssignmentSchema) => { startAddTransition(async () => { if (evcpUsers.length === 0) return try { const selectedRoleIds = data.evcpRoles.map(v => Number(v)) const userIds = evcpUsers.map(v => v.user_id) // assignRolesToUsers는 이미 기존 관계를 삭제하고 새로 삽입하므로 // 최종 선택된 역할들만 전달하면 됩니다 const result = await assignRolesToUsers(selectedRoleIds, userIds) if (result.error) { toast.error(`역할 업데이트 실패: ${result.error}`) return } form.reset() setOpen(false) setRegularEvaluationAssigned({}) // 변경사항 계산해서 피드백 const initialRoleIds = initialSelectedRoles.map(v => Number(v)) const addedRoles = selectedRoleIds.filter(roleId => !initialRoleIds.includes(roleId)) const removedRoles = initialRoleIds.filter(roleId => !selectedRoleIds.includes(roleId)) if (addedRoles.length > 0 && removedRoles.length > 0) { toast.success(`역할이 성공적으로 업데이트되었습니다 (추가: ${addedRoles.length}, 제거: ${removedRoles.length})`) } else if (addedRoles.length > 0) { toast.success(`${addedRoles.length}개 역할이 성공적으로 추가되었습니다`) } else if (removedRoles.length > 0) { toast.success(`${removedRoles.length}개 역할이 성공적으로 제거되었습니다`) } else { toast.info("변경사항이 없습니다") } } catch (error) { console.error("역할 업데이트 실패:", error) toast.error("역할 업데이트에 실패했습니다") } }) }, [evcpUsers, form, initialSelectedRoles]) // 정기평가 role 관련 경고 메시지 생성 const regularEvaluationWarning = React.useMemo(() => { if (selectedRegularEvaluationRoles.length === 0) return null if (isCheckingRegularEvaluation) { return ( 정기평가 role 할당 상태를 확인하고 있습니다... ) } if (blockedRegularEvaluationRoles.length > 0) { const blockedRoleNames = blockedRegularEvaluationRoles.map(roleId => { const role = evcpRoles.find(r => String(r.id) === roleId) return role?.name || roleId }) return ( 할당 불가: 다음 정기평가 role이 이미 다른 유저에게 할당되어 있습니다:
{blockedRoleNames.join(", ")}
정기평가 role은 한 명의 유저에게만 할당할 수 있습니다.
) } if (selectedRegularEvaluationRoles.length > 0) { const availableRoleNames = selectedRegularEvaluationRoles.map(roleId => { const role = evcpRoles.find(r => String(r.id) === roleId) return role?.name || roleId }) return ( 정기평가 role을 할당할 수 있습니다: {availableRoleNames.join(", ")} ) } return null }, [ selectedRegularEvaluationRoles, isCheckingRegularEvaluation, blockedRegularEvaluationRoles, evcpRoles ]) // 현재 역할 상태 요약 const roleStatusSummary = React.useMemo(() => { const allRoles = roleAnalysis.filter(r => r.status === 'all').length const someRoles = roleAnalysis.filter(r => r.status === 'some').length const totalRoles = roleAnalysis.length return { allRoles, someRoles, totalRoles } }, [roleAnalysis]) return ( {evcpUsers.length}명 사용자의 역할 편집
선택된 사용자들의 역할을 편집할 수 있습니다. 기존 역할 상태가 표시됩니다.
공통 역할: {roleStatusSummary.allRoles}개 일부 역할: {roleStatusSummary.someRoles}개 전체 역할: {roleStatusSummary.totalRoles}개
{/* evcp 롤 선택 */} {evcpUsers.length > 0 && ( ( eVCP 역할 선택 (체크: 할당됨, 해제: 제거됨) { console.log('MultiSelect value changed:', values) field.onChange(values) }} defaultValue={initialSelectedRoles} /> {/* 역할 상태 설명 */}
모든 사용자: 선택된 모든 사용자에게 할당된 역할
일부 사용자: 일부 사용자에게만 할당된 역할
• 역할을 체크하면 모든 사용자에게 할당되고, 해제하면 모든 사용자에서 제거됩니다
{/* 정기평가 관련 경고 메시지 */} {regularEvaluationWarning && (
{regularEvaluationWarning}
)}
)} /> )}
) }