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:
조현수 2024-10-02 13:28:57 +09:00
commit 76fd7cf419
11 changed files with 75 additions and 56 deletions

View File

@ -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>

View File

@ -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}

View File

@ -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"

View File

@ -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' },
}));

View File

@ -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(),
},
];

View File

@ -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),

View File

@ -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"

View File

@ -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),

View File

@ -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;

View File

@ -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;
}

View 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}`;
}