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}
+
+ {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) => (
+
+ ))}
+
+
+ );
+}