diff --git a/client/src/components/NavigationConfirmPopup/index.style.ts b/client/src/components/NavigationConfirmPopup/index.style.ts new file mode 100644 index 00000000..d8d07884 --- /dev/null +++ b/client/src/components/NavigationConfirmPopup/index.style.ts @@ -0,0 +1,10 @@ +import { cva } from "class-variance-authority"; + +export const buttonStyles = cva("flex-1 py-400 rounded-1000 h-body-1-regular transition-all", { + variants: { + variant: { + primary: "bg-s-blue border border-s-blue text-n-white hover:bg-s-hover", + secondary: "bg-n-white border border-s-blue text-s-blue hover:bg-neutral-100", + }, + }, +}); diff --git a/client/src/components/NavigationConfirmPopup/index.tsx b/client/src/components/NavigationConfirmPopup/index.tsx new file mode 100644 index 00000000..1b421c44 --- /dev/null +++ b/client/src/components/NavigationConfirmPopup/index.tsx @@ -0,0 +1,48 @@ +import { useEffect } from "react"; +import { buttonStyles } from "./index.style"; + +interface NavigationConfirmPopUpProps { + handleConfirm: () => void; + handleClose: () => void; +} + +export default function NavigationConfirmPopUp({ + handleConfirm, + handleClose, +}: NavigationConfirmPopUpProps) { + useEffect(() => { + document.body.style.overflow = "hidden"; + + return () => { + document.body.style.overflow = "unset"; + }; + }, []); + + return ( +
+
+
+

+ 이 페이지를 떠나면 모든 변경 사항이 저장되지 않습니다. +
+ 페이지를 떠나시겠습니까? +

+ +
+ + +
+
+
+ ); +} diff --git a/client/src/components/PopUp/index.stories.tsx b/client/src/components/PhoneNumberPopUp/index.stories.tsx similarity index 84% rename from client/src/components/PopUp/index.stories.tsx rename to client/src/components/PhoneNumberPopUp/index.stories.tsx index 784b9690..9d81ee3e 100644 --- a/client/src/components/PopUp/index.stories.tsx +++ b/client/src/components/PhoneNumberPopUp/index.stories.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import type { Meta } from "@storybook/react"; -import Component, { PopUpProps } from "./index"; +import Component, { PhoneNumberPopUpProps } from "./index"; const meta = { title: "PopUp", @@ -15,7 +15,7 @@ const meta = { export default meta; -const PopUp = (args: PopUpProps) => { +const PopUp = (args: PhoneNumberPopUpProps) => { const [phoneNumber, setPhoneNumber] = useState(args.phoneNumber); const handlePhoneNumberChange = (val: string) => { @@ -32,7 +32,7 @@ const PopUp = (args: PopUpProps) => { ); }; -export const Default = (args: PopUpProps) => ( +export const Default = (args: PhoneNumberPopUpProps) => ( <> diff --git a/client/src/components/PopUp/index.tsx b/client/src/components/PhoneNumberPopUp/index.tsx similarity index 97% rename from client/src/components/PopUp/index.tsx rename to client/src/components/PhoneNumberPopUp/index.tsx index 4fbac935..849f3763 100644 --- a/client/src/components/PopUp/index.tsx +++ b/client/src/components/PhoneNumberPopUp/index.tsx @@ -4,19 +4,19 @@ import CTAButton from "../CTAButton"; import CheckBox from "../CheckBox"; import Input from "../Input"; -export interface PopUpProps { +export interface PhoneNumberPopUpProps { phoneNumber: string; handlePhoneNumberChange: (val: string) => void; handlePhoneNumberConfirm: (val: string) => void; handleClose: () => void; } -export default function PopUp({ +export default function PhoneNumberPopUp({ phoneNumber = "", handlePhoneNumberChange, handlePhoneNumberConfirm, handleClose, -}: PopUpProps) { +}: PhoneNumberPopUpProps) { const [isUserInfoCheck, setIsUserInfoCheck] = useState(true); const [isMarketingInfoCheck, setIsMarketingInfoCheck] = useState(true); const [canConfirm, setCanConfirm] = useState(false); diff --git a/client/src/features/Rush/Common/Headline.tsx b/client/src/features/Rush/Common/Headline.tsx index aad6d7ab..79bdb37d 100644 --- a/client/src/features/Rush/Common/Headline.tsx +++ b/client/src/features/Rush/Common/Headline.tsx @@ -5,7 +5,7 @@ import CTAButton from "@/components/CTAButton"; import Scroll from "@/components/Scroll"; import { ASCEND, ASCEND_DESCEND, SCROLL_MOTION } from "@/constants/animation.ts"; import { useAuth } from "@/hooks/useAuth.ts"; -import usePopup from "@/hooks/usePopup.tsx"; +import usePhoneNumberPopUp from "@/hooks/usePhoneNumberPopup"; import useToast from "@/hooks/useToast.tsx"; import { GetTotalRushEventsResponse } from "@/types/rushApi.ts"; import { SectionKeyProps } from "@/types/sections.ts"; @@ -21,7 +21,7 @@ export function Headline({ id, handleClickScroll }: HeadlineProps) { const { phoneNumberState, handlePhoneNumberChange, handlePhoneNumberConfirm } = useAuth("/rush/game"); - const { handleOpenPopup, PopupComponent } = usePopup({ + const { handleOpenPopup, PopupComponent } = usePhoneNumberPopUp({ phoneNumber: phoneNumberState, handlePhoneNumberChange, handlePhoneNumberConfirm, diff --git a/client/src/features/RushGame/RushGameSections/FinalResult.tsx b/client/src/features/RushGame/RushGameSections/FinalResult.tsx index 02893571..d0bf137f 100644 --- a/client/src/features/RushGame/RushGameSections/FinalResult.tsx +++ b/client/src/features/RushGame/RushGameSections/FinalResult.tsx @@ -24,18 +24,13 @@ function getWinStatus(ratio: number, oppositeRatio: number): WinStatus { return ratio > oppositeRatio ? WIN_STATUS.WIN : WIN_STATUS.LOSE; } -interface FinalResultProps { - unblockNavigation: () => void; -} - -export default function FinalResult({ unblockNavigation }: FinalResultProps) { +export default function FinalResult() { const [cookies] = useCookies([COOKIE_KEY.ACCESS_TOKEN]); const { cardOptions, userParticipatedStatus, userSelectedOption } = useRushGameStateContext(); const { getRushResult, resultData } = useFetchRushResult(); useEffect(() => { getRushResult(cookies[COOKIE_KEY.ACCESS_TOKEN]); - unblockNavigation(); }, []); const isWinner = resultData?.isWinner; diff --git a/client/src/features/RushGame/RushGameSections/SelectedCard.tsx b/client/src/features/RushGame/RushGameSections/SelectedCard.tsx index 161da784..29029066 100644 --- a/client/src/features/RushGame/RushGameSections/SelectedCard.tsx +++ b/client/src/features/RushGame/RushGameSections/SelectedCard.tsx @@ -52,11 +52,7 @@ function SelectedCardCurrentRatio({ onClick }: SelectedCardDetailsProps) { ); } -interface SelectedCardProps { - unblockNavigation: () => void; -} - -export default function SelectedCard({ unblockNavigation }: SelectedCardProps) { +export default function SelectedCard() { const { toggleContents, toggle } = useToggleContents({ useDuration: false }); const fetchRushBalance = useFetchRushBalance(); @@ -67,7 +63,6 @@ export default function SelectedCard({ unblockNavigation }: SelectedCardProps) { useEffect(() => { fetchRushBalance(); - unblockNavigation(); }, []); return ( diff --git a/client/src/hooks/useBlockNavigation.ts b/client/src/hooks/useBlockNavigation.ts index e43d4c0c..2d9acbef 100644 --- a/client/src/hooks/useBlockNavigation.ts +++ b/client/src/hooks/useBlockNavigation.ts @@ -1,16 +1,29 @@ import { useEffect, useState } from "react"; -import { unstable_usePrompt, useLocation } from "react-router-dom"; +import { useBlocker, useLocation } from "react-router-dom"; -export function useBlockNavigation(message: string) { +export function useBlockNavigation() { const location = useLocation(); const [isBlocking, setIsBlocking] = useState(false); - unstable_usePrompt({ when: isBlocking, message }); + const blocker = useBlocker(isBlocking); + const isBlockedNavigation = blocker.state === "blocked"; const unblockNavigation = () => { setIsBlocking(false); }; + const cancelNavigation = () => { + if (blocker.state === "blocked") { + blocker.reset(); + } + }; + + const proceedNavigation = () => { + if (blocker.state === "blocked") { + blocker.proceed(); + } + }; + const handleBeforeUnload = (e: BeforeUnloadEvent) => { if (isBlocking) { e.preventDefault(); @@ -25,6 +38,7 @@ export function useBlockNavigation(message: string) { setIsBlocking(false); }; }, [location]); + useEffect(() => { window.addEventListener("beforeunload", handleBeforeUnload); @@ -33,5 +47,5 @@ export function useBlockNavigation(message: string) { }; }, [isBlocking]); - return { unblockNavigation }; + return { unblockNavigation, isBlockedNavigation, proceedNavigation, cancelNavigation }; } diff --git a/client/src/hooks/usePopup.tsx b/client/src/hooks/usePhoneNumberPopup.tsx similarity index 81% rename from client/src/hooks/usePopup.tsx rename to client/src/hooks/usePhoneNumberPopup.tsx index 7173a039..b7c85f0c 100644 --- a/client/src/hooks/usePopup.tsx +++ b/client/src/hooks/usePhoneNumberPopup.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from "react"; -import Popup, { PopUpProps } from "@/components/PopUp"; +import PhoneNumberPopUp, { PhoneNumberPopUpProps } from "@/components/PhoneNumberPopUp"; -export default function usePopup({ +export default function usePhoneNumberPopUp({ phoneNumber, handlePhoneNumberChange, handlePhoneNumberConfirm, -}: Omit) { +}: Omit) { const [isVisible, setIsVisible] = useState(false); useEffect(() => { @@ -26,7 +26,7 @@ export default function usePopup({ }; const PopupComponent = isVisible ? ( -
+ + {isBlockedNavigation && ( + + )} ); } diff --git a/client/src/pages/Lottery/index.tsx b/client/src/pages/Lottery/index.tsx index cca523f5..6712f1a3 100644 --- a/client/src/pages/Lottery/index.tsx +++ b/client/src/pages/Lottery/index.tsx @@ -16,7 +16,7 @@ import { } from "@/features/Lottery"; import { useAuth } from "@/hooks/useAuth.ts"; import useHeaderStyleObserver from "@/hooks/useHeaderStyleObserver.ts"; -import usePopup from "@/hooks/usePopup"; +import usePhoneNumberPopUp from "@/hooks/usePhoneNumberPopup"; import useScrollToTarget from "@/hooks/useScrollToTarget"; import useScrollTop from "@/hooks/useScrollTop"; import useToast from "@/hooks/useToast"; @@ -36,7 +36,7 @@ export default function Lottery() { const { phoneNumberState, handlePhoneNumberChange, handlePhoneNumberConfirm } = useAuth("/lottery/custom"); - const { handleOpenPopup, PopupComponent } = usePopup({ + const { handleOpenPopup, PopupComponent } = usePhoneNumberPopUp({ phoneNumber: phoneNumberState, handlePhoneNumberChange, handlePhoneNumberConfirm, diff --git a/client/src/pages/RushGame/index.tsx b/client/src/pages/RushGame/index.tsx index d3d8ac44..25341583 100644 --- a/client/src/pages/RushGame/index.tsx +++ b/client/src/pages/RushGame/index.tsx @@ -11,14 +11,10 @@ import useRushGameStateContext from "@/hooks/Contexts/useRushGameStateContext.ts import { useFetchRushUserParticipationStatus } from "@/hooks/RushGame/useFetchRushUserParticipationStatus.ts"; import { useFetchTodayRushEvent } from "@/hooks/RushGame/useFetchTodayRushEvent.ts"; import useSetGamePhase from "@/hooks/RushGame/useSetGamePhase.ts"; -import { useBlockNavigation } from "@/hooks/useBlockNavigation.ts"; import { GetTotalRushEventsResponse } from "@/types/rushApi.ts"; export default function RushGame() { const [cookies] = useCookies([COOKIE_KEY.ACCESS_TOKEN]); - const { unblockNavigation } = useBlockNavigation( - "이 페이지를 떠나면 모든 변경 사항이 저장되지 않습니다. 페이지를 떠나시겠습니까?" - ); const { getTodayRushEvent } = useFetchTodayRushEvent(); const gameState = useRushGameStateContext(); const { getRushUserParticipationStatus, userParticipatedStatus } = @@ -43,11 +39,11 @@ export default function RushGame() { if (!gameState.userParticipatedStatus) { return ; } else { - return ; + return ; } } case CARD_PHASE.COMPLETED: - return ; + return ; default: return null; }