Merge branch 'fe/feat/66-member-manage-modal' into 'fe/develop'
Feat: MemberManageModal 구현 - S11P21S002-66 See merge request s11-s-project/S11P21S002!20
This commit is contained in:
commit
53cb5b1388
@ -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: '뷰어',
|
||||
};
|
||||
|
||||
|
141
frontend/src/components/MemberManageModal/MemberManageForm.tsx
Normal file
141
frontend/src/components/MemberManageModal/MemberManageForm.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
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' | 'manager' | 'editor' | 'viewer';
|
||||
|
||||
const roles: Role[] = ['admin', 'manager', 'editor', 'viewer'];
|
||||
|
||||
const roleToStr: { [key in Role]: string } = {
|
||||
admin: '관리자',
|
||||
manager: '매니저',
|
||||
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<typeof formSchema>;
|
||||
|
||||
interface Member {
|
||||
email: string;
|
||||
role: Role;
|
||||
}
|
||||
|
||||
interface MemberManageFormProps {
|
||||
members: Member[];
|
||||
onSubmit: (data: MemberManageFormValues) => void;
|
||||
}
|
||||
|
||||
export default function MemberManageForm({ members, onSubmit }: MemberManageFormProps) {
|
||||
const form = useForm<MemberManageFormValues>({
|
||||
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', 'manager', 'editor', 'viewer'];
|
||||
|
||||
const sortedGroupedMembers = Object.entries(groupedMembers).sort(
|
||||
([roleA], [roleB]) => roleOrder.indexOf(roleA as Role) - roleOrder.indexOf(roleB as Role)
|
||||
);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-5"
|
||||
>
|
||||
<div className="flex w-[530px] flex-col gap-[var(--size-space-200)]">
|
||||
{sortedGroupedMembers.map(([role, groupMembers]) => {
|
||||
if (!groupMembers || groupMembers.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={role}
|
||||
className="flex flex-col gap-3"
|
||||
>
|
||||
<FormLabel className="body-strong">{roleToStr[role as Role]}</FormLabel>
|
||||
{groupMembers.map((member, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center gap-4 ${index === groupMembers.length - 1 ? 'mb-4' : ''}`}
|
||||
>
|
||||
<FormField
|
||||
name={`members.${members.findIndex((m) => m.email === member.email)}.email`}
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="email@example.com"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
name={`members.${members.findIndex((m) => m.email === member.email)}.role`}
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="역할을 선택해주세요." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles.map((role) => (
|
||||
<SelectItem
|
||||
key={role}
|
||||
value={role}
|
||||
>
|
||||
{roleToStr[role]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outlinePrimary"
|
||||
disabled={!form.formState.isValid}
|
||||
>
|
||||
역할 설정
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
34
frontend/src/components/MemberManageModal/index.stories.tsx
Normal file
34
frontend/src/components/MemberManageModal/index.stories.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import MemberManageModal from '.';
|
||||
|
||||
const meta: Meta<typeof MemberManageModal> = {
|
||||
title: 'Modal/MemberManageModal',
|
||||
component: MemberManageModal,
|
||||
argTypes: {
|
||||
title: { control: 'text' },
|
||||
members: { control: 'object' },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof MemberManageModal>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: '프로젝트 멤버 관리하기',
|
||||
members: [
|
||||
{ email: 'admin1@example.com', role: 'admin' },
|
||||
{ email: 'admin2@example.com', role: 'admin' },
|
||||
{ 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' },
|
||||
{ email: 'editor3@example.com', role: 'editor' },
|
||||
{ email: 'editor4@example.com', role: 'editor' },
|
||||
],
|
||||
onClose: () => console.log('Modal Closed'),
|
||||
onSubmit: (data) => console.log('Submitted Data:', data),
|
||||
},
|
||||
};
|
39
frontend/src/components/MemberManageModal/index.tsx
Normal file
39
frontend/src/components/MemberManageModal/index.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import MemberManageForm, { MemberManageFormValues } from './MemberManageForm';
|
||||
import XIcon from '@/assets/icons/x.svg?react';
|
||||
|
||||
type Role = 'admin' | 'manager' | '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 (
|
||||
<div className="flex w-[610px] flex-col gap-10 rounded-3xl border px-10 py-5 shadow-lg">
|
||||
<header className="flex w-full items-center gap-5">
|
||||
<h1 className="small-title flex-1">{title}</h1>
|
||||
<button
|
||||
className="flex h-8 w-8 items-center justify-center"
|
||||
onClick={onClose}
|
||||
>
|
||||
<XIcon className="stroke-gray-900" />
|
||||
</button>
|
||||
</header>
|
||||
<MemberManageForm
|
||||
onSubmit={onSubmit}
|
||||
members={members}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user