diff --git a/frontend/src/components/ReviewList/ReviewItem.tsx b/frontend/src/components/ReviewList/ReviewItem.tsx new file mode 100644 index 0000000..4784329 --- /dev/null +++ b/frontend/src/components/ReviewList/ReviewItem.tsx @@ -0,0 +1,55 @@ +import { Briefcase, Tag, Box, Layers, Pen } from 'lucide-react'; + +interface ReviewItemProps { + title: string; + createdTime: string; + creatorName: string; + project: string; + status: string; + type: { text: 'Classification' | 'Detection' | 'Polygon' | 'Polyline'; color: string }; +} + +const typeIcons: Record<'Classification' | 'Detection' | 'Polygon' | 'Polyline', JSX.Element> = { + Classification: , + Detection: , + Polygon: , + Polyline: , +}; + +const typeStyles: Record<'Classification' | 'Detection' | 'Polygon' | 'Polyline', string> = { + Classification: '#a2eeef', + Detection: '#d4c5f9', + Polygon: '#f9c5d4', + Polyline: '#c5f9d4', +}; + +export default function ReviewItem({ title, createdTime, creatorName, project, status, type }: ReviewItemProps) { + const icon = typeIcons[type.text]; + const bgColor = typeStyles[type.text]; + + return ( +
+
+

{title}

+

by {creatorName}

+
+ +

{project}

+
+ {type && ( +
+ {icon} + {type.text} +
+ )} +
+
+
{status}
+

Created at {createdTime}

+
+
+ ); +} diff --git a/frontend/src/components/ReviewList/ReviewSearchInput.tsx b/frontend/src/components/ReviewList/ReviewSearchInput.tsx new file mode 100644 index 0000000..45b667f --- /dev/null +++ b/frontend/src/components/ReviewList/ReviewSearchInput.tsx @@ -0,0 +1,48 @@ +import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'; +import SearchInput from '@/components/ui/search-input'; +import { cn } from '@/lib/utils'; + +const sortOptions = [ + { value: 'latest', label: '최신 순' }, + { value: 'oldest', label: '오래된 순' }, + { value: 'comments', label: '댓글 많은 순' }, + { value: 'updates', label: '업데이트 많은 순' }, +]; + +interface ReviewSearchInputProps { + onSearchChange: (value: string) => void; + onSortChange: (value: string) => void; + sortValue: string; +} + +export default function ReviewSearchInput({ onSearchChange, onSortChange, sortValue }: ReviewSearchInputProps) { + return ( +
+
+ onSearchChange(e.target.value)} + /> + +
+
+ ); +} diff --git a/frontend/src/components/ReviewList/index.stories.tsx b/frontend/src/components/ReviewList/index.stories.tsx new file mode 100644 index 0000000..2e9d32d --- /dev/null +++ b/frontend/src/components/ReviewList/index.stories.tsx @@ -0,0 +1,63 @@ +import '@/index.css'; +import type { Meta, StoryObj } from '@storybook/react'; +import ReviewList from '.'; + +const meta: Meta = { + title: 'Components/ReviewList', + component: ReviewList, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + acceptedCount: 10, + rejectedCount: 5, + pendingCount: 7, + totalCount: 22, + items: [ + { + title: '갤럭시22 생산 라인 이물질 분류', + createdTime: '2 hours ago', + creatorName: 'Kim Tae Su', + project: 'Project A', + type: 'Classification', + status: 'needs_review', + }, + { + title: '갤럭시 흠집 객체 탐지', + createdTime: '3 hours ago', + creatorName: 'Kim Tae Su', + project: 'Project B', + type: 'Detection', + status: 'completed', + }, + { + title: '갤럭시 흠집 경계 폴리곤', + createdTime: '5 hours ago', + creatorName: 'Kim Tae Su', + project: 'Project C', + type: 'Polygon', + status: 'in_progress', + }, + { + title: '갤럭시 흠집 폴리라인', + createdTime: '1 day ago', + creatorName: 'Kim Tae Su', + project: 'Project D', + type: 'Polyline', + status: 'completed', + }, + { + title: '갤럭시 흠집 디텍션 허가 요청', + createdTime: '6 hours ago', + creatorName: 'Kim Tae Su', + project: 'Project E', + type: 'Detection', + status: 'pending', + }, + ], + }, +}; diff --git a/frontend/src/components/ReviewList/index.tsx b/frontend/src/components/ReviewList/index.tsx new file mode 100644 index 0000000..7abaf45 --- /dev/null +++ b/frontend/src/components/ReviewList/index.tsx @@ -0,0 +1,142 @@ +import { useState } from 'react'; +import ReviewItem from './ReviewItem'; +import ReviewSearchInput from './ReviewSearchInput'; + +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; + }[]; +} + +const typeColors: Record<'Classification' | 'Detection' | 'Polygon' | 'Polyline', string> = { + Classification: '#a2eeef', + Detection: '#d4c5f9', + Polygon: '#f9c5d4', + Polyline: '#c5f9d4', +}; + +export default function ReviewList({ + acceptedCount, + rejectedCount, + pendingCount, + totalCount, + items, +}: ReviewListProps): JSX.Element { + const [activeTab, setActiveTab] = useState('pending'); + const [searchQuery, 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(); + } + }); + + return ( +
+
+
+ + + + + + + +
+
+ +
+ +
+ +
+ {filteredItems.map((item, index) => ( + + ))} +
+
+ ); +}