From 3266a3cd0b12c45bb5450bacd9071943615c1714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Thu, 5 Sep 2024 17:54:35 +0900 Subject: [PATCH 1/6] =?UTF-8?q?Fix:=20=EB=A6=AC=EB=B7=B0=ED=80=98=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=A6=AC=EB=B7=B0=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ReviewList/ReviewItem.tsx | 55 +++++++ .../ReviewList/ReviewSearchInput.tsx | 48 ++++++ .../components/ReviewList/index.stories.tsx | 63 ++++++++ frontend/src/components/ReviewList/index.tsx | 142 ++++++++++++++++++ 4 files changed, 308 insertions(+) create mode 100644 frontend/src/components/ReviewList/ReviewItem.tsx create mode 100644 frontend/src/components/ReviewList/ReviewSearchInput.tsx create mode 100644 frontend/src/components/ReviewList/index.stories.tsx create mode 100644 frontend/src/components/ReviewList/index.tsx 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) => ( + + ))} +
+
+ ); +} From a0dc26719b80841e05f6ac9e13b4d44d0f8e0ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Thu, 5 Sep 2024 17:56:56 +0900 Subject: [PATCH 2/6] =?UTF-8?q?Feat:=20=EB=A6=AC=EB=B7=B0=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20Tas?= =?UTF-8?q?kList=EB=A5=BC=20=EB=8C=80=EC=B2=B4=ED=95=A0=20TaskList=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/TaskList/TaskItem.tsx | 75 +++++++++ .../components/TaskList/TaskSearchInput.tsx | 48 ++++++ .../src/components/TaskList/index.stories.tsx | 82 ++++++++++ frontend/src/components/TaskList/index.tsx | 145 ++++++++++++++++++ 4 files changed, 350 insertions(+) create mode 100644 frontend/src/components/TaskList/TaskItem.tsx create mode 100644 frontend/src/components/TaskList/TaskSearchInput.tsx create mode 100644 frontend/src/components/TaskList/index.stories.tsx create mode 100644 frontend/src/components/TaskList/index.tsx diff --git a/frontend/src/components/TaskList/TaskItem.tsx b/frontend/src/components/TaskList/TaskItem.tsx new file mode 100644 index 0000000..7f3978d --- /dev/null +++ b/frontend/src/components/TaskList/TaskItem.tsx @@ -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: , + Detection: , + Polygon: , + Polyline: , +}; + +export default function TaskItem({ + title, + createdTime, + creatorName, + project, + status, + commentsCount, + updatesCount, + lastUpdated, + type, + bgColor = '#f3f4f6', + memberCount, +}: TaskItemProps) { + const icon = typeIcons[type]; + + return ( +
+
+

{title}

+

+ {createdTime} by {creatorName} +

+
+ +

{project}

+
+
+ {icon} + {type} +
+
+
+
+
{status}
+
+ + {commentsCount} +
+
+ + {updatesCount} +
+
+

Updated at {lastUpdated}

+
{`멤버 ${memberCount}명`}
+
+
+ ); +} diff --git a/frontend/src/components/TaskList/TaskSearchInput.tsx b/frontend/src/components/TaskList/TaskSearchInput.tsx new file mode 100644 index 0000000..bb486ae --- /dev/null +++ b/frontend/src/components/TaskList/TaskSearchInput.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 TaskSearchInputProps { + onSearchChange: (value: string) => void; + onSortChange: (value: string) => void; + sortValue: string; +} + +export default function TaskSearchInput({ onSearchChange, onSortChange, sortValue }: TaskSearchInputProps) { + return ( +
+
+ onSearchChange(e.target.value)} + /> + +
+
+ ); +} diff --git a/frontend/src/components/TaskList/index.stories.tsx b/frontend/src/components/TaskList/index.stories.tsx new file mode 100644 index 0000000..c5bd9d5 --- /dev/null +++ b/frontend/src/components/TaskList/index.stories.tsx @@ -0,0 +1,82 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import TaskList from '.'; + +const meta: Meta = { + title: 'Components/TaskList', + component: TaskList, +}; + +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', + 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, + }, + ], + }, +}; diff --git a/frontend/src/components/TaskList/index.tsx b/frontend/src/components/TaskList/index.tsx new file mode 100644 index 0000000..822d64c --- /dev/null +++ b/frontend/src/components/TaskList/index.tsx @@ -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 ( +
+
+
+ + + + + +
+
+ +
+ +
+ +
+ {filteredItems.map((item, index) => ( + + ))} +
+
+ ); +} From 250cd864c54dfe2205db7728a1626b726976aa43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Thu, 5 Sep 2024 17:58:07 +0900 Subject: [PATCH 3/6] =?UTF-8?q?Feat:=20=EC=9B=8C=ED=81=AC=EC=8A=A4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20Admin=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=ED=8A=B8=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/AdminLayout/index.stories.tsx | 44 +++++++++++++++ frontend/src/components/AdminLayout/index.tsx | 32 +++++++++++ .../src/components/AdminMenuSidebar/index.tsx | 32 +++++++++++ .../components/AdminProjectSidebar/index.tsx | 54 +++++++++++++++++++ 4 files changed, 162 insertions(+) create mode 100644 frontend/src/components/AdminLayout/index.stories.tsx create mode 100644 frontend/src/components/AdminLayout/index.tsx create mode 100644 frontend/src/components/AdminMenuSidebar/index.tsx create mode 100644 frontend/src/components/AdminProjectSidebar/index.tsx diff --git a/frontend/src/components/AdminLayout/index.stories.tsx b/frontend/src/components/AdminLayout/index.stories.tsx new file mode 100644 index 0000000..9b60b9c --- /dev/null +++ b/frontend/src/components/AdminLayout/index.stories.tsx @@ -0,0 +1,44 @@ +import '@/index.css'; +import { Meta, StoryObj } from '@storybook/react'; +import AdminLayout from './index'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { Workspace } from '@/types'; + +const meta: Meta = { + title: 'Layout/AdminLayout', + component: AdminLayout, + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; + +type Story = StoryObj; + +const workspace: Workspace = { + id: 1, + name: 'Workspace Alpha', + projects: [ + { + id: 1, + name: 'Project Alpha', + type: 'Segmentation', + children: [], + }, + { + id: 2, + name: 'Project Beta', + type: 'Classification', + children: [], + }, + ], +}; + +export const Default: Story = { + render: () => ( + + + + ), +}; diff --git a/frontend/src/components/AdminLayout/index.tsx b/frontend/src/components/AdminLayout/index.tsx new file mode 100644 index 0000000..39e24cb --- /dev/null +++ b/frontend/src/components/AdminLayout/index.tsx @@ -0,0 +1,32 @@ +import { Outlet } from 'react-router-dom'; +import Header from '../Header'; +import { ResizablePanelGroup, ResizablePanel } from '../ui/resizable'; +import AdminProjectSidebar from '../AdminProjectSidebar'; +import AdminMenuSidebar from '../AdminMenuSidebar'; +import { Workspace } from '@/types'; + +interface AdminLayoutProps { + workspace: Workspace; +} + +export default function AdminLayout({ workspace }: AdminLayoutProps) { + return ( + <> +
+
+ + + +
+ +
+
+ +
+
+ + ); +} diff --git a/frontend/src/components/AdminMenuSidebar/index.tsx b/frontend/src/components/AdminMenuSidebar/index.tsx new file mode 100644 index 0000000..154e604 --- /dev/null +++ b/frontend/src/components/AdminMenuSidebar/index.tsx @@ -0,0 +1,32 @@ +import { useNavigate } from 'react-router-dom'; +import { cn } from '@/lib/utils'; + +export default function AdminMenuSidebar() { + const navigate = useNavigate(); + + const menuItems = [ + { label: '작업', path: '/admin/tasks' }, + { label: '리뷰', path: '/admin/reviews' }, + { label: '멤버 관리', path: '/admin/members' }, + ]; + + return ( +
+

메뉴

+
+ {menuItems.map((item) => ( + + ))} +
+
+ ); +} diff --git a/frontend/src/components/AdminProjectSidebar/index.tsx b/frontend/src/components/AdminProjectSidebar/index.tsx new file mode 100644 index 0000000..7e984fb --- /dev/null +++ b/frontend/src/components/AdminProjectSidebar/index.tsx @@ -0,0 +1,54 @@ +import { ResizablePanel, ResizableHandle } from '../ui/resizable'; +import { Project } from '@/types'; +import { useNavigate } from 'react-router-dom'; +import { SquarePen } from 'lucide-react'; +import { Button } from '../ui/button'; + +interface AdminProjectSidebarProps { + workspaceName: string; + projects: Project[]; +} + +export default function AdminProjectSidebar({ workspaceName, projects }: AdminProjectSidebarProps): JSX.Element { + const navigate = useNavigate(); + + return ( + <> + +
+

+ {workspaceName} +

+ + +
+
+ {projects.map((project) => ( + + ))} +
+
+ + + ); +} From e754d6b31f16ba3b3243c5e9cd90e683769076db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Thu, 5 Sep 2024 17:59:04 +0900 Subject: [PATCH 4/6] =?UTF-8?q?Feat:=20=EB=A9=A4=EB=B2=84=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EB=AA=A8=EB=8B=AC=EC=9D=84=20=EB=B0=94=ED=83=95?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=96=B4=EB=93=9C=EB=AF=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=EC=9A=A9=20=EB=A9=A4=EB=B2=84=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminMemberManageForm.tsx | 139 ++++++++++++++++++ .../AdminMemberManage/index.stories.tsx | 40 +++++ .../components/AdminMemberManage/index.tsx | 64 ++++++++ 3 files changed, 243 insertions(+) create mode 100644 frontend/src/components/AdminMemberManage/AdminMemberManageForm.tsx create mode 100644 frontend/src/components/AdminMemberManage/index.stories.tsx create mode 100644 frontend/src/components/AdminMemberManage/index.tsx diff --git a/frontend/src/components/AdminMemberManage/AdminMemberManageForm.tsx b/frontend/src/components/AdminMemberManage/AdminMemberManageForm.tsx new file mode 100644 index 0000000..b8abdff --- /dev/null +++ b/frontend/src/components/AdminMemberManage/AdminMemberManageForm.tsx @@ -0,0 +1,139 @@ +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'; +import { Input } from '../ui/input'; +import { Button } from '../ui/button'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; + +type Role = 'admin' | 'editor' | 'viewer'; + +const roles: Role[] = ['admin', 'editor', 'viewer']; + +const roleToStr: { [key in Role]: string } = { + admin: '관리자', + editor: '에디터', + viewer: '뷰어', +}; + +const formSchema = z.object({ + members: z.array( + z.object({ + email: z.string().email({ message: '올바른 이메일 형식을 입력해주세요.' }), + role: z.enum(roles as [Role, ...Role[]], { errorMap: () => ({ message: '역할을 선택해주세요.' }) }), + }) + ), +}); + +export type MemberManageFormValues = z.infer; + +interface Member { + email: string; + role: Role; +} + +interface AdminMemberManageFormProps { + members: Member[]; + onSubmit: (data: MemberManageFormValues) => void; +} + +export default function AdminMemberManageForm({ members, onSubmit }: AdminMemberManageFormProps) { + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { members }, + }); + + const groupedMembers = members.reduce<{ [key: string]: { email: string; role: Role }[] }>((acc, member) => { + if (!acc[member.role]) acc[member.role] = []; + acc[member.role].push(member); + return acc; + }, {}); + + const roleOrder: Role[] = ['admin', 'editor', 'viewer']; + + const sortedGroupedMembers = Object.entries(groupedMembers).sort( + ([roleA], [roleB]) => roleOrder.indexOf(roleA as Role) - roleOrder.indexOf(roleB as Role) + ); + + return ( +
+ +
+ {sortedGroupedMembers.map(([role, groupMembers]) => { + if (!groupMembers || groupMembers.length === 0) return null; + + return ( +
+ {roleToStr[role as Role]} + {groupMembers.map((member, index) => ( +
+ m.email === member.email)}.email`} + control={form.control} + render={({ field }) => ( + + + + + + + )} + /> + m.email === member.email)}.role`} + control={form.control} + render={({ field }) => ( + + + + + + + )} + /> +
+ ))} +
+ ); + })} +
+ +
+ + ); +} diff --git a/frontend/src/components/AdminMemberManage/index.stories.tsx b/frontend/src/components/AdminMemberManage/index.stories.tsx new file mode 100644 index 0000000..0c7d414 --- /dev/null +++ b/frontend/src/components/AdminMemberManage/index.stories.tsx @@ -0,0 +1,40 @@ +import { Meta, StoryObj } from '@storybook/react'; +import AdminMemberManage from '.'; +import { MemberManageFormValues } from './AdminMemberManageForm'; + +const meta: Meta = { + title: 'Components/AdminMemberManage', + component: AdminMemberManage, + argTypes: { + title: { control: 'text' }, + members: { control: 'object' }, + projects: { control: 'object' }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + title: '프로젝트 멤버 관리하기', + members: [ + { email: 'admin1@example.com', role: 'admin' }, + { email: 'admin2@example.com', role: 'admin' }, + { email: 'viewer3@example.com', role: 'viewer' }, + { email: 'editor1@example.com', role: 'editor' }, + { email: 'editor2@example.com', role: 'editor' }, + { email: 'editor3@example.com', role: 'editor' }, + { email: 'editor4@example.com', role: 'editor' }, + ], + projects: [ + { id: 'project-1', name: '프로젝트 A' }, + { id: 'project-2', name: '프로젝트 B' }, + { id: 'project-3', name: '프로젝트 C' }, + ], + onProjectChange: (projectId: string) => console.log('Selected Project:', projectId), + onMemberInvite: () => console.log('Invite member'), + onSubmit: (data: MemberManageFormValues) => console.log('Submitted:', data), + }, +}; diff --git a/frontend/src/components/AdminMemberManage/index.tsx b/frontend/src/components/AdminMemberManage/index.tsx new file mode 100644 index 0000000..4951792 --- /dev/null +++ b/frontend/src/components/AdminMemberManage/index.tsx @@ -0,0 +1,64 @@ +import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'; +import AdminMemberManageForm, { MemberManageFormValues } from './AdminMemberManageForm'; +import { Button } from '@/components/ui/button'; + +type Role = 'admin' | 'editor' | 'viewer'; + +interface Member { + email: string; + role: Role; +} + +interface Project { + id: string; + name: string; +} + +export default function AdminMemberManage({ + title = '멤버 관리', + projects, + onProjectChange, + onSubmit, + members, + onMemberInvite, +}: { + title?: string; + projects: Project[]; + onProjectChange: (projectId: string) => void; + onSubmit: (data: MemberManageFormValues) => void; + members: Member[]; + onMemberInvite: () => void; +}) { + return ( +
+
+

{title}

+ + +
+ +
+ ); +} From d0951d0ede92d9a2ff483f57fd1a87e06a589b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Thu, 5 Sep 2024 17:59:48 +0900 Subject: [PATCH 5/6] =?UTF-8?q?Refactor:=20=EC=9D=B4=EB=A6=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=ED=8C=8C=EC=9D=BC=20=EB=93=B1=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReviewQuest/ReviewRequestItem.stories.tsx | 39 ----- .../ReviewQuest/ReviewRequestItem.tsx | 80 ---------- .../ReviewQuest/ReviewSearchInput.tsx | 48 ------ .../components/ReviewQuest/index.stories.tsx | 78 --------- frontend/src/components/ReviewQuest/index.tsx | 151 ------------------ 5 files changed, 396 deletions(-) delete mode 100644 frontend/src/components/ReviewQuest/ReviewRequestItem.stories.tsx delete mode 100644 frontend/src/components/ReviewQuest/ReviewRequestItem.tsx delete mode 100644 frontend/src/components/ReviewQuest/ReviewSearchInput.tsx delete mode 100644 frontend/src/components/ReviewQuest/index.stories.tsx delete mode 100644 frontend/src/components/ReviewQuest/index.tsx diff --git a/frontend/src/components/ReviewQuest/ReviewRequestItem.stories.tsx b/frontend/src/components/ReviewQuest/ReviewRequestItem.stories.tsx deleted file mode 100644 index a58e92d..0000000 --- a/frontend/src/components/ReviewQuest/ReviewRequestItem.stories.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import '@/index.css'; -import type { Meta, StoryObj } from '@storybook/react'; -import ReviewRequestItem from './ReviewRequestItem'; - -const meta: Meta = { - title: 'Components/ReviewRequestItem', - component: ReviewRequestItem, - argTypes: { - title: { control: 'text', description: 'Title of the review request' }, - createdTime: { control: 'text', description: 'Time when the review was created' }, - creatorName: { control: 'text', description: 'Name of the creator' }, - project: { control: 'text', description: 'Project name' }, - status: { control: 'text', description: 'Status of the review request' }, - commentsCount: { control: 'number', description: 'Number of comments' }, - updatesCount: { control: 'number', description: 'Number of updates' }, - lastUpdated: { control: 'text', description: 'Time when the review was last updated' }, - type: { - control: 'object', - description: 'Label for the request type with text and color', - }, - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - title: 'Feat: 워크스페이스 멤버 추가 구현 - S11P21S002-37', - createdTime: '!24 · created 2 hours ago', - creatorName: '김태수', - project: 'be/develop', - status: 'Merged', - commentsCount: 4, - updatesCount: 1, - lastUpdated: 'updated 1 hour ago', - type: { text: 'Classification', color: '#a2eeef' }, - }, -}; diff --git a/frontend/src/components/ReviewQuest/ReviewRequestItem.tsx b/frontend/src/components/ReviewQuest/ReviewRequestItem.tsx deleted file mode 100644 index 3bdaf9a..0000000 --- a/frontend/src/components/ReviewQuest/ReviewRequestItem.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Briefcase, MessageCircle, RefreshCw, Tag, Box, Layers, Pen } from 'lucide-react'; - -interface ReviewRequestItemProps { - title: string; - createdTime: string; - creatorName: string; - project: string; - status: string; - commentsCount: number; - updatesCount: number; - lastUpdated: 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 ReviewRequestItem({ - title, - createdTime, - creatorName, - project, - status, - commentsCount, - updatesCount, - lastUpdated, - type, -}: ReviewRequestItemProps) { - const icon = typeIcons[type.text]; - const bgColor = typeStyles[type.text]; - - return ( -
-
-

{title}

-

- {createdTime} by {creatorName} -

-
- -

{project}

-
- {type && ( -
- {icon} - {type.text} -
- )} -
-
-
-
{status}
-
- - {commentsCount} -
-
- - {updatesCount} -
-
-

{lastUpdated}

-
-
- ); -} diff --git a/frontend/src/components/ReviewQuest/ReviewSearchInput.tsx b/frontend/src/components/ReviewQuest/ReviewSearchInput.tsx deleted file mode 100644 index 45b667f..0000000 --- a/frontend/src/components/ReviewQuest/ReviewSearchInput.tsx +++ /dev/null @@ -1,48 +0,0 @@ -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/ReviewQuest/index.stories.tsx b/frontend/src/components/ReviewQuest/index.stories.tsx deleted file mode 100644 index 21fb3df..0000000 --- a/frontend/src/components/ReviewQuest/index.stories.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import '@/index.css'; -import type { Meta, StoryObj } from '@storybook/react'; -import ReviewRequest from '.'; - -const meta: Meta = { - title: 'Components/ReviewRequest', - component: ReviewRequest, -}; - -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', - commentsCount: 4, - updatesCount: 1, - lastUpdated: '1 hour ago', - }, - { - title: '갤럭시 흠집 객체 탐지', - createdTime: '3 hours ago', - creatorName: 'Kim Tae Su', - project: 'Project B', - type: 'Detection', - status: 'completed', - commentsCount: 2, - updatesCount: 3, - lastUpdated: '30 minutes ago', - }, - { - 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', - }, - { - title: '갤럭시 흠집 폴리라인', - createdTime: '1 day ago', - creatorName: 'Kim Tae Su', - project: 'Project D', - type: 'Polyline', - status: 'completed', - commentsCount: 5, - updatesCount: 0, - lastUpdated: '20 minutes ago', - }, - { - title: '갤럭시 흠집 디텍션 허가 요청', - createdTime: '6 hours ago', - creatorName: 'Kim Tae Su', - project: 'Project E', - type: 'Detection', - status: 'pending', - commentsCount: 1, - updatesCount: 4, - lastUpdated: '3 hours ago', - }, - ], - }, -}; diff --git a/frontend/src/components/ReviewQuest/index.tsx b/frontend/src/components/ReviewQuest/index.tsx deleted file mode 100644 index 80e3267..0000000 --- a/frontend/src/components/ReviewQuest/index.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { useState } from 'react'; -import ReviewRequestItem from './ReviewRequestItem'; -import ReviewSearchInput from './ReviewSearchInput'; - -interface ReviewRequestProps { - 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; - }[]; -} - -const typeColors: Record<'Classification' | 'Detection' | 'Polygon' | 'Polyline', string> = { - Classification: '#a2eeef', - Detection: '#d4c5f9', - Polygon: '#f9c5d4', - Polyline: '#c5f9d4', -}; - -export default function ReviewRequest({ - acceptedCount, - rejectedCount, - pendingCount, - totalCount, - items, -}: ReviewRequestProps): 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 ['in_progress', 'pending'].includes(item.status.toLowerCase()); - 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 ( -
-
-
- - - - - - - -
-
- -
- -
- -
- {filteredItems.map((item, index) => ( - - ))} -
-
- ); -} From 6719bc08230cd1b7586d0d8735262e4775ed30ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Mon, 9 Sep 2024 08:37:11 +0900 Subject: [PATCH 6/6] =?UTF-8?q?Design:=20admin-page=20side=20bar=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/AdminMenuSidebar/index.tsx | 34 +++++++++++-------- .../components/AdminProjectSidebar/index.tsx | 10 +++--- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/AdminMenuSidebar/index.tsx b/frontend/src/components/AdminMenuSidebar/index.tsx index 154e604..e3fcac6 100644 --- a/frontend/src/components/AdminMenuSidebar/index.tsx +++ b/frontend/src/components/AdminMenuSidebar/index.tsx @@ -11,21 +11,25 @@ export default function AdminMenuSidebar() { ]; return ( -
-

메뉴

-
- {menuItems.map((item) => ( - - ))} +
+
+
+

메뉴

+
+
+ {menuItems.map((item) => ( + + ))} +
); diff --git a/frontend/src/components/AdminProjectSidebar/index.tsx b/frontend/src/components/AdminProjectSidebar/index.tsx index 7e984fb..b48d3b7 100644 --- a/frontend/src/components/AdminProjectSidebar/index.tsx +++ b/frontend/src/components/AdminProjectSidebar/index.tsx @@ -18,13 +18,13 @@ export default function AdminProjectSidebar({ workspaceName, projects }: AdminPr minSize={15} maxSize={35} defaultSize={20} - className="flex h-full flex-col bg-gray-100" + className="flex h-full flex-col border-r border-gray-200 bg-gray-100" > -
-

+
+

{workspaceName}

-