diff --git a/frontend/src/features/killingParts/components/RegisterPart.tsx b/frontend/src/features/killingParts/components/RegisterPart.tsx index 30cd7ab6..3536473c 100644 --- a/frontend/src/features/killingParts/components/RegisterPart.tsx +++ b/frontend/src/features/killingParts/components/RegisterPart.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { useAuthContext } from '@/features/auth/components/AuthProvider'; import useCollectingPartContext from '@/features/killingParts/hooks/useCollectingPartContext'; import useVideoPlayerContext from '@/features/youtube/hooks/useVideoPlayerContext'; -import { useConfirmContext } from '@/shared/components/ConfirmModal/hooks/useConfirmContext'; +import { useConfirmModal } from '@/shared/components/ConfirmModal/hooks/useConfirmModal'; import Spacing from '@/shared/components/Spacing'; import { useMutation } from '@/shared/hooks/useMutation'; import { toPlayingTimeText } from '@/shared/utils/convertTime'; @@ -13,7 +13,7 @@ const RegisterPart = () => { const { user } = useAuthContext(); const { interval, partStartTime, songId } = useCollectingPartContext(); const video = useVideoPlayerContext(); - const { confirmPopup } = useConfirmContext(); + const { openConfirmModal } = useConfirmModal(); const voteTimeText = toPlayingTimeText(partStartTime, partStartTime + interval); const { mutateData: createKillingPart } = useMutation(postKillingPart); const navigate = useNavigate(); @@ -24,7 +24,7 @@ const RegisterPart = () => { const submitKillingPart = async () => { video.pause(); - const isConfirmed = await confirmPopup({ + const isConfirmed = await openConfirmModal({ title: `${user?.nickname}님의 파트 저장`, content: ( diff --git a/frontend/src/features/songs/components/KillingPartTrack.tsx b/frontend/src/features/songs/components/KillingPartTrack.tsx index de30e1be..a58b593c 100644 --- a/frontend/src/features/songs/components/KillingPartTrack.tsx +++ b/frontend/src/features/songs/components/KillingPartTrack.tsx @@ -8,7 +8,7 @@ import { useAuthContext } from '@/features/auth/components/AuthProvider'; import LoginModal from '@/features/auth/components/LoginModal'; import { deleteMemberParts } from '@/features/member/remotes/memberParts'; import useVideoPlayerContext from '@/features/youtube/hooks/useVideoPlayerContext'; -import { useConfirmContext } from '@/shared/components/ConfirmModal/hooks/useConfirmContext'; +import { useConfirmModal } from '@/shared/components/ConfirmModal/hooks/useConfirmModal'; import useTimerContext from '@/shared/components/Timer/hooks/useTimerContext'; import useToastContext from '@/shared/components/Toast/hooks/useToastContext'; import { GA_ACTIONS, GA_CATEGORIES } from '@/shared/constants/GAEventName'; @@ -45,7 +45,7 @@ const KillingPartTrack = ({ }: KillingPartTrackProps) => { const { showToast } = useToastContext(); const { seekTo, pause, playerState, videoPlayer } = useVideoPlayerContext(); - const { confirmPopup } = useConfirmContext(); + const { openConfirmModal } = useConfirmModal(); const { heartIcon, toggleKillingPartLikes } = useKillingPartLikes({ likeCount, likeStatus, @@ -154,7 +154,7 @@ const KillingPartTrack = ({ const { mutateData: deleteMemberPart } = useMutation(() => deleteMemberParts(partId)); const handleClickDeletePart = async () => { - const isConfirmed = await confirmPopup({ + const isConfirmed = await openConfirmModal({ title: '내 파트 삭제', content:

정말 삭제하시겠습니까?

, confirmation: '삭제', diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 01b97e8f..ea385441 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; import { RouterProvider } from 'react-router-dom'; import { ThemeProvider } from 'styled-components'; -import ConfirmModalProvider from '@/shared/components/ConfirmModal/ConfirmModalProvider'; import GlobalStyles from '@/shared/styles/GlobalStyles'; import AuthProvider from './features/auth/components/AuthProvider'; import { loadIFrameApi } from './features/youtube/remotes/loadIframeApi'; @@ -37,9 +36,7 @@ async function main() { - - - + diff --git a/frontend/src/pages/EditProfilePage.tsx b/frontend/src/pages/EditProfilePage.tsx index 2c850a61..7c8165c0 100644 --- a/frontend/src/pages/EditProfilePage.tsx +++ b/frontend/src/pages/EditProfilePage.tsx @@ -4,14 +4,14 @@ import shookshook from '@/assets/icon/shookshook.svg'; import { useAuthContext } from '@/features/auth/components/AuthProvider'; import WITHDRAWAL_MESSAGE from '@/features/member/constants/withdrawalMessage'; import { deleteMember } from '@/features/member/remotes/member'; -import { useConfirmContext } from '@/shared/components/ConfirmModal/hooks/useConfirmContext'; +import { useConfirmModal } from '@/shared/components/ConfirmModal/hooks/useConfirmModal'; import Spacing from '@/shared/components/Spacing'; import ROUTE_PATH from '@/shared/constants/path'; import { useMutation } from '@/shared/hooks/useMutation'; const EditProfilePage = () => { const { user, logout } = useAuthContext(); - const { confirmPopup } = useConfirmContext(); + const { openConfirmModal } = useConfirmModal(); const { mutateData: withdrawal } = useMutation(deleteMember); const navigate = useNavigate(); @@ -21,7 +21,7 @@ const EditProfilePage = () => { } const handleClickWithdrawal = async () => { - const isConfirmed = await confirmPopup({ + const isConfirmed = await openConfirmModal({ title: '회원 탈퇴', content: {WITHDRAWAL_MESSAGE}, confirmation: '탈퇴', diff --git a/frontend/src/shared/components/ConfirmModal/ConfirmModal.stories.tsx b/frontend/src/shared/components/ConfirmModal/ConfirmModal.stories.tsx index 9bc0284e..53b27c15 100644 --- a/frontend/src/shared/components/ConfirmModal/ConfirmModal.stories.tsx +++ b/frontend/src/shared/components/ConfirmModal/ConfirmModal.stories.tsx @@ -1,31 +1,22 @@ import styled from 'styled-components'; -import ConfirmModalProvider from './ConfirmModalProvider'; -import { useConfirmContext } from './hooks/useConfirmContext'; +import { useConfirmModal } from './hooks/useConfirmModal'; import type { Meta, StoryObj } from '@storybook/react'; -const meta: Meta = { - title: 'shared/Confirm', - component: ConfirmModalProvider, - decorators: [ - (Story) => ( - - - - ), - ], +const meta: Meta = { + title: 'shared/ConfirmModal', }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Example: Story = { render: () => { const Modal = () => { - const { confirmPopup } = useConfirmContext(); + const { openConfirmModal } = useConfirmModal(); const clickHiByeBtn = async () => { - const isConfirmed = await confirmPopup({ + const isConfirmed = await openConfirmModal({ title: '하이바이 모달', content: ( <> @@ -47,7 +38,7 @@ export const Example: Story = { // denial과 confirmation 기본값은 '닫기'와 '확인'입니다. const clickOpenCloseBtn = async () => { - const isConfirmed = await confirmPopup({ + const isConfirmed = await openConfirmModal({ title: '오쁜클로즈 모달', content: ( <> diff --git a/frontend/src/shared/components/ConfirmModal/ConfirmModal.tsx b/frontend/src/shared/components/ConfirmModal/ConfirmModal.tsx index 0db6f83e..af8b11b9 100644 --- a/frontend/src/shared/components/ConfirmModal/ConfirmModal.tsx +++ b/frontend/src/shared/components/ConfirmModal/ConfirmModal.tsx @@ -1,9 +1,12 @@ import { Flex } from 'shook-layout'; import styled, { css } from 'styled-components'; +import Modal from '../Modal/Modal'; import Spacing from '../Spacing'; import type { ReactNode } from 'react'; interface ConfirmModalProps { + isOpen: boolean; + closeModal: () => void; title: string; content: ReactNode; denial: string; @@ -13,6 +16,8 @@ interface ConfirmModalProps { } const ConfirmModal = ({ + isOpen, + closeModal, title, content, denial, @@ -25,62 +30,27 @@ const ConfirmModal = ({ }; return ( - <> - - - - {title} - - - {content} - - - - {denial} - - - {confirmation} - - - - + + + {title} + + + {content} + + + + {denial} + + + {confirmation} + + + ); }; export default ConfirmModal; -const Backdrop = styled.div` - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - - width: 100%; - height: 100%; - margin: 0; - padding: 0; - - background-color: rgba(0, 0, 0, 0.7); -`; - -const Container = styled.section` - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - min-width: 320px; - margin: 0 auto; - padding: 24px; - - color: ${({ theme: { color } }) => color.white}; - - background-color: ${({ theme: { color } }) => color.black300}; - border: none; - border-radius: 16px; -`; - const ButtonFlex = styled(Flex)` width: 100%; `; diff --git a/frontend/src/shared/components/ConfirmModal/ConfirmModalProvider.tsx b/frontend/src/shared/components/ConfirmModal/ConfirmModalProvider.tsx deleted file mode 100644 index 3a6b0145..00000000 --- a/frontend/src/shared/components/ConfirmModal/ConfirmModalProvider.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import { createContext, useCallback, useEffect, useState, useRef } from 'react'; -import { createPortal } from 'react-dom'; -import ConfirmModal from './ConfirmModal'; -import type { ReactNode } from 'react'; - -export const ConfirmContext = createContext Promise; -}>(null); - -interface ModalContents { - title: string; - content: ReactNode; - denial?: string; - confirmation?: string; -} - -const ConfirmModalProvider = ({ children }: { children: ReactNode }) => { - const [isOpen, setIsOpen] = useState(false); - const resolverRef = useRef<{ - resolve: (value: boolean) => void; - } | null>(null); - const [modalContents, setModalContents] = useState({ - title: '', - content: '', - denial: '닫기', - confirmation: '확인', - }); - const { title, content, denial, confirmation } = modalContents; - - // ContextAPI를 통해 confirm 함수만 제공합니다. - const confirmPopup = (contents: ModalContents) => { - openModal(); - setModalContents(contents); - - const promise = new Promise((resolve) => { - resolverRef.current = { resolve }; - }); - - return promise; - }; - - const closeModal = () => { - setIsOpen(false); - }; - - const openModal = () => { - setIsOpen(true); - }; - - const resolveConfirmation = (status: boolean) => { - if (resolverRef?.current) { - resolverRef.current.resolve(status); - } - }; - - const onDeny = useCallback(() => { - resolveConfirmation(false); - closeModal(); - }, []); - - const onConfirm = useCallback(() => { - resolveConfirmation(true); - closeModal(); - }, []); - - const onKeyDown = useCallback(({ key }: KeyboardEvent) => { - if (key === 'Escape') { - resolveConfirmation(false); - closeModal(); - } - }, []); - - useEffect(() => { - if (isOpen) { - document.addEventListener('keydown', onKeyDown); - document.body.style.overflow = 'hidden'; - } - - return () => { - document.removeEventListener('keydown', onKeyDown); - document.body.style.overflow = 'auto'; - }; - }, [isOpen]); - - return ( - - {children} - {isOpen && - createPortal( - , - document.body - )} - - ); -}; - -export default ConfirmModalProvider; diff --git a/frontend/src/shared/components/ConfirmModal/hooks/useConfirmContext.ts b/frontend/src/shared/components/ConfirmModal/hooks/useConfirmContext.ts deleted file mode 100644 index e9bcfc94..00000000 --- a/frontend/src/shared/components/ConfirmModal/hooks/useConfirmContext.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from 'react'; -import { ConfirmContext } from '../ConfirmModalProvider'; - -export const useConfirmContext = () => { - const contextValue = useContext(ConfirmContext); - if (!contextValue) { - throw new Error('ConfirmContext Provider 내부에서 사용 가능합니다.'); - } - - return contextValue; -}; diff --git a/frontend/src/shared/components/ConfirmModal/hooks/useConfirmModal.tsx b/frontend/src/shared/components/ConfirmModal/hooks/useConfirmModal.tsx new file mode 100644 index 00000000..c917ca98 --- /dev/null +++ b/frontend/src/shared/components/ConfirmModal/hooks/useConfirmModal.tsx @@ -0,0 +1,59 @@ +import { useOverlay } from '@/shared/hooks/useOverlay'; +import ConfirmModal from '../ConfirmModal'; +import type { ReactNode } from 'react'; + +interface OpenConfirmModalProps { + /** + * 제목 + */ + title: string; + /** + * 내용 + */ + content: ReactNode; + /** + * 취소 버튼 이름 + */ + denial?: string; + /** + * 확인 버튼 이름 + */ + confirmation?: string; +} + +export const useConfirmModal = () => { + const overlay = useOverlay(); + + const openConfirmModal = ({ + title, + content, + denial = '닫기', + confirmation = '확인', + }: OpenConfirmModalProps) => { + return new Promise((resolve) => + overlay.open(({ isOpen, close }) => ( + { + resolve(false); + close(); + }} + title={title} + content={content} + denial={denial} + confirmation={confirmation} + onDeny={() => { + resolve(false); + close(); + }} + onConfirm={() => { + resolve(true); + close(); + }} + /> + )) + ); + }; + + return { openConfirmModal }; +};