diff --git a/package.json b/package.json
index 06766473..ce6bafbe 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"dayjs": "^1.11.11",
"event-source-polyfill": "^1.0.31",
"jotai": "^2.8.4",
+ "overlay-kit": "^1.8.4",
"react": "^18.3.1",
"react-datepicker": "^7.3.0",
"react-dom": "^18.3.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f6278e4f..2cea45cf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -35,6 +35,9 @@ importers:
jotai:
specifier: ^2.8.4
version: 2.8.4(@types/react@18.3.3)(react@18.3.1)
+ overlay-kit:
+ specifier: ^1.8.4
+ version: 1.8.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react:
specifier: ^18.3.1
version: 18.3.1
@@ -2616,6 +2619,12 @@ packages:
outvariant@1.4.3:
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
+ overlay-kit@1.8.4:
+ resolution: {integrity: sha512-CqFrMWStiLDqW6jr8Zj9O/n8eSBAnrHe2w6M77cFiEn724xmigk56sPcYtQXCYDcbvwV47oJjoKQQvfutL5yFw==}
+ peerDependencies:
+ react: ^16.8 || ^17 || ^18 || ^19
+ react-dom: ^16.8 || ^17 || ^18 || ^19
+
p-cancelable@2.1.1:
resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==}
engines: {node: '>=8'}
@@ -6372,6 +6381,11 @@ snapshots:
outvariant@1.4.3: {}
+ overlay-kit@1.8.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
p-cancelable@2.1.1: {}
p-limit@3.1.0:
diff --git a/src/app/App.tsx b/src/app/App.tsx
index 019d04f3..49b688cc 100644
--- a/src/app/App.tsx
+++ b/src/app/App.tsx
@@ -15,6 +15,7 @@ const App = () => {
+
diff --git a/src/app/pages/AllowedServicePage/AllowedServiceGroupDetail/Content/AllowedServiceGroupDetailContent.tsx b/src/app/pages/AllowedServicePage/AllowedServiceGroupDetail/Content/AllowedServiceGroupDetailContent.tsx
index 0c7a4dac..84556948 100644
--- a/src/app/pages/AllowedServicePage/AllowedServiceGroupDetail/Content/AllowedServiceGroupDetailContent.tsx
+++ b/src/app/pages/AllowedServicePage/AllowedServiceGroupDetail/Content/AllowedServiceGroupDetailContent.tsx
@@ -1,10 +1,11 @@
-import { ReactNode, useRef } from 'react';
+import { ReactNode } from 'react';
import Dropdown from '@/shared/components/Dropdown/Dropdown';
import FaviconImage from '@/shared/components/FaviconImage/FaviconImage';
-import ModalWrapper, { ModalWrapperRef } from '@/shared/components/ModalWrapper/ModalWrapper';
import Spacer from '@/shared/components/Spacer/Spacer';
+import { overlay } from '@/shared/utils/overlay';
+
import { AllowedServiceGroupDetailSiteType } from '@/shared/types/allowedService';
import MeatBallDefaultIcon from '@/shared/assets/svgs/common/ic_meatball_default.svg?react';
@@ -66,23 +67,38 @@ export const AllowedServiceGroupDetailContentTableRow = ({
activeGroupId,
...allowedSiteData
}: AllowedServiceGroupDetailContentRootTableRowProps) => {
- const domainAllowModalRef = useRef(null);
- const confirmDeleteModalRef = useRef(null);
-
- const handleOpenDomainAllowModal = () => {
- domainAllowModalRef.current?.open();
- };
-
- const handleCloseDomainAllowModal = () => {
- domainAllowModalRef.current?.close();
- };
-
- const handleOpenConfirmDeleteModal = () => {
- confirmDeleteModalRef.current?.open();
+ const handleDomainAllowModal = () => {
+ overlay({
+ backdrop: true,
+ content: ({ close }) => (
+ {
+ allowToMergeParentDomain.mutate({
+ allowedGroupId: activeGroupId,
+ siteUrl: allowedSiteData.siteUrl,
+ });
+ close();
+ }}
+ onCancel={close}
+ />
+ ),
+ });
};
- const handleCloseConfirmDeleteModal = () => {
- confirmDeleteModalRef.current?.close();
+ const handleConfirmDeleteModal = () => {
+ overlay({
+ backdrop: true,
+ content: ({ close }) => (
+ {
+ close();
+ onDeleteAllowedSite();
+ }}
+ pageName={allowedSiteData.pageName}
+ />
+ ),
+ });
};
const allowToMergeParentDomain = usePostMergeToParentDomain();
@@ -110,44 +126,18 @@ export const AllowedServiceGroupDetailContentTableRow = ({
-
+
{
- handleOpenConfirmDeleteModal();
+ handleConfirmDeleteModal();
}}
/>
-
- {() => (
- {
- allowToMergeParentDomain.mutate({
- allowedGroupId: activeGroupId,
- siteUrl: allowedSiteData.siteUrl,
- });
- handleCloseDomainAllowModal();
- }}
- onCancel={handleCloseDomainAllowModal}
- />
- )}
-
-
- {() => (
- {
- handleCloseConfirmDeleteModal();
- onDeleteAllowedSite();
- }}
- pageName={allowedSiteData.pageName}
- />
- )}
-
);
};
diff --git a/src/app/pages/AllowedServicePage/AllowedServicePage.tsx b/src/app/pages/AllowedServicePage/AllowedServicePage.tsx
index 6633a14f..f55de5e9 100644
--- a/src/app/pages/AllowedServicePage/AllowedServicePage.tsx
+++ b/src/app/pages/AllowedServicePage/AllowedServicePage.tsx
@@ -4,13 +4,13 @@ import { useQueryClient } from '@tanstack/react-query';
import AutoFixedGrid from '@/shared/components/AutoFixedGrid/AutoFixedGrid';
import ModalContentsFriends from '@/shared/components/ModalContentsFriends/ModalContentsFriends';
-import ModalWrapper, { ModalWrapperRef } from '@/shared/components/ModalWrapper/ModalWrapper';
import NotificationPanel from '@/shared/components/NotificationPanel/NotificationPanel';
import Spacer from '@/shared/components/Spacer/Spacer';
import TextField from '@/shared/components/TextField/TextField';
import useClickOutside from '@/shared/hooks/useClickOutside';
+import { overlay } from '@/shared/utils/overlay';
import { isUrlValid } from '@/shared/utils/validation';
import { ColorPaletteType } from '@/shared/types/allowedService';
@@ -49,10 +49,6 @@ const AllowedServicePage = () => {
const [isNotificationVisible, setIsNotificationVisible] = useState(false);
const queryClient = useQueryClient();
-
- const friendsModalRef = useRef(null);
- const requireTitleModalRef = useRef(null);
-
const bellIconRef = useRef(null);
const notificationPanelRef = useRef(null);
@@ -173,7 +169,7 @@ const AllowedServicePage = () => {
const handleAddAllowedService = (urlInput: string, activeGroupId: number | null) => {
if (!activeGroupId) {
- handleOpenRequireTitleModal();
+ handleRequireTitleModal();
return;
}
if (activeGroupId && !isPending) {
@@ -259,22 +255,24 @@ const AllowedServicePage = () => {
}
}, [allowedServiceGroupDetail, setTitleInput]);
- const handleOpenFriendsModal = () => {
- friendsModalRef.current?.open();
+ const handleFriendsModal = () => {
+ overlay({
+ backdrop: true,
+ content: ({ isOpen }) => ,
+ });
};
- const handleOpenRequireTitleModal = () => {
- requireTitleModalRef.current?.open();
- };
-
- const handleCloseRequireTitleModal = () => {
- requireTitleModalRef.current?.close();
+ const handleRequireTitleModal = () => {
+ overlay({
+ backdrop: true,
+ content: ({ close }) => ,
+ });
};
return (
-
-
-
- {() => (
- {
- registerServiceModalRef.current?.close();
- onCreateTodayTodos();
- }}
- onConfirm={() => {
- registerServiceModalRef.current?.close();
- navigate(allowedServicePath);
- }}
- />
- )}
-
);
};
diff --git a/src/app/pages/HomePage/HomePage.tsx b/src/app/pages/HomePage/HomePage.tsx
index 5fe3f6a1..10fb7c79 100644
--- a/src/app/pages/HomePage/HomePage.tsx
+++ b/src/app/pages/HomePage/HomePage.tsx
@@ -7,13 +7,13 @@ import { useLocation, useNavigate } from 'react-router-dom';
import AutoFixedGrid from '@/shared/components/AutoFixedGrid/AutoFixedGrid';
import ModalContentsFriends from '@/shared/components/ModalContentsFriends/ModalContentsFriends';
-import ModalWrapper, { ModalWrapperRef } from '@/shared/components/ModalWrapper/ModalWrapper';
import NotificationPanel from '@/shared/components/NotificationPanel/NotificationPanel';
import Spacer from '@/shared/components/Spacer/Spacer';
import useClickOutside from '@/shared/hooks/useClickOutside';
import { getThisWeekRange } from '@/shared/utils/date';
+import { overlay } from '@/shared/utils/overlay';
import { getDailyCategoryTask, isTaskExist, splitTasksByCompletion } from '@/shared/utils/tasks';
import { TaskType } from '@/shared/types/tasks';
@@ -60,11 +60,8 @@ const HomePage = () => {
const categoryRef = useRef(null);
const boxAddCategoryRef = useRef(null);
- const friendsModalRef = useRef(null);
const notificationPanelRef = useRef(null);
const bellIconRef = useRef(null);
- const timerRestrictionModalRef = useRef(null);
- const timerErrorModalRef = useRef(null);
const [isNotificationVisible, setIsNotificationVisible] = useState(false);
@@ -161,12 +158,25 @@ const HomePage = () => {
);
};
- const handleCloseTimerErrorModal = () => {
- timerErrorModalRef.current?.close();
+ const handleFriendsModal = () => {
+ overlay({
+ backdrop: true,
+ content: ({ isOpen }) => ,
+ });
};
- const handleOpenFriendsModal = () => {
- friendsModalRef.current?.open();
+ const handleTimerErrorModal = () => {
+ overlay({
+ backdrop: true,
+ content: ({ close }) => ,
+ });
+ };
+
+ const handleTimerRestrictionModal = () => {
+ overlay({
+ backdrop: true,
+ content: ({ close }) => ,
+ });
};
const toggleNotification = () => {
@@ -204,7 +214,7 @@ const HomePage = () => {
const handleSelectedDateChange = (date: Dayjs) => {
if (addingTodayTodoStatus && !todayDate.isSame(date, 'day')) {
- timerRestrictionModalRef.current?.open();
+ handleTimerRestrictionModal();
return;
}
setSelectedDate(date);
@@ -262,7 +272,7 @@ const HomePage = () => {
useEffect(() => {
if (isTimerError) {
- timerErrorModalRef.current?.open();
+ handleTimerErrorModal();
}
}, [isTimerError]);
@@ -360,7 +370,7 @@ const HomePage = () => {
-
+
@@ -477,24 +487,6 @@ const HomePage = () => {
/>
-
- {({ isModalOpen }) => }
-
-
-
- {() => (
- {
- timerRestrictionModalRef.current?.close();
- }}
- />
- )}
-
-
-
- {() => }
-
-
{isNotificationVisible && }
);
diff --git a/src/app/pages/TimerPage/Carousel/Carousel.tsx b/src/app/pages/TimerPage/Carousel/Carousel.tsx
index f4cb653f..bfc376bb 100644
--- a/src/app/pages/TimerPage/Carousel/Carousel.tsx
+++ b/src/app/pages/TimerPage/Carousel/Carousel.tsx
@@ -3,10 +3,11 @@ import { useRef } from 'react';
import ButtonArrowSVG from '@/shared/components/ButtonArrowSVG/ButtonArrowSVG';
import ButtonRadius8 from '@/shared/components/ButtonRadius8/ButtonRadius8';
import ModalContentsFriends from '@/shared/components/ModalContentsFriends/ModalContentsFriends';
-import ModalWrapper, { ModalWrapperRef } from '@/shared/components/ModalWrapper/ModalWrapper';
import useCarousel from '@/shared/hooks/useCarousel';
+import { overlay } from '@/shared/utils/overlay';
+
import { Direction } from '@/shared/types/global';
import { useGetTimerFriends } from '@/shared/apisV2/timer/timer.queries';
@@ -17,15 +18,17 @@ import CarouselFriend from './CarouselFriend';
* 타이머 페이지 하단 친구 캐러셀 컴포넌트
*/
const Carousel = () => {
- const friendsModalRef = useRef(null);
const carouselRef = useRef(null);
const { data: friendsList } = useGetTimerFriends();
const { handleNext, handlePrev } = useCarousel({ carouselRef });
- const handleOpenFriendsModal = () => {
- friendsModalRef.current?.open();
+ const handleFriendsModal = () => {
+ overlay({
+ backdrop: true,
+ content: ({ isOpen }) => ,
+ });
};
return (
@@ -35,7 +38,7 @@ const Carousel = () => {
{!friendsList?.data || friendsList.data.length === 0 ? (
함께 몰입할 친구를 추가해보아요!
- 친구 추가하기
+ 친구 추가하기
) : (
<>
@@ -55,9 +58,6 @@ const Carousel = () => {
)}
-
- {({ isModalOpen }) => }
-
);
};
diff --git a/src/app/router/Router.tsx b/src/app/router/Router.tsx
index eced980a..055e109a 100644
--- a/src/app/router/Router.tsx
+++ b/src/app/router/Router.tsx
@@ -1,4 +1,5 @@
import type { Router } from '@remix-run/router';
+import { OverlayProvider } from 'overlay-kit';
import { Suspense, lazy } from 'react';
import { Outlet, createBrowserRouter, createHashRouter } from 'react-router-dom';
@@ -22,83 +23,94 @@ const TimerPage = lazy(() => import('@/pages/TimerPage/TimerPage'));
const routerInfo = [
{
- //public 라우트들
+ // 리액트 라우터 컨텍스트 내에서 적용되어야 하는 프로바이더 (내부 컴포넌트가 리액트 라우터 API를 사용하는 경우 )
path: '/',
element: (
-
+
-
+
),
children: [
{
- path: ROUTES_CONFIG.login.path,
+ //public 라우트들
+ path: '/',
element: (
- }>
-
-
+
+
+
),
- },
- {
- path: ROUTES_CONFIG.redirect.path,
- element: ,
- },
- ],
- },
-
- {
- //권한이 있어야 접근 가능한 라우트들
- path: '/',
- element: ,
- children: [
- {
- path: '',
- element: ,
children: [
{
- path: ROUTES_CONFIG.home.path,
- element: (
-
-
-
- ),
- },
- {
- path: ROUTES_CONFIG.onboarding.path,
- element: (
-
-
-
- ),
- },
- {
- path: ROUTES_CONFIG.timer.path,
+ path: ROUTES_CONFIG.login.path,
element: (
}>
-
+
),
},
{
- path: ROUTES_CONFIG.allowedService.path,
- element: (
-
-
-
- ),
+ path: ROUTES_CONFIG.redirect.path,
+ element: ,
},
],
},
- ],
- },
- {
- //404 페이지
- path: '*',
- element: (
-
-
-
- ),
+ {
+ //권한이 있어야 접근 가능한 라우트들
+ path: '/',
+ element: ,
+ children: [
+ {
+ path: '',
+ element: ,
+ children: [
+ {
+ path: ROUTES_CONFIG.home.path,
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: ROUTES_CONFIG.onboarding.path,
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: ROUTES_CONFIG.timer.path,
+ element: (
+ }>
+
+
+ ),
+ },
+ {
+ path: ROUTES_CONFIG.allowedService.path,
+ element: (
+
+
+
+ ),
+ },
+ ],
+ },
+ ],
+ },
+
+ {
+ //404 페이지
+ path: '*',
+ element: (
+
+
+
+ ),
+ },
+ ],
},
];
diff --git a/src/app/shared/components/ModalWrapper/ModalWrapper.tsx b/src/app/shared/components/ModalWrapper/ModalWrapper.tsx
deleted file mode 100644
index c6e13194..00000000
--- a/src/app/shared/components/ModalWrapper/ModalWrapper.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { MouseEvent, ReactNode, forwardRef, useImperativeHandle, useRef, useState } from 'react';
-import { createPortal } from 'react-dom';
-
-import './styles/dialog.css';
-
-interface ModalWrapperProps {
- children: (props: { isModalOpen: boolean }) => ReactNode;
- backdrop?: boolean;
-}
-
-export interface ModalWrapperRef {
- open: () => void;
- close: () => void;
-}
-
-const ModalWrapper = forwardRef(function Modal(
- { children, backdrop = false },
- ref,
-) {
- const [isModalOpen, setIsModalOpen] = useState(false);
- const dialog = useRef(null);
-
- useImperativeHandle(ref, () => ({
- open() {
- dialog.current?.showModal();
- setIsModalOpen(true);
- },
- close() {
- dialog.current?.close();
- setIsModalOpen(false);
- },
- }));
- const modalElement = document.getElementById('modal');
-
- const handleClick = (e: MouseEvent) => {
- if (e.target === dialog.current) {
- dialog.current?.close();
- setIsModalOpen(false);
- }
- };
-
- if (!modalElement) {
- return null;
- }
-
- const content =
- typeof children === 'function'
- ? (children as (props: { isModalOpen: boolean }) => ReactNode)({ isModalOpen })
- : children;
-
- return createPortal(
- ,
- modalElement,
- );
-});
-
-export default ModalWrapper;
diff --git a/src/app/shared/components/ModalWrapper/styles/dialog.css b/src/app/shared/components/ModalWrapper/styles/dialog.css
deleted file mode 100644
index 3fa0f22a..00000000
--- a/src/app/shared/components/ModalWrapper/styles/dialog.css
+++ /dev/null
@@ -1,7 +0,0 @@
-.custom-dialog::backdrop {
- background-color: rgba(0, 0, 0, 0.7);
- }
-
-.custom-dialog:not(.with-backdrop)::backdrop {
- background-color: transparent;
-}
\ No newline at end of file
diff --git a/src/app/shared/layout/Sidebar/ModalContentsSetting/AccountContent/AccountContent.tsx b/src/app/shared/layout/Sidebar/ModalContentsSetting/AccountContent/AccountContent.tsx
index 873e1394..eb51c6e5 100644
--- a/src/app/shared/layout/Sidebar/ModalContentsSetting/AccountContent/AccountContent.tsx
+++ b/src/app/shared/layout/Sidebar/ModalContentsSetting/AccountContent/AccountContent.tsx
@@ -1,11 +1,12 @@
-import React, { useRef, useState } from 'react';
+import React, { useState } from 'react';
import ButtonRadius8 from '@/shared/components/ButtonRadius8/ButtonRadius8';
import ButtonStatusToggle from '@/shared/components/ButtonStatusToggle/ButtonStatusToggle';
-import ModalWrapper, { ModalWrapperRef } from '@/shared/components/ModalWrapper/ModalWrapper';
import { useLogout } from '@/shared/hooks/useLogout';
+import { overlay } from '@/shared/utils/overlay';
+
import { UserProfileType } from '@/shared/types/profile';
import ArrowRightIcon from '@/shared/assets/svgs/arrow_right.svg?react';
@@ -17,9 +18,6 @@ import { useDeleteAccount, usePutChangeProfile } from '@/shared/apisV2/setting/s
type AccountContentProps = UserProfileType;
const AccountContent = ({ ...props }: AccountContentProps) => {
- const logoutModalRef = useRef(null);
- const deleteAccountModalRef = useRef(null);
-
const { mutate: changeProfile } = usePutChangeProfile();
const { mutate: deleteAccount } = useDeleteAccount();
@@ -38,20 +36,26 @@ const AccountContent = ({ ...props }: AccountContentProps) => {
changeProfile({ name: userName, imageUrl: props.imageUrl, isPushEnabled: isToggleOn });
};
- const handleCloseLogoutModal = () => {
- logoutModalRef.current?.close();
- };
-
- const handleCloseDeleteAccountModal = () => {
- deleteAccountModalRef.current?.close();
+ const handleLogoutModal = () => {
+ overlay({
+ backdrop: true,
+ content: ({ close }) => (
+
+ ),
+ });
};
- const handleOpenLogoutModal = () => {
- logoutModalRef.current?.open();
- };
-
- const handleOpenDeleteAccountModal = () => {
- deleteAccountModalRef.current?.open();
+ const handleDeleteAccounttModal = () => {
+ overlay({
+ backdrop: true,
+ content: ({ close }) => (
+
+ ),
+ });
};
const handleDeleteAccount = () => {
@@ -95,7 +99,7 @@ const AccountContent = ({ ...props }: AccountContentProps) => {
모든 기기에서 로그아웃
본 기기를 포함한 모든 기기에서 로그아웃합니다.
-
+
@@ -107,7 +111,7 @@ const AccountContent = ({ ...props }: AccountContentProps) => {
계정을 영구적으로 삭제하고 모든 워크스페이스에서 액세스 권한을 제거합니다.
-
+
@@ -122,24 +126,6 @@ const AccountContent = ({ ...props }: AccountContentProps) => {
변경사항 저장
-
- {(_) => (
-
- )}
-
-
- {(_) => (
-
- )}
-
>
);
};
diff --git a/src/app/shared/layout/Sidebar/Sidebar.tsx b/src/app/shared/layout/Sidebar/Sidebar.tsx
index c89cc5d4..6914f60e 100644
--- a/src/app/shared/layout/Sidebar/Sidebar.tsx
+++ b/src/app/shared/layout/Sidebar/Sidebar.tsx
@@ -1,8 +1,6 @@
-import { useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
-import ModalWrapper, { ModalWrapperRef } from '@/shared/components/ModalWrapper/ModalWrapper';
-
+import { overlay } from '@/shared/utils/overlay';
import { getActivePath } from '@/shared/utils/path';
import LogoIcon from '@/shared/assets/svgs/common/ic_logo.svg?react';
@@ -14,16 +12,11 @@ import { ROUTES_CONFIG } from '@/router/routesConfig';
import ModalContentsSetting from './ModalContentsSetting/ModalContentsSetting';
const Sidebar = () => {
- const modalRef = useRef(null);
const navigate = useNavigate();
const location = useLocation();
const pathName = getActivePath(location.pathname);
- const openSettings = () => {
- modalRef.current?.open();
- };
-
const navigateHome = () => {
navigate(ROUTES_CONFIG.home.path);
};
@@ -32,6 +25,13 @@ const Sidebar = () => {
navigate(ROUTES_CONFIG.allowedService.path);
};
+ const handleSettingModal = () => {
+ overlay({
+ backdrop: true,
+ content: ({ isOpen }) => ,
+ });
+ };
+
return (
<>
-
+
-
- {({ isModalOpen }) => }
-
>
);
};
diff --git a/src/app/shared/utils/overlay.tsx b/src/app/shared/utils/overlay.tsx
new file mode 100644
index 00000000..5084f3a5
--- /dev/null
+++ b/src/app/shared/utils/overlay.tsx
@@ -0,0 +1,23 @@
+import { overlay as overlayKit } from 'overlay-kit';
+
+import { ReactNode } from 'react';
+
+interface OverlayProps {
+ backdrop?: boolean;
+ content: (props: { isOpen: boolean; close: () => void }) => ReactNode;
+}
+
+export const overlay = ({ content, backdrop = true }: OverlayProps) => {
+ overlayKit.open(({ isOpen, close }) =>
+ isOpen ? (
+
+
e.stopPropagation()}>{content({ isOpen, close })}
+
+ ) : null,
+ );
+};