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 { 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="역할을 선택해주세요." />
|
||||||
|
@ -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 ? (
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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" />
|
||||||
|
Loading…
Reference in New Issue
Block a user