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

View File

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

View File

@ -3,6 +3,7 @@ import { useParams, useLocation } from 'react-router-dom';
import useAuthStore from '@/stores/useAuthStore';
import useAddWorkspaceMemberQuery from '@/queries/workspaces/useAddWorkspaceMemberQuery';
import useAddProjectMemberQuery from '@/queries/projects/useAddProjectMemberQuery';
import useWorkspaceMembersQuery from '@/queries/workspaces/useWorkspaceMembersQuery';
import MemberAddModal from '../MemberAddModal';
import { MemberAddFormValues } from '../MemberAddModal/MemberAddForm';
import WorkspaceMemberManageForm from './WorkspaceMemberManageForm';
@ -17,6 +18,8 @@ export default function AdminMemberManage() {
const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0;
const { data: workspaceMembers = [] } = useWorkspaceMembersQuery(Number(workspaceId));
const addWorkspaceMember = useAddWorkspaceMemberQuery();
const addProjectMember = useAddProjectMemberQuery();
@ -49,8 +52,8 @@ export default function AdminMemberManage() {
<MemberAddModal onSubmit={handleMemberInvite} />
</header>
{workspaceId && <WorkspaceMemberManageForm />}
{projectId && <ProjectMemberManageForm />}
{workspaceId && !projectId && <WorkspaceMemberManageForm members={workspaceMembers} />}
{projectId && <ProjectMemberManageForm workspaceMembers={workspaceMembers} />}
</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';
export default function AdminMenuSidebar() {
const navigate = useNavigate();
const location = useLocation();
const { workspaceId } = useParams<{ workspaceId: string }>();
const menuItems = [
@ -23,18 +23,22 @@ export default function AdminMenuSidebar() {
<h2 className="w-full overflow-hidden text-ellipsis whitespace-nowrap"></h2>
</header>
<div className="flex flex-col gap-1 px-2.5">
{menuItems.map((item) => (
<button
key={item.label}
className={cn(
'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'
)}
onClick={() => navigate(item.path)}
>
{item.label}
</button>
))}
{menuItems.map((item) => {
const isActive = location.pathname.startsWith(item.path);
return (
<Link
key={item.label}
to={`${item.path}${location.search}`}
className={cn(
'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',
isActive ? 'bg-gray-300 font-semibold' : ''
)}
>
{item.label}
</Link>
);
})}
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
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 useProjectListQuery from '@/queries/projects/useProjectListQuery';
import useCreateProjectQuery from '@/queries/projects/useCreateProjectQuery';
@ -7,9 +7,9 @@ import useWorkspaceQuery from '@/queries/workspaces/useWorkspaceQuery';
import { ProjectRequest } from '@/types';
import useAuthStore from '@/stores/useAuthStore';
import ProjectCreateModal from '../ProjectCreateModal';
import { cn } from '@/lib/utils';
export default function AdminProjectSidebar(): JSX.Element {
const navigate = useNavigate();
const location = useLocation();
const { workspaceId } = useParams<{ workspaceId: string }>();
const profile = useAuthStore((state) => state.profile);
@ -31,20 +31,7 @@ export default function AdminProjectSidebar(): JSX.Element {
});
};
const handleProjectClick = (projectId: number) => {
const searchParams = new URLSearchParams(location.search);
searchParams.set('projectId', String(projectId));
navigate({
search: `?${searchParams.toString()}`,
});
};
const handleHeaderClick = () => {
navigate({
pathname: `/admin/${workspaceId}`,
search: '',
});
};
const selectedProjectId = new URLSearchParams(location.search).get('projectId');
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">
<h1
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}
</h1>
@ -70,15 +59,24 @@ export default function AdminProjectSidebar(): JSX.Element {
/>
</header>
<div className="flex flex-col gap-2 p-4">
{projects.map((project) => (
<button
key={project.id}
className="body cursor-pointer rounded-md px-3 py-2 text-left hover:bg-gray-200"
onClick={() => handleProjectClick(project.id)}
>
{project.title}
</button>
))}
{projects.map((project) => {
const isActive = String(project.id) === selectedProjectId;
return (
<Link
key={project.id}
to={{
pathname: location.pathname,
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>
</ResizablePanel>
<ResizableHandle className="bg-gray-300" />