Refactor: admin 페이지 리팩토링 1차
This commit is contained in:
parent
5a2a708d77
commit
ae6e09a5c1
@ -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>
|
||||
);
|
||||
}
|
@ -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',
|
||||
|
@ -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' : ''
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
33
frontend/src/pages/ProjectReviewList.tsx
Normal file
33
frontend/src/pages/ProjectReviewList.tsx
Normal 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)}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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>
|
||||
) : (
|
33
frontend/src/pages/WorkspaceReviewList.tsx
Normal file
33
frontend/src/pages/WorkspaceReviewList.tsx
Normal 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)}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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 />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user