diff --git a/.eslintrc.json b/.eslintrc.json
index 7f56a58..bae8d23 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -48,6 +48,11 @@
{
"html": "ignore"
}
+ ],
+ "react/require-default-props": [
+ "error", {
+ "functions" : "defaultArguments"
+ }
]
}
}
diff --git a/.storybook/main.ts b/.storybook/main.ts
index d98b990..5f559f5 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -14,5 +14,23 @@ const config: StorybookConfig = {
options: {},
},
staticDirs: ['../public'],
+ webpackFinal: async (config) => {
+ config.module = config.module || {}
+ config.module.rules = config.module.rules || []
+ const imageRule = config.module.rules.find((rule) =>
+ // @ts-ignore
+ rule?.['test']?.test('.svg'),
+ )
+ if (imageRule) {
+ // @ts-ignore
+ imageRule['exclude'] = /\.svg$/
+ }
+ config.module.rules.push({
+ test: /\.svg$/,
+ use: ['@svgr/webpack'],
+ })
+
+ return config
+ },
}
export default config
diff --git a/.storybook/preview-body.html b/.storybook/preview-body.html
new file mode 100644
index 0000000..0288277
--- /dev/null
+++ b/.storybook/preview-body.html
@@ -0,0 +1 @@
+
diff --git a/package.json b/package.json
index 58dfa96..3c9ab23 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
},
"dependencies": {
"@svgr/webpack": "^8.1.0",
+ "@storybook/preview-api": "^8.1.11",
"eslint-import-resolver-typescript": "^3.6.1",
"next": "14.2.4",
"react": "^18",
diff --git a/public/icons/bottom_modal.svg b/public/icons/bottom_modal.svg
new file mode 100644
index 0000000..6d3911a
--- /dev/null
+++ b/public/icons/bottom_modal.svg
@@ -0,0 +1,14 @@
+
diff --git a/public/icons/modal.svg b/public/icons/modal.svg
new file mode 100644
index 0000000..15dde8e
--- /dev/null
+++ b/public/icons/modal.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
index f3515d6..ae117fc 100644
--- a/src/components/Button/index.tsx
+++ b/src/components/Button/index.tsx
@@ -3,17 +3,17 @@ import { twMerge } from 'tailwind-merge'
interface ButtonProps extends React.ButtonHTMLAttributes {
variant?: 'primary' | 'secondary'
- size: 'sm' | 'md' | 'lg'
+ size?: 'sm' | 'md' | 'lg'
className?: string
isSubmit?: boolean
}
function Button({
- className,
- size,
+ className = '',
+ size = 'md',
children,
- variant,
- isSubmit,
+ variant = 'primary',
+ isSubmit = false,
...props
}: ButtonProps) {
const getSizeClass = () => {
@@ -51,10 +51,4 @@ function Button({
)
}
-Button.defaultProps = {
- variant: 'primary',
- className: '',
- isSubmit: false,
-}
-
export default Button
diff --git a/src/components/Modal/Modal.stories.tsx b/src/components/Modal/Modal.stories.tsx
new file mode 100644
index 0000000..7cd3a5a
--- /dev/null
+++ b/src/components/Modal/Modal.stories.tsx
@@ -0,0 +1,152 @@
+/* eslint-disable react-hooks/rules-of-hooks,@typescript-eslint/naming-convention */
+/* eslint-disable react/jsx-props-no-spreading */
+/* eslint-disable-next-line @typescript-eslint/naming-convention */
+
+import React from 'react'
+import type { Meta, StoryObj } from '@storybook/react'
+import { useArgs } from '@storybook/preview-api'
+import BottomModalSvg from 'public/icons/bottom_modal.svg'
+import ModalSvg from 'public/icons/modal.svg'
+import Button from '@/components/Button'
+import Modal from './index'
+
+const meta: Meta = {
+ title: 'Modal',
+ component: Modal,
+ argTypes: {
+ isOpen: { control: 'boolean' },
+ },
+}
+
+export default meta
+type Story = StoryObj
+
+export const Bottom_Modal_Confirm: Story = {
+ render: () => {
+ const [{ isOpen }, setIsOpen] = useArgs()
+
+ const onClose = () => {
+ setIsOpen({ isOpen: false })
+ }
+ return (
+
+
+
+ }>
+
+
+ 링크가 복사되었습니다!
+
+ {'POLABO를\n지인들에게도 알려주세요!'}
+
+
+
+
+
+
+
+
+ )
+ },
+}
+
+export const Bottom_Modal_Confirm_Cancel: Story = {
+ render: () => {
+ const [{ isOpen }, setIsOpen] = useArgs()
+
+ const onClose = () => {
+ setIsOpen({ isOpen: false })
+ }
+ return (
+
+
+
+ }>
+
+
+ 링크가 복사되었습니다!
+
+ {'POLABO를\n지인들에게도 알려주세요!'}
+
+
+
+
+
+
+
+
+ )
+ },
+}
+
+export const Center_Modal_Confirm: Story = {
+ render: () => {
+ const [{ isOpen }, setIsOpen] = useArgs()
+
+ const onClose = () => {
+ setIsOpen({ isOpen: false })
+ }
+ return (
+
+
+
+ }>
+
+
+ 링크가 복사되었습니다!
+
+ {'POLABO를\n지인들에게도 알려주세요!'}
+
+
+
+
+
+
+
+
+ )
+ },
+}
+
+export const Center_Modal_Confirm_Cancel: Story = {
+ render: () => {
+ const [{ isOpen }, setIsOpen] = useArgs()
+
+ const onClose = () => {
+ setIsOpen({ isOpen: false })
+ }
+ return (
+
+
+
+ }>
+
+
+ 링크가 복사되었습니다!
+
+ {'POLABO를\n지인들에게도 알려주세요!'}
+
+
+
+
+
+
+
+
+ )
+ },
+}
diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx
new file mode 100644
index 0000000..6720404
--- /dev/null
+++ b/src/components/Modal/index.tsx
@@ -0,0 +1,225 @@
+'use client'
+
+import React, {
+ createContext,
+ ReactNode,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react'
+import ReactDOM from 'react-dom'
+import Close from 'public/icons/close.svg'
+import Button from '@/components/Button'
+
+interface ModalContextProps {
+ isVisible: boolean
+ onClose: () => void
+}
+
+const ModalContext = createContext({
+ isVisible: false,
+ onClose: () => {},
+})
+
+const ModalOverlay = ({
+ children,
+ handleTransitionEnd,
+}: {
+ children: ReactNode
+ handleTransitionEnd: () => void
+}) => {
+ const { isVisible, onClose } = useContext(ModalContext)
+
+ const handleClick = (event: React.MouseEvent) => {
+ if (event.target === event.currentTarget) {
+ onClose()
+ }
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+interface ModalProps {
+ isOpen: boolean
+ onClose: () => void
+ children: ReactNode
+}
+
+function Modal({ isOpen, onClose, children }: ModalProps) {
+ const [isVisible, setIsVisible] = useState(false)
+
+ useEffect(() => {
+ setIsVisible(isOpen)
+ }, [isOpen])
+
+ const handleTransitionEnd = () => {
+ if (!isVisible) {
+ onClose()
+ }
+ }
+
+ const context = useMemo(
+ () => ({
+ isVisible,
+ onClose: () => setIsVisible(false),
+ }),
+ [isVisible, setIsVisible],
+ )
+
+ return isOpen
+ ? ReactDOM.createPortal(
+
+
+ {children}
+
+ ,
+ document.getElementById('modal-root') as HTMLElement,
+ )
+ : null
+}
+
+const CenterModal = ({
+ icon,
+ children,
+}: {
+ icon: ReactNode
+ children: ReactNode
+}) => {
+ return (
+
+ {icon && (
+
+ {icon}
+
+ )}
+ {children}
+
+ )
+}
+
+const BottomModal = ({
+ icon,
+ children,
+}: {
+ icon: ReactNode
+ children: ReactNode
+}) => {
+ const { isVisible } = useContext(ModalContext)
+ return (
+
+ {icon && (
+
{icon}
+ )}
+ {children}
+
+ )
+}
+
+const ModalClose = () => {
+ const { onClose } = useContext(ModalContext)
+ return (
+
+ )
+}
+
+const ModalBody = ({ children }: { children: ReactNode }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+const ModalBodyTitle = ({ children }: { children: ReactNode }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+const ModalBodyContent = ({ children }: { children: ReactNode }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+const ModalFooter = ({ children }: { children: ReactNode }) => {
+ return {children}
+}
+
+const ModalFooterConfirm = ({
+ confirmText,
+ onConfirm = () => {},
+}: {
+ confirmText: string
+ onConfirm?: () => void
+}) => {
+ const { onClose } = useContext(ModalContext)
+
+ const clickHandler = () => {
+ onClose()
+ onConfirm()
+ }
+
+ return (
+
+ )
+}
+
+const ModalFooterConfirmCancel = ({
+ cancelText,
+ confirmText,
+ onConfirm = () => {},
+}: {
+ cancelText: string
+ confirmText: string
+ onConfirm?: () => void
+}) => {
+ const { onClose } = useContext(ModalContext)
+
+ const clickHandler = () => {
+ onClose()
+ onConfirm()
+ }
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
+Modal.CenterModal = CenterModal
+Modal.BottomModal = BottomModal
+Modal.Close = ModalClose
+Modal.Body = ModalBody
+Modal.BodyTitle = ModalBodyTitle
+Modal.BodyContent = ModalBodyContent
+Modal.Footer = ModalFooter
+Modal.FooterConfirm = ModalFooterConfirm
+Modal.FooterConfirmCancel = ModalFooterConfirmCancel
+
+export default Modal
diff --git a/src/components/TagButton/index.tsx b/src/components/TagButton/index.tsx
index 7ae6202..3a5ce33 100644
--- a/src/components/TagButton/index.tsx
+++ b/src/components/TagButton/index.tsx
@@ -8,9 +8,9 @@ interface TagButtonProps extends React.ButtonHTMLAttributes {
}
function TagButton({
- className,
children,
- isSubmit,
+ className = '',
+ isSubmit = false,
disabled,
size,
...props
@@ -43,9 +43,4 @@ function TagButton({
)
}
-TagButton.defaultProps = {
- className: '',
- isSubmit: false,
-}
-
export default TagButton