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 = () => {
-
- - {({ 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( - - {content} - , - 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, + ); +};