Refactor: admin 페이지 리팩토링 1차

This commit is contained in:
정현조 2024-09-20 23:39:31 +09:00
parent 5a2a708d77
commit ae6e09a5c1
13 changed files with 218 additions and 329 deletions

View File

@ -1,65 +0,0 @@
import { useState } from 'react';
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 MemberAddModal from '../MemberAddModal';
import { MemberAddFormValues } from '../MemberAddModal/MemberAddForm';
import WorkspaceMemberManageForm from './WorkspaceMemberManageForm';
import ProjectMemberManageForm from './ProjectMemberManageForm';
import WorkspaceMemberAddModal from '../WorkspaceMemberAddModal';
export default function AdminMemberManage() {
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 memberId = profile?.id || 0;
const addWorkspaceMember = useAddWorkspaceMemberQuery();
const addProjectMember = useAddProjectMemberQuery();
const [, setInviteModalOpen] = useState(false);
const handleMemberInvite = (data: MemberAddFormValues) => {
if (workspaceId && !projectId) {
addWorkspaceMember.mutate({
workspaceId: Number(workspaceId),
memberId: memberId,
newMemberId: data.memberId,
});
} else if (projectId && Number(projectId) > 0) {
addProjectMember.mutate({
projectId: Number(projectId),
memberId: memberId,
newMember: {
memberId: data.memberId,
privilegeType: data.role,
},
});
}
setInviteModalOpen(false);
};
return (
<div className="flex w-full flex-col gap-6 border-b-[0.67px] border-[#dcdcde] bg-[#fbfafd] p-6">
<header className="flex w-full items-center gap-4">
<h1 className="flex-1 text-lg font-semibold text-[#333238]"> </h1>
{projectId ? (
<MemberAddModal onSubmit={handleMemberInvite} />
) : (
<WorkspaceMemberAddModal
workspaceId={Number(workspaceId)}
memberId={memberId}
/>
)}
</header>
{workspaceId && !projectId && <WorkspaceMemberManageForm />}
{projectId && <ProjectMemberManageForm />}
</div>
);
}

View File

@ -28,7 +28,7 @@ export default function AdminMenuSidebar() {
return (
<Link
key={item.label}
to={`${item.path}${location.search}`}
to={item.path}
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',

View File

@ -12,7 +12,7 @@ import { cn } from '@/lib/utils';
export default function AdminProjectSidebar(): JSX.Element {
const location = useLocation();
const navigate = useNavigate();
const { workspaceId } = useParams<{ workspaceId: string }>();
const { workspaceId, projectId } = useParams<{ workspaceId: string; projectId?: string }>();
const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0;
@ -32,8 +32,6 @@ export default function AdminProjectSidebar(): JSX.Element {
});
};
const selectedProjectId = new URLSearchParams(location.search).get('projectId');
const handleHeaderClick = () => {
navigate({
pathname: location.pathname,
@ -41,6 +39,16 @@ export default function AdminProjectSidebar(): JSX.Element {
});
};
const getNewPath = (newProjectId: string) => {
if (location.pathname.includes('reviews')) {
return `/admin/${workspaceId}/reviews/${newProjectId}`;
}
if (location.pathname.includes('members')) {
return `/admin/${workspaceId}/members/${newProjectId}`;
}
return location.pathname;
};
return (
<>
<ResizablePanel
@ -66,14 +74,11 @@ export default function AdminProjectSidebar(): JSX.Element {
</header>
<div className="flex flex-col gap-2 p-4">
{projects.map((project) => {
const isActive = String(project.id) === selectedProjectId;
const isActive = projectId === String(project.id);
return (
<Link
key={project.id}
to={{
pathname: location.pathname,
search: `?projectId=${project.id}`,
}}
to={getNewPath(String(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' : ''

View File

@ -1,74 +0,0 @@
import { useState } from 'react';
import ReviewItem from './ReviewItem';
import ReviewSearchInput from './ReviewSearchInput';
import useReviewByStatusQuery from '@/queries/reviews/useReviewByStatusQuery';
import useAuthStore from '@/stores/useAuthStore';
interface ProjectReviewListProps {
projectId: number;
workspaceId: number;
}
export default function ProjectReviewList({ projectId, workspaceId }: ProjectReviewListProps): JSX.Element {
const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0;
const [activeTab, setActiveTab] = useState<'REQUESTED' | 'APPROVED' | 'REJECTED' | 'all'>('REQUESTED');
const [, setSearchQuery] = useState('');
const [sortValue, setSortValue] = useState('latest');
const { data: projectReviews = [] } = useReviewByStatusQuery(
projectId,
memberId,
activeTab !== 'all' ? activeTab : undefined
);
return (
<div className="relative w-full">
<div className="relative w-full px-4">
<div className="flex w-full items-center border-b-[0.67px] border-solid border-[#dcdcde]">
{['REQUESTED', 'APPROVED', 'REJECTED', 'all'].map((tab) => (
<button
key={tab}
className={`flex h-12 w-[100px] items-center justify-between px-3 ${
activeTab === tab ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''
}`}
onClick={() => setActiveTab(tab as typeof activeTab)}
>
<span className={`text-sm ${activeTab === tab ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
{tab === 'REQUESTED' ? '요청' : tab === 'APPROVED' ? '승인' : tab === 'REJECTED' ? '거부' : '전체'}
</span>
</button>
))}
</div>
</div>
<div className="relative w-full px-4">
<ReviewSearchInput
onSearchChange={setSearchQuery}
onSortChange={setSortValue}
sortValue={sortValue}
/>
</div>
<div className="relative w-full overflow-y-auto px-4">
{projectReviews.length === 0 ? (
<div className="py-4 text-center"> .</div>
) : (
projectReviews.map((item) => (
<ReviewItem
key={item.reviewId}
workspaceId={workspaceId}
reviewId={item.reviewId}
title={item.title}
createdTime={item.createAt}
creatorName={item.author.nickname}
projectId={item.projectId}
status={item.status}
/>
))
)}
</div>
</div>
);
}

View File

@ -1,73 +0,0 @@
import { useState } from 'react';
import ReviewItem from './ReviewItem';
import ReviewSearchInput from './ReviewSearchInput';
import useWorkspaceReviewsQuery from '@/queries/workspaces/useWorkspaceReviewsQuery';
import useAuthStore from '@/stores/useAuthStore';
interface WorkspaceReviewListProps {
workspaceId: number;
}
export default function WorkspaceReviewList({ workspaceId }: WorkspaceReviewListProps): JSX.Element {
const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0;
const [activeTab, setActiveTab] = useState<'REQUESTED' | 'APPROVED' | 'REJECTED' | 'all'>('REQUESTED');
const [, setSearchQuery] = useState('');
const [sortValue, setSortValue] = useState('latest');
const { data: workspaceReviews = [] } = useWorkspaceReviewsQuery(
workspaceId,
memberId,
activeTab !== 'all' ? activeTab : undefined
);
return (
<div className="relative w-full">
<div className="relative w-full px-4">
<div className="flex w-full items-center border-b-[0.67px] border-solid border-[#dcdcde]">
{['REQUESTED', 'APPROVED', 'REJECTED', 'all'].map((tab) => (
<button
key={tab}
className={`flex h-12 w-[100px] items-center justify-between px-3 ${
activeTab === tab ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''
}`}
onClick={() => setActiveTab(tab as typeof activeTab)}
>
<span className={`text-sm ${activeTab === tab ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
{tab === 'REQUESTED' ? '요청' : tab === 'APPROVED' ? '승인' : tab === 'REJECTED' ? '거부' : '전체'}
</span>
</button>
))}
</div>
</div>
<div className="relative w-full px-4">
<ReviewSearchInput
onSearchChange={setSearchQuery}
onSortChange={setSortValue}
sortValue={sortValue}
/>
</div>
<div className="relative w-full overflow-y-auto px-4">
{workspaceReviews.length === 0 ? (
<div className="py-4 text-center"> .</div>
) : (
workspaceReviews.map((item) => (
<ReviewItem
key={item.reviewId}
workspaceId={workspaceId}
reviewId={item.reviewId}
title={item.title}
createdTime={item.createAt}
creatorName={item.author.nickname}
projectId={item.projectId}
status={item.status}
/>
))
)}
</div>
</div>
);
}

View File

@ -1,63 +0,0 @@
import '@/index.css';
import type { Meta, StoryObj } from '@storybook/react';
import ReviewList from '.';
const meta: Meta<typeof ReviewList> = {
title: 'Components/ReviewList',
component: ReviewList,
};
export default meta;
type Story = StoryObj<typeof ReviewList>;
export const Default: Story = {
args: {
acceptedCount: 10,
rejectedCount: 5,
pendingCount: 7,
totalCount: 22,
items: [
{
title: '갤럭시22 생산 라인 이물질 분류',
createdTime: '2 hours ago',
creatorName: 'Kim Tae Su',
project: 'Project A',
type: 'Classification',
status: 'needs_review',
},
{
title: '갤럭시 흠집 객체 탐지',
createdTime: '3 hours ago',
creatorName: 'Kim Tae Su',
project: 'Project B',
type: 'Detection',
status: 'completed',
},
{
title: '갤럭시 흠집 경계 폴리곤',
createdTime: '5 hours ago',
creatorName: 'Kim Tae Su',
project: 'Project C',
type: 'Polygon',
status: 'in_progress',
},
{
title: '갤럭시 흠집 폴리라인',
createdTime: '1 day ago',
creatorName: 'Kim Tae Su',
project: 'Project D',
type: 'Polyline',
status: 'completed',
},
{
title: '갤럭시 흠집 디텍션 허가 요청',
createdTime: '6 hours ago',
creatorName: 'Kim Tae Su',
project: 'Project E',
type: 'Detection',
status: 'pending',
},
],
},
};

View File

@ -1,19 +1,72 @@
import { useLocation, useParams } from 'react-router-dom';
import ProjectReviewList from './ProjectReviewList';
import WorkspaceReviewList from './WorkspaceReviewList';
import ReviewItem from './ReviewItem';
import ReviewSearchInput from './ReviewSearchInput';
import { ReviewResponse } from '@/types';
export default function ReviewList(): JSX.Element {
const { workspaceId } = useParams<{ workspaceId: string }>();
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
const projectId = searchParams.get('projectId');
interface ReviewListProps {
reviews: ReviewResponse[];
activeTab: 'REQUESTED' | 'APPROVED' | 'REJECTED' | 'all';
setActiveTab: React.Dispatch<React.SetStateAction<'REQUESTED' | 'APPROVED' | 'REJECTED' | 'all'>>;
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
sortValue: string;
setSortValue: React.Dispatch<React.SetStateAction<string>>;
workspaceId: number;
}
return projectId && Number(projectId) > 0 ? (
<ProjectReviewList
projectId={Number(projectId)}
workspaceId={Number(workspaceId)}
/>
) : (
<WorkspaceReviewList workspaceId={Number(workspaceId)} />
export default function ReviewList({
reviews,
activeTab,
setActiveTab,
setSearchQuery,
sortValue,
setSortValue,
workspaceId,
}: ReviewListProps) {
return (
<div className="relative w-full">
<div className="relative w-full px-4">
<div className="flex w-full items-center border-b-[0.67px] border-solid border-[#dcdcde]">
{['REQUESTED', 'APPROVED', 'REJECTED', 'all'].map((tab) => (
<button
key={tab}
className={`flex h-12 w-[100px] items-center justify-between px-3 ${
activeTab === tab ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''
}`}
onClick={() => setActiveTab(tab as typeof activeTab)}
>
<span className={`text-sm ${activeTab === tab ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
{tab === 'REQUESTED' ? '요청' : tab === 'APPROVED' ? '승인' : tab === 'REJECTED' ? '거부' : '전체'}
</span>
</button>
))}
</div>
</div>
<div className="relative w-full px-4">
<ReviewSearchInput
onSearchChange={setSearchQuery}
onSortChange={setSortValue}
sortValue={sortValue}
/>
</div>
<div className="relative w-full overflow-y-auto px-4">
{reviews.length === 0 ? (
<div className="py-4 text-center"> .</div>
) : (
reviews.map((item) => (
<ReviewItem
key={item.reviewId}
workspaceId={workspaceId}
reviewId={item.reviewId}
title={item.title}
createdTime={item.createAt}
creatorName={item.author.nickname}
projectId={item.projectId}
status={item.status}
/>
))
)}
</div>
</div>
);
}

View File

@ -1,16 +1,16 @@
import { useLocation, useParams } from 'react-router-dom';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import useUpdateProjectMemberPrivilegeQuery from '@/queries/projects/useUpdateProjectMemberPrivilegeQuery';
import useRemoveProjectMemberQuery from '@/queries/projects/useRemoveProjectMemberQuery';
import { useParams, useLocation } from 'react-router-dom';
import useProjectMembersQuery from '@/queries/projects/useProjectMembersQuery';
import useWorkspaceMembersQuery from '@/queries/workspaces/useWorkspaceMembersQuery';
import useAuthStore from '@/stores/useAuthStore';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import useUpdateProjectMemberPrivilegeQuery from '@/queries/projects/useUpdateProjectMemberPrivilegeQuery';
import useRemoveProjectMemberQuery from '@/queries/projects/useRemoveProjectMemberQuery';
import useAddProjectMemberQuery from '@/queries/projects/useAddProjectMemberQuery';
import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
type Role = 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER' | 'NONE';
const roles: Role[] = ['ADMIN', 'MANAGER', 'EDITOR', 'VIEWER', 'NONE'];
const roleToStr: { [key in Role]: string } = {
ADMIN: '관리자',
MANAGER: '매니저',
@ -19,17 +19,22 @@ const roleToStr: { [key in Role]: string } = {
NONE: '역할 없음',
};
export default function ProjectMemberManageForm() {
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
const projectId = searchParams.get('projectId');
const { workspaceId } = useParams<{ workspaceId: string }>();
export default function ProjectMemberManage() {
const { workspaceId, projectId } = useParams<{ workspaceId: string; projectId: string }>();
const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0;
const { data: workspaceMembers = [] } = useWorkspaceMembersQuery(Number(workspaceId));
const queryClient = useQueryClient();
const location = useLocation();
useEffect(() => {
if (projectId) {
queryClient.invalidateQueries({ queryKey: ['projectMembers', projectId, memberId] });
}
}, [location.pathname, projectId, memberId, queryClient]);
const { data: projectMembers = [] } = useProjectMembersQuery(Number(projectId), memberId);
const { data: workspaceMembers = [] } = useWorkspaceMembersQuery(Number(workspaceId));
const { mutate: updatePrivilege } = useUpdateProjectMemberPrivilegeQuery();
const { mutate: removeMember } = useRemoveProjectMemberQuery();
const { mutate: addProjectMember } = useAddProjectMemberQuery();
@ -79,7 +84,11 @@ export default function ProjectMemberManageForm() {
};
return (
<div className="flex w-full flex-col gap-4">
<div className="flex w-full flex-col gap-6 border-b-[0.67px] border-[#dcdcde] bg-[#fbfafd] p-6">
<header className="flex w-full items-center gap-4">
<h1 className="flex-1 text-lg font-semibold text-[#333238]"> </h1>
</header>
{sortedMembers.map((member) => (
<div
key={`${member.memberId}-${member.nickname}`}
@ -89,8 +98,7 @@ export default function ProjectMemberManageForm() {
<div className="flex-1">
<Select
onValueChange={(value) => handleRoleChange(member.memberId, value as Role)}
defaultValue={member.privilegeType || 'NONE'}
disabled={member.privilegeType === 'ADMIN'}
defaultValue={member.privilegeType}
>
<SelectTrigger>
<SelectValue placeholder="역할을 선택해주세요." />
@ -100,7 +108,6 @@ export default function ProjectMemberManageForm() {
<SelectItem
key={role}
value={role}
disabled={role === 'ADMIN'}
>
{roleToStr[role]}
</SelectItem>

View File

@ -0,0 +1,33 @@
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import useReviewByStatusQuery from '@/queries/reviews/useReviewByStatusQuery';
import useAuthStore from '@/stores/useAuthStore';
import ReviewList from '@/components/ReviewList';
export default function ProjectReviewList() {
const { workspaceId, projectId } = useParams<{ workspaceId: string; projectId: string }>();
const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0;
const [activeTab, setActiveTab] = useState<'REQUESTED' | 'APPROVED' | 'REJECTED' | 'all'>('REQUESTED');
const [, setSearchQuery] = useState('');
const [sortValue, setSortValue] = useState('latest');
const { data: projectReviews = [] } = useReviewByStatusQuery(
Number(projectId),
memberId,
activeTab !== 'all' ? activeTab : undefined
);
return (
<ReviewList
reviews={projectReviews}
activeTab={activeTab}
setActiveTab={setActiveTab}
setSearchQuery={setSearchQuery}
sortValue={sortValue}
setSortValue={setSortValue}
workspaceId={Number(workspaceId)}
/>
);
}

View File

@ -1,12 +1,25 @@
import useWorkspaceMembersQuery from '@/queries/workspaces/useWorkspaceMembersQuery';
import { useParams } from 'react-router-dom';
import useWorkspaceMembersQuery from '@/queries/workspaces/useWorkspaceMembersQuery';
import useAuthStore from '@/stores/useAuthStore';
import WorkspaceMemberAddModal from '@/components/WorkspaceMemberAddModal';
export default function WorkspaceMemberManageForm() {
export default function WorkspaceMemberManage() {
const { workspaceId } = useParams<{ workspaceId: string }>();
const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0;
const { data: members = [] } = useWorkspaceMembersQuery(Number(workspaceId));
return (
<div className="flex w-full flex-col gap-4">
<div className="flex w-full flex-col gap-6 border-b-[0.67px] border-[#dcdcde] bg-[#fbfafd] p-6">
<header className="flex w-full items-center gap-4">
<h1 className="flex-1 text-lg font-semibold text-[#333238]"> </h1>
<WorkspaceMemberAddModal
workspaceId={Number(workspaceId)}
memberId={memberId}
/>
</header>
{members.length === 0 ? (
<div className="py-4 text-center"> .</div>
) : (

View File

@ -0,0 +1,33 @@
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import useWorkspaceReviewsQuery from '@/queries/workspaces/useWorkspaceReviewsQuery';
import useAuthStore from '@/stores/useAuthStore';
import ReviewList from '@/components/ReviewList';
export default function WorkspaceReviewList() {
const { workspaceId } = useParams<{ workspaceId: string }>();
const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0;
const [activeTab, setActiveTab] = useState<'REQUESTED' | 'APPROVED' | 'REJECTED' | 'all'>('REQUESTED');
const [, setSearchQuery] = useState('');
const [sortValue, setSortValue] = useState('latest');
const { data: workspaceReviews = [] } = useWorkspaceReviewsQuery(
Number(workspaceId),
memberId,
activeTab !== 'all' ? activeTab : undefined
);
return (
<ReviewList
reviews={workspaceReviews}
activeTab={activeTab}
setActiveTab={setActiveTab}
setSearchQuery={setSearchQuery}
sortValue={sortValue}
setSortValue={setSortValue}
workspaceId={Number(workspaceId)}
/>
);
}

View File

@ -1,19 +1,21 @@
import PageLayout from '@/components/PageLayout';
// import ImageCanvas from '@/components/ImageCanvas';
import Home from '@/pages/Home';
import WorkspaceBrowseDetail from '@/pages/WorkspaceBrowseDetail';
import WorkspaceBrowseLayout from '@/components/WorkspaceBrowseLayout';
import WorkspaceLayout from '@/components/WorkspaceLayout';
import AdminLayout from '@/components/AdminLayout';
import ReviewList from '@/components/ReviewList';
import AdminMemberManage from '@/components/AdminMemberManage';
import WorkspaceReviewList from '@/pages/WorkspaceReviewList';
import ProjectReviewList from '@/pages/ProjectReviewList';
import WorkspaceMemberManage from '@/pages/WorkspaceMemberManage';
import ProjectMemberManage from '@/pages/ProjectMemberManage';
import OAuthCallback from '@/components/OAuthCallback';
import { createBrowserRouter } from 'react-router-dom';
import { Suspense } from 'react';
import Home from '@/pages/Home';
import WorkspaceBrowseIndex from '@/pages/WorkspaceBrowseIndex';
import AdminIndex from '@/pages/AdminIndex';
import LabelCanvas from '@/pages/LabelCanvas';
import ReviewDetail from '@/components/ReviewDetail';
import ReviewDetail from '@/pages/ReviewDetail';
export const webPath = {
home: () => '/',
@ -86,15 +88,33 @@ const router = createBrowserRouter([
},
{
path: 'reviews',
element: <ReviewList />,
},
{
path: 'reviews/:projectId/:reviewId',
element: <ReviewDetail />,
children: [
{
index: true,
element: <WorkspaceReviewList />,
},
{
path: ':projectId',
element: <ProjectReviewList />,
},
{
path: ':projectId/:reviewId',
element: <ReviewDetail />,
},
],
},
{
path: 'members',
element: <AdminMemberManage />,
children: [
{
index: true,
element: <WorkspaceMemberManage />,
},
{
path: ':projectId',
element: <ProjectMemberManage />,
},
],
},
],
},