Feat : admin 페이지 부분 가완성

This commit is contained in:
정현조 2024-09-20 08:21:56 +09:00
parent cd4427fd74
commit 62cb367727
4 changed files with 63 additions and 167 deletions

View File

@ -1,12 +1,13 @@
import { useParams } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Form, FormControl, FormField, FormItem, FormMessage } from '../ui/form'; import { Form, FormControl, FormField, FormItem, FormMessage } from '../ui/form';
import { Input } from '../ui/input'; import { Input } from '../ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { ProjectMemberResponse } from '@/types';
import useUpdateProjectMemberPrivilegeQuery from '@/queries/projects/useUpdateProjectMemberPrivilegeQuery'; import useUpdateProjectMemberPrivilegeQuery from '@/queries/projects/useUpdateProjectMemberPrivilegeQuery';
import useProjectMembersQuery from '@/queries/projects/useProjectMembersQuery';
import useAuthStore from '@/stores/useAuthStore';
type Role = 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER'; type Role = 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER';
@ -31,12 +32,14 @@ const formSchema = z.object({
export type ProjectMemberManageFormValues = z.infer<typeof formSchema>; export type ProjectMemberManageFormValues = z.infer<typeof formSchema>;
interface ProjectMemberManageFormProps { export default function ProjectMemberManageForm() {
members: ProjectMemberResponse[]; const location = useLocation();
} const searchParams = new URLSearchParams(location.search);
const projectId = searchParams.get('projectId');
const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0;
export default function ProjectMemberManageForm({ members }: ProjectMemberManageFormProps) { const { data: members = [] } = useProjectMembersQuery(Number(projectId), memberId);
const { projectId } = useParams<{ projectId: string }>();
const { mutate: updatePrivilege } = useUpdateProjectMemberPrivilegeQuery(); const { mutate: updatePrivilege } = useUpdateProjectMemberPrivilegeQuery();
const form = useForm<ProjectMemberManageFormValues>({ const form = useForm<ProjectMemberManageFormValues>({

View File

@ -1,125 +1,30 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useForm } from 'react-hook-form'; import useWorkspaceMembersQuery from '@/queries/workspaces/useWorkspaceMembersQuery';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { Form, FormControl, FormField, FormItem, FormMessage } from '../ui/form';
import { Input } from '../ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { WorkspaceMemberResponse } from '@/types';
import useUpdateProjectMemberPrivilegeQuery from '@/queries/projects/useUpdateProjectMemberPrivilegeQuery';
type Role = 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER'; export default function WorkspaceMemberManageForm() {
const roles: Role[] = ['ADMIN', 'MANAGER', 'EDITOR', 'VIEWER'];
const roleToStr: { [key in Role]: string } = {
ADMIN: '관리자',
MANAGER: '매니저',
EDITOR: '에디터',
VIEWER: '뷰어',
};
const formSchema = z.object({
members: z.array(
z.object({
memberId: z.number(),
nickname: z.string().nonempty('닉네임을 입력하세요.'),
role: z.enum(roles as [Role, ...Role[]], { errorMap: () => ({ message: '역할을 선택해주세요.' }) }),
})
),
});
export type WorkspaceMemberManageFormValues = z.infer<typeof formSchema>;
interface WorkspaceMemberManageFormProps {
members: WorkspaceMemberResponse[];
}
export default function WorkspaceMemberManageForm({ members }: WorkspaceMemberManageFormProps) {
const { workspaceId } = useParams<{ workspaceId: string }>(); const { workspaceId } = useParams<{ workspaceId: string }>();
const { mutate: updatePrivilege } = useUpdateProjectMemberPrivilegeQuery();
const form = useForm<WorkspaceMemberManageFormValues>({ const { data: members = [] } = useWorkspaceMembersQuery(Number(workspaceId));
resolver: zodResolver(formSchema),
defaultValues: {
members: members.map((m) => ({
memberId: m.memberId,
nickname: m.nickname,
role: m.privilegeType as Role,
})),
},
});
const handleRoleChange = (memberId: number, role: Role) => {
updatePrivilege({
workspaceId: Number(workspaceId),
memberId,
privilegeData: {
memberId,
privilegeType: role,
},
});
};
return ( return (
<Form {...form}> <div className="flex w-full flex-col gap-4">
<div className="flex w-full flex-col gap-4"> {members.length === 0 ? (
{members.map((member, index) => ( <div className="py-4 text-center"> .</div>
) : (
members.map((member) => (
<div <div
key={member.memberId} key={member.memberId}
className="flex items-center gap-4" className="flex items-center gap-4 border-b pb-2"
> >
<FormField <img
name={`members.${index}.nickname`} src={member.profileImage}
control={form.control} alt={member.nickname}
render={({ field }) => ( className="h-8 w-8 rounded-full"
<FormItem className="flex-1">
<FormControl>
<Input
placeholder="닉네임을 입력하세요."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<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>
)}
/> />
<span>{member.nickname}</span>
</div> </div>
))} ))
</div> )}
</Form> </div>
); );
} }

View File

@ -1,35 +1,35 @@
import { useState } from 'react'; import { useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams, useLocation } from 'react-router-dom';
import useAuthStore from '@/stores/useAuthStore'; import useAuthStore from '@/stores/useAuthStore';
import useAddWorkspaceMemberQuery from '@/queries/workspaces/useAddWorkspaceMemberQuery'; import useAddWorkspaceMemberQuery from '@/queries/workspaces/useAddWorkspaceMemberQuery';
import useAddProjectMemberQuery from '@/queries/projects/useAddProjectMemberQuery'; import useAddProjectMemberQuery from '@/queries/projects/useAddProjectMemberQuery';
import useWorkspaceMembersQuery from '@/queries/workspaces/useWorkspaceMembersQuery';
import useProjectMembersQuery from '@/queries/projects/useProjectMembersQuery';
import MemberAddModal from '../MemberAddModal'; import MemberAddModal from '../MemberAddModal';
import { MemberAddFormValues } from '../MemberAddModal/MemberAddForm'; import { MemberAddFormValues } from '../MemberAddModal/MemberAddForm';
import WorkspaceMemberManageForm from './WorkspaceMemberManageForm'; import WorkspaceMemberManageForm from './WorkspaceMemberManageForm';
import ProjectMemberManageForm from './ProjectMemberManageForm'; import ProjectMemberManageForm from './ProjectMemberManageForm';
export default function AdminMemberManage() { export default function AdminMemberManage() {
const { workspaceId, projectId } = useParams<{ workspaceId?: string; projectId?: string }>(); const { workspaceId } = useParams<{ workspaceId: string }>();
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
const projectId = searchParams.get('projectId');
const profile = useAuthStore((state) => state.profile); const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0; const memberId = profile?.id || 0;
const addWorkspaceMember = useAddWorkspaceMemberQuery();
const addProjectMember = useAddProjectMemberQuery();
const [, setInviteModalOpen] = useState(false); const [, setInviteModalOpen] = useState(false);
const handleMemberInvite = (data: MemberAddFormValues) => { const handleMemberInvite = (data: MemberAddFormValues) => {
if (workspaceId) { if (workspaceId) {
const addWorkspaceMember = useAddWorkspaceMemberQuery();
addWorkspaceMember.mutate({ addWorkspaceMember.mutate({
workspaceId: Number(workspaceId), workspaceId: Number(workspaceId),
memberId: memberId, memberId: memberId,
newMember: { newMemberId: data.memberId,
memberId: 0,
privilegeType: data.role,
},
}); });
} else if (projectId) { } else if (projectId && Number(projectId) > 0) {
const addProjectMember = useAddProjectMemberQuery();
addProjectMember.mutate({ addProjectMember.mutate({
projectId: Number(projectId), projectId: Number(projectId),
memberId: memberId, memberId: memberId,
@ -49,10 +49,8 @@ export default function AdminMemberManage() {
<MemberAddModal onSubmit={handleMemberInvite} /> <MemberAddModal onSubmit={handleMemberInvite} />
</header> </header>
{workspaceId && <WorkspaceMemberManageForm members={useWorkspaceMembersQuery(Number(workspaceId)).data || []} />} {workspaceId && <WorkspaceMemberManageForm />}
{projectId && ( {projectId && <ProjectMemberManageForm />}
<ProjectMemberManageForm members={useProjectMembersQuery(Number(projectId), memberId).data || []} />
)}
</div> </div>
); );
} }

View File

@ -20,22 +20,14 @@ const privilegeTypeToStr: { [key in PrivilegeType]: string } = {
}; };
const formSchema = z.object({ const formSchema = z.object({
email: z memberId: z.number().nonnegative({ message: '멤버를 선택하세요.' }),
.string()
.email({
message: '올바른 이메일 형식을 입력해주세요.',
})
.max(40)
.min(1, {
message: '초대할 멤버의 이메일 주소를 입력해주세요.',
}),
role: z.enum(privilegeTypes), role: z.enum(privilegeTypes),
}); });
export type MemberAddFormValues = z.infer<typeof formSchema>; export type MemberAddFormValues = z.infer<typeof formSchema>;
const defaultValues: Partial<MemberAddFormValues> = { const defaultValues: Partial<MemberAddFormValues> = {
email: '', memberId: 0,
role: undefined, role: undefined,
}; };
@ -48,43 +40,40 @@ export default function MemberAddForm({ onSubmit }: { onSubmit: (data: MemberAdd
const [keyword, setKeyword] = useState(''); const [keyword, setKeyword] = useState('');
const { data: members } = useSearchMembersByEmailQuery(keyword); const { data: members } = useSearchMembersByEmailQuery(keyword);
const handleMemberSelect = (memberId: number) => {
form.setValue('memberId', memberId);
};
return ( return (
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-5" className="flex flex-col gap-5"
> >
<FormField <FormItem>
name="email" <FormLabel className="body-strong"></FormLabel>
control={form.control} <FormControl>
render={({ field }) => ( <SearchInput
<FormItem> placeholder="초대할 멤버의 이메일을 검색하세요."
<FormLabel className="body-strong"></FormLabel> value={keyword}
<FormControl> onChange={(e) => setKeyword(e.target.value)}
<SearchInput />
placeholder="초대할 멤버의 이메일을 검색하세요." </FormControl>
value={field.value} <FormMessage />
onChange={(e) => { </FormItem>
field.onChange(e);
setKeyword(e.target.value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{members && ( {members && (
<ul className="mt-2"> <ul className="mt-2">
{members.map((member) => ( {members.map((member) => (
<li <li
key={member.id} key={member.id}
className="py-1" className={`cursor-pointer py-1 ${form.getValues('memberId') === member.id ? 'bg-gray-200' : ''}`}
onClick={() => handleMemberSelect(member.id)}
> >
<img <img
src={member.profileImage} src={member.profileImage}
alt={member.nickname} alt={member.nickname}
className="h-8 w-8 rounded-full" className="inline h-8 w-8 rounded-full"
/> />
<span className="ml-2"> <span className="ml-2">
{member.nickname} ({member.email}) {member.nickname} ({member.email})
@ -124,10 +113,11 @@ export default function MemberAddForm({ onSubmit }: { onSubmit: (data: MemberAdd
</FormItem> </FormItem>
)} )}
/> />
<Button <Button
type="submit" type="submit"
variant="outlinePrimary" variant="outlinePrimary"
disabled={!form.formState.isValid} disabled={!form.formState.isValid || !form.getValues('memberId')}
> >
</Button> </Button>