Merge branch 'fe/refactor/improve-design' into 'fe/develop'
Refactor: 리뷰 리팩토링 및 디자인 개선 - S11P21S002-248 See merge request s11-s-project/S11P21S002!260
This commit is contained in:
commit
76fd7cf419
@ -2,6 +2,8 @@ import { Briefcase, Tag, Box, Layers } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import useProjectQuery from '@/queries/projects/useProjectQuery';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import { cn } from '@/lib/utils';
|
||||
import formatDateTime from '@/utils/formatDateTime';
|
||||
import { ReviewStatus } from '@/types';
|
||||
|
||||
interface ReviewItemProps {
|
||||
@ -43,11 +45,11 @@ export default function ReviewItem({
|
||||
>
|
||||
<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">
|
||||
<p className="text-sm font-semibold text-[#333238]">{title}</p>
|
||||
<p className="mt-1 text-xs text-[#737278]">by {creatorName}</p>
|
||||
<p className="text-sm font-semibold text-black">{title}</p>
|
||||
<p className="mt-1 text-xs text-gray-500">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]">{projectData?.title}</p>
|
||||
<Briefcase className="h-3 w-3 text-gray-500" />
|
||||
<p className="ml-1 text-xs text-gray-500">{projectData?.title}</p>
|
||||
</div>
|
||||
{type && (
|
||||
<div
|
||||
@ -60,8 +62,19 @@ export default function ReviewItem({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<div className="rounded-full bg-[#cbe2f9] px-3 py-0.5 text-center text-xs text-[#0b5cad]">{status}</div>
|
||||
<p className="text-xs text-[#737278]">Created at {createdTime}</p>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-full px-3 py-0.5 text-center text-xs',
|
||||
status === 'APPROVED'
|
||||
? 'bg-green-100 text-green-600'
|
||||
: status === 'REJECTED'
|
||||
? 'bg-red-100 text-red-600'
|
||||
: 'bg-blue-100 text-blue-600'
|
||||
)}
|
||||
>
|
||||
{status}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">Created at {formatDateTime(createdTime)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
@ -4,8 +4,8 @@ import { ReviewResponse, ReviewStatus } from '@/types';
|
||||
|
||||
interface ReviewListProps {
|
||||
reviews: ReviewResponse[];
|
||||
activeTab: ReviewStatus | 'all';
|
||||
setActiveTab: React.Dispatch<React.SetStateAction<ReviewStatus | 'all'>>;
|
||||
activeTab: ReviewStatus | 'ALL';
|
||||
setActiveTab: React.Dispatch<React.SetStateAction<ReviewStatus | 'ALL'>>;
|
||||
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
|
||||
sortValue: string;
|
||||
setSortValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
@ -22,18 +22,18 @@ export default function ReviewList({
|
||||
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) => (
|
||||
<div className="relative w-full px-4">
|
||||
<div className="relative w-full">
|
||||
<div className="flex w-full items-center border-b-[1px] border-solid border-gray-300">
|
||||
{['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]' : ''
|
||||
className={`flex h-12 w-[100px] items-center justify-center px-3 ${
|
||||
activeTab === tab ? 'border-b-[3px] border-blue-500' : 'border-b-[3px] border-transparent'
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab as typeof activeTab)}
|
||||
>
|
||||
<span className={`text-sm ${activeTab === tab ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
|
||||
<span className={`text-sm ${activeTab === tab ? 'font-semibold' : 'font-normal'} text-black`}>
|
||||
{tab === 'REQUESTED' ? '요청' : tab === 'APPROVED' ? '승인' : tab === 'REJECTED' ? '거부' : '전체'}
|
||||
</span>
|
||||
</button>
|
||||
@ -41,7 +41,7 @@ export default function ReviewList({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full px-4">
|
||||
<div className="relative w-full">
|
||||
<ReviewSearchInput
|
||||
onSearchChange={setSearchQuery}
|
||||
onSortChange={setSortValue}
|
||||
@ -49,7 +49,7 @@ export default function ReviewList({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full overflow-y-auto px-4">
|
||||
<div className="relative w-full overflow-y-auto">
|
||||
{reviews.length === 0 ? (
|
||||
<div className="py-4 text-center">리뷰가 없습니다.</div>
|
||||
) : (
|
||||
@ -59,7 +59,7 @@ export default function ReviewList({
|
||||
workspaceId={workspaceId}
|
||||
reviewId={item.reviewId}
|
||||
title={item.title}
|
||||
createdTime={item.createAt}
|
||||
createdTime={item.createdAt}
|
||||
creatorName={item.author.nickname}
|
||||
projectId={item.projectId}
|
||||
status={item.status}
|
||||
|
@ -64,7 +64,7 @@ export default function ProjectFileItem({
|
||||
size={12}
|
||||
className="shrink-0 stroke-blue-400"
|
||||
/>
|
||||
) : item.status === 'REVIEW_REJECTED' ? (
|
||||
) : item.status === 'REVIEW_REJECT' ? (
|
||||
<CircleSlash
|
||||
size={12}
|
||||
className="shrink-0 stroke-red-400"
|
||||
|
@ -25,8 +25,8 @@ export const reviewHandlers = [
|
||||
dataPath: 'https://example.com/data1.json',
|
||||
},
|
||||
],
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
author: { id: 1, nickname: 'Author', profileImage: '', email: 'author@example.com' },
|
||||
reviewer: { id: 2, nickname: 'Reviewer', profileImage: '', email: 'reviewer@example.com' },
|
||||
};
|
||||
@ -49,8 +49,8 @@ export const reviewHandlers = [
|
||||
title: reviewData.title,
|
||||
content: reviewData.content,
|
||||
status: 'REQUESTED',
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
author: { id: 1, nickname: 'Author', profileImage: '', email: 'author@example.com' },
|
||||
};
|
||||
|
||||
@ -75,8 +75,8 @@ export const reviewHandlers = [
|
||||
title: reviewData.title,
|
||||
content: reviewData.content,
|
||||
status: 'REQUESTED',
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
author: { id: 1, nickname: 'Author', profileImage: '', email: 'author@example.com' },
|
||||
};
|
||||
|
||||
@ -112,8 +112,8 @@ export const reviewHandlers = [
|
||||
title: `Updated Review ${reviewId}`,
|
||||
content: 'Updated content',
|
||||
status: statusRequest.reviewStatus,
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
author: { id: 1, nickname: 'Author', profileImage: '', email: 'author@example.com' },
|
||||
};
|
||||
|
||||
@ -145,9 +145,9 @@ export const reviewHandlers = [
|
||||
reviewId: index + 1,
|
||||
title: `Review ${index + 1}`,
|
||||
content: `Review content ${index + 1}`,
|
||||
status: (reviewStatus || 'REQUESTED') as ReviewStatus,
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
status: reviewStatus as ReviewStatus,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
author: { id: 1, nickname: 'Author', profileImage: '', email: 'author@example.com' },
|
||||
}));
|
||||
|
||||
|
@ -141,8 +141,8 @@ export const workspaceHandlers = [
|
||||
profileImage: 'reviewer1.jpg',
|
||||
email: 'reviewer1@example.com',
|
||||
},
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
reviewId: 2,
|
||||
@ -156,8 +156,8 @@ export const workspaceHandlers = [
|
||||
profileImage: 'reviewer2.jpg',
|
||||
email: 'reviewer2@example.com',
|
||||
},
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -11,12 +11,12 @@ export default function ProjectReviewList() {
|
||||
const profile = useAuthStore((state) => state.profile);
|
||||
const memberId = profile?.id || 0;
|
||||
|
||||
const [activeTab, setActiveTab] = useState<ReviewStatus | 'all'>('REQUESTED');
|
||||
const [activeTab, setActiveTab] = useState<ReviewStatus | 'ALL'>('REQUESTED');
|
||||
const [, setSearchQuery] = useState('');
|
||||
const [sortValue, setSortValue] = useState('latest');
|
||||
|
||||
const sortDirection = sortValue === 'latest' ? 0 : 1;
|
||||
const reviewStatus = activeTab !== 'all' ? activeTab : undefined;
|
||||
const reviewStatus = activeTab !== 'ALL' ? activeTab : undefined;
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = useReviewByStatusQuery(
|
||||
Number(projectId),
|
||||
|
@ -60,25 +60,25 @@ export default function ReviewDetail(): JSX.Element {
|
||||
return (
|
||||
<div className="review-detail-container p-4">
|
||||
<div className="header mb-4">
|
||||
<h1 className="text-2xl font-bold">{reviewDetail.title}</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
작성자: {reviewDetail.author.nickname} ({reviewDetail.author.email})
|
||||
<h1 className="heading mb-2">{reviewDetail.title}</h1>
|
||||
<p className="body-small text-gray-500">
|
||||
작성자 : {reviewDetail.author.nickname} ({reviewDetail.author.email})
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">작성일: {new Date(reviewDetail.createAt).toLocaleDateString()}</p>
|
||||
<p className="text-sm text-gray-500">수정일: {new Date(reviewDetail.updateAt).toLocaleDateString()}</p>
|
||||
<p className="body-small text-gray-500">작성일 : {new Date(reviewDetail.createdAt).toLocaleDateString()}</p>
|
||||
<p className="body-small text-gray-500">수정일 : {new Date(reviewDetail.updatedAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full px-4">
|
||||
<div className="flex w-full items-center border-b-[0.67px] border-solid border-[#dcdcde]">
|
||||
<div className="relative w-full">
|
||||
<div className="flex w-full items-center border-b-[1px] border-solid border-gray-300">
|
||||
{['content', 'images'].map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={`flex h-12 w-[100px] items-center justify-center px-3 ${
|
||||
activeTab === tab ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''
|
||||
activeTab === tab ? 'border-b-[3px] border-blue-500' : 'border-b-[3px] border-transparent'
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab as 'content' | 'images')}
|
||||
>
|
||||
<span className={`text-sm ${activeTab === tab ? 'font-semibold' : 'font-normal'} text-[#333238]`}>
|
||||
<span className={`text-sm ${activeTab === tab ? 'font-semibold' : 'font-normal'} text-black`}>
|
||||
{tab === 'content' ? '내용' : '이미지'}
|
||||
</span>
|
||||
</button>
|
||||
@ -111,7 +111,7 @@ export default function ReviewDetail(): JSX.Element {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(reviewDetail.reviewStatus === 'APPROVED' || reviewDetail.reviewStatus === 'REJECT') && (
|
||||
{(reviewDetail.reviewStatus === 'APPROVED' || reviewDetail.reviewStatus === 'REJECTED') && (
|
||||
<div className="reviewer-info mt-6">
|
||||
<h2 className="text-lg font-semibold">
|
||||
리뷰 상태: {reviewDetail.reviewStatus === 'APPROVED' ? '승인됨' : '거부됨'}
|
||||
@ -126,7 +126,7 @@ export default function ReviewDetail(): JSX.Element {
|
||||
<p className="font-bold">{reviewDetail.reviewer.nickname}</p>
|
||||
<p className="text-gray-500">{reviewDetail.reviewer.email}</p>
|
||||
<p className="text-gray-500">
|
||||
{reviewDetail.reviewStatus === 'APPROVED' ? '승인한 사람:' : '거부한 사람:'}{' '}
|
||||
{reviewDetail.reviewStatus === 'APPROVED' ? '승인한 사람 : ' : '거부한 사람 : '}
|
||||
{reviewDetail.reviewer.nickname}
|
||||
</p>
|
||||
</div>
|
||||
@ -137,7 +137,7 @@ export default function ReviewDetail(): JSX.Element {
|
||||
<Link to={`/admin/${workspaceId}/reviews`}>
|
||||
<Button variant="black">목록으로 돌아가기</Button>
|
||||
</Link>
|
||||
{isAdminOrManager && reviewDetail.reviewStatus !== 'APPROVED' && reviewDetail.reviewStatus !== 'REJECT' && (
|
||||
{isAdminOrManager && reviewDetail.reviewStatus !== 'APPROVED' && reviewDetail.reviewStatus !== 'REJECTED' && (
|
||||
<>
|
||||
<Button
|
||||
variant="red"
|
||||
|
@ -11,12 +11,12 @@ export default function WorkspaceReviewList() {
|
||||
const profile = useAuthStore((state) => state.profile);
|
||||
const memberId = profile?.id || 0;
|
||||
|
||||
const [activeTab, setActiveTab] = useState<ReviewStatus | 'all'>('REQUESTED');
|
||||
const [activeTab, setActiveTab] = useState<ReviewStatus | 'ALL'>('REQUESTED');
|
||||
const [, setSearchQuery] = useState('');
|
||||
const [sortValue, setSortValue] = useState('latest');
|
||||
|
||||
const sortDirection = sortValue === 'latest' ? 0 : 1;
|
||||
const reviewStatus = activeTab !== 'all' ? activeTab : undefined;
|
||||
const reviewStatus = activeTab !== 'ALL' ? activeTab : undefined;
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = useWorkspaceReviewsQuery(
|
||||
Number(workspaceId),
|
||||
|
@ -1,4 +1,4 @@
|
||||
export type ImageStatus = 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'REVIEW_REJECTED' | 'COMPLETED';
|
||||
export type ImageStatus = 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'REVIEW_REJECT' | 'COMPLETED';
|
||||
|
||||
export interface ImageResponse {
|
||||
id: number;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ImageStatus } from './imageTypes';
|
||||
import { MemberResponse } from './memberTypes';
|
||||
|
||||
export type ReviewStatus = 'REQUESTED' | 'APPROVED' | 'REJECT';
|
||||
export type ReviewStatus = 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||
|
||||
// 리뷰 관련 DTO
|
||||
export interface ReviewRequest {
|
||||
@ -17,8 +17,8 @@ export interface ReviewResponse {
|
||||
content: string;
|
||||
status: ReviewStatus;
|
||||
author: MemberResponse;
|
||||
createAt: string;
|
||||
updateAt: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ReviewStatusRequest {
|
||||
@ -39,8 +39,8 @@ export interface ReviewDetailResponse {
|
||||
content: string;
|
||||
reviewStatus: ReviewStatus;
|
||||
images: ReviewImageResponse[];
|
||||
createAt: string;
|
||||
updateAt: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
author: MemberResponse;
|
||||
reviewer: MemberResponse;
|
||||
}
|
||||
|
6
frontend/src/utils/formatDateTime.ts
Normal file
6
frontend/src/utils/formatDateTime.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export default function formatDateTime(dateTimeString: string): string {
|
||||
const [date, time] = dateTimeString.split('T');
|
||||
const [hours, minutes] = time.split(':');
|
||||
|
||||
return `${date} ${hours}:${minutes}`;
|
||||
}
|
Loading…
Reference in New Issue
Block a user