Feat: 워크스페이스 리뷰 전체 조회 구현
This commit is contained in:
parent
4b48718cbe
commit
15bf3b8d80
@ -1,5 +1,5 @@
|
||||
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) {
|
||||
return api
|
||||
@ -49,10 +49,21 @@ export async function addWorkspaceMember(workspaceId: number, memberId: number,
|
||||
.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
|
||||
.delete(`/workspaces/${workspaceId}/members/${targetMemberId}`, {
|
||||
params: { memberId },
|
||||
.get<ReviewResponse[]>(`/workspaces/${workspaceId}/reviews`, {
|
||||
params: {
|
||||
memberId,
|
||||
limitPage,
|
||||
...(reviewStatus ? { reviewStatus } : {}),
|
||||
...(lastReviewId ? { lastReviewId } : {}),
|
||||
},
|
||||
})
|
||||
.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 { ResizablePanelGroup, ResizablePanel } from '../ui/resizable';
|
||||
import AdminProjectSidebar from '../AdminProjectSidebar';
|
||||
import AdminMenuSidebar from '../AdminMenuSidebar';
|
||||
|
||||
export default function AdminLayout() {
|
||||
const { projectId } = useParams<{ projectId?: string }>();
|
||||
const isIndexPage = useMatch({ path: '/admin/:workspaceId', end: true });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header className="fixed left-0 top-0" />
|
||||
<div className="mt-16 h-[calc(100vh-64px)] w-screen">
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<AdminProjectSidebar />
|
||||
{!isIndexPage && <AdminProjectSidebar />}
|
||||
|
||||
<ResizablePanel className="flex w-full items-center">
|
||||
<main className="h-full grow">
|
||||
<Outlet />
|
||||
</main>
|
||||
</ResizablePanel>
|
||||
{projectId && <AdminMenuSidebar />}
|
||||
<AdminMenuSidebar />
|
||||
</ResizablePanelGroup>
|
||||
{isIndexPage && (
|
||||
<main className="h-full w-full">
|
||||
<Outlet />
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -3,11 +3,17 @@ import { cn } from '@/lib/utils';
|
||||
|
||||
export default function AdminMenuSidebar() {
|
||||
const navigate = useNavigate();
|
||||
const { workspaceId, projectId } = useParams<{ workspaceId: string; projectId?: string }>();
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
|
||||
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 (
|
||||
|
@ -1,20 +1,24 @@
|
||||
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 useProjectListQuery from '@/queries/projects/useProjectListQuery';
|
||||
import useCreateProjectQuery from '@/queries/projects/useCreateProjectQuery';
|
||||
import useWorkspaceQuery from '@/queries/workspaces/useWorkspaceQuery';
|
||||
import { ProjectRequest } from '@/types';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import ProjectCreateModal from '../ProjectCreateModal';
|
||||
|
||||
export default function AdminProjectSidebar(): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
const profile = useAuthStore((state) => state.profile);
|
||||
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 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 (
|
||||
<>
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
{workspaceId}
|
||||
<h1
|
||||
className="heading w-full cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap text-xl font-bold text-gray-900"
|
||||
onClick={handleHeaderClick}
|
||||
>
|
||||
{workspaceTitle}
|
||||
</h1>
|
||||
<button className="p-2">
|
||||
<SquarePen size={16} />
|
||||
@ -52,7 +74,7 @@ export default function AdminProjectSidebar(): JSX.Element {
|
||||
<button
|
||||
key={project.id}
|
||||
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}
|
||||
</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 { ProjectResponse } from '@/types';
|
||||
|
||||
import useProjectQuery from '@/queries/projects/useProjectQuery';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
interface ReviewItemProps {
|
||||
title: string;
|
||||
createdTime: string;
|
||||
creatorName: string;
|
||||
project: ProjectResponse;
|
||||
status: string;
|
||||
type: { text: 'classification' | 'detection' | 'segmentation'; color: string };
|
||||
projectId: number;
|
||||
status: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||
type?: { text: 'classification' | 'detection' | 'segmentation'; color: string };
|
||||
}
|
||||
|
||||
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" />,
|
||||
};
|
||||
|
||||
export default function ReviewItem({ title, createdTime, creatorName, project, status, type }: ReviewItemProps) {
|
||||
const icon = typeIcons[project.projectType];
|
||||
|
||||
export default function ReviewItem({ title, createdTime, creatorName, projectId, status, type }: ReviewItemProps) {
|
||||
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 (
|
||||
<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">
|
||||
@ -26,7 +28,7 @@ export default function ReviewItem({ title, createdTime, creatorName, project, s
|
||||
<p className="mt-1 text-xs text-[#737278]">by {creatorName}</p>
|
||||
<div className="mt-1 flex items-center">
|
||||
<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>
|
||||
{type && (
|
||||
<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 ReviewItem from './ReviewItem';
|
||||
import ReviewSearchInput from './ReviewSearchInput';
|
||||
import useReviewByStatusQuery from '@/queries/reviews/useReviewByStatusQuery';
|
||||
import useProjectQuery from '@/queries/projects/useProjectQuery';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import ProjectReviewList from './ProjectReviewList';
|
||||
import WorkspaceReviewList from './WorkspaceReviewList';
|
||||
|
||||
export default function ReviewList(): JSX.Element {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const profile = useAuthStore((state) => state.profile);
|
||||
const memberId = profile?.id || 0;
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
const location = useLocation();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const projectId = searchParams.get('projectId');
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'REQUESTED' | 'APPROVED' | 'REJECTED' | 'all'>('REQUESTED');
|
||||
const [, setSearchQuery] = useState('');
|
||||
const [sortValue, setSortValue] = useState('latest');
|
||||
|
||||
const { data: project } = useProjectQuery(Number(projectId), memberId);
|
||||
|
||||
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 === '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>
|
||||
return projectId && Number(projectId) > 0 ? (
|
||||
<ProjectReviewList
|
||||
projectId={Number(projectId)}
|
||||
workspaceId={Number(workspaceId)}
|
||||
/>
|
||||
) : (
|
||||
<WorkspaceReviewList workspaceId={Number(workspaceId)} />
|
||||
);
|
||||
}
|
||||
|
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: (
|
||||
<Suspense fallback={<div></div>}>
|
||||
<AdminLayout />
|
||||
|
@ -159,6 +159,7 @@ export interface ReviewRequest {
|
||||
// 리뷰 응답 DTO
|
||||
export interface ReviewResponse {
|
||||
reviewId: number;
|
||||
projectId: number;
|
||||
title: string;
|
||||
content: string;
|
||||
status: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||
@ -175,9 +176,11 @@ export interface ReviewStatusRequest {
|
||||
|
||||
// 리뷰 이미지 응답 DTO
|
||||
export interface ReviewImageResponse {
|
||||
id: number; // 이미지 ID
|
||||
imageTitle: string; // 이미지 파일 제목
|
||||
id: number;
|
||||
imageTitle: string;
|
||||
status: 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'COMPLETED';
|
||||
imagePath: string;
|
||||
dataPath: string;
|
||||
}
|
||||
|
||||
// 리뷰 디테일 응답 DTO
|
||||
@ -187,6 +190,14 @@ export interface ReviewDetailResponse {
|
||||
content: string;
|
||||
reviewStatus: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||
images: ReviewImageResponse[];
|
||||
createAt: string;
|
||||
updateAt: string;
|
||||
email: string;
|
||||
profileImage: string;
|
||||
nickname: string;
|
||||
reviewerEmail: string;
|
||||
reviewerProfileImage: string;
|
||||
reviewerNickname: string;
|
||||
}
|
||||
// 프로젝트 멤버 응답 DTO
|
||||
export interface ProjectMemberResponse {
|
||||
|
Loading…
Reference in New Issue
Block a user