diff --git a/.eslintrc.json b/.eslintrc.json index a556732b..c3767f46 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -56,6 +56,12 @@ } ] } + }, + { + "files": ["*.ts", "*.tsx"], + "rules": { + "no-undef": "off" + } } ] } diff --git a/pages/_app.tsx b/pages/_app.tsx index 0256c491..132e4bae 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -3,6 +3,7 @@ import type { AppProps } from 'next/app'; import '@/src/styles/global.css'; import Layout from '@/src/components/Layout/Layout'; +import Modal from '@/src/components/Modal'; import TanstackQueryProvider from '@/src/components/Providers/TanstackQueryProvider'; import Toast from '@/src/components/Toast/Toast'; import { pretendard } from '@/src/styles/font'; @@ -14,6 +15,7 @@ const App = ({ Component, pageProps }: AppProps) => { + diff --git a/src/components/Modal/Modal.stories.tsx b/src/components/Modal/Modal.stories.tsx new file mode 100644 index 00000000..4277ebf2 --- /dev/null +++ b/src/components/Modal/Modal.stories.tsx @@ -0,0 +1,31 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { useModalStore } from '@/src/stores/modalStore'; + +import Modal from '.'; + +const meta: Meta = { + title: 'Components/Modal', + component: Modal, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; + +type Story = StoryObj; + +export const DeleteArticle: Story = { + render: function Render() { + const { openModal } = useModalStore(); + + openModal('deleteArticle'); + + return ; + }, +}; diff --git a/src/components/Modal/ModalContainer.tsx b/src/components/Modal/ModalContainer.tsx new file mode 100644 index 00000000..13d9409f --- /dev/null +++ b/src/components/Modal/ModalContainer.tsx @@ -0,0 +1,15 @@ +import type { PropsWithChildren } from 'react'; + +import Portal from '../Portal'; +import * as styles from './style.css'; + +const ModalContainer = ({ children }: PropsWithChildren) => { + return ( + +
+
{children}
+ + ); +}; + +export default ModalContainer; diff --git a/src/components/Modal/components/DeleteArticle.tsx b/src/components/Modal/components/DeleteArticle.tsx new file mode 100644 index 00000000..4ea5f779 --- /dev/null +++ b/src/components/Modal/components/DeleteArticle.tsx @@ -0,0 +1,45 @@ +import { assignInlineVars } from '@vanilla-extract/dynamic'; + +import { useModalStore } from '@/src/stores/modalStore'; +import { COLORS } from '@/src/styles/tokens'; + +import ModalContainer from '../ModalContainer'; +import * as styles from '../style.css'; + +const DeleteArticle = () => { + const { closeModal } = useModalStore(); + + return ( + + 해당 글을 삭제할까요? +

+ {'삭제한 글은 복구할 수 없어요!\n삭제하시겠어요'} +

+
+ + +
+
+ ); +}; + +export default DeleteArticle; diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx new file mode 100644 index 00000000..8a9180bd --- /dev/null +++ b/src/components/Modal/index.tsx @@ -0,0 +1,13 @@ +import { useModalStore } from '@/src/stores/modalStore'; + +import DeleteArticle from './components/DeleteArticle'; + +const Modal = () => { + const { type } = useModalStore(); + + if (type === 'deleteArticle') return ; + + return null; +}; + +export default Modal; diff --git a/src/components/Modal/stores/modalStore.ts b/src/components/Modal/stores/modalStore.ts deleted file mode 100644 index e81c7f9f..00000000 --- a/src/components/Modal/stores/modalStore.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { createStore } from 'zustand'; -import { useStoreWithEqualityFn as useStore } from 'zustand/traditional'; - -const initialModalData = { - title: '', - firstButtonText: '', -}; - -interface ModalData { - title: string; - description?: string; - firstButtonText: string; - secondButtonText?: string; -} - -interface State { - modalData: ModalData; -} - -interface Action { - openModal: (modalData: ModalData) => void; -} - -export const modalStore = createStore((set) => ({ - modalData: initialModalData, - openModal: (modalData) => set({ modalData }), -})); - -export const useModalStore = ( - selector: (state: State & Action) => T, - equals?: (a: T, b: T) => boolean, -) => useStore(modalStore, selector, equals); diff --git a/src/components/Modal/style.css.ts b/src/components/Modal/style.css.ts new file mode 100644 index 00000000..ab4398cf --- /dev/null +++ b/src/components/Modal/style.css.ts @@ -0,0 +1,69 @@ +import { createVar, style } from '@vanilla-extract/css'; + +import { sprinkles } from '@/src/styles/sprinkles.css'; +import { COLORS } from '@/src/styles/tokens'; +import { middleLayer, positionCenter, topLayer } from '@/src/styles/utils.css'; + +export const modalStyle = style([ + positionCenter, + topLayer, + { + padding: '32px', + width: '400px', + borderRadius: '16px', + backgroundColor: COLORS['Grey/White'], + }, +]); + +export const dimmed = style([ + middleLayer, + { + backgroundColor: COLORS['Dim/50'], + position: 'fixed', + left: '0', + top: '0', + right: '0', + bottom: '0', + }, +]); + +export const title = style([ + sprinkles({ + typography: '20/Title/Semibold', + }), + { + display: 'block', + marginBottom: '18px', + }, +]); + +export const body = style([ + sprinkles({ + typography: '15/Body/Regular', + }), + { + display: 'block', + }, +]); + +export const buttonColor = createVar(); +export const buttonBackgroundColor = createVar(); + +export const button = style([ + sprinkles({ + typography: '15/Title/Medium', + }), + { + width: '164px', + color: buttonColor, + backgroundColor: buttonBackgroundColor, + padding: '16px 40px', + borderRadius: '8px', + marginTop: '24px', + selectors: { + '& + &': { + marginLeft: '8px', + }, + }, + }, +]); diff --git a/src/components/Portal/index.tsx b/src/components/Portal/index.tsx new file mode 100644 index 00000000..965a2187 --- /dev/null +++ b/src/components/Portal/index.tsx @@ -0,0 +1,31 @@ +import type { PropsWithChildren } from 'react'; +import { useEffect } from 'react'; +import ReactDOM from 'react-dom'; + +interface PortalProps { + id: 'modal-root'; + tag?: keyof HTMLElementTagNameMap; +} + +const Portal = ({ + children, + id, + tag = 'div', +}: PropsWithChildren) => { + const root = document.createElement(tag); + root.id = id; + + useEffect(() => { + if (root) { + document.body.appendChild(root); + } + + return () => { + document.body.removeChild(root); + }; + }, [root]); + + return ReactDOM.createPortal(children, root); +}; + +export default Portal; diff --git a/src/components/Toast/Toast.stories.tsx b/src/components/Toast/Toast.stories.tsx index 908e589f..7a93fc3e 100644 --- a/src/components/Toast/Toast.stories.tsx +++ b/src/components/Toast/Toast.stories.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { TOAST_DURATION_TIME } from '@/src/models/toastModel'; -import { useToast } from '@/src/stores/toastStore'; +import { useToastStore } from '@/src/stores/toastStore'; import Toast from './Toast'; @@ -23,7 +23,7 @@ type Story = StoryObj; export const Basic: Story = { render: function Render() { - const { showToast } = useToast(); + const { showToast } = useToastStore(); useEffect(() => { showToast({ message: '테스트', type: TOAST_DURATION_TIME.SHOW }); @@ -42,7 +42,7 @@ export const Basic: Story = { export const WithAction: Story = { render: function Render() { - const { showToast } = useToast(); + const { showToast } = useToastStore(); useEffect(() => { showToast({ message: '테스트', type: TOAST_DURATION_TIME.ACTION }); diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx index eb839641..0b8d76fa 100644 --- a/src/components/Toast/Toast.tsx +++ b/src/components/Toast/Toast.tsx @@ -1,16 +1,16 @@ import { useEffect } from 'react'; -import { useToast } from '@/src/stores/toastStore'; +import { useToastStore } from '@/src/stores/toastStore'; import * as styles from './style.css'; const Toast = () => { - const { toastData, hideToast } = useToast(); + const { toastData, hideToast } = useToastStore(); const { message, type, isToastVisible } = toastData; useEffect(() => { - let timer; + let timer: ReturnType; if (isToastVisible) { timer = setTimeout(() => hideToast(), type); diff --git a/src/components/Toast/style.css.ts b/src/components/Toast/style.css.ts index e62458c0..cfdbc184 100644 --- a/src/components/Toast/style.css.ts +++ b/src/components/Toast/style.css.ts @@ -1,7 +1,7 @@ import { recipe } from '@vanilla-extract/recipes'; import { sprinkles } from '@/src/styles/sprinkles.css'; -import { theme } from '@/src/styles/theme.css'; +import { COLORS } from '@/src/styles/tokens'; import { modalLayer } from '@/src/styles/utils.css'; export const toast = recipe({ @@ -11,9 +11,9 @@ export const toast = recipe({ typography: '14/Body/Regular', }), { - backgroundColor: theme.colors['Dim/70'], + backgroundColor: COLORS['Dim/70'], position: 'fixed', - color: theme.colors['Grey/White'], + color: COLORS['Grey/White'], bottom: 0, left: '50%', transform: 'translateX(-50%) translateY(30px)', diff --git a/src/stores/modalStore.ts b/src/stores/modalStore.ts new file mode 100644 index 00000000..662a160b --- /dev/null +++ b/src/stores/modalStore.ts @@ -0,0 +1,23 @@ +import { createStore } from 'zustand'; +import { shallow } from 'zustand/shallow'; +import { useStoreWithEqualityFn as useStore } from 'zustand/traditional'; + +type ModalType = 'deleteArticle'; + +interface State { + type: ModalType | null; +} + +interface Action { + openModal: (type: ModalType) => void; + closeModal: () => void; +} + +export const modalStore = createStore((set) => ({ + type: null, + openModal: (type) => set({ type }), + closeModal: () => set({ type: null }), +})); + +export const useModalStore = () => + useStore(modalStore, (state) => state, shallow); diff --git a/src/stores/toastStore.ts b/src/stores/toastStore.ts index 9291ccb4..f03ac402 100644 --- a/src/stores/toastStore.ts +++ b/src/stores/toastStore.ts @@ -39,17 +39,5 @@ export const toastStore = createStore((set, get) => ({ }, })); -const useToastStore = ( - selector: (state: State & Action) => T, - equals?: (a: T, b: T) => boolean, -) => useStore(toastStore, selector, equals); - -export const useToast = () => - useToastStore( - (state) => ({ - toastData: state.toastData, - showToast: state.showToast, - hideToast: state.hideToast, - }), - shallow, - ); +export const useToastStore = () => + useStore(toastStore, (state) => state, shallow); diff --git a/tsconfig.json b/tsconfig.json index cfddc22b..1da36090 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { + "strict": true, "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, - "strict": false, "noEmit": true, "esModuleInterop": true, "module": "esnext",