Refactor: 멤버 관리 페이지 완성

This commit is contained in:
정현조 2024-09-20 09:51:54 +09:00
parent 998cb358ba
commit b6cc61dfde
5 changed files with 98 additions and 65 deletions

View File

@ -6,18 +6,20 @@ 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 useUpdateProjectMemberPrivilegeQuery from '@/queries/projects/useUpdateProjectMemberPrivilegeQuery'; import useUpdateProjectMemberPrivilegeQuery from '@/queries/projects/useUpdateProjectMemberPrivilegeQuery';
import useRemoveProjectMemberQuery from '@/queries/projects/useRemoveProjectMemberQuery';
import useProjectMembersQuery from '@/queries/projects/useProjectMembersQuery'; import useProjectMembersQuery from '@/queries/projects/useProjectMembersQuery';
import useAuthStore from '@/stores/useAuthStore'; import useAuthStore from '@/stores/useAuthStore';
type Role = 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER'; type Role = 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER' | 'NONE';
const roles: Role[] = ['ADMIN', 'MANAGER', 'EDITOR', 'VIEWER']; const roles: Role[] = ['ADMIN', 'MANAGER', 'EDITOR', 'VIEWER', 'NONE'];
const roleToStr: { [key in Role]: string } = { const roleToStr: { [key in Role]: string } = {
ADMIN: '관리자', ADMIN: '관리자',
MANAGER: '매니저', MANAGER: '매니저',
EDITOR: '에디터', EDITOR: '에디터',
VIEWER: '뷰어', VIEWER: '뷰어',
NONE: '역할 없음',
}; };
const formSchema = z.object({ const formSchema = z.object({
@ -32,42 +34,64 @@ const formSchema = z.object({
export type ProjectMemberManageFormValues = z.infer<typeof formSchema>; export type ProjectMemberManageFormValues = z.infer<typeof formSchema>;
export default function ProjectMemberManageForm() { interface ProjectMemberManageFormProps {
workspaceMembers: Array<{ memberId: number; nickname: string }>;
}
export default function ProjectMemberManageForm({ workspaceMembers }: ProjectMemberManageFormProps) {
const location = useLocation(); const location = useLocation();
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
const projectId = searchParams.get('projectId'); 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 { data: members = [] } = useProjectMembersQuery(Number(projectId), memberId); const { data: projectMembers = [] } = useProjectMembersQuery(Number(projectId), memberId);
const { mutate: updatePrivilege } = useUpdateProjectMemberPrivilegeQuery(); const { mutate: updatePrivilege } = useUpdateProjectMemberPrivilegeQuery();
const { mutate: removeMember } = useRemoveProjectMemberQuery();
const noRoleMembers = workspaceMembers.filter((wm) => !projectMembers.some((pm) => pm.memberId === wm.memberId));
const form = useForm<ProjectMemberManageFormValues>({ const form = useForm<ProjectMemberManageFormValues>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
members: members.map((m) => ({ members: [
memberId: m.memberId, ...projectMembers.map((m) => ({
nickname: m.nickname, memberId: m.memberId,
role: m.privilegeType as Role, nickname: m.nickname,
})), role: m.privilegeType as Role,
})),
...noRoleMembers.map((m) => ({
memberId: m.memberId,
nickname: m.nickname,
role: 'NONE' as Role,
})),
],
}, },
}); });
const handleRoleChange = (memberId: number, role: Role) => { const handleRoleChange = (memberId: number, role: Role) => {
updatePrivilege({ if (role === 'NONE') {
projectId: Number(projectId), removeMember({
memberId, projectId: Number(projectId),
privilegeData: { memberId: memberId,
targetMemberId: memberId,
});
} else {
updatePrivilege({
projectId: Number(projectId),
memberId, memberId,
privilegeType: role, privilegeData: {
}, memberId,
}); privilegeType: role,
},
});
}
}; };
return ( return (
<Form {...form}> <Form {...form}>
<div className="flex w-full flex-col gap-4"> <div className="flex w-full flex-col gap-4">
{members.map((member, index) => ( {form.getValues('members').map((member, index) => (
<div <div
key={member.memberId} key={member.memberId}
className="flex items-center gap-4" className="flex items-center gap-4"
@ -100,6 +124,7 @@ export default function ProjectMemberManageForm() {
handleRoleChange(member.memberId, value as Role); handleRoleChange(member.memberId, value as Role);
}} }}
defaultValue={field.value} defaultValue={field.value}
disabled={member.role === 'ADMIN'}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="역할을 선택해주세요." /> <SelectValue placeholder="역할을 선택해주세요." />

View File

@ -1,11 +1,14 @@
import { useParams } from 'react-router-dom'; interface WorkspaceMember {
import useWorkspaceMembersQuery from '@/queries/workspaces/useWorkspaceMembersQuery'; memberId: number;
nickname: string;
profileImage: string;
}
export default function WorkspaceMemberManageForm() { interface WorkspaceMemberManageFormProps {
const { workspaceId } = useParams<{ workspaceId: string }>(); members: WorkspaceMember[];
}
const { data: members = [] } = useWorkspaceMembersQuery(Number(workspaceId));
export default function WorkspaceMemberManageForm({ members }: WorkspaceMemberManageFormProps) {
return ( return (
<div className="flex w-full flex-col gap-4"> <div className="flex w-full flex-col gap-4">
{members.length === 0 ? ( {members.length === 0 ? (

View File

@ -3,6 +3,7 @@ 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 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';
@ -17,6 +18,8 @@ export default function AdminMemberManage() {
const profile = useAuthStore((state) => state.profile); const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0; const memberId = profile?.id || 0;
const { data: workspaceMembers = [] } = useWorkspaceMembersQuery(Number(workspaceId));
const addWorkspaceMember = useAddWorkspaceMemberQuery(); const addWorkspaceMember = useAddWorkspaceMemberQuery();
const addProjectMember = useAddProjectMemberQuery(); const addProjectMember = useAddProjectMemberQuery();
@ -49,8 +52,8 @@ export default function AdminMemberManage() {
<MemberAddModal onSubmit={handleMemberInvite} /> <MemberAddModal onSubmit={handleMemberInvite} />
</header> </header>
{workspaceId && <WorkspaceMemberManageForm />} {workspaceId && !projectId && <WorkspaceMemberManageForm members={workspaceMembers} />}
{projectId && <ProjectMemberManageForm />} {projectId && <ProjectMemberManageForm workspaceMembers={workspaceMembers} />}
</div> </div>
); );
} }

View File

@ -1,8 +1,8 @@
import { useNavigate, useParams } from 'react-router-dom'; import { Link, useLocation, useParams } from 'react-router-dom';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
export default function AdminMenuSidebar() { export default function AdminMenuSidebar() {
const navigate = useNavigate(); const location = useLocation();
const { workspaceId } = useParams<{ workspaceId: string }>(); const { workspaceId } = useParams<{ workspaceId: string }>();
const menuItems = [ const menuItems = [
@ -23,18 +23,22 @@ export default function AdminMenuSidebar() {
<h2 className="w-full overflow-hidden text-ellipsis whitespace-nowrap"></h2> <h2 className="w-full overflow-hidden text-ellipsis whitespace-nowrap"></h2>
</header> </header>
<div className="flex flex-col gap-1 px-2.5"> <div className="flex flex-col gap-1 px-2.5">
{menuItems.map((item) => ( {menuItems.map((item) => {
<button const isActive = location.pathname.startsWith(item.path);
key={item.label} return (
className={cn( <Link
'body cursor-pointer rounded-md px-3 py-2 text-left text-gray-800 hover:bg-gray-200', key={item.label}
'transition-colors focus:bg-gray-300 focus:outline-none' to={`${item.path}${location.search}`}
)} className={cn(
onClick={() => navigate(item.path)} 'body cursor-pointer rounded-md px-3 py-2 text-left text-gray-800 hover:bg-gray-200',
> 'transition-colors focus:bg-gray-300 focus:outline-none',
{item.label} isActive ? 'bg-gray-300 font-semibold' : ''
</button> )}
))} >
{item.label}
</Link>
);
})}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
import { ResizablePanel, ResizableHandle } from '../ui/resizable'; import { ResizablePanel, ResizableHandle } from '../ui/resizable';
import { useNavigate, useLocation, useParams } from 'react-router-dom'; import { Link, useLocation, useParams } from 'react-router-dom';
import { SquarePen } from 'lucide-react'; import { SquarePen } from 'lucide-react';
import useProjectListQuery from '@/queries/projects/useProjectListQuery'; import useProjectListQuery from '@/queries/projects/useProjectListQuery';
import useCreateProjectQuery from '@/queries/projects/useCreateProjectQuery'; import useCreateProjectQuery from '@/queries/projects/useCreateProjectQuery';
@ -7,9 +7,9 @@ import useWorkspaceQuery from '@/queries/workspaces/useWorkspaceQuery';
import { ProjectRequest } from '@/types'; import { ProjectRequest } from '@/types';
import useAuthStore from '@/stores/useAuthStore'; import useAuthStore from '@/stores/useAuthStore';
import ProjectCreateModal from '../ProjectCreateModal'; import ProjectCreateModal from '../ProjectCreateModal';
import { cn } from '@/lib/utils';
export default function AdminProjectSidebar(): JSX.Element { export default function AdminProjectSidebar(): JSX.Element {
const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { workspaceId } = useParams<{ workspaceId: string }>(); const { workspaceId } = useParams<{ workspaceId: string }>();
const profile = useAuthStore((state) => state.profile); const profile = useAuthStore((state) => state.profile);
@ -31,20 +31,7 @@ export default function AdminProjectSidebar(): JSX.Element {
}); });
}; };
const handleProjectClick = (projectId: number) => { const selectedProjectId = new URLSearchParams(location.search).get('projectId');
const searchParams = new URLSearchParams(location.search);
searchParams.set('projectId', String(projectId));
navigate({
search: `?${searchParams.toString()}`,
});
};
const handleHeaderClick = () => {
navigate({
pathname: `/admin/${workspaceId}`,
search: '',
});
};
return ( return (
<> <>
@ -57,7 +44,9 @@ export default function AdminProjectSidebar(): JSX.Element {
<header className="flex w-full items-center justify-between gap-2 border-b border-gray-200 p-4"> <header className="flex w-full items-center justify-between gap-2 border-b border-gray-200 p-4">
<h1 <h1
className="heading w-full cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap text-xl font-bold text-gray-900" className="heading w-full cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap text-xl font-bold text-gray-900"
onClick={handleHeaderClick} onClick={() => {
window.history.replaceState({}, '', location.pathname);
}}
> >
{workspaceTitle} {workspaceTitle}
</h1> </h1>
@ -70,15 +59,24 @@ export default function AdminProjectSidebar(): JSX.Element {
/> />
</header> </header>
<div className="flex flex-col gap-2 p-4"> <div className="flex flex-col gap-2 p-4">
{projects.map((project) => ( {projects.map((project) => {
<button const isActive = String(project.id) === selectedProjectId;
key={project.id} return (
className="body cursor-pointer rounded-md px-3 py-2 text-left hover:bg-gray-200" <Link
onClick={() => handleProjectClick(project.id)} key={project.id}
> to={{
{project.title} pathname: location.pathname,
</button> search: `?projectId=${project.id}`,
))} }}
className={cn(
'body cursor-pointer rounded-md px-3 py-2 text-left hover:bg-gray-200',
isActive ? 'bg-gray-300 font-semibold' : ''
)}
>
{project.title}
</Link>
);
})}
</div> </div>
</ResizablePanel> </ResizablePanel>
<ResizableHandle className="bg-gray-300" /> <ResizableHandle className="bg-gray-300" />