Feat: 리뷰리스트 기반으로 TaskList를 대체할 TaskList 컴포넌트 구현

This commit is contained in:
정현조 2024-09-05 17:56:56 +09:00
parent 3266a3cd0b
commit a0dc26719b
4 changed files with 350 additions and 0 deletions

View File

@ -0,0 +1,75 @@
import { Briefcase, MessageCircle, RefreshCw, Tag, Box, Layers, Pen } from 'lucide-react';
interface TaskItemProps {
title: string;
createdTime: string;
creatorName: string;
project: string;
status: string;
commentsCount: number;
updatesCount: number;
lastUpdated: string;
type: 'Classification' | 'Detection' | 'Polygon' | 'Polyline';
bgColor?: string;
memberCount: number;
}
const typeIcons: Record<'Classification' | 'Detection' | 'Polygon' | 'Polyline', JSX.Element> = {
Classification: <Tag className="h-4 w-4 text-white" />,
Detection: <Box className="h-4 w-4 text-white" />,
Polygon: <Layers className="h-4 w-4 text-white" />,
Polyline: <Pen className="h-4 w-4 text-white" />,
};
export default function TaskItem({
title,
createdTime,
creatorName,
project,
status,
commentsCount,
updatesCount,
lastUpdated,
type,
bgColor = '#f3f4f6',
memberCount,
}: TaskItemProps) {
const icon = typeIcons[type];
return (
<div className="flex h-[120px] 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]">
{createdTime} 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}</p>
</div>
<div
className="mt-1 inline-flex max-w-fit items-center gap-1 rounded-full px-3 py-1 text-xs text-white"
style={{ backgroundColor: bgColor, padding: '1px 5px' }}
>
{icon}
<span className="overflow-hidden text-ellipsis whitespace-nowrap">{type}</span>
</div>
</div>
<div className="flex flex-col items-end gap-1">
<div className="flex items-center gap-2">
<div className="rounded-full bg-[#cbe2f9] px-3 py-0.5 text-center text-xs text-[#0b5cad]">{status}</div>
<div className="flex items-center gap-1">
<MessageCircle className="h-4 w-4 text-[#737278]" />
<span className="text-sm">{commentsCount}</span>
</div>
<div className="flex items-center gap-1">
<RefreshCw className="h-4 w-4 text-[#737278]" />
<span className="text-sm">{updatesCount}</span>
</div>
</div>
<p className="text-xs text-[#737278]">Updated at {lastUpdated}</p>
<div className="text-sm text-gray-500 dark:text-gray-400">{`멤버 ${memberCount}`}</div>
</div>
</div>
);
}

View File

@ -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 TaskSearchInputProps {
onSearchChange: (value: string) => void;
onSortChange: (value: string) => void;
sortValue: string;
}
export default function TaskSearchInput({ onSearchChange, onSortChange, sortValue }: TaskSearchInputProps) {
return (
<div className={cn('flex w-full items-center justify-between border-b-[0.67px] border-[#ececef] bg-[#fbfafd] p-4')}>
<div className="flex flex-1 items-center gap-4">
<SearchInput
className="flex-1"
placeholder="검색 또는 필터..."
onChange={(e) => onSearchChange(e.target.value)}
/>
<Select
value={sortValue}
onValueChange={onSortChange}
>
<SelectTrigger className="w-[180px] rounded-md border border-gray-200">
<SelectValue placeholder="정렬 기준 선택" />
</SelectTrigger>
<SelectContent>
{sortOptions.map((option) => (
<SelectItem
key={option.value}
value={option.value}
>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
);
}

View File

@ -0,0 +1,82 @@
import type { Meta, StoryObj } from '@storybook/react';
import TaskList from '.';
const meta: Meta<typeof TaskList> = {
title: 'Components/TaskList',
component: TaskList,
};
export default meta;
type Story = StoryObj<typeof TaskList>;
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',
commentsCount: 4,
updatesCount: 1,
lastUpdated: '1 hour ago',
memberCount: 3,
},
{
title: '갤럭시 흠집 객체 탐지',
createdTime: '3 hours ago',
creatorName: 'Kim Tae Su',
project: 'Project B',
type: 'Detection',
status: 'completed',
commentsCount: 2,
updatesCount: 3,
lastUpdated: '30 minutes ago',
memberCount: 5,
},
{
title: '갤럭시 흠집 경계 폴리곤',
createdTime: '5 hours ago',
creatorName: 'Kim Tae Su',
project: 'Project C',
type: 'Polygon',
status: 'in_progress',
commentsCount: 3,
updatesCount: 2,
lastUpdated: '2 hours ago',
memberCount: 4,
},
{
title: '갤럭시 흠집 폴리라인',
createdTime: '1 day ago',
creatorName: 'Kim Tae Su',
project: 'Project D',
type: 'Polyline',
status: 'completed',
commentsCount: 5,
updatesCount: 0,
lastUpdated: '20 minutes ago',
memberCount: 6,
},
{
title: '갤럭시 흠집 디텍션',
createdTime: '6 hours ago',
creatorName: 'Kim Tae Su',
project: 'Project E',
type: 'Detection',
status: 'pending',
commentsCount: 1,
updatesCount: 4,
lastUpdated: '3 hours ago',
memberCount: 2,
},
],
},
};

View File

@ -0,0 +1,145 @@
import { useState } from 'react';
import TaskItem from './TaskItem';
import TaskSearchInput from './TaskSearchInput';
interface TaskListProps {
acceptedCount: number;
rejectedCount: number;
pendingCount: number;
totalCount: number;
items: {
title: string;
createdTime: string;
creatorName: string;
project: string;
type: 'Classification' | 'Detection' | 'Polygon' | 'Polyline';
status: string;
commentsCount: number;
updatesCount: number;
lastUpdated: string;
memberCount: number;
}[];
}
const typeColors: Record<'Classification' | 'Detection' | 'Polygon' | 'Polyline', string> = {
Classification: '#a2eeef',
Detection: '#d4c5f9',
Polygon: '#f9c5d4',
Polyline: '#c5f9d4',
};
export default function TaskList({ acceptedCount, pendingCount, totalCount, items }: TaskListProps): JSX.Element {
const [activeTab, setActiveTab] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
const [sortValue, setSortValue] = useState('latest');
const filteredItems = items
.filter((item) => {
if (activeTab === 'in_progress') return item.status.toLowerCase() === 'in_progress';
if (activeTab === 'completed') return item.status.toLowerCase() === 'completed';
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();
case 'comments':
return b.commentsCount - a.commentsCount;
case 'updates':
return b.updatesCount - a.updatesCount;
default:
return new Date(b.createdTime).getTime() - new Date(a.createdTime).getTime();
}
});
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 === 'all' ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''
}`}
onClick={() => setActiveTab('all')}
>
<span
className={`text-sm ${
activeTab === 'all' ? 'font-semibold text-[#333238]' : 'font-normal text-[#737278]'
}`}
>
</span>
<span className="flex h-4 w-6 items-center justify-center rounded-[160px] bg-[#ececef] text-xs text-[#626168]">
{totalCount}
</span>
</button>
<button
className={`flex h-12 w-[100px] items-center justify-between px-3 ${
activeTab === 'in_progress' ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''
}`}
onClick={() => setActiveTab('in_progress')}
>
<span
className={`text-sm ${
activeTab === 'in_progress' ? 'font-semibold text-[#333238]' : 'font-normal text-[#737278]'
}`}
>
</span>
<span className="flex h-4 w-6 items-center justify-center rounded-[160px] bg-[#ececef] text-xs text-[#626168]">
{pendingCount}
</span>
</button>
<button
className={`flex h-12 w-[100px] items-center justify-between px-3 ${
activeTab === 'completed' ? 'shadow-[inset_0px_-2px_0px_#1f75cb]' : ''
}`}
onClick={() => setActiveTab('completed')}
>
<span
className={`text-sm ${
activeTab === 'completed' ? 'font-semibold text-[#333238]' : 'font-normal text-[#737278]'
}`}
>
</span>
<span className="flex h-4 w-6 items-center justify-center rounded-[160px] bg-[#ececef] text-xs text-[#626168]">
{acceptedCount}
</span>
</button>
</div>
</div>
<div className="relative w-full px-4">
<TaskSearchInput
onSearchChange={setSearchQuery}
onSortChange={setSortValue}
sortValue={sortValue}
/>
</div>
<div className="relative w-full overflow-y-auto px-4">
{filteredItems.map((item, index) => (
<TaskItem
key={index}
title={item.title}
createdTime={item.createdTime}
creatorName={item.creatorName}
project={item.project}
status={item.status}
commentsCount={item.commentsCount}
updatesCount={item.updatesCount}
lastUpdated={item.lastUpdated}
type={item.type}
bgColor={typeColors[item.type]}
memberCount={item.memberCount}
/>
))}
</div>
</div>
);
}