diff --git a/package-lock.json b/package-lock.json index 9296376..4ef61b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.13", - "framer-motion": "^11.15.0", + "framer-motion": "^11.18.2", "init": "^0.1.2", "jest": "^29.7.0", "msw": "^2.7.0", @@ -10319,13 +10319,12 @@ } }, "node_modules/framer-motion": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz", - "integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==", - "license": "MIT", + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", "dependencies": { - "motion-dom": "^11.14.3", - "motion-utils": "^11.14.3", + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { @@ -13305,16 +13304,17 @@ } }, "node_modules/motion-dom": { - "version": "11.14.3", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz", - "integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==", - "license": "MIT" + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "dependencies": { + "motion-utils": "^11.18.1" + } }, "node_modules/motion-utils": { - "version": "11.14.3", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz", - "integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==", - "license": "MIT" + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==" }, "node_modules/ms": { "version": "2.1.3", diff --git a/package.json b/package.json index ce18120..3775787 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.13", - "framer-motion": "^11.15.0", + "framer-motion": "^11.18.2", "init": "^0.1.2", "jest": "^29.7.0", "msw": "^2.7.0", diff --git a/src/shared/constants/motion.ts b/src/shared/constants/motion.ts new file mode 100644 index 0000000..33f91e1 --- /dev/null +++ b/src/shared/constants/motion.ts @@ -0,0 +1,6 @@ +export const MODAL_MOTION = { + initial: { opacity: 0 }, + animate: { opacity: 1 }, + exit: { opacity: 0 }, + transition: { duration: 0.3 }, +}; diff --git a/src/shared/ui/Modal/Modal.stories.tsx b/src/shared/ui/Modal/Modal.stories.tsx new file mode 100644 index 0000000..c259092 --- /dev/null +++ b/src/shared/ui/Modal/Modal.stories.tsx @@ -0,0 +1,73 @@ +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { Modal } from './Modal'; + +export default { + title: 'components/common/Modal', + component: Modal, + tags: ['autodocs'], + argTypes: { + isOpen: { + control: 'boolean', + defaultValue: false, + }, + mode: { + control: { type: 'select' }, + options: ['wait', 'sync', 'popLayout'], + defaultValue: 'sync', + }, + }, + parameters: { + docs: { + description: { + component: + '사용자가 원하는 상태 모드에 따라 **모달을 띄우고 닫을 수 있는** 컴포넌트입니다.', + }, + }, + }, +} satisfies Meta; + +export const Basic: StoryObj = { + args: { + isOpen: false, + }, + render: (args) => { + const [isOpen, setIsOpen] = useState(args.isOpen); + + const openModal = () => setIsOpen(true); + const closeModal = () => setIsOpen(false); + + return ( +
+ + + +
+
ddingdong 모달입니다.
+ +
+ + +
+
+
+
+ ); + }, +}; diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx new file mode 100644 index 0000000..df4d127 --- /dev/null +++ b/src/shared/ui/Modal/Modal.tsx @@ -0,0 +1,50 @@ +import React, { useEffect, useRef } from 'react'; +import { motion } from 'framer-motion'; + +import { MODAL_MOTION } from '@/shared/constants/motion'; + +import { Portal } from '../Portal'; + +type Props = { + isOpen: boolean; + closeModal: () => void; + children: React.ReactNode; + mode?: 'wait' | 'sync' | 'popLayout'; +}; + +export function Modal({ isOpen, closeModal, children, mode }: Props) { + const modalRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(e.target as Node)) { + closeModal(); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen, closeModal]); + + return ( + + +
e.stopPropagation()} + className="items-center justify-center rounded-lg bg-white shadow-[0px_20px_24px_rgba(16,24,40,0.1),_0px_8px_8px_-4px_rgba(16,24,40,0.04)]" + > + {children} +
+
+
+ ); +} diff --git a/src/shared/ui/Modal/index.ts b/src/shared/ui/Modal/index.ts new file mode 100644 index 0000000..8deb0a3 --- /dev/null +++ b/src/shared/ui/Modal/index.ts @@ -0,0 +1 @@ +export { Modal } from './Modal'; diff --git a/src/shared/ui/Portal/Portal.tsx b/src/shared/ui/Portal/Portal.tsx new file mode 100644 index 0000000..726f57d --- /dev/null +++ b/src/shared/ui/Portal/Portal.tsx @@ -0,0 +1,21 @@ +import { AnimatePresence } from 'framer-motion'; +import ReactDom from 'react-dom'; + +type Props = { + isOpen: boolean; + mode?: 'wait' | 'sync' | 'popLayout'; + children: React.ReactNode; +}; + +export function Portal({ isOpen, mode = 'sync', children }: Props) { + if (typeof window === 'undefined' || !isOpen) { + return null; + } + + const container = document.body as HTMLElement; + + return ReactDom.createPortal( + {children}, + container + ); +} diff --git a/src/shared/ui/Portal/index.tsx b/src/shared/ui/Portal/index.tsx new file mode 100644 index 0000000..ca6e556 --- /dev/null +++ b/src/shared/ui/Portal/index.tsx @@ -0,0 +1 @@ +export { Portal } from './Portal';