diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-15 10:07:09 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-15 10:07:09 +0000 |
| commit | 4eb7532f822c821fb6b69bf103bd075fefba769b (patch) | |
| tree | b4bcf6c0bf791d71569f3f35498ed256bf7cfaf3 /lib/users/table | |
| parent | 660c7888d885badab7af3e96f9c16bd0172ad0f1 (diff) | |
(대표님) 20250715 협력사 정기평가, spreadJS, roles 서비스에 함수 추가
Diffstat (limited to 'lib/users/table')
| -rw-r--r-- | lib/users/table/assign-roles-dialog.tsx | 353 | ||||
| -rw-r--r-- | lib/users/table/users-table-toolbar-actions.tsx | 6 | ||||
| -rw-r--r-- | lib/users/table/users-table.tsx | 5 |
3 files changed, 311 insertions, 53 deletions
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<typeof Dialog> { users: Row<UserView>["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<RoleView[]>([]) // 회사 목록 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<CreateRoleAssignmentSchema>({ 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 ( + <Alert key="checking"> + <Loader className="h-4 w-4 animate-spin" /> + <AlertDescription> + 정기평가 role 할당 상태를 확인하고 있습니다... + </AlertDescription> + </Alert> + ) + } + + if (blockedRegularEvaluationRoles.length > 0) { + const blockedRoleNames = blockedRegularEvaluationRoles.map(roleId => { + const role = evcpRoles.find(r => String(r.id) === roleId) + return role?.name || roleId + }) + + return ( + <Alert key="blocked" variant="destructive"> + <AlertTriangle className="h-4 w-4" /> + <AlertDescription> + <strong>할당 불가:</strong> 다음 정기평가 role이 이미 다른 유저에게 할당되어 있습니다: + <br /> + <strong>{blockedRoleNames.join(", ")}</strong> + <br /> + 정기평가 role은 한 명의 유저에게만 할당할 수 있습니다. + </AlertDescription> + </Alert> + ) + } + + if (selectedRegularEvaluationRoles.length > 0) { + const availableRoleNames = selectedRegularEvaluationRoles.map(roleId => { + const role = evcpRoles.find(r => String(r.id) === roleId) + return role?.name || roleId + }) + + return ( + <Alert key="available"> + <Check className="h-4 w-4" /> + <AlertDescription> + 정기평가 role을 할당할 수 있습니다: <strong>{availableRoleNames.join(", ")}</strong> + </AlertDescription> + </Alert> + ) + } + + 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 ( <Dialog open={open} onOpenChange={handleDialogOpenChange}> <DialogTrigger asChild> <Button variant="default" size="sm"> <UserRoundPlus className="mr-2 size-4" aria-hidden="true" /> - Assign Role ({users.length}) + 역할 편집 ({users.length}명) </Button> </DialogTrigger> - <DialogContent> + <DialogContent className="max-w-2xl"> <DialogHeader> - <DialogTitle>Assign Roles to {evcpUsers.length} Users</DialogTitle> - <DialogDescription> - Role을 Multi-select 하시기 바랍니다. + <DialogTitle className="flex items-center gap-2"> + <Users className="size-5" /> + {evcpUsers.length}명 사용자의 역할 편집 + </DialogTitle> + <DialogDescription className="space-y-2"> + <div>선택된 사용자들의 역할을 편집할 수 있습니다. 기존 역할 상태가 표시됩니다.</div> + <div className="flex gap-2 text-sm"> + <Badge variant="secondary"> + 공통 역할: {roleStatusSummary.allRoles}개 + </Badge> + <Badge variant="outline"> + 일부 역할: {roleStatusSummary.someRoles}개 + </Badge> + <Badge variant="secondary"> + 전체 역할: {roleStatusSummary.totalRoles}개 + </Badge> + </div> </DialogDescription> </DialogHeader> - <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <div className="space-y-4 py-4"> {/* evcp 롤 선택 */} - {evcpUsers.length > 0 && + {evcpUsers.length > 0 && ( <FormField control={form.control} name="evcpRoles" render={({ field }) => ( <FormItem> - <FormLabel>eVCP Role</FormLabel> + <FormLabel className="flex items-center gap-2"> + eVCP 역할 선택 + <span className="text-sm text-muted-foreground"> + (체크: 할당됨, 해제: 제거됨) + </span> + </FormLabel> <FormControl> <MultiSelect - options={evcpRoles.map((role) => ({ 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} /> </FormControl> <FormMessage /> + + {/* 역할 상태 설명 */} + <div className="text-sm text-muted-foreground space-y-1"> + <div>• <strong>모든 사용자</strong>: 선택된 모든 사용자에게 할당된 역할</div> + <div>• <strong>일부 사용자</strong>: 일부 사용자에게만 할당된 역할</div> + <div>• 역할을 체크하면 모든 사용자에게 할당되고, 해제하면 모든 사용자에서 제거됩니다</div> + </div> + + {/* 정기평가 관련 경고 메시지 */} + {regularEvaluationWarning && ( + <div className="mt-2"> + {regularEvaluationWarning} + </div> + )} </FormItem> )} /> - } + )} </div> <DialogFooter> @@ -171,11 +419,16 @@ export function AssignRoleDialog({ users }: AssignRoleDialogProps) { onClick={() => setOpen(false)} disabled={isAddPending} > - Cancel + 취소 </Button> <Button type="submit" - disabled={form.formState.isSubmitting || isAddPending} + disabled={ + form.formState.isSubmitting || + isAddPending || + !canSubmit || + isCheckingRegularEvaluation + } > {isAddPending && ( <Loader @@ -183,7 +436,7 @@ export function AssignRoleDialog({ users }: AssignRoleDialogProps) { aria-hidden="true" /> )} - Assgin + 역할 업데이트 </Button> </DialogFooter> </form> diff --git a/lib/users/table/users-table-toolbar-actions.tsx b/lib/users/table/users-table-toolbar-actions.tsx index 106953a6..eef93546 100644 --- a/lib/users/table/users-table-toolbar-actions.tsx +++ b/lib/users/table/users-table-toolbar-actions.tsx @@ -10,15 +10,16 @@ import { Button } from "@/components/ui/button" -import { UserView } from "@/db/schema/users" +import { Role, RoleView, UserView } from "@/db/schema/users" import { DeleteUsersDialog } from "@/lib/admin-users/table/delete-ausers-dialog" import { AssignRoleDialog } from "./assign-roles-dialog" interface UsersTableToolbarActionsProps { table: Table<UserView> + roles: RoleView[] } -export function UsersTableToolbarActions({ table }: UsersTableToolbarActionsProps) { +export function UsersTableToolbarActions({ table, roles }: UsersTableToolbarActionsProps) { // 파일 input을 숨기고, 버튼 클릭 시 참조해 클릭하는 방식 const fileInputRef = React.useRef<HTMLInputElement>(null) @@ -36,6 +37,7 @@ export function UsersTableToolbarActions({ table }: UsersTableToolbarActionsProp users={table .getFilteredSelectedRowModel() .rows.map((row) => row.original)} + roles={roles} /> ) : null} diff --git a/lib/users/table/users-table.tsx b/lib/users/table/users-table.tsx index 53cb961e..784c1e5d 100644 --- a/lib/users/table/users-table.tsx +++ b/lib/users/table/users-table.tsx @@ -39,6 +39,9 @@ export function UserTable({ promises }: UsersTableProps) { React.use(promises) + console.log(roles,"user") + + const [rowAction, setRowAction] = React.useState<DataTableRowAction<UserView> | null>(null) @@ -139,7 +142,7 @@ export function UserTable({ promises }: UsersTableProps) { filterFields={advancedFilterFields} shallow={false} > - <UsersTableToolbarActions table={table}/> + <UsersTableToolbarActions table={table} roles={roles}/> </DataTableAdvancedToolbar> </DataTable> |
