From 4eb7532f822c821fb6b69bf103bd075fefba769b Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 15 Jul 2025 10:07:09 +0000 Subject: (대표님) 20250715 협력사 정기평가, spreadJS, roles 서비스에 함수 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/users/table/assign-roles-dialog.tsx | 353 +++++++++++++++++++++++++++----- 1 file changed, 303 insertions(+), 50 deletions(-) (limited to 'lib/users/table/assign-roles-dialog.tsx') diff --git a/lib/users/table/assign-roles-dialog.tsx b/lib/users/table/assign-roles-dialog.tsx index 003f6500..7bc7e138 100644 --- a/lib/users/table/assign-roles-dialog.tsx +++ b/lib/users/table/assign-roles-dialog.tsx @@ -21,9 +21,11 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" -import { Check, ChevronsUpDown, Loader, UserRoundPlus } from "lucide-react" +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" @@ -41,8 +43,8 @@ import { CommandItem, CommandEmpty, } from "@/components/ui/command" -import { assignRolesToUsers, getAllRoleView } from "@/lib/roles/services" -import { RoleView } from "@/db/schema/users" +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" @@ -51,26 +53,81 @@ 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 }: AssignRoleDialogProps) { +export function AssignRoleDialog({ users, roles }: AssignRoleDialogProps) { const [open, setOpen] = React.useState(false) const [isAddPending, startAddTransition] = React.useTransition() - const [roles, setRoles] = React.useState([]) // 회사 목록 const [loading, setLoading] = React.useState(false) + const [regularEvaluationAssigned, setRegularEvaluationAssigned] = React.useState<{[roleId: string]: boolean}>({}) + const [isCheckingRegularEvaluation, setIsCheckingRegularEvaluation] = React.useState(false) - const partnersRoles = roles.filter(v => v.domain === "partners") - const evcpRoles = roles.filter(v => v.domain === "evcp") + // 메모이제이션된 필터링된 역할들 + 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]) - React.useEffect(() => { - getAllRoleView("evcp").then((res) => { - setRoles(res) + // 선택된 사용자들의 역할 분석 + 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), @@ -79,89 +136,280 @@ export function AssignRoleDialog({ users }: AssignRoleDialogProps) { }, }) - - function handleDialogOpenChange(nextOpen: boolean) { + const handleDialogOpenChange = React.useCallback((nextOpen: boolean) => { if (!nextOpen) { - form.reset() + // 다이얼로그가 닫힐 때 리셋 + form.reset({ + evcpRoles: [], + }) + setRegularEvaluationAssigned({}) } setOpen(nextOpen) - } + }, [form]) - const evcpUsers = users.filter(v => v.user_domain === "evcp"); + // 선택된 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]) - async function onSubmit(data: CreateRoleAssignmentSchema) { - console.log(data.evcpRoles.map((v)=>Number(v))) - startAddTransition(async () => { + // 정기평가 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]) - // if(partnerUsers.length>0){ - // const result = await assignRolesToUsers( partnerUsers.map(v=>v.user_id) ,data.partnersRoles) + // 할당 불가능한 정기평가 role 확인 + const blockedRegularEvaluationRoles = React.useMemo(() => { + return selectedRegularEvaluationRoles.filter(roleId => + regularEvaluationAssigned[roleId] === true + ) + }, [selectedRegularEvaluationRoles, regularEvaluationAssigned]) - // if (result.error) { - // toast.error(`에러: ${result.error}`) - // return - // } - // } + // 제출 가능 여부 + const canSubmit = React.useMemo(() => + blockedRegularEvaluationRoles.length === 0, [blockedRegularEvaluationRoles]) - if (evcpUsers.length > 0) { - const result = await assignRolesToUsers( data.evcpRoles.map((v)=>Number(v)), evcpUsers.map(v => v.user_id)) + // 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}`) + toast.error(`역할 업데이트 실패: ${result.error}`) return } - } - form.reset() - setOpen(false) - toast.success("Role assgined") + 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 ( - + - Assign Roles to {evcpUsers.length} Users - - Role을 Multi-select 하시기 바랍니다. + + + {evcpUsers.length}명 사용자의 역할 편집 + + +
선택된 사용자들의 역할을 편집할 수 있습니다. 기존 역할 상태가 표시됩니다.
+
+ + 공통 역할: {roleStatusSummary.allRoles}개 + + + 일부 역할: {roleStatusSummary.someRoles}개 + + + 전체 역할: {roleStatusSummary.totalRoles}개 + +
-
{/* evcp 롤 선택 */} - {evcpUsers.length > 0 && + {evcpUsers.length > 0 && ( ( - eVCP Role + + eVCP 역할 선택 + + (체크: 할당됨, 해제: 제거됨) + + ({ value: String(role.id), label: role.name }))} + key={`multiselect-${open}-${initialSelectedRoles.join(',')}`} + options={multiSelectOptions} onValueChange={(values) => { - field.onChange(values); + console.log('MultiSelect value changed:', values) + field.onChange(values) }} - + defaultValue={initialSelectedRoles} /> + + {/* 역할 상태 설명 */} +
+
모든 사용자: 선택된 모든 사용자에게 할당된 역할
+
일부 사용자: 일부 사용자에게만 할당된 역할
+
• 역할을 체크하면 모든 사용자에게 할당되고, 해제하면 모든 사용자에서 제거됩니다
+
+ + {/* 정기평가 관련 경고 메시지 */} + {regularEvaluationWarning && ( +
+ {regularEvaluationWarning} +
+ )}
)} /> - } + )}
@@ -171,11 +419,16 @@ export function AssignRoleDialog({ users }: AssignRoleDialogProps) { onClick={() => setOpen(false)} disabled={isAddPending} > - Cancel + 취소
-- cgit v1.2.3