+ 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;