From 72372ebecdc550e7598b996e8e5a3e755a1b5862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Thu, 29 Aug 2024 15:27:55 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Feat:=20MemberManageModal=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20-=20S11P21S002-66?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MemberManageModal/MemberManageForm.tsx | 135 ++++++++++++++++++ .../MemberManageModal/index.stories.tsx | 34 +++++ .../components/MemberManageModal/index.tsx | 39 +++++ 3 files changed, 208 insertions(+) create mode 100644 frontend/src/components/MemberManageModal/MemberManageForm.tsx create mode 100644 frontend/src/components/MemberManageModal/index.stories.tsx create mode 100644 frontend/src/components/MemberManageModal/index.tsx diff --git a/frontend/src/components/MemberManageModal/MemberManageForm.tsx b/frontend/src/components/MemberManageModal/MemberManageForm.tsx new file mode 100644 index 0000000..34fae98 --- /dev/null +++ b/frontend/src/components/MemberManageModal/MemberManageForm.tsx @@ -0,0 +1,135 @@ +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: '뷰어', +}; + +// Adjusted form schema to match the required Role type +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 MemberManageFormProps { + members: Member[]; + onSubmit: (data: MemberManageFormValues) => void; +} + +export default function MemberManageForm({ members, onSubmit }: MemberManageFormProps) { + 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; + }, {}); + + return ( +
+ +
+ {Object.entries(groupedMembers).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/MemberManageModal/index.stories.tsx b/frontend/src/components/MemberManageModal/index.stories.tsx new file mode 100644 index 0000000..c15c201 --- /dev/null +++ b/frontend/src/components/MemberManageModal/index.stories.tsx @@ -0,0 +1,34 @@ +import { Meta, StoryObj } from '@storybook/react'; +import MemberManageModal from '.'; + +const meta: Meta = { + title: 'Modal/MemberManageModal', + component: MemberManageModal, + argTypes: { + title: { control: 'text' }, + members: { 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: 'viewer1@example.com', role: 'viewer' }, + { email: 'viewer2@example.com', role: 'viewer' }, + { 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' }, + ], + onClose: () => console.log('Modal Closed'), + onSubmit: (data) => console.log('Submitted Data:', data), + }, +}; diff --git a/frontend/src/components/MemberManageModal/index.tsx b/frontend/src/components/MemberManageModal/index.tsx new file mode 100644 index 0000000..c62eda2 --- /dev/null +++ b/frontend/src/components/MemberManageModal/index.tsx @@ -0,0 +1,39 @@ +import MemberManageForm, { MemberManageFormValues } from './MemberManageForm'; +import XIcon from '@/assets/icons/x.svg?react'; + +type Role = 'admin' | 'editor' | 'viewer'; + +interface Member { + email: string; + role: Role; +} + +export default function MemberManageModal({ + title = '멤버 관리', + onClose, + onSubmit, + members, +}: { + title?: string; + onClose: () => void; + onSubmit: (data: MemberManageFormValues) => void; + members: Member[]; +}) { + return ( +
+
+

{title}

+ +
+ +
+ ); +} From f2587276849f4a155394ea97900089f1206f11ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Thu, 29 Aug 2024 16:05:35 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Refactor:=20Role=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EB=A7=A4=EB=8B=88=EC=A0=80=20?= =?UTF-8?q?=EC=97=90=EB=94=94=ED=84=B0=20=EB=B7=B0=EC=96=B4=EB=A1=9C=20?= =?UTF-8?q?=ED=99=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/MemberAddModal/MemberAddForm.tsx | 7 ++++--- .../MemberManageModal/MemberManageForm.tsx | 14 ++++++++++---- .../components/MemberManageModal/index.stories.tsx | 4 ++-- .../src/components/MemberManageModal/index.tsx | 2 +- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/MemberAddModal/MemberAddForm.tsx b/frontend/src/components/MemberAddModal/MemberAddForm.tsx index a9d2a5a..9c4b83b 100644 --- a/frontend/src/components/MemberAddModal/MemberAddForm.tsx +++ b/frontend/src/components/MemberAddModal/MemberAddForm.tsx @@ -6,13 +6,14 @@ import { Input } from '../ui/input'; import { Button } from '../ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; -type Role = 'admin' | 'editor' | 'viewer'; +type Role = 'admin' | 'manager' | 'editor' | 'viewer'; -const roles: Role[] = ['admin', 'editor', 'viewer']; +const roles: Role[] = ['admin', 'manager', 'editor', 'viewer']; const roleToStr: { [key in Role]: string } = { admin: '관리자', - editor: '사용자', + manager: '매니저', + editor: '에디터', viewer: '뷰어', }; diff --git a/frontend/src/components/MemberManageModal/MemberManageForm.tsx b/frontend/src/components/MemberManageModal/MemberManageForm.tsx index 34fae98..223cdc2 100644 --- a/frontend/src/components/MemberManageModal/MemberManageForm.tsx +++ b/frontend/src/components/MemberManageModal/MemberManageForm.tsx @@ -6,17 +6,17 @@ import { Input } from '../ui/input'; import { Button } from '../ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; -type Role = 'admin' | 'editor' | 'viewer'; +type Role = 'admin' | 'manager' | 'editor' | 'viewer'; -const roles: Role[] = ['admin', 'editor', 'viewer']; +const roles: Role[] = ['admin', 'manager', 'editor', 'viewer']; const roleToStr: { [key in Role]: string } = { admin: '관리자', + manager: '매니저', editor: '에디터', viewer: '뷰어', }; -// Adjusted form schema to match the required Role type const formSchema = z.object({ members: z.array( z.object({ @@ -50,6 +50,12 @@ export default function MemberManageForm({ members, onSubmit }: MemberManageForm return acc; }, {}); + const roleOrder: Role[] = ['admin', 'manager', 'editor', 'viewer']; + + const sortedGroupedMembers = Object.entries(groupedMembers).sort( + ([roleA], [roleB]) => roleOrder.indexOf(roleA as Role) - roleOrder.indexOf(roleB as Role) + ); + return (
- {Object.entries(groupedMembers).map(([role, groupMembers]) => { + {sortedGroupedMembers.map(([role, groupMembers]) => { if (!groupMembers || groupMembers.length === 0) return null; return ( diff --git a/frontend/src/components/MemberManageModal/index.stories.tsx b/frontend/src/components/MemberManageModal/index.stories.tsx index c15c201..97c6172 100644 --- a/frontend/src/components/MemberManageModal/index.stories.tsx +++ b/frontend/src/components/MemberManageModal/index.stories.tsx @@ -20,8 +20,8 @@ export const Default: Story = { members: [ { email: 'admin1@example.com', role: 'admin' }, { email: 'admin2@example.com', role: 'admin' }, - { email: 'viewer1@example.com', role: 'viewer' }, - { email: 'viewer2@example.com', role: 'viewer' }, + { email: 'manager1@example.com', role: 'manager' }, + { email: 'manager2@example.com', role: 'manager' }, { email: 'viewer3@example.com', role: 'viewer' }, { email: 'editor1@example.com', role: 'editor' }, { email: 'editor2@example.com', role: 'editor' }, diff --git a/frontend/src/components/MemberManageModal/index.tsx b/frontend/src/components/MemberManageModal/index.tsx index c62eda2..41d316e 100644 --- a/frontend/src/components/MemberManageModal/index.tsx +++ b/frontend/src/components/MemberManageModal/index.tsx @@ -1,7 +1,7 @@ import MemberManageForm, { MemberManageFormValues } from './MemberManageForm'; import XIcon from '@/assets/icons/x.svg?react'; -type Role = 'admin' | 'editor' | 'viewer'; +type Role = 'admin' | 'manager' | 'editor' | 'viewer'; interface Member { email: string;