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 { Button } from '../ui/button';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
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 } = {
|
const roleToStr: { [key in Role]: string } = {
|
||||||
admin: '관리자',
|
admin: '관리자',
|
||||||
editor: '사용자',
|
manager: '매니저',
|
||||||
|
editor: '에디터',
|
||||||
viewer: '뷰어',
|
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