Feat: 리뷰 작업중
This commit is contained in:
parent
b9aa255486
commit
1f53a9321b
@ -1,5 +1,5 @@
|
||||
import api from '@/api/axiosConfig';
|
||||
import { ProjectListResponse, ProjectResponse } from '@/types';
|
||||
import { ProjectListResponse, ProjectResponse, ProjectMemberRequest, ProjectMemberResponse } from '@/types';
|
||||
|
||||
export async function getProjectList(
|
||||
workspaceId: number,
|
||||
@ -46,32 +46,6 @@ export async function deleteProject(projectId: number, memberId: number) {
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function addProjectMember(
|
||||
projectId: number,
|
||||
memberId: number,
|
||||
newMemberId: number,
|
||||
privilegeType: string
|
||||
) {
|
||||
return api
|
||||
.post(
|
||||
`/projects/${projectId}/members`,
|
||||
{ memberId: newMemberId, privilegeType },
|
||||
{
|
||||
params: { memberId },
|
||||
}
|
||||
)
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function removeProjectMember(projectId: number, memberId: number, targetMemberId: number) {
|
||||
return api
|
||||
.delete(`/projects/${projectId}/members`, {
|
||||
params: { memberId },
|
||||
data: { memberId: targetMemberId },
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function createProject(
|
||||
workspaceId: number,
|
||||
memberId: number,
|
||||
@ -83,3 +57,43 @@ export async function createProject(
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
// 프로젝트 멤버 조회
|
||||
export async function getProjectMembers(projectId: number, memberId: number) {
|
||||
return api
|
||||
.get<ProjectMemberResponse[]>(`/projects/${projectId}/members`, {
|
||||
params: { memberId },
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
// 프로젝트 멤버 추가
|
||||
export async function addProjectMember(projectId: number, memberId: number, newMember: ProjectMemberRequest) {
|
||||
return api
|
||||
.post<ProjectMemberResponse>(`/projects/${projectId}/members`, newMember, {
|
||||
params: { memberId },
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
// 프로젝트 멤버 권한 수정
|
||||
export async function updateProjectMemberPrivilege(
|
||||
projectId: number,
|
||||
memberId: number,
|
||||
privilegeData: ProjectMemberRequest
|
||||
) {
|
||||
return api
|
||||
.put<ProjectMemberResponse>(`/projects/${projectId}/members`, privilegeData, {
|
||||
params: { memberId },
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
// 프로젝트 멤버 삭제
|
||||
export async function removeProjectMember(projectId: number, memberId: number, targetMemberId: number) {
|
||||
return api
|
||||
.delete(`/projects/${projectId}/members`, {
|
||||
params: { memberId, targetMemberId },
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
@ -50,11 +50,21 @@ export async function updateReviewStatus(projectId: number, reviewId: number, me
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
// 리뷰 상태별 조회
|
||||
export async function getReviewByStatus(projectId: number, memberId: number, reviewStatus: string) {
|
||||
export async function getReviewByStatus(
|
||||
projectId: number,
|
||||
memberId: number,
|
||||
reviewStatus?: 'REQUESTED' | 'APPROVED' | 'REJECTED',
|
||||
lastReviewId?: number,
|
||||
limitPage: number = 10
|
||||
) {
|
||||
return api
|
||||
.get<ReviewResponse[]>(`/projects/${projectId}/reviews`, {
|
||||
params: { memberId, reviewStatus },
|
||||
params: {
|
||||
memberId,
|
||||
limitPage,
|
||||
...(reviewStatus ? { reviewStatus } : {}),
|
||||
...(lastReviewId ? { lastReviewId } : {}),
|
||||
},
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
@ -1,72 +1,24 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Outlet, useParams } from 'react-router-dom';
|
||||
import Header from '../Header';
|
||||
import { ResizablePanelGroup, ResizablePanel } from '../ui/resizable';
|
||||
import AdminProjectSidebar from '../AdminProjectSidebar';
|
||||
import AdminMenuSidebar from '../AdminMenuSidebar';
|
||||
import { Workspace } from '@/types';
|
||||
|
||||
interface AdminLayoutProps {
|
||||
workspace?: Workspace;
|
||||
}
|
||||
|
||||
export default function AdminLayout({ workspace }: AdminLayoutProps) {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
|
||||
const numericWorkspaceId = workspaceId ? parseInt(workspaceId, 10) : 0;
|
||||
|
||||
const effectiveWorkspace: Workspace = workspace || {
|
||||
id: numericWorkspaceId,
|
||||
name: workspaceId ? `workspace-${workspaceId}` : 'default-workspace',
|
||||
projects: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'project1',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'project2',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'project3',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'project4',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'project5',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
export default function AdminLayout() {
|
||||
const { projectId } = useParams<{ projectId?: string }>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header className="fixed left-0 top-0" />
|
||||
<div className="mt-16 h-[calc(100vh-64px)] w-screen">
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<AdminProjectSidebar
|
||||
workspaceName={effectiveWorkspace.name}
|
||||
projects={effectiveWorkspace.projects}
|
||||
/>
|
||||
<AdminProjectSidebar />
|
||||
<ResizablePanel className="flex w-full items-center">
|
||||
<main className="h-full grow">
|
||||
<Outlet />
|
||||
</main>
|
||||
</ResizablePanel>
|
||||
<AdminMenuSidebar />
|
||||
{projectId && <AdminMenuSidebar />}
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</>
|
||||
|
@ -3,11 +3,11 @@ import { cn } from '@/lib/utils';
|
||||
|
||||
export default function AdminMenuSidebar() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { workspaceId, projectId } = useParams<{ workspaceId: string; projectId?: string }>();
|
||||
|
||||
const menuItems = [
|
||||
{ label: '리뷰', path: `/admin/${id}/review` },
|
||||
{ label: '멤버 관리', path: `/admin/${id}/members` },
|
||||
{ label: '리뷰', path: `/admin/${workspaceId}${projectId ? `/project/${projectId}` : ''}/reviews` },
|
||||
{ label: '멤버 관리', path: `/admin/${workspaceId}${projectId ? `/project/${projectId}` : ''}/members` },
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -1,16 +1,31 @@
|
||||
import { ResizablePanel, ResizableHandle } from '../ui/resizable';
|
||||
import { Project } from '@/types';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { SquarePen } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import useProjectListQuery from '@/queries/useProjectListQuery';
|
||||
import { useCreateProject } from '@/hooks/useProjectHooks';
|
||||
import { ProjectRequest } from '@/types';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import ProjectCreateModal from '../ProjectCreateModal';
|
||||
|
||||
interface AdminProjectSidebarProps {
|
||||
workspaceName: string;
|
||||
projects: Project[];
|
||||
}
|
||||
|
||||
export default function AdminProjectSidebar({ workspaceName, projects }: AdminProjectSidebarProps): JSX.Element {
|
||||
export default function AdminProjectSidebar(): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
const profile = useAuthStore((state) => state.profile);
|
||||
const memberId = profile?.id || 0;
|
||||
|
||||
const { data: projectsResponse } = useProjectListQuery(Number(workspaceId), memberId);
|
||||
|
||||
const projects = projectsResponse?.workspaceResponses ?? [];
|
||||
|
||||
const createProject = useCreateProject();
|
||||
|
||||
const handleCreateProject = (data: ProjectRequest) => {
|
||||
createProject.mutate({
|
||||
workspaceId: Number(workspaceId),
|
||||
memberId,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -22,28 +37,24 @@ export default function AdminProjectSidebar({ workspaceName, projects }: AdminPr
|
||||
>
|
||||
<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">
|
||||
{workspaceName}
|
||||
{workspaceId}
|
||||
</h1>
|
||||
<button className="p-2">
|
||||
<SquarePen size={16} />
|
||||
</button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
className="caption border-gray-800 bg-gray-100"
|
||||
onClick={() => console.log('New project')}
|
||||
>
|
||||
새 프로젝트
|
||||
</Button>
|
||||
<ProjectCreateModal
|
||||
buttonClass="caption border-gray-800 bg-gray-100"
|
||||
onSubmit={handleCreateProject}
|
||||
/>
|
||||
</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={() => navigate(`/project/${project.id}`)}
|
||||
onClick={() => navigate(`/admin/${workspaceId}/project/${project.id}`)}
|
||||
>
|
||||
{project.name}
|
||||
{project.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@ export interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export default function Header({ className, ...props }: HeaderProps) {
|
||||
const location = useLocation();
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
const { workspaceId, projectId } = useParams<{ workspaceId: string; projectId?: string }>();
|
||||
const isWorkspaceIdNaN = isNaN(Number(workspaceId));
|
||||
|
||||
const isHomePage = location.pathname === '/';
|
||||
@ -46,7 +46,7 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
labeling
|
||||
</Link>
|
||||
<Link
|
||||
to={`/admin/${workspaceId}`}
|
||||
to={`/admin/${workspaceId}/project/${projectId ?? ''}`}
|
||||
className={cn('text-color-text-default-default', 'font-body', 'text-sm sm:text-base md:text-lg')}
|
||||
>
|
||||
admin
|
||||
|
@ -5,8 +5,6 @@ import { cn } from '@/lib/utils';
|
||||
const sortOptions = [
|
||||
{ value: 'latest', label: '최신 순' },
|
||||
{ value: 'oldest', label: '오래된 순' },
|
||||
{ value: 'comments', label: '댓글 많은 순' },
|
||||
{ value: 'updates', label: '업데이트 많은 순' },
|
||||
];
|
||||
|
||||
interface ReviewSearchInputProps {
|
||||
|
@ -1,152 +1,63 @@
|
||||
import { useState } from 'react';
|
||||
import ReviewItem from './ReviewItem';
|
||||
import ReviewSearchInput from './ReviewSearchInput';
|
||||
import useReviewByStatusQuery from '@/queries/useReviewByStatusQuery';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
interface ReviewListProps {
|
||||
acceptedCount?: number;
|
||||
rejectedCount?: number;
|
||||
pendingCount?: number;
|
||||
totalCount?: number;
|
||||
items?: {
|
||||
title: string;
|
||||
createdTime: string;
|
||||
creatorName: string;
|
||||
project: string;
|
||||
type: 'Classification' | 'Detection' | 'Polygon' | 'Polyline';
|
||||
status: string;
|
||||
}[];
|
||||
}
|
||||
export default function ReviewList(): JSX.Element {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const profile = useAuthStore((state) => state.profile);
|
||||
const memberId = profile?.id || 0;
|
||||
|
||||
const typeColors: Record<'Classification' | 'Detection' | 'Polygon' | 'Polyline', string> = {
|
||||
Classification: '#a2eeef',
|
||||
Detection: '#d4c5f9',
|
||||
Polygon: '#f9c5d4',
|
||||
Polyline: '#c5f9d4',
|
||||
};
|
||||
|
||||
const defaultItems: ReviewListProps['items'] = [
|
||||
{
|
||||
title: '리뷰 항목 1',
|
||||
createdTime: '2024-09-09T10:00:00Z',
|
||||
creatorName: '사용자 1',
|
||||
project: '프로젝트 A',
|
||||
type: 'Classification',
|
||||
status: 'needs_review',
|
||||
},
|
||||
{
|
||||
title: '리뷰 항목 2',
|
||||
createdTime: '2024-09-08T14:30:00Z',
|
||||
creatorName: '사용자 2',
|
||||
project: '프로젝트 B',
|
||||
type: 'Detection',
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
title: '리뷰 항목 3',
|
||||
createdTime: '2024-09-07T08:45:00Z',
|
||||
creatorName: '사용자 3',
|
||||
project: '프로젝트 C',
|
||||
type: 'Polygon',
|
||||
status: 'in_progress',
|
||||
},
|
||||
{
|
||||
title: '리뷰 항목 4',
|
||||
createdTime: '2024-09-06T10:20:00Z',
|
||||
creatorName: '사용자 4',
|
||||
project: '프로젝트 D',
|
||||
type: 'Polyline',
|
||||
status: 'pending',
|
||||
},
|
||||
];
|
||||
|
||||
export default function ReviewList({
|
||||
acceptedCount = 1,
|
||||
rejectedCount = 1,
|
||||
pendingCount = 1,
|
||||
totalCount = 3,
|
||||
items = defaultItems,
|
||||
}: ReviewListProps): JSX.Element {
|
||||
const [activeTab, setActiveTab] = useState('pending');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<'REQUESTED' | 'APPROVED' | 'REJECTED' | 'all'>('REQUESTED');
|
||||
const [, setSearchQuery] = useState('');
|
||||
const [sortValue, setSortValue] = useState('latest');
|
||||
|
||||
const filteredItems = (items ?? [])
|
||||
.filter((item) => {
|
||||
if (activeTab === 'pending') return item.status.toLowerCase() === 'needs_review';
|
||||
if (activeTab === 'accepted') return item.status.toLowerCase() === 'completed';
|
||||
if (activeTab === 'rejected')
|
||||
return item.status.toLowerCase() === 'in_progress' || item.status.toLowerCase() === 'pending';
|
||||
if (activeTab === 'all') return true;
|
||||
return false;
|
||||
})
|
||||
.filter((item) => item.title.includes(searchQuery))
|
||||
.sort((a, b) => {
|
||||
switch (sortValue) {
|
||||
case 'oldest':
|
||||
return new Date(a.createdTime).getTime() - new Date(b.createdTime).getTime();
|
||||
default:
|
||||
return new Date(b.createdTime).getTime() - new Date(a.createdTime).getTime();
|
||||
}
|
||||
});
|
||||
const { data: reviews = [] } = useReviewByStatusQuery(
|
||||
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 === 'pending' ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''
|
||||
}`}
|
||||
onClick={() => setActiveTab('pending')}
|
||||
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 === 'pending' ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
|
||||
<span className={`text-sm ${activeTab === 'REQUESTED' ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
|
||||
요청
|
||||
</span>
|
||||
<span className="flex h-4 w-6 items-center justify-center rounded-[160px] bg-[#ececef] text-xs text-[#626168]">
|
||||
{pendingCount}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`flex h-12 w-[100px] items-center justify-between px-3 ${
|
||||
activeTab === 'accepted' ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''
|
||||
}`}
|
||||
onClick={() => setActiveTab('accepted')}
|
||||
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 === 'accepted' ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
|
||||
<span className={`text-sm ${activeTab === 'APPROVED' ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
|
||||
승인
|
||||
</span>
|
||||
<span className="flex h-4 w-6 items-center justify-center rounded-[160px] bg-[#ececef] text-xs text-[#626168]">
|
||||
{acceptedCount}
|
||||
</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')}
|
||||
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 className={`text-sm ${activeTab === 'REJECTED' ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
|
||||
거부
|
||||
</span>
|
||||
<span className="flex h-4 w-6 items-center justify-center rounded-[160px] bg-[#ececef] text-xs text-[#626168]">
|
||||
{rejectedCount}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`flex h-12 w-[100px] items-center justify-between px-3 ${
|
||||
activeTab === 'all' ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''
|
||||
}`}
|
||||
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>
|
||||
<span className="flex h-4 w-6 items-center justify-center rounded-[160px] bg-[#ececef] text-xs text-[#626168]">
|
||||
{totalCount}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -160,15 +71,15 @@ export default function ReviewList({
|
||||
</div>
|
||||
|
||||
<div className="relative w-full overflow-y-auto px-4">
|
||||
{filteredItems.map((item, index) => (
|
||||
{reviews.map((item) => (
|
||||
<ReviewItem
|
||||
key={index}
|
||||
key={item.reviewId}
|
||||
title={item.title}
|
||||
createdTime={item.createdTime}
|
||||
creatorName={item.creatorName}
|
||||
project={item.project}
|
||||
createdTime={item.createAt}
|
||||
creatorName={item.nickname}
|
||||
project={item.content}
|
||||
status={item.status}
|
||||
type={{ text: item.type, color: typeColors[item.type] }}
|
||||
type={{ text: 'Classification', color: '#a2eeef' }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -113,8 +113,15 @@
|
||||
// });
|
||||
// };
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { createProject, updateProject, deleteProject, addProjectMember, removeProjectMember } from '@/api/projectApi';
|
||||
import { ProjectResponse, ProjectRequest, ProjectMemberRequest } from '@/types';
|
||||
import {
|
||||
createProject,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
addProjectMember,
|
||||
updateProjectMemberPrivilege,
|
||||
removeProjectMember,
|
||||
} from '@/api/projectApi';
|
||||
import { ProjectResponse, ProjectRequest, ProjectMemberRequest, ProjectMemberResponse } from '@/types';
|
||||
|
||||
export const useCreateProject = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@ -151,23 +158,41 @@ export const useDeleteProject = () => {
|
||||
|
||||
export const useAddProjectMember = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<void, Error, { projectId: number; memberId: number; data: ProjectMemberRequest }>({
|
||||
mutationFn: ({ projectId, memberId, data }) =>
|
||||
addProjectMember(projectId, memberId, data.memberId, data.privilegeType),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['project', variables.projectId] });
|
||||
return useMutation<
|
||||
ProjectMemberResponse,
|
||||
Error,
|
||||
{ projectId: number; memberId: number; newMember: ProjectMemberRequest }
|
||||
>({
|
||||
mutationFn: ({ projectId, memberId, newMember }) => addProjectMember(projectId, memberId, newMember),
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['projectMembers', projectId] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 프로젝트 멤버 권한 수정 훅
|
||||
export const useUpdateProjectMemberPrivilege = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<
|
||||
ProjectMemberResponse,
|
||||
Error,
|
||||
{ projectId: number; memberId: number; privilegeData: ProjectMemberRequest }
|
||||
>({
|
||||
mutationFn: ({ projectId, memberId, privilegeData }) =>
|
||||
updateProjectMemberPrivilege(projectId, memberId, privilegeData),
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['projectMembers', projectId] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 프로젝트 멤버 삭제 훅
|
||||
export const useRemoveProjectMember = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<void, Error, { projectId: number; memberId: number; targetMemberId: number }>({
|
||||
mutationFn: ({ projectId, memberId, targetMemberId }) => removeProjectMember(projectId, memberId, targetMemberId),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['project', variables.projectId] });
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['projectMembers', projectId] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
15
frontend/src/pages/AdminIndex.tsx
Normal file
15
frontend/src/pages/AdminIndex.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { Smile } from 'lucide-react';
|
||||
|
||||
export default function AdminIndex() {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<Smile
|
||||
size={48}
|
||||
className="mb-2 text-gray-300"
|
||||
/>
|
||||
<div className="body text-gray-400">프로젝트를 선택하거나 생성하세요.</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
10
frontend/src/queries/useProjectMembersQuery.ts
Normal file
10
frontend/src/queries/useProjectMembersQuery.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { getProjectMembers } from '@/api/projectApi';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { ProjectMemberResponse } from '@/types';
|
||||
|
||||
export default function useProjectMembersQuery(projectId: number, memberId: number) {
|
||||
return useSuspenseQuery<ProjectMemberResponse[]>({
|
||||
queryKey: ['projectMembers', projectId, memberId],
|
||||
queryFn: () => getProjectMembers(projectId, memberId),
|
||||
});
|
||||
}
|
@ -1,7 +1,11 @@
|
||||
import { getReviewByStatus } from '@/api/reviewApi';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
|
||||
export default function useReviewByStatusQuery(projectId: number, memberId: number, reviewStatus: string) {
|
||||
export default function useReviewByStatusQuery(
|
||||
projectId: number,
|
||||
memberId: number,
|
||||
reviewStatus: 'REQUESTED' | 'APPROVED' | 'REJECTED' | undefined
|
||||
) {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ['reviewByStatus', projectId, reviewStatus],
|
||||
queryFn: () => getReviewByStatus(projectId, memberId, reviewStatus),
|
||||
|
@ -9,16 +9,13 @@ import ReviewList from '@/components/ReviewList';
|
||||
import AdminMemberManage from '@/components/AdminMemberManage';
|
||||
import OAuthCallback from '@/components/OAuthCallback';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { Suspense } from 'react';
|
||||
import WorkspaceBrowseIndex from '@/pages/WorkspaceBrowseIndex';
|
||||
|
||||
import AdminIndex from '@/pages/AdminIndex';
|
||||
export const webPath = {
|
||||
home: () => '/',
|
||||
browse: () => '/browse',
|
||||
workspace: () => '/workspace',
|
||||
// workspace: (workspaceId: string, projectId?: string) =>
|
||||
// projectId ? `/workspace/${workspaceId}/project/${projectId}` : `/workspace/${workspaceId}`,
|
||||
admin: () => `/admin`,
|
||||
oauthCallback: () => '/redirect/oauth2',
|
||||
};
|
||||
@ -73,15 +70,15 @@ const router = createBrowserRouter([
|
||||
],
|
||||
},
|
||||
{
|
||||
path: `${webPath.admin()}/:workspaceId`,
|
||||
path: `${webPath.admin()}/:workspaceId/project/:projectId?`,
|
||||
element: <AdminLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Navigate to="review" />,
|
||||
element: <AdminIndex />,
|
||||
},
|
||||
{
|
||||
path: 'review',
|
||||
path: 'reviews',
|
||||
element: <ReviewList />,
|
||||
},
|
||||
{
|
||||
|
@ -149,23 +149,52 @@ export interface AutoLabelingResponse {
|
||||
data: string;
|
||||
}
|
||||
|
||||
// 리뷰 요청 DTO
|
||||
export interface ReviewRequest {
|
||||
title: string;
|
||||
content: string;
|
||||
imageIds: number[];
|
||||
}
|
||||
|
||||
// 리뷰 응답 DTO
|
||||
export interface ReviewResponse {
|
||||
reviewId: number;
|
||||
title: string;
|
||||
content: string;
|
||||
status: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||
nickname: string;
|
||||
email: string;
|
||||
createAt: string;
|
||||
updateAt: string;
|
||||
}
|
||||
|
||||
// 리뷰 상태 요청 DTO
|
||||
export interface ReviewStatusRequest {
|
||||
reviewStatus: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||
}
|
||||
|
||||
// 리뷰 이미지 응답 DTO
|
||||
export interface ReviewImageResponse {
|
||||
id: number; // 이미지 ID
|
||||
imageTitle: string; // 이미지 파일 제목
|
||||
status: 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'COMPLETED';
|
||||
}
|
||||
|
||||
// 리뷰 디테일 응답 DTO
|
||||
export interface ReviewDetailResponse {
|
||||
reviewId: number;
|
||||
title: string;
|
||||
content: string;
|
||||
reviewStatus: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||
images: ReviewImageResponse[];
|
||||
}
|
||||
// 프로젝트 멤버 응답 DTO
|
||||
export interface ProjectMemberResponse {
|
||||
memberId: number;
|
||||
nickname: string;
|
||||
profileImage: string;
|
||||
privilegeType: 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER';
|
||||
}
|
||||
export interface FolderIdResponse {
|
||||
id: number;
|
||||
title: string;
|
||||
@ -175,7 +204,7 @@ export interface ImageDetailResponse {
|
||||
id: number;
|
||||
imageTitle: string;
|
||||
imageUrl: string;
|
||||
data: string | null; // PENDING 상태라면 null
|
||||
data: string | null;
|
||||
status: 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'COMPLETED';
|
||||
}
|
||||
|
||||
@ -192,12 +221,16 @@ export interface LabelSaveRequest {
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface ReviewDetailResponse {
|
||||
reviewId: number;
|
||||
title: string;
|
||||
content: string;
|
||||
reviewStatus: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||
images: ImageResponse[];
|
||||
export interface ProjectMemberResponse {
|
||||
memberId: number;
|
||||
nickname: string;
|
||||
profileImage: string;
|
||||
privilegeType: 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER';
|
||||
}
|
||||
|
||||
export interface ProjectMemberRequest {
|
||||
memberId: number;
|
||||
privilegeType: 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER';
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
|
Loading…
Reference in New Issue
Block a user