Refactor: 멤버 관리 페이지 완성
This commit is contained in:
parent
998cb358ba
commit
b6cc61dfde
@ -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="역할을 선택해주세요." />
|
||||
|
@ -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 ? (
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
Loading…
Reference in New Issue
Block a user