Feat: 워크스페이스 리뷰 전체 조회 구현
This commit is contained in:
parent
4b48718cbe
commit
15bf3b8d80
@ -1,5 +1,5 @@
|
|||||||
import api from '@/api/axiosConfig';
|
import api from '@/api/axiosConfig';
|
||||||
import { WorkspaceListResponse, WorkspaceRequest, WorkspaceResponse } from '@/types';
|
import { WorkspaceListResponse, WorkspaceRequest, WorkspaceResponse, ReviewResponse } from '@/types';
|
||||||
|
|
||||||
export async function getWorkspaceList(memberId: number, lastWorkspaceId?: number, limit?: number) {
|
export async function getWorkspaceList(memberId: number, lastWorkspaceId?: number, limit?: number) {
|
||||||
return api
|
return api
|
||||||
@ -49,10 +49,21 @@ export async function addWorkspaceMember(workspaceId: number, memberId: number,
|
|||||||
.then(({ data }) => data);
|
.then(({ data }) => data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeWorkspaceMember(workspaceId: number, memberId: number, targetMemberId: number) {
|
export async function getWorkspaceReviews(
|
||||||
|
workspaceId: number,
|
||||||
|
memberId: number,
|
||||||
|
reviewStatus?: 'REQUESTED' | 'APPROVED' | 'REJECTED',
|
||||||
|
lastReviewId?: number,
|
||||||
|
limitPage: number = 10
|
||||||
|
) {
|
||||||
return api
|
return api
|
||||||
.delete(`/workspaces/${workspaceId}/members/${targetMemberId}`, {
|
.get<ReviewResponse[]>(`/workspaces/${workspaceId}/reviews`, {
|
||||||
params: { memberId },
|
params: {
|
||||||
|
memberId,
|
||||||
|
limitPage,
|
||||||
|
...(reviewStatus ? { reviewStatus } : {}),
|
||||||
|
...(lastReviewId ? { lastReviewId } : {}),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.then(({ data }) => data);
|
.then(({ data }) => data);
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,31 @@
|
|||||||
import { Outlet, useParams } from 'react-router-dom';
|
import { Outlet, useMatch } from 'react-router-dom';
|
||||||
import Header from '../Header';
|
import Header from '../Header';
|
||||||
import { ResizablePanelGroup, ResizablePanel } from '../ui/resizable';
|
import { ResizablePanelGroup, ResizablePanel } from '../ui/resizable';
|
||||||
import AdminProjectSidebar from '../AdminProjectSidebar';
|
import AdminProjectSidebar from '../AdminProjectSidebar';
|
||||||
import AdminMenuSidebar from '../AdminMenuSidebar';
|
import AdminMenuSidebar from '../AdminMenuSidebar';
|
||||||
|
|
||||||
export default function AdminLayout() {
|
export default function AdminLayout() {
|
||||||
const { projectId } = useParams<{ projectId?: string }>();
|
const isIndexPage = useMatch({ path: '/admin/:workspaceId', end: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header className="fixed left-0 top-0" />
|
<Header className="fixed left-0 top-0" />
|
||||||
<div className="mt-16 h-[calc(100vh-64px)] w-screen">
|
<div className="mt-16 h-[calc(100vh-64px)] w-screen">
|
||||||
<ResizablePanelGroup direction="horizontal">
|
<ResizablePanelGroup direction="horizontal">
|
||||||
<AdminProjectSidebar />
|
{!isIndexPage && <AdminProjectSidebar />}
|
||||||
|
|
||||||
<ResizablePanel className="flex w-full items-center">
|
<ResizablePanel className="flex w-full items-center">
|
||||||
<main className="h-full grow">
|
<main className="h-full grow">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
{projectId && <AdminMenuSidebar />}
|
<AdminMenuSidebar />
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
|
{isIndexPage && (
|
||||||
|
<main className="h-full w-full">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -3,11 +3,17 @@ import { cn } from '@/lib/utils';
|
|||||||
|
|
||||||
export default function AdminMenuSidebar() {
|
export default function AdminMenuSidebar() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { workspaceId, projectId } = useParams<{ workspaceId: string; projectId?: string }>();
|
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ label: '리뷰', path: `/admin/${workspaceId}${projectId ? `/project/${projectId}` : ''}/reviews` },
|
{
|
||||||
{ label: '멤버 관리', path: `/admin/${workspaceId}${projectId ? `/project/${projectId}` : ''}/members` },
|
label: '리뷰',
|
||||||
|
path: `/admin/${workspaceId}/reviews`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '멤버 관리',
|
||||||
|
path: `/admin/${workspaceId}/members`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
import { ResizablePanel, ResizableHandle } from '../ui/resizable';
|
import { ResizablePanel, ResizableHandle } from '../ui/resizable';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, 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';
|
||||||
|
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';
|
||||||
|
|
||||||
export default function AdminProjectSidebar(): JSX.Element {
|
export default function AdminProjectSidebar(): JSX.Element {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
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);
|
||||||
const memberId = profile?.id || 0;
|
const memberId = profile?.id || 0;
|
||||||
|
|
||||||
const { data: projectsResponse } = useProjectListQuery(Number(workspaceId), memberId);
|
const { data: workspaceData } = useWorkspaceQuery(Number(workspaceId), memberId);
|
||||||
|
const workspaceTitle = workspaceData?.title || `Workspace-${workspaceId}`;
|
||||||
|
|
||||||
|
const { data: projectsResponse } = useProjectListQuery(Number(workspaceId), memberId);
|
||||||
const projects = projectsResponse?.workspaceResponses ?? [];
|
const projects = projectsResponse?.workspaceResponses ?? [];
|
||||||
|
|
||||||
const createProject = useCreateProjectQuery();
|
const createProject = useCreateProjectQuery();
|
||||||
@ -27,6 +31,21 @@ 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: '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
@ -36,8 +55,11 @@ export default function AdminProjectSidebar(): JSX.Element {
|
|||||||
className="flex h-full flex-col border-r border-gray-200 bg-gray-100"
|
className="flex h-full flex-col border-r border-gray-200 bg-gray-100"
|
||||||
>
|
>
|
||||||
<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 className="heading w-full overflow-hidden text-ellipsis whitespace-nowrap text-xl font-bold text-gray-900">
|
<h1
|
||||||
{workspaceId}
|
className="heading w-full cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap text-xl font-bold text-gray-900"
|
||||||
|
onClick={handleHeaderClick}
|
||||||
|
>
|
||||||
|
{workspaceTitle}
|
||||||
</h1>
|
</h1>
|
||||||
<button className="p-2">
|
<button className="p-2">
|
||||||
<SquarePen size={16} />
|
<SquarePen size={16} />
|
||||||
@ -52,7 +74,7 @@ export default function AdminProjectSidebar(): JSX.Element {
|
|||||||
<button
|
<button
|
||||||
key={project.id}
|
key={project.id}
|
||||||
className="body cursor-pointer rounded-md px-3 py-2 text-left hover:bg-gray-200"
|
className="body cursor-pointer rounded-md px-3 py-2 text-left hover:bg-gray-200"
|
||||||
onClick={() => navigate(`/admin/${workspaceId}/project/${project.id}`)}
|
onClick={() => handleProjectClick(project.id)}
|
||||||
>
|
>
|
||||||
{project.title}
|
{project.title}
|
||||||
</button>
|
</button>
|
||||||
|
72
frontend/src/components/ReviewList/ProjectReviewList.tsx
Normal file
72
frontend/src/components/ReviewList/ProjectReviewList.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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 }: 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}
|
||||||
|
title={item.title}
|
||||||
|
createdTime={item.createAt}
|
||||||
|
creatorName={item.nickname}
|
||||||
|
projectId={item.projectId}
|
||||||
|
status={item.status}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
import { Briefcase, Tag, Box, Layers } from 'lucide-react';
|
import { Briefcase, Tag, Box, Layers } from 'lucide-react';
|
||||||
import { ProjectResponse } from '@/types';
|
import useProjectQuery from '@/queries/projects/useProjectQuery';
|
||||||
|
import useAuthStore from '@/stores/useAuthStore';
|
||||||
interface ReviewItemProps {
|
interface ReviewItemProps {
|
||||||
title: string;
|
title: string;
|
||||||
createdTime: string;
|
createdTime: string;
|
||||||
creatorName: string;
|
creatorName: string;
|
||||||
project: ProjectResponse;
|
projectId: number;
|
||||||
status: string;
|
status: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||||
type: { text: 'classification' | 'detection' | 'segmentation'; color: string };
|
type?: { text: 'classification' | 'detection' | 'segmentation'; color: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeIcons: Record<'classification' | 'detection' | 'segmentation', JSX.Element> = {
|
const typeIcons: Record<'classification' | 'detection' | 'segmentation', JSX.Element> = {
|
||||||
@ -16,9 +16,11 @@ const typeIcons: Record<'classification' | 'detection' | 'segmentation', JSX.Ele
|
|||||||
segmentation: <Layers className="h-4 w-4 text-white" />,
|
segmentation: <Layers className="h-4 w-4 text-white" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ReviewItem({ title, createdTime, creatorName, project, status, type }: ReviewItemProps) {
|
export default function ReviewItem({ title, createdTime, creatorName, projectId, status, type }: ReviewItemProps) {
|
||||||
const icon = typeIcons[project.projectType];
|
const profile = useAuthStore((state) => state.profile);
|
||||||
|
const memberId = profile?.id || 0;
|
||||||
|
const icon = type ? typeIcons[type.text] : null;
|
||||||
|
const { data: projectData } = useProjectQuery(projectId, memberId);
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[100px] w-full items-center justify-between border-b-[0.67px] border-[#ececef] bg-[#fbfafd] p-4">
|
<div className="flex h-[100px] w-full items-center justify-between border-b-[0.67px] border-[#ececef] bg-[#fbfafd] p-4">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@ -26,7 +28,7 @@ export default function ReviewItem({ title, createdTime, creatorName, project, s
|
|||||||
<p className="mt-1 text-xs text-[#737278]">by {creatorName}</p>
|
<p className="mt-1 text-xs text-[#737278]">by {creatorName}</p>
|
||||||
<div className="mt-1 flex items-center">
|
<div className="mt-1 flex items-center">
|
||||||
<Briefcase className="h-3 w-3 text-[#737278]" />
|
<Briefcase className="h-3 w-3 text-[#737278]" />
|
||||||
<p className="ml-1 text-xs text-[#737278]">{project.title}</p>
|
<p className="ml-1 text-xs text-[#737278]">{projectData.title}</p>
|
||||||
</div>
|
</div>
|
||||||
{type && (
|
{type && (
|
||||||
<div
|
<div
|
||||||
|
71
frontend/src/components/ReviewList/WorkspaceReviewList.tsx
Normal file
71
frontend/src/components/ReviewList/WorkspaceReviewList.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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}
|
||||||
|
title={item.title}
|
||||||
|
createdTime={item.createAt}
|
||||||
|
creatorName={item.nickname}
|
||||||
|
projectId={item.projectId}
|
||||||
|
status={item.status}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,99 +1,19 @@
|
|||||||
import { useState } from 'react';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import ReviewItem from './ReviewItem';
|
import ProjectReviewList from './ProjectReviewList';
|
||||||
import ReviewSearchInput from './ReviewSearchInput';
|
import WorkspaceReviewList from './WorkspaceReviewList';
|
||||||
import useReviewByStatusQuery from '@/queries/reviews/useReviewByStatusQuery';
|
|
||||||
import useProjectQuery from '@/queries/projects/useProjectQuery';
|
|
||||||
import useAuthStore from '@/stores/useAuthStore';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
export default function ReviewList(): JSX.Element {
|
export default function ReviewList(): JSX.Element {
|
||||||
const { projectId } = useParams<{ projectId: string }>();
|
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||||
const profile = useAuthStore((state) => state.profile);
|
const location = useLocation();
|
||||||
const memberId = profile?.id || 0;
|
const searchParams = new URLSearchParams(location.search);
|
||||||
|
const projectId = searchParams.get('projectId');
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<'REQUESTED' | 'APPROVED' | 'REJECTED' | 'all'>('REQUESTED');
|
return projectId && Number(projectId) > 0 ? (
|
||||||
const [, setSearchQuery] = useState('');
|
<ProjectReviewList
|
||||||
const [sortValue, setSortValue] = useState('latest');
|
projectId={Number(projectId)}
|
||||||
|
workspaceId={Number(workspaceId)}
|
||||||
const { data: project } = useProjectQuery(Number(projectId), memberId);
|
/>
|
||||||
|
) : (
|
||||||
const { data: reviews = [] } = useReviewByStatusQuery(
|
<WorkspaceReviewList workspaceId={Number(workspaceId)} />
|
||||||
Number(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]">
|
|
||||||
<button
|
|
||||||
className={`flex h-12 w-[100px] items-center justify-between px-3 ${activeTab === 'REQUESTED' ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''}`}
|
|
||||||
onClick={() => setActiveTab('REQUESTED')}
|
|
||||||
>
|
|
||||||
<span className={`text-sm ${activeTab === 'REQUESTED' ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
|
|
||||||
요청
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className={`flex h-12 w-[100px] items-center justify-between px-3 ${activeTab === 'APPROVED' ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''}`}
|
|
||||||
onClick={() => setActiveTab('APPROVED')}
|
|
||||||
>
|
|
||||||
<span className={`text-sm ${activeTab === 'APPROVED' ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
|
|
||||||
승인
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className={`flex h-12 w-[100px] items-center justify-between px-3 ${activeTab === 'REJECTED' ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''}`}
|
|
||||||
onClick={() => setActiveTab('REJECTED')}
|
|
||||||
>
|
|
||||||
<span className={`text-sm ${activeTab === 'REJECTED' ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
|
|
||||||
거부
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className={`flex h-12 w-[100px] items-center justify-between px-3 ${activeTab === 'all' ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''}`}
|
|
||||||
onClick={() => setActiveTab('all')}
|
|
||||||
>
|
|
||||||
<span className={`text-sm ${activeTab === 'all' ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
|
|
||||||
전체
|
|
||||||
</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.map((item) => (
|
|
||||||
<ReviewItem
|
|
||||||
key={item.reviewId}
|
|
||||||
title={item.title}
|
|
||||||
createdTime={item.createAt}
|
|
||||||
creatorName={item.nickname}
|
|
||||||
project={project}
|
|
||||||
status={item.status}
|
|
||||||
type={{
|
|
||||||
text: project.projectType,
|
|
||||||
color:
|
|
||||||
project.projectType === 'classification'
|
|
||||||
? '#a2eeef'
|
|
||||||
: project.projectType === 'detection'
|
|
||||||
? '#d4c5f9'
|
|
||||||
: '#f9c5d4',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
15
frontend/src/queries/workspaces/useWorkspaceReviewsQuery.tsx
Normal file
15
frontend/src/queries/workspaces/useWorkspaceReviewsQuery.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { getWorkspaceReviews } from '@/api/workspaceApi';
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export default function useWorkspaceReviewsQuery(
|
||||||
|
workspaceId: number,
|
||||||
|
memberId: number,
|
||||||
|
reviewStatus?: 'REQUESTED' | 'APPROVED' | 'REJECTED',
|
||||||
|
lastReviewId?: number,
|
||||||
|
limitPage?: number
|
||||||
|
) {
|
||||||
|
return useSuspenseQuery({
|
||||||
|
queryKey: ['workspaceReviews', workspaceId, reviewStatus, lastReviewId],
|
||||||
|
queryFn: () => getWorkspaceReviews(workspaceId, memberId, reviewStatus, lastReviewId, limitPage),
|
||||||
|
});
|
||||||
|
}
|
@ -71,7 +71,7 @@ const router = createBrowserRouter([
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${webPath.admin()}/:workspaceId/project/:projectId?`,
|
path: `${webPath.admin()}/:workspaceId`,
|
||||||
element: (
|
element: (
|
||||||
<Suspense fallback={<div></div>}>
|
<Suspense fallback={<div></div>}>
|
||||||
<AdminLayout />
|
<AdminLayout />
|
||||||
|
@ -159,6 +159,7 @@ export interface ReviewRequest {
|
|||||||
// 리뷰 응답 DTO
|
// 리뷰 응답 DTO
|
||||||
export interface ReviewResponse {
|
export interface ReviewResponse {
|
||||||
reviewId: number;
|
reviewId: number;
|
||||||
|
projectId: number;
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
status: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
status: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||||
@ -175,9 +176,11 @@ export interface ReviewStatusRequest {
|
|||||||
|
|
||||||
// 리뷰 이미지 응답 DTO
|
// 리뷰 이미지 응답 DTO
|
||||||
export interface ReviewImageResponse {
|
export interface ReviewImageResponse {
|
||||||
id: number; // 이미지 ID
|
id: number;
|
||||||
imageTitle: string; // 이미지 파일 제목
|
imageTitle: string;
|
||||||
status: 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'COMPLETED';
|
status: 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'COMPLETED';
|
||||||
|
imagePath: string;
|
||||||
|
dataPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 리뷰 디테일 응답 DTO
|
// 리뷰 디테일 응답 DTO
|
||||||
@ -187,6 +190,14 @@ export interface ReviewDetailResponse {
|
|||||||
content: string;
|
content: string;
|
||||||
reviewStatus: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
reviewStatus: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||||
images: ReviewImageResponse[];
|
images: ReviewImageResponse[];
|
||||||
|
createAt: string;
|
||||||
|
updateAt: string;
|
||||||
|
email: string;
|
||||||
|
profileImage: string;
|
||||||
|
nickname: string;
|
||||||
|
reviewerEmail: string;
|
||||||
|
reviewerProfileImage: string;
|
||||||
|
reviewerNickname: string;
|
||||||
}
|
}
|
||||||
// 프로젝트 멤버 응답 DTO
|
// 프로젝트 멤버 응답 DTO
|
||||||
export interface ProjectMemberResponse {
|
export interface ProjectMemberResponse {
|
||||||
|
Loading…
Reference in New Issue
Block a user