diff --git a/frontend/src/component/authmodal/AuthModal.tsx b/frontend/src/component/authmodal/AuthModal.tsx new file mode 100644 index 00000000..a13689a8 --- /dev/null +++ b/frontend/src/component/authmodal/AuthModal.tsx @@ -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) => { + 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 ( + + {modalType === 'login' ? ( + <> + + + + + + ) : ( + <> + + + + + + + + + )} + + ); +}; diff --git a/frontend/src/component/common/modal/Modal.tsx b/frontend/src/component/common/modal/Modal.tsx new file mode 100644 index 00000000..f0c3be5f --- /dev/null +++ b/frontend/src/component/common/modal/Modal.tsx @@ -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 ( +
+
+ {props.children} +
+
+ ); +}; + +Modal.Header = ModalHeader; +Modal.Footer = ModalFooter; +Modal.Input = ModalInput; diff --git a/frontend/src/component/common/modal/ModalFooter.tsx b/frontend/src/component/common/modal/ModalFooter.tsx new file mode 100644 index 00000000..8612db9a --- /dev/null +++ b/frontend/src/component/common/modal/ModalFooter.tsx @@ -0,0 +1,29 @@ +interface IModalFooterProps { + /** 메인 버튼의 텍스트입니다. */ + text: string; + /** 메인 버튼 클릭 시 호출되는 함수입니다. */ + onClick?: () => void; + /** 보조 버튼의 텍스트입니다 (선택 사항) */ + text2?: string; + /** 보조 버튼 클릭 시 호출되는 함수입니다 (선택 사항) */ + onClick2?: () => void; +} + +export const ModalFooter = (props: IModalFooterProps) => ( +
+ + {props.text2 ? ( + + ) : ( + '' + )} +
+); diff --git a/frontend/src/component/common/modal/ModalHeader.tsx b/frontend/src/component/common/modal/ModalHeader.tsx new file mode 100644 index 00000000..83bf4129 --- /dev/null +++ b/frontend/src/component/common/modal/ModalHeader.tsx @@ -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) => ( +
+

{props.content}

+ +
+); diff --git a/frontend/src/component/common/modal/ModalInput.tsx b/frontend/src/component/common/modal/ModalInput.tsx new file mode 100644 index 00000000..53d3fc08 --- /dev/null +++ b/frontend/src/component/common/modal/ModalInput.tsx @@ -0,0 +1,25 @@ +interface IModalInputProps { + /** 입력 필드의 제목입니다. */ + title: string; + /** 입력 필드의 이름 속성입니다. */ + name: string; + /** 입력 필드의 placeholder 텍스트입니다. */ + placeholder: string; + /** 입력 필드의 현재 값입니다. */ + value: string; + /** 입력 필드 값이 변경될 때 호출되는 함수입니다. */ + onChange: (e: React.ChangeEvent) => void; +} + +export const ModalInput = (props: IModalInputProps) => ( +
+

{props.title}

+ +
+); diff --git a/frontend/src/hooks/useModal.ts b/frontend/src/hooks/useModal.ts new file mode 100644 index 00000000..4488860b --- /dev/null +++ b/frontend/src/hooks/useModal.ts @@ -0,0 +1,10 @@ +import { useState } from 'react'; + +export const useModal = () => { + const [isOpen, setIsOpen] = useState(false); + + const onOpen = () => setIsOpen(true); + const onClose = () => setIsOpen(false); + + return { isOpen, onOpen, onClose }; +}; diff --git a/frontend/src/stories/AuthModal.stories.tsx b/frontend/src/stories/AuthModal.stories.tsx new file mode 100644 index 00000000..544e774c --- /dev/null +++ b/frontend/src/stories/AuthModal.stories.tsx @@ -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; + +const LoginModalTemplate: Story = args => { + const { onClose } = useModal(); + + return ; +}; + +const RegisterModalTemplate: Story = args => { + const { onClose } = useModal(); + + return ; +}; + +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' }], + }, +}; diff --git a/frontend/src/stories/common/modal/Modal.stories.tsx b/frontend/src/stories/common/modal/Modal.stories.tsx new file mode 100644 index 00000000..4fb7b29b --- /dev/null +++ b/frontend/src/stories/common/modal/Modal.stories.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { Meta, Story } from '@storybook/react'; +import { Modal } from '@/component/common/modal/Modal'; +import { useModal } from '@/hooks/useModal'; +import { fn } from '@storybook/test'; + +export default { + title: 'Components/common/modal/Modal', + component: Modal, + tags: ['autodocs'], +} as Meta; + +const LoginModalTemplate: Story = () => { + const { isOpen, onOpen, onClose } = useModal(); + + return ( +
+ + + + + + + + +
+ ); +}; + +const RegisterModalTemplate: Story = () => { + const { isOpen, onOpen, onClose } = useModal(); + + return ( +
+ + + + + + + + + + + +
+ ); +}; + +export const LoginModal = LoginModalTemplate.bind({}); +LoginModal.parameters = { + docs: { + description: { + story: '로그인 모달 컴포넌트를 렌더링합니다.', + }, + }, + backgrounds: { + default: 'gray', + values: [{ name: 'gray', value: '#f3f4f6' }], + }, +}; + +export const RegisterModal = RegisterModalTemplate.bind({}); +RegisterModal.parameters = { + docs: { + description: { + story: '회원가입 모달 컴포넌트를 렌더링합니다.', + }, + }, + backgrounds: { + default: 'gray', + values: [{ name: 'gray', value: '#f3f4f6' }], + }, +}; diff --git a/frontend/src/stories/common/modal/ModalFooter.stories.tsx b/frontend/src/stories/common/modal/ModalFooter.stories.tsx new file mode 100644 index 00000000..767fca48 --- /dev/null +++ b/frontend/src/stories/common/modal/ModalFooter.stories.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Meta, Story } from '@storybook/react'; +import { ModalFooter } from '@/component/common/modal/ModalFooter'; +import { fn } from '@storybook/test'; + +export default { + title: 'Components/common/modal/ModalFooter', + component: ModalFooter, + parameters: { layout: 'centered' }, + tags: ['autodocs'], +} as Meta; + +const Template: Story = args => ; + +export const OneButton = Template.bind({}); +OneButton.args = { + text: '회원가입', + onClick: fn(), +}; + +export const TwoButton = Template.bind({}); +TwoButton.args = { + text: '로그인', + onClick: fn(), + text2: '회원가입', + onClick2: fn(), +}; diff --git a/frontend/src/stories/common/modal/ModalHeader.stories.tsx b/frontend/src/stories/common/modal/ModalHeader.stories.tsx new file mode 100644 index 00000000..ca09941b --- /dev/null +++ b/frontend/src/stories/common/modal/ModalHeader.stories.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Meta, Story } from '@storybook/react'; +import { ModalHeader } from '@/component/common/modal/ModalHeader'; +import { fn } from '@storybook/test'; + +export default { + title: 'Components/common/modal/ModalHeader', + component: ModalHeader, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, +} as Meta; + +const Template: Story = args => ; + +export const DefaultHeader = Template.bind({}); +DefaultHeader.args = { + content: 'Login', + onClose: fn(), +}; diff --git a/frontend/src/stories/common/modal/ModalInput.stories.tsx b/frontend/src/stories/common/modal/ModalInput.stories.tsx new file mode 100644 index 00000000..8a4968f7 --- /dev/null +++ b/frontend/src/stories/common/modal/ModalInput.stories.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Meta, Story } from '@storybook/react'; +import { ModalInput } from '@/component/common/modal/ModalInput'; +import { fn } from '@storybook/test'; + +export default { + title: 'Components/common/modal/ModalInput', + component: ModalInput, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} as Meta; + +const Template: Story = args => ; + +export const DefaultInput = Template.bind({}); +DefaultInput.args = { + title: 'ID', + name: 'id', + placeholder: '사용할 ID를 입력해주세요.', + value: '', + onChange: fn(), +}; + +export const NoTitle = Template.bind({}); +NoTitle.args = { + title: '', + name: 'confirmPw', + placeholder: '비밀번호를 한 번 더 입력해주세요.', + value: '', + onChange: fn(), +};