Feat: 멤버 관리 70% 구현
This commit is contained in:
parent
1f53a9321b
commit
889cf872d0
@ -1,25 +1,29 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form';
|
||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '../ui/form';
|
||||
import { Input } from '../ui/input';
|
||||
import { Button } from '../ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||
import { ProjectMemberResponse } from '@/types';
|
||||
import { useUpdateProjectMemberPrivilege } from '@/hooks/useProjectHooks';
|
||||
|
||||
type Role = 'admin' | 'editor' | 'viewer';
|
||||
type Role = 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER';
|
||||
|
||||
const roles: Role[] = ['admin', 'editor', 'viewer'];
|
||||
const roles: Role[] = ['ADMIN', 'MANAGER', 'EDITOR', 'VIEWER'];
|
||||
|
||||
const roleToStr: { [key in Role]: string } = {
|
||||
admin: '관리자',
|
||||
editor: '에디터',
|
||||
viewer: '뷰어',
|
||||
ADMIN: '관리자',
|
||||
MANAGER: '매니저',
|
||||
EDITOR: '에디터',
|
||||
VIEWER: '뷰어',
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
members: z.array(
|
||||
z.object({
|
||||
email: z.string().email({ message: '올바른 이메일 형식을 입력해주세요.' }),
|
||||
memberId: z.number(),
|
||||
nickname: z.string().nonempty('닉네임을 입력하세요.'),
|
||||
role: z.enum(roles as [Role, ...Role[]], { errorMap: () => ({ message: '역할을 선택해주세요.' }) }),
|
||||
})
|
||||
),
|
||||
@ -27,113 +31,95 @@ const formSchema = z.object({
|
||||
|
||||
export type MemberManageFormValues = z.infer<typeof formSchema>;
|
||||
|
||||
interface Member {
|
||||
email: string;
|
||||
role: Role;
|
||||
}
|
||||
|
||||
interface AdminMemberManageFormProps {
|
||||
members: Member[];
|
||||
onSubmit: (data: MemberManageFormValues) => void;
|
||||
members: ProjectMemberResponse[];
|
||||
}
|
||||
|
||||
export default function AdminMemberManageForm({ members, onSubmit }: AdminMemberManageFormProps) {
|
||||
export default function AdminMemberManageForm({ members }: AdminMemberManageFormProps) {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const { mutate: updatePrivilege } = useUpdateProjectMemberPrivilege();
|
||||
|
||||
const form = useForm<MemberManageFormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { members },
|
||||
defaultValues: {
|
||||
members: members.map((m) => ({
|
||||
memberId: m.memberId,
|
||||
nickname: m.nickname,
|
||||
role: m.privilegeType as Role,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
const groupedMembers = members.reduce<{ [key: string]: { email: string; role: Role }[] }>((acc, member) => {
|
||||
if (!acc[member.role]) acc[member.role] = [];
|
||||
acc[member.role].push(member);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const roleOrder: Role[] = ['admin', 'editor', 'viewer'];
|
||||
|
||||
const sortedGroupedMembers = Object.entries(groupedMembers).sort(
|
||||
([roleA], [roleB]) => roleOrder.indexOf(roleA as Role) - roleOrder.indexOf(roleB as Role)
|
||||
);
|
||||
const handleRoleChange = (memberId: number, role: Role) => {
|
||||
updatePrivilege({
|
||||
projectId: Number(projectId),
|
||||
memberId,
|
||||
privilegeData: {
|
||||
memberId,
|
||||
privilegeType: role,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{sortedGroupedMembers.map(([role, groupMembers]) => {
|
||||
if (!groupMembers || groupMembers.length === 0) return null;
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{members.map((member, index) => (
|
||||
<div
|
||||
key={member.memberId}
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
<FormField
|
||||
name={`members.${index}.nickname`}
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="닉네임을 입력하세요."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
return (
|
||||
<div
|
||||
key={role}
|
||||
className="flex flex-col gap-3"
|
||||
>
|
||||
<FormLabel className="text-sm font-semibold text-[#333238]">{roleToStr[role as Role]}</FormLabel>
|
||||
{groupMembers.map((member, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
<FormField
|
||||
name={`members.${members.findIndex((m) => m.email === member.email)}.email`}
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="email@example.com"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
name={`members.${members.findIndex((m) => m.email === member.email)}.role`}
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="역할을 선택해주세요." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles.map((role) => (
|
||||
<SelectItem
|
||||
key={role}
|
||||
value={role}
|
||||
>
|
||||
{roleToStr[role]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outlinePrimary"
|
||||
disabled={!form.formState.isValid}
|
||||
>
|
||||
역할 설정
|
||||
</Button>
|
||||
</form>
|
||||
<FormField
|
||||
name={`members.${index}.role`}
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
handleRoleChange(member.memberId, value as Role);
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="역할을 선택해주세요." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles.map((role) => (
|
||||
<SelectItem
|
||||
key={role}
|
||||
value={role}
|
||||
>
|
||||
{roleToStr[role]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
@ -1,70 +1,45 @@
|
||||
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select';
|
||||
import AdminMemberManageForm, { MemberManageFormValues } from './AdminMemberManageForm';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useState } from 'react';
|
||||
import AdminMemberManageForm from './AdminMemberManageForm';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useProjectMembersQuery from '@/queries/useProjectMembersQuery';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import { useAddProjectMember } from '@/hooks/useProjectHooks';
|
||||
import MemberAddModal from '../MemberAddModal';
|
||||
import { MemberAddFormValues } from '../MemberAddModal/MemberAddForm';
|
||||
|
||||
type Role = 'admin' | 'editor' | 'viewer';
|
||||
export default function AdminMemberManage() {
|
||||
const { projectId } = useParams<{ workspaceId: string; projectId: string }>();
|
||||
const profile = useAuthStore((state) => state.profile);
|
||||
const memberId = profile?.id || 0;
|
||||
|
||||
interface Member {
|
||||
email: string;
|
||||
role: Role;
|
||||
}
|
||||
const { data: members = [] } = useProjectMembersQuery(Number(projectId), memberId);
|
||||
const addProjectMember = useAddProjectMember();
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
const [, setInviteModalOpen] = useState(false);
|
||||
|
||||
const handleMemberInvite = (data: MemberAddFormValues) => {
|
||||
addProjectMember.mutate({
|
||||
projectId: Number(projectId),
|
||||
memberId: memberId,
|
||||
newMember: {
|
||||
// Todo : 멤버 id로 수정하는 로직 수정해야한다.
|
||||
// memberId: data.email,
|
||||
memberId: 0,
|
||||
privilegeType: data.role,
|
||||
},
|
||||
});
|
||||
console.log('Invited:', data);
|
||||
setInviteModalOpen(false);
|
||||
};
|
||||
|
||||
export default function AdminMemberManage({
|
||||
title = '멤버 관리',
|
||||
projects = [
|
||||
{ id: 'project-1', name: '프로젝트 A' },
|
||||
{ id: 'project-2', name: '프로젝트 B' },
|
||||
],
|
||||
onProjectChange = (projectId: string) => console.log('Selected Project:', projectId),
|
||||
onSubmit = (data: MemberManageFormValues) => console.log('Submitted:', data),
|
||||
members = [
|
||||
{ email: 'admin1@example.com', role: 'admin' },
|
||||
{ email: 'viewer2@example.com', role: 'viewer' },
|
||||
],
|
||||
onMemberInvite = () => console.log('Invite member'),
|
||||
}: {
|
||||
title?: string;
|
||||
projects?: Project[];
|
||||
onProjectChange?: (projectId: string) => void;
|
||||
onSubmit?: (data: MemberManageFormValues) => void;
|
||||
members?: Member[];
|
||||
onMemberInvite?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-6 border-b-[0.67px] border-[#dcdcde] bg-[#fbfafd] p-6">
|
||||
<header className="flex w-full items-center gap-4">
|
||||
<h1 className="flex-1 text-lg font-semibold text-[#333238]">{title}</h1>
|
||||
<Button
|
||||
variant="outlinePrimary"
|
||||
onClick={onMemberInvite}
|
||||
>
|
||||
멤버 초대하기
|
||||
</Button>
|
||||
<Select onValueChange={onProjectChange}>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="프로젝트 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projects.map((project) => (
|
||||
<SelectItem
|
||||
key={project.id}
|
||||
value={project.id}
|
||||
>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<h1 className="flex-1 text-lg font-semibold text-[#333238]">멤버 관리</h1>
|
||||
<MemberAddModal onSubmit={handleMemberInvite} />
|
||||
</header>
|
||||
<AdminMemberManageForm
|
||||
onSubmit={onSubmit}
|
||||
members={members}
|
||||
/>
|
||||
|
||||
<AdminMemberManageForm members={members} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -6,14 +6,15 @@ import { Input } from '../ui/input';
|
||||
import { Button } from '../ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||
|
||||
type Role = 'admin' | 'editor' | 'viewer';
|
||||
type PrivilegeType = 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER';
|
||||
|
||||
const roles: Role[] = ['admin', 'editor', 'viewer'];
|
||||
const privilegeTypes: readonly ['ADMIN', 'MANAGER', 'EDITOR', 'VIEWER'] = ['ADMIN', 'MANAGER', 'EDITOR', 'VIEWER'];
|
||||
|
||||
const roleToStr: { [key in Role]: string } = {
|
||||
admin: '관리자',
|
||||
editor: '에디터',
|
||||
viewer: '뷰어',
|
||||
const privilegeTypeToStr: { [key in PrivilegeType]: string } = {
|
||||
ADMIN: '관리자',
|
||||
MANAGER: '매니저',
|
||||
EDITOR: '에디터',
|
||||
VIEWER: '뷰어',
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
@ -26,7 +27,7 @@ const formSchema = z.object({
|
||||
.min(1, {
|
||||
message: '초대할 멤버의 이메일 주소를 입력해주세요.',
|
||||
}),
|
||||
role: z.enum(['admin', 'editor', 'viewer']),
|
||||
role: z.enum(privilegeTypes),
|
||||
});
|
||||
|
||||
export type MemberAddFormValues = z.infer<typeof formSchema>;
|
||||
@ -80,12 +81,12 @@ export default function MemberAddForm({ onSubmit }: { onSubmit: (data: MemberAdd
|
||||
<SelectValue placeholder="초대할 멤버의 역할을 선택해주세요." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles.map((role) => (
|
||||
{privilegeTypes.map((role) => (
|
||||
<SelectItem
|
||||
key={role}
|
||||
value={role}
|
||||
>
|
||||
{roleToStr[role]}
|
||||
{privilegeTypeToStr[role]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
@ -1,27 +1,44 @@
|
||||
import React from 'react';
|
||||
import MemberAddForm, { MemberAddFormValues } from './MemberAddForm';
|
||||
import XIcon from '@/assets/icons/x.svg?react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
export default function MemberAddModal({
|
||||
title = '새 멤버 초대',
|
||||
onClose,
|
||||
onSubmit,
|
||||
}: {
|
||||
title?: string;
|
||||
onClose: () => void;
|
||||
interface MemberAddModalProps {
|
||||
onSubmit: (data: MemberAddFormValues) => void;
|
||||
}) {
|
||||
buttonClass?: string;
|
||||
}
|
||||
|
||||
export default function MemberAddModal({ onSubmit, buttonClass = '' }: MemberAddModalProps) {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
const handleOpen = () => setIsOpen(true);
|
||||
const handleClose = () => setIsOpen(false);
|
||||
|
||||
return (
|
||||
<div className="flex w-[610px] flex-col gap-10 rounded-3xl border px-10 py-5 shadow-lg">
|
||||
<header className="flex gap-5">
|
||||
<h1 className="small-title w-full">{title}</h1>
|
||||
<button
|
||||
className="flex h-8 w-8 items-center justify-center"
|
||||
onClick={onClose}
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outlinePrimary"
|
||||
className={`${buttonClass}`}
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<XIcon className="stroke-gray-900" />
|
||||
</button>
|
||||
</header>
|
||||
<MemberAddForm onSubmit={onSubmit} />
|
||||
</div>
|
||||
<Plus size={16} />
|
||||
<span>멤버 초대하기</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader title="새 멤버 초대" />
|
||||
<MemberAddForm
|
||||
onSubmit={(data: MemberAddFormValues) => {
|
||||
onSubmit(data);
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
@ -71,7 +71,11 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: `${webPath.admin()}/:workspaceId/project/:projectId?`,
|
||||
element: <AdminLayout />,
|
||||
element: (
|
||||
<Suspense fallback={<div></div>}>
|
||||
<AdminLayout />
|
||||
</Suspense>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
|
Loading…
Reference in New Issue
Block a user