Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE][Feat] #59 : AuthModal 컴포넌트 #177

Merged
merged 11 commits into from
Nov 17, 2024
139 changes: 139 additions & 0 deletions frontend/src/component/authmodal/AuthModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import React, { useState } from 'react';
import { Modal } from '@/component/common/modal/Modal';

interface IAuthModalProps {
/** 모달이 열려 있는지 여부를 나타냅니다. */
isOpen: boolean;
/** 모달을 닫는 함수입니다. */
onClose: () => void;
/** 모달의 타입을 결정하는 값으로, 'login' 또는 'register'를 가집니다. */
type: 'login' | 'register';
}

export const AuthModal = (props: IAuthModalProps) => {
const [loginData, setLoginData] = useState({
id: '',
pw: '',
});

const [registerData, setRegisterData] = useState({
id: '',
email: '',
name: '',
pw: '',
confirmPw: '',
});

const [modalType, setModalType] = useState<'login' | 'register'>(props.type);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
juwon5272 marked this conversation as resolved.
Show resolved Hide resolved
const { name, value } = e.target;

if (modalType === 'login') {
setLoginData(prevState => ({
...prevState,
[name]: value,
}));
} else {
setRegisterData(prevState => ({
...prevState,
[name]: value,
}));
}
};

const handleLoginClick = () => {
console.log('로그인 데이터:', loginData);
};

const handleSignUpClick = () => {
if (registerData.pw !== registerData.confirmPw) {
alert('비밀번호가 일치하지 않습니다.');
return;
}
console.log('회원가입 데이터:', registerData);
};

const switchToRegister = () => {
setModalType('register');
};

const switchToLogin = () => {
setModalType('login');
};

return (
<Modal isOpen={props.isOpen} onClose={props.onClose}>
{modalType === 'login' ? (
<>
<Modal.Header content="Log In" onClose={props.onClose} />
<Modal.Input
title="ID"
name="id"
placeholder="ID"
value={loginData.id}
onChange={handleChange}
/>
<Modal.Input
title="PW"
name="pw"
placeholder="PW"
value={loginData.pw}
onChange={handleChange}
/>
<Modal.Footer
text="로그인"
onClick={handleLoginClick}
text2="회원가입"
onClick2={switchToRegister}
/>
</>
) : (
<>
<Modal.Header content="Sign Up" onClose={props.onClose} />
<Modal.Input
title="ID"
name="id"
placeholder="사용할 ID를 입력해주세요."
value={registerData.id}
onChange={handleChange}
/>
<Modal.Input
title="Email"
name="email"
placeholder="Email 주소를 입력해주세요."
value={registerData.email}
onChange={handleChange}
/>
<Modal.Input
title="Name"
name="name"
placeholder="이름을 입력해주세요."
value={registerData.name}
onChange={handleChange}
/>
<Modal.Input
title="PW"
name="pw"
placeholder="사용할 비밀번호를 입력해주세요."
value={registerData.pw}
onChange={handleChange}
/>
<Modal.Input
title=""
name="confirmPw"
placeholder="비밀번호를 한 번 더 입력해주세요."
value={registerData.confirmPw}
onChange={handleChange}
/>
<Modal.Footer
text="회원가입"
onClick={handleSignUpClick}
text2="로그인"
onClick2={switchToLogin}
/>
</>
)}
</Modal>
);
};
27 changes: 27 additions & 0 deletions frontend/src/component/common/modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { ModalHeader } from '@/component/common/modal/ModalHeader';
import { ModalInput } from '@/component/common/modal/ModalInput';
import { ModalFooter } from '@/component/common/modal/ModalFooter';

interface IModalProps {
/** 모달이 열려 있는지 여부를 나타냅니다. */
isOpen: boolean;
/** 모달 내에서 렌더링할 자식 요소들입니다. */
children: React.ReactNode;
}

export const Modal = (props: IModalProps) => {
if (!props.isOpen) return null;

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-20">
<div className="relative w-[22rem] max-w-lg rounded-2xl bg-white px-6 shadow-lg">
{props.children}
</div>
</div>
);
};

Modal.Header = ModalHeader;
Modal.Footer = ModalFooter;
Modal.Input = ModalInput;
29 changes: 29 additions & 0 deletions frontend/src/component/common/modal/ModalFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
interface IModalFooterProps {
/** 메인 버튼의 텍스트입니다. */
text: string;
/** 메인 버튼 클릭 시 호출되는 함수입니다. */
onClick?: () => void;
/** 보조 버튼의 텍스트입니다 (선택 사항) */
text2?: string;
/** 보조 버튼 클릭 시 호출되는 함수입니다 (선택 사항) */
onClick2?: () => void;
juwon5272 marked this conversation as resolved.
Show resolved Hide resolved
}

export const ModalFooter = (props: IModalFooterProps) => (
<div className="flex w-full flex-row-reverse items-center justify-start gap-5 rounded-lg bg-white py-4 shadow-sm">
<button
type="button"
className="bg-blueGray-600 h-[40px] rounded-lg px-4 py-2 text-sm font-semibold text-white hover:bg-blue-800"
onClick={props.onClick}
>
{props.text}
</button>
{props.text2 ? (
<button type="button" className="text-grayscale-400 text-xs" onClick={props.onClick2}>
{props.text2}
</button>
) : (
''
)}
</div>
);
18 changes: 18 additions & 0 deletions frontend/src/component/common/modal/ModalHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MdClear } from 'react-icons/md';
import { Button } from '../button/Button';

interface IModalHeaderProps {
/** 모달 헤더의 제목 텍스트입니다. */
content: string;
/** 모달을 닫는 함수입니다. */
onClose: () => void;
}

export const ModalHeader = (props: IModalHeaderProps) => (
<div className="flex flex-row items-start justify-between border-gray-200 py-4">
<h2 className="h-7 text-lg font-bold leading-7">{props.content}</h2>
<Button onClick={props.onClose}>
<MdClear />
</Button>
</div>
);
25 changes: 25 additions & 0 deletions frontend/src/component/common/modal/ModalInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
interface IModalInputProps {
/** 입력 필드의 제목입니다. */
title: string;
/** 입력 필드의 이름 속성입니다. */
name: string;
/** 입력 필드의 placeholder 텍스트입니다. */
placeholder: string;
/** 입력 필드의 현재 값입니다. */
value: string;
/** 입력 필드 값이 변경될 때 호출되는 함수입니다. */
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

export const ModalInput = (props: IModalInputProps) => (
<div className="flex flex-row items-center justify-between py-2">
<p className="font-medium">{props.title}</p>
<input
className="border-grayscale-75 text-grayscale-50 h-[32px] w-[250px] rounded-md border px-3 focus:outline-none focus:ring-1 focus:ring-blue-500"
name={props.name}
placeholder={props.placeholder}
value={props.value}
onChange={props.onChange}
/>
</div>
);
10 changes: 10 additions & 0 deletions frontend/src/hooks/useModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useState } from 'react';

export const useModal = () => {
const [isOpen, setIsOpen] = useState<boolean>(false);

const onOpen = () => setIsOpen(true);
const onClose = () => setIsOpen(false);

return { isOpen, onOpen, onClose };
};
59 changes: 59 additions & 0 deletions frontend/src/stories/AuthModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { Meta, Story } from '@storybook/react';
import { useModal } from '@/hooks/useModal';
import { AuthModal } from '@/component/authmodal/AuthModal';

export default {
title: 'Components/AuthModal',
component: AuthModal,
parameters: {
layout: 'fullscreen',
},
tags: ['autodocs'],
} as Meta<typeof AuthModal>;

const LoginModalTemplate: Story = args => {
const { onClose } = useModal();

return <AuthModal {...args} onClose={onClose} />;
};

const RegisterModalTemplate: Story = args => {
const { onClose } = useModal();

return <AuthModal {...args} onClose={onClose} />;
};

export const LoginModal = LoginModalTemplate.bind({});
LoginModal.args = {
isOpen: true,
type: 'login',
};
LoginModal.parameters = {
docs: {
description: {
story: '로그인 모달 컴포넌트를 렌더링합니다.',
},
},
backgrounds: {
default: 'gray',
values: [{ name: 'gray', value: '#f3f4f6' }],
},
};

export const RegisterModal = RegisterModalTemplate.bind({});
RegisterModal.args = {
isOpen: true,
type: 'register',
};
RegisterModal.parameters = {
docs: {
description: {
story: '회원가입 모달 컴포넌트를 렌더링합니다.',
},
},
backgrounds: {
default: 'gray',
values: [{ name: 'gray', value: '#f3f4f6' }],
},
};
Loading
Loading