diff --git a/src/pages/@owner/account/@modal/(.)select-bank-bottom-sheet/SelectBankBottomSheet.tsx b/src/pages/@owner/account/@bottom-sheet/SelectBankBottomSheet.tsx similarity index 100% rename from src/pages/@owner/account/@modal/(.)select-bank-bottom-sheet/SelectBankBottomSheet.tsx rename to src/pages/@owner/account/@bottom-sheet/SelectBankBottomSheet.tsx diff --git a/src/pages/@owner/account/@modal/(.)confirm-delete-modal/ConfirmDeleteModal.tsx b/src/pages/@owner/account/@modal/(.)confirm-delete-modal/ConfirmDeleteModal.tsx deleted file mode 100644 index c6d6e589..00000000 --- a/src/pages/@owner/account/@modal/(.)confirm-delete-modal/ConfirmDeleteModal.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Overlay from '@layout/overlay/Overlay'; -import Button from '@ui/button/Button'; - -interface ConfirmDeleteModalProps { - isOpen: boolean; - handleClose: () => void; - handleClickConfirm: () => void; - handleClickCancel: () => void; -} - -export default function ConfirmDeleteModal({ - isOpen, - handleClose, - handleClickConfirm, - handleClickCancel, -}: ConfirmDeleteModalProps) { - return ( - -
-
-

- 이 계좌를 삭제할까요? -

-

- 삭제 후에는 되돌릴 수 없습니다. -

-
-
- - -
-
-
- ); -} diff --git a/src/pages/@owner/account/@modal/(.)confirm-exit-modal/ConfirmExitModal.tsx b/src/pages/@owner/account/@modal/(.)confirm-exit-modal/ConfirmExitModal.tsx deleted file mode 100644 index cd3acc7e..00000000 --- a/src/pages/@owner/account/@modal/(.)confirm-exit-modal/ConfirmExitModal.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Overlay from '@layout/overlay/Overlay'; -import Button from '@ui/button/Button'; - -interface ConfirmExitModalProps { - isOpen: boolean; - handleClose: () => void; - handleClickConfirm: () => void; - handleClickCancel: () => void; -} - -export default function ConfirmExitModal({ - isOpen, - handleClose, - handleClickConfirm, - handleClickCancel, -}: ConfirmExitModalProps) { - return ( - -
-
-

정말 나가시겠어요?

-

- 작성 중인 내용은 저장되지 않으며, -
- 나가면 모두 삭제됩니다. -

-
-
- - -
-
-
- ); -} diff --git a/src/pages/@owner/account/@modal/(.)save-account-modal/SaveAccountModal.tsx b/src/pages/@owner/account/@modal/(.)save-account-modal/SaveAccountModal.tsx deleted file mode 100644 index 381d0ff8..00000000 --- a/src/pages/@owner/account/@modal/(.)save-account-modal/SaveAccountModal.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import Overlay from '@layout/overlay/Overlay'; -import { type AccountFormData } from '@pages/@owner/account/schemas/account.schema'; - -import Button from '@ui/button/Button'; - -interface SaveAccountModalProps { - formData: AccountFormData; - isOpen: boolean; - handleClose: () => void; - handleConfirm: () => void; - handleCancel: () => void; -} - -export default function SaveAccountModal({ - formData, - isOpen, - handleClose, - handleConfirm, - handleCancel, -}: SaveAccountModalProps) { - return ( - -
-

저장정보 확인

-
-
-
-

은행

-

- {formData.bankName} -

-
-
-

예금주

-

- {formData.accountHolderName} -

-
-
-

계좌번호

-

- {formData.accountNumber} -

-
-
- -
-
-
- - ); -} diff --git a/src/pages/@owner/account/@modal/AccountModals.tsx b/src/pages/@owner/account/@modal/AccountModals.tsx new file mode 100644 index 00000000..d006b6c3 --- /dev/null +++ b/src/pages/@owner/account/@modal/AccountModals.tsx @@ -0,0 +1,83 @@ +import ConfirmModal from '@components/ui/modal-confirm/ConfirmModal'; +import type { AccountFormData } from '@pages/@owner/account/schemas/account.schema'; + +interface ModalHandler { + isOpen: boolean; + handleClose: () => void; + handleConfirm: () => void; + handleCancel: () => void; +} + +interface AccountPageModalsProps { + formData: AccountFormData; + exitModal: ModalHandler; + saveModal: ModalHandler; + deleteModal: ModalHandler; +} + +export default function AccountModals({ + formData, + exitModal, + saveModal, + deleteModal, +}: AccountPageModalsProps) { + return ( + <> + {/* 이탈 방지 모달*/} + + {/* 저장 전 확인 모달 */} + +
+
+
+

은행

+

+ {formData.bankName} +

+
+
+

예금주

+

+ {formData.accountHolderName} +

+
+
+

계좌번호

+

+ {formData.accountNumber} +

+
+
+ + } + rightLabel='저장' + handleClickRight={saveModal.handleConfirm} + handleClickLeft={saveModal.handleCancel} + /> + {/* 계좌 삭제 재확인 모달 */} + + + ); +} diff --git a/src/pages/@owner/account/@modal/index.ts b/src/pages/@owner/account/@modal/index.ts deleted file mode 100644 index 8e84c438..00000000 --- a/src/pages/@owner/account/@modal/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default as SelectBankBottomSheet } from './(.)select-bank-bottom-sheet/SelectBankBottomSheet'; -export { default as ConfirmExitModal } from './(.)confirm-exit-modal/ConfirmExitModal'; -export { default as SaveAccountModal } from './(.)save-account-modal/SaveAccountModal'; -export { default as ConfirmDeleteModal } from './(.)confirm-delete-modal/ConfirmDeleteModal'; diff --git a/src/pages/@owner/account/AccountForm.tsx b/src/pages/@owner/account/AccountForm.tsx index 3b1f3763..32f81de5 100644 --- a/src/pages/@owner/account/AccountForm.tsx +++ b/src/pages/@owner/account/AccountForm.tsx @@ -1,19 +1,15 @@ -import Button from '@ui/button/Button'; import Information from '@components/information/Information'; -import Navigation from '@layout/navigation/Navigation'; -import Input from '@ui/input/Input'; -import { Icon } from '@icon/Icon'; -import { cn } from '@utils/cn'; +import FormFieldLayout from '@components/layout/form/FormFieldLayout'; import ErrorText from '@form/error-text/ErrorText'; +import { Icon } from '@icon/Icon'; import Loading from '@layout/loading/Loading'; +import Navigation from '@layout/navigation/Navigation'; +import SelectBankBottomSheet from '@pages/@owner/account/@bottom-sheet/SelectBankBottomSheet'; import { useAccountPage } from '@pages/@owner/account/hooks/use-account-page'; -import { - SelectBankBottomSheet, - ConfirmExitModal, - SaveAccountModal, - ConfirmDeleteModal, -} from '@pages/@owner/account/@modal'; -import FormFieldLayout from '@components/layout/form/FormFieldLayout'; +import Button from '@ui/button/Button'; +import Input from '@ui/input/Input'; +import { cn } from '@utils/cn'; +import AccountModals from './@modal/AccountModals'; export default function Account() { const { @@ -51,24 +47,11 @@ export default function Account() { handleChange={bankModal.handleChange} bank={formData.bankName} /> - - - void; - handleClickConfirm: () => void; -} - -export default function DeleteFoodTruckConfirmModal({ - isOpen, - handleClose, - handleClickConfirm, -}: DeleteFoodTruckConfirmModalProps) { - return ( - -
-
-

- 이 푸드트럭을 삭제할까요? -

-

- 삭제 후에는 되돌릴 수 없습니다. -

-
-
- - -
-
-
- ); -} diff --git a/src/pages/@owner/food-truck-management/FoodTruckManagement.tsx b/src/pages/@owner/food-truck-management/FoodTruckManagement.tsx index 73d38265..1e49fd6c 100644 --- a/src/pages/@owner/food-truck-management/FoodTruckManagement.tsx +++ b/src/pages/@owner/food-truck-management/FoodTruckManagement.tsx @@ -1,20 +1,20 @@ -import { useNavigate } from 'react-router-dom'; -import { useInView } from 'react-intersection-observer'; -import { useEffect } from 'react'; -import Navigation from '@layout/navigation/Navigation'; -import { Icon } from '@icon/Icon'; import Information from '@components/information/Information'; -import Button from '@ui/button/Button'; +import Spinner from '@components/spinner/Spinner'; +import ConfirmModal from '@components/ui/modal-confirm/ConfirmModal'; +import useToast from '@hooks/use-toast'; +import { Icon } from '@icon/Icon'; +import Loading from '@layout/loading/Loading'; +import Navigation from '@layout/navigation/Navigation'; +import { useFoodTruckEditMode } from '@pages/@owner/food-truck-management/hooks/use-food-truck-edit-mode'; import { useGetOwnerFoodTrucks } from '@pages/@owner/food-truck-management/hooks/use-food-truck-list'; -import { cn } from '@utils/cn'; import { ROUTES } from '@router/constant/routes'; -import DeleteFoodTruckConfirm from '@pages/@owner/food-truck-management/@modal/(.)delete-food-truck-confirm-modal/DeleteFoodTruckConfirmModal'; -import Loading from '@layout/loading/Loading'; import FoodTruckCard from '@shared/components/food-truck/FoodTruckCard'; -import { useFoodTruckEditMode } from '@pages/@owner/food-truck-management/hooks/use-food-truck-edit-mode'; -import Spinner from '@components/spinner/Spinner'; +import Button from '@ui/button/Button'; +import { cn } from '@utils/cn'; import type { MyFoodTruckResponse } from 'apis/data-contracts'; -import useToast from '@hooks/use-toast'; +import { useEffect } from 'react'; +import { useInView } from 'react-intersection-observer'; +import { useNavigate } from 'react-router-dom'; import { VIEWED_STATUS } from './constants/viewed-status'; export default function FoodTruckManagement() { @@ -66,10 +66,14 @@ export default function FoodTruckManagement() { return ( <> - -
- - {isOnboarding ? '신청하시겠습니까?' : '아직 완료되지 않았어요!'} - - - {isOnboarding ? '최종 승인까지 영업일 기준 3-5일이 소요돼요.' : '페이지를 나가면 작성 중인 내용이 사라집니다.'} - -
- {isOnboarding ? ( - <> - - - - ) : ( - <> - - - - )} -
-
- + title={modalConfig.title} + description={modalConfig.description} + rightLabel={modalConfig.rightLabel} + leftLabel={modalConfig.leftLabel} + handleClickRight={modalConfig.onClickRight} + handleClickLeft={modalConfig.onClickLeft} + /> ); } diff --git a/src/pages/@owner/menu/MenuEdit.tsx b/src/pages/@owner/menu/MenuEdit.tsx index beaf4717..63d4cada 100644 --- a/src/pages/@owner/menu/MenuEdit.tsx +++ b/src/pages/@owner/menu/MenuEdit.tsx @@ -6,8 +6,9 @@ import { Icon } from '@components/icon/Icon'; import { convertURLtoFile } from '@utils/convert-image-url'; import useToast from '@shared/hooks/use-toast'; import { FormProvider } from 'react-hook-form'; -import { MenuDeleteModal, MenuForm } from '@pages/@owner/menu/components'; +import { MenuForm } from '@pages/@owner/menu/components'; import { useEditMenu, useFormValidation } from '@pages/@owner/menu/hooks'; +import ConfirmModal from '@components/ui/modal-confirm/ConfirmModal'; export default function MenuEdit() { const location = useLocation(); @@ -75,10 +76,15 @@ export default function MenuEdit() { return ( - void; - handleConfirmDelete: () => void; -} - -export default function MenuDeleteModal({ - isModalOpen, - handleCloseModal, - handleConfirmDelete, -}: MenuDeleteModalProps) { - return ( - -
- - 이 메뉴를 삭제할까요? - - - 삭제 후에는 되돌릴 수 없습니다. - -
- - -
-
-
- ); -} diff --git a/src/pages/@owner/menu/components/index.ts b/src/pages/@owner/menu/components/index.ts index c60aaeda..c2b3e704 100644 --- a/src/pages/@owner/menu/components/index.ts +++ b/src/pages/@owner/menu/components/index.ts @@ -1,5 +1,4 @@ export { default as MenuForm } from './MenuForm'; -export { default as MenuDeleteModal } from './MenuDeleteModal'; export { default as MenuListHeader } from './MenuListHeader'; export { default as Menus } from './Menus'; export { default as ListSortBottomSheet } from './ListSortBottomSheet'; diff --git a/src/pages/@owner/message-list/@modal/(.)delete-message-bottom-sheet/DeleteMessageBottomSheet.tsx b/src/pages/@owner/message-list/@bottom-sheet/DeleteMessageBottomSheet.tsx similarity index 100% rename from src/pages/@owner/message-list/@modal/(.)delete-message-bottom-sheet/DeleteMessageBottomSheet.tsx rename to src/pages/@owner/message-list/@bottom-sheet/DeleteMessageBottomSheet.tsx diff --git a/src/pages/@owner/message-list/@modal/(.)confirm-delete-modal/ConfirmExitModal.tsx b/src/pages/@owner/message-list/@modal/(.)confirm-delete-modal/ConfirmExitModal.tsx deleted file mode 100644 index 1d5010a4..00000000 --- a/src/pages/@owner/message-list/@modal/(.)confirm-delete-modal/ConfirmExitModal.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Overlay from '@layout/overlay/Overlay'; -import Button from '@ui/button/Button'; - -interface ConfirmDeleteModalProps { - isOpen: boolean; - handleClose: () => void; - handleClickConfirm: () => void; - handleClickCancel: () => void; -} - -export default function ConfirmDeleteModal({ - isOpen, - handleClose, - handleClickConfirm, - handleClickCancel, -}: ConfirmDeleteModalProps) { - return ( - -
-
-

- 이 메시지를 삭제할까요? -

-

- 삭제 후에는 되돌릴 수 없습니다. -

-
-
- - -
-
-
- ); -} diff --git a/src/pages/@owner/message-list/@modal/(.)confirm-modal/ConfirmModal.tsx b/src/pages/@owner/message-list/@modal/(.)confirm-modal/ConfirmModal.tsx deleted file mode 100644 index 5ca912dd..00000000 --- a/src/pages/@owner/message-list/@modal/(.)confirm-modal/ConfirmModal.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import Overlay from '@layout/overlay/Overlay'; -import Button from '@ui/button/Button'; - -interface ConfirmModalProps { - isOpen: boolean; - handleClose: () => void; - handleClickConfirm: () => void; - handleClickCancel: () => void; -} - -export default function ConfirmModal({ - isOpen, - handleClose, - handleClickConfirm, - handleClickCancel, -}: ConfirmModalProps) { - return ( - -
-
-

- 저장하지 않고 나가시겠습니까? -

-

- 작성 중인 내용은 저장되지 않으며, -
- 나가면 모두 삭제됩니다. -

-
-
- - -
-
-
- ); -} diff --git a/src/pages/@owner/message-list/MessageForm.tsx b/src/pages/@owner/message-list/MessageForm.tsx index 5145320f..61db4697 100644 --- a/src/pages/@owner/message-list/MessageForm.tsx +++ b/src/pages/@owner/message-list/MessageForm.tsx @@ -1,19 +1,19 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import Button from '@ui/button/Button'; +import ConfirmModal from '@components/ui/modal-confirm/ConfirmModal'; +import { zodResolver } from '@hookform/resolvers/zod'; +import useToast from '@hooks/use-toast'; import { Icon } from '@icon/Icon'; +import Loading from '@layout/loading/Loading'; import Navigation from '@layout/navigation/Navigation'; -import ConfirmModal from '@pages/@owner/message-list/@modal/(.)confirm-modal/ConfirmModal'; import { usePostOwnerChatTemplates } from '@pages/@owner/message-list/hooks/use-owner-message'; -import useToast from '@hooks/use-toast'; -import Loading from '@layout/loading/Loading'; -import Textarea from '@ui/text-area/Textarea'; import { chatTemplateSchema, type ChatTemplateFormType, } from '@pages/@owner/message-list/schemas/message-list.schema'; -import { zodResolver } from '@hookform/resolvers/zod'; +import Button from '@ui/button/Button'; +import Textarea from '@ui/text-area/Textarea'; +import { useState } from 'react'; import { useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; export default function MessageForm() { const navigate = useNavigate(); @@ -81,8 +81,10 @@ export default function MessageForm() { - } handleLeftClick={handleClickBack} /> -
+
-
+
- -
-
- -
+ title={'선택한 대화를 삭제할까요?'} + description={`${selectChatList.size}건이 삭제되며 되돌릴 수 없습니다.`} + handleClickRight={handleCloseModal} + handleClickLeft={handleCloseModal} + /> +
{(chatList ?? []).map(item => { return ( { // BottomSheet states @@ -51,32 +51,13 @@ const Home = () => { return (
{/* Modal */} - -
-

모달 제목

-

모달 내용입니다.

-
- - -
-
-
+ title={'모달 제목'} + description={'모달 내용입니다.'} + /> + {/* BottomSheet */} void; -} - -export default function DeleteAccountModal({ - isOpen, - handleClose, -}: DeleteAccountModalProps) { - const navigate = useNavigate(); - - const handleDeleteAccount = () => { - // TODO: 회원탈퇴 api 및 토스트 메시지 추가 - alert('회원탈퇴 되셨습니다.'); - navigate('/'); - }; - - return ( - -
-
- - 정말 탈퇴하시겠어요? - - - 탈퇴하면 모든 정보가 삭제되며, -
복구할 수 없습니다. -
-
-
- - -
-
-
- ); -} diff --git a/src/pages/profile-setting/ProfileSetting.tsx b/src/pages/profile-setting/ProfileSetting.tsx index 8cd00f46..dbd9deee 100644 --- a/src/pages/profile-setting/ProfileSetting.tsx +++ b/src/pages/profile-setting/ProfileSetting.tsx @@ -1,17 +1,17 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import Navigation from '@layout/navigation/Navigation'; -import { ROUTES } from '@router/constant/routes'; import { Icon } from '@components/icon/Icon'; -import Button from '@ui/button/Button'; +import ConfirmModal from '@components/ui/modal-confirm/ConfirmModal'; import Loading from '@layout/loading/Loading'; -import DeleteAccountModal from '@pages/profile-setting/@modal/(.)delete-account-modal/DeleteAccountModal'; +import Navigation from '@layout/navigation/Navigation'; +import { useGetUserInfo } from '@pages/mypage/hooks/use-user-data'; import { AgreementSection, ProfileImageSection, UserDataSection, } from '@pages/profile-setting/components'; -import { useGetUserInfo } from '@pages/mypage/hooks/use-user-data'; +import { ROUTES } from '@router/constant/routes'; +import Button from '@ui/button/Button'; +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; export default function ProfileSetting() { const navigate = useNavigate(); @@ -41,12 +41,26 @@ export default function ProfileSetting() { setIsModalOpen(false); }; + const handleDeleteAccount = () => { + // TODO: 회원탈퇴 api 및 토스트 메시지 추가 + alert('회원탈퇴 되셨습니다.'); + navigate(ROUTES.HOME); + }; + const handleEditProfile = () => { navigate(ROUTES.PROFILE_SETTING_EDIT); }; return ( <> + } handleLeftClick={handleGoBack} @@ -68,25 +82,24 @@ export default function ProfileSetting() {
-
+
-
+
- ); } diff --git a/src/shared/components/ui/modal-alert/AlertModal.stories.tsx b/src/shared/components/ui/modal-alert/AlertModal.stories.tsx new file mode 100644 index 00000000..4fb608e9 --- /dev/null +++ b/src/shared/components/ui/modal-alert/AlertModal.stories.tsx @@ -0,0 +1,45 @@ +import AlertModal from '@components/ui/modal-alert/AlertModal'; +import type { Meta, StoryObj } from '@storybook/react-vite'; + +const meta: Meta = { + title: 'Components/UI/Modal/AlertModal', + component: AlertModal, + parameters: { + layout: 'centered', + docs: { + description: { + component: '확인만 받는 모달', + }, + }, + }, + args: { + isOpen: true, + handleClose: () => + console.info('AlertModal: handleClose (확인/닫기 클릭됨)'), + confirmLabel: '확인', + }, + argTypes: { + description: { control: 'text' }, + isOpen: { control: 'boolean' }, + handleClose: { table: { disable: true } }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: '저장정보 확인', + description: '계좌번호가 일치하지 않습니다. 다시 확인해 주세요.', + }, +}; + +export const LongContent: Story = { + args: { + title: '저장정보 확인 저장정보 확인저장정보 확인 저장정보 확인', + description: + '저장정보 확인 저장정보 확인저장정보 확인 저장정보 확인저장정보 확인 저장정보 확인저장정보 확인 저장정보 확인저장정보 확인 저장정보 확인저장정보 확인 저장정보 확인', + confirmLabel: '닫기', + }, +}; diff --git a/src/shared/components/ui/modal-alert/AlertModal.tsx b/src/shared/components/ui/modal-alert/AlertModal.tsx new file mode 100644 index 00000000..6c0bfa59 --- /dev/null +++ b/src/shared/components/ui/modal-alert/AlertModal.tsx @@ -0,0 +1,37 @@ +import Modal from '@components/ui/modal/Modal'; + +interface AlertModalProps { + isOpen: boolean; + handleClose: () => void; + title: string; + description: string; + confirmLabel?: string; +} + +export default function AlertModal({ + isOpen, + handleClose, + title, + description, + confirmLabel = '확인', +}: AlertModalProps) { + const footer = ( + + ); + + return ( + + ); +} diff --git a/src/shared/components/ui/modal-confirm/ConfirmModal.stories.tsx b/src/shared/components/ui/modal-confirm/ConfirmModal.stories.tsx new file mode 100644 index 00000000..cb5f0960 --- /dev/null +++ b/src/shared/components/ui/modal-confirm/ConfirmModal.stories.tsx @@ -0,0 +1,52 @@ +import ConfirmModal from '@components/ui/modal-confirm/ConfirmModal'; +import type { Meta, StoryObj } from '@storybook/react-vite'; + +const meta: Meta = { + title: 'Components/UI/Modal/ConfirmModal', + component: ConfirmModal, + parameters: { + layout: 'centered', + docs: { + description: { + component: + '사용자가 직접 확인하거나 취소가 필요한 작업을 수행할 때 사용.', + }, + }, + }, + args: { + isOpen: true, + handleClose: () => console.info('ConfirmModal: handleClose (배경 클릭 등)'), + handleClickRight: () => + console.info('ConfirmModal: handleClickRight (확인 클릭)'), + handleClickLeft: () => + console.info('ConfirmModal: handleClickLeft (취소 클릭)'), + }, + argTypes: { + description: { control: 'text' }, + isOpen: { control: 'boolean' }, + handleClose: { table: { disable: true } }, + handleClickRight: { table: { disable: true } }, + handleClickLeft: { table: { disable: true } }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: '작성 취소', + description: '작성 중인 내용이 있습니다. 정말로 나가시겠습니까?', + rightLabel: '나가기', + leftLabel: '취소', + }, +}; + +export const DestructiveAction: Story = { + args: { + title: '계좌번호 삭제', + description: '삭제된 계좌번호는 복구할 수 없습니다. 계속하시겠습니까?', + rightLabel: '삭제', + leftLabel: '취소', + }, +}; diff --git a/src/shared/components/ui/modal-confirm/ConfirmModal.tsx b/src/shared/components/ui/modal-confirm/ConfirmModal.tsx new file mode 100644 index 00000000..8bb0e744 --- /dev/null +++ b/src/shared/components/ui/modal-confirm/ConfirmModal.tsx @@ -0,0 +1,53 @@ +import Modal from '@components/ui/modal/Modal'; +import type { ReactNode } from 'react'; + +interface ConfirmModalProps { + isOpen: boolean; + handleClose: () => void; + title: string; + description: string | ReactNode; + leftLabel?: string; + rightLabel?: string; + handleClickRight: () => void; + handleClickLeft: () => void; +} + +export default function ConfirmModal({ + isOpen, + handleClose, + title, + description, + rightLabel = '확인', + leftLabel = '취소', + handleClickRight, + handleClickLeft, +}: ConfirmModalProps) { + const footer = ( + <> + + + + ); + + return ( + + ); +} diff --git a/src/shared/components/ui/modal/Modal.stories.tsx b/src/shared/components/ui/modal/Modal.stories.tsx new file mode 100644 index 00000000..7a1b2b9a --- /dev/null +++ b/src/shared/components/ui/modal/Modal.stories.tsx @@ -0,0 +1,100 @@ +import Modal from '@components/ui/modal/Modal'; +import type { Meta, StoryObj } from '@storybook/react-vite'; + +const meta: Meta = { + title: 'Components/UI/Modal', + component: Modal, + parameters: { + layout: 'centered', + docs: { + description: { + component: ` +다목적 모달 컴포넌트입니다. 두 가지 사용 방식을 지원합니다: +1. **Common Mode**: \`title\`, \`description\`, \`footer\` props를 전달하여 표준화된 레이아웃을 빠르게 구성합니다. +2. **Primitive Mode**: \`Modal.Header\`, \`Modal.Body\` 등을 직접 조합하여 복잡한 커스텀 UI를 구성합니다. + `, + }, + }, + }, + tags: ['autodocs'], + argTypes: { + isOpen: { control: 'boolean', description: '모달 표시 여부' }, + title: { control: 'text', description: '[Common Mode] 모달 제목' }, + description: { control: 'text', description: '[Common Mode] 본문 내용' }, + children: { + control: false, + description: '[Primitive Mode] 커스텀 내부 요소', + }, + footer: { + control: false, + description: '[Common Mode] 푸터 요소 (버튼 등)', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +/** + * 현재 일반적으로 사용되는 형태. + * title 속성을 사용한다면 현재 버튼 디자인에 맞게 모달을 채울 수 있음. + */ +export const CommonLayout: Story = { + args: { + isOpen: true, + title: '프로젝트 삭제', + description: + '이 프로젝트를 정말 삭제하시겠습니까?\n삭제된 데이터는 복구할 수 없습니다.', + handleClose: () => console.info('Close requested'), + footer: ( + <> + + + + ), + }, +}; + +/** + * 추후 모달 내부에 현재 디자인과 다른 내용이 올 경우 대비 + * children으로 Modal.Header, Modal.Body 등을 직접 배치 + */ +export const CompositionExample: Story = { + render: args => ( + + + 커스텀 구성 예시 + + Alert이나 Confirm으로 해결되지 않는{' '} + 복잡한 UI가 필요할 때 +
+ 이렇게 직접 조합해서 사용합니다. +
+
+ +
+ 중간에 다른 div를 섞어서 디자인을 변경할 수도 있습니다. +
+ + + + 자세히 보기 + + + +
+ ), + args: { + isOpen: true, + handleClose: () => console.info('Modal: handleClose (Overlay 클릭)'), + }, +}; diff --git a/src/shared/components/ui/modal/Modal.tsx b/src/shared/components/ui/modal/Modal.tsx new file mode 100644 index 00000000..9eb73388 --- /dev/null +++ b/src/shared/components/ui/modal/Modal.tsx @@ -0,0 +1,93 @@ +import Overlay from '@components/layout/overlay/Overlay'; +import { cn } from '@utils/cn'; +import type { MouseEvent, ReactNode } from 'react'; + +interface ModalProps { + isOpen: boolean; + handleClose: () => void; + children?: ReactNode; + className?: string; + title?: string; + description?: string | ReactNode; + footer?: ReactNode; +} +interface ModalSubComponentProps { + children: ReactNode; + className?: string; +} + +const Header = ({ children, className = '' }: ModalSubComponentProps) => ( +
+ {children} +
+); + +const Title = ({ children, className = '' }: ModalSubComponentProps) => ( +

{children}

+); + +const Body = ({ children, className = '' }: ModalSubComponentProps) => { + if (typeof children === 'string') { + return ( +

+ {children} +

+ ); + } + return
{children}
; +}; + +const Footer = ({ children, className = '' }: ModalSubComponentProps) => ( +
{children}
+); + +export default function Modal({ + isOpen, + handleClose, + children, + className, + title, + description, + footer, +}: ModalProps) { + const stopPropagation = (e: MouseEvent) => { + e.stopPropagation(); + }; + const isCommonLayoutMode = !!title; + + return ( + +
+ {isCommonLayoutMode ? ( + <> +
+ {title} + {description && {description}} +
+ {footer && } + + ) : ( + children + )} +
+
+ ); +} + +Modal.Header = Header; +Modal.Title = Title; +Modal.Body = Body; +Modal.Footer = Footer;