diff --git a/apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx b/apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx index 4dacd38c..71763bcd 100644 --- a/apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx +++ b/apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx @@ -1,5 +1,5 @@ -import { Progress, Button, sendGAEvent } from '@pinback/design-system/ui'; -import { useState, useEffect, lazy, Suspense } from 'react'; +import { Progress, Button } from '@pinback/design-system/ui'; +import { lazy, Suspense } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import SocialLoginStep from './step/SocialLoginStep'; const StoryStep = lazy(() => import('./step/StoryStep')); @@ -8,21 +8,13 @@ const AlarmStep = lazy(() => import('./step/AlarmStep')); const MacStep = lazy(() => import('./step/MacStep')); const FinalStep = lazy(() => import('./step/FinalStep')); import { cva } from 'class-variance-authority'; -import { usePostSignUp } from '@shared/apis/queries'; -import { useNavigate, useLocation } from 'react-router-dom'; -import { firebaseConfig } from '../../../../firebase-config'; -import { initializeApp } from 'firebase/app'; -import { getMessaging, getToken } from 'firebase/messaging'; -import { registerServiceWorker } from '@pages/onBoarding/utils/registerServiceWorker'; -import { AlarmsType } from '@constants/alarms'; -import { normalizeTime } from '@pages/onBoarding/utils/formatRemindTime'; const stepProgress = [{ progress: 33 }, { progress: 66 }, { progress: 100 }]; import { Step, - stepOrder, StepType, storySteps, } from '@pages/onBoarding/constants/onboardingSteps'; +import { useOnboardingFunnel } from '@pages/onBoarding/hooks/useOnboardingFunnel'; const variants = { slideIn: (direction: number) => ({ @@ -50,78 +42,16 @@ const CardStyle = cva( ); const MainCard = () => { - const navigate = useNavigate(); - const location = useLocation(); - const { mutate: postSignData } = usePostSignUp(); - - const [step, setStep] = useState(Step.STORY_0); - const [direction, setDirection] = useState(0); - const [alarmSelected, setAlarmSelected] = useState<1 | 2 | 3>(1); - const [isMac, setIsMac] = useState(false); - const [userEmail, setUserEmail] = useState(''); - const [remindTime, setRemindTime] = useState('09:00'); - const [fcmToken, setFcmToken] = useState(null); - const [jobShareAgree, setJobShareAgree] = useState(true); - - useEffect(() => { - const params = new URLSearchParams(location.search); - const storedEmail = localStorage.getItem('email'); - if (storedEmail) { - setUserEmail(storedEmail); - } - - const stepParam = params.get('step') as StepType; - if (stepParam && Object.values(Step).includes(stepParam)) { - setStep(stepParam); - } - }, [location.search]); - - const app = initializeApp(firebaseConfig); - const messaging = getMessaging(app); - - const requestFCMToken = async (): Promise => { - try { - const permission = await Notification.requestPermission(); - registerServiceWorker(); - - if (permission !== 'granted') { - alert('알림 권한 허용이 필요합니다!'); - return null; - } - - const forFcmtoken = await getToken(messaging, { - vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY, - }); - - if (forFcmtoken) { - return forFcmtoken; - } else { - alert('토큰 생성 실패. 다시 시도해주세요.'); - return null; - } - } catch (error) { - console.error('FCM 토큰 받는 도중 오류:', error); - alert('알림 설정 중 오류가 발생했습니다. 다시 시도해주세요.'); - return null; - } - }; - - useEffect(() => { - const ua = navigator.userAgent.toLowerCase(); - if (ua.includes('mac os') || ua.includes('iphone') || ua.includes('ipad')) { - setIsMac(true); - } - - (async () => { - const token = await requestFCMToken(); - if (token) { - setFcmToken(token); - localStorage.setItem('FcmToken', token); - } else { - alert('푸시 알람 설정 에러'); - } - })(); - }, []); + const { + step, + direction, + alarmSelected, + jobShareAgree, + setAlarmSelected, + setJobShareAgree, + nextStep, + prevStep, + } = useOnboardingFunnel(); const renderStep = () => { switch (step) { @@ -153,64 +83,6 @@ const MainCard = () => { } }; - const nextStep = async () => { - const idx = stepOrder.indexOf(step); - const next = stepOrder[idx + 1]; - const isAlarmStep = step === Step.ALARM; - const isFinalStep = step === Step.FINAL; - const isMacStep = next === Step.MAC; - const shouldSkipMacStep = isMacStep && !isMac; - - if (isAlarmStep) { - if (alarmSelected === 1) setRemindTime('09:00'); - else if (alarmSelected === 2) setRemindTime('20:00'); - else { - const raw = AlarmsType[alarmSelected - 1].time; - setRemindTime(normalizeTime(raw)); - } - } - - if (shouldSkipMacStep) { - setDirection(1); - setStep(Step.FINAL); - navigate(`/onboarding?step=${Step.FINAL}`); - return; - } - - if (isFinalStep) { - postSignData( - { email: userEmail, remindDefault: remindTime, fcmToken }, - { - onSuccess: () => (window.location.href = '/'), - onError: () => { - const savedEmail = localStorage.getItem('email'); - if (savedEmail) window.location.href = '/'; - }, - } - ); - return; - } - - setDirection(1); - setStep(next); - navigate(`/onboarding?step=${next}`); - sendGAEvent( - `onboard-step-${idx + 1}`, - `onboard-step-${idx + 1}`, - `onboard-step-${idx + 1}` - ); - }; - - const prevStep = () => { - const idx = stepOrder.indexOf(step); - if (idx > 0) { - const previous = stepOrder[idx - 1]; - setDirection(-1); - setStep(previous); - navigate(`/onboarding?step=${previous}`); - } - }; - return (
({ + steps: stepOrder, + initialStep: Step.STORY_0, + }); + + const [direction, setDirection] = useState(0); + const [alarmSelected, setAlarmSelected] = useState(1); + const [isMac, setIsMac] = useState(false); + const [userEmail, setUserEmail] = useState(''); + const [remindTime, setRemindTime] = useState('09:00'); + const [fcmToken, setFcmToken] = useState(null); + const [jobShareAgree, setJobShareAgree] = useState(true); + + useEffect(() => { + const storedEmail = localStorage.getItem('email'); + if (storedEmail) { + setUserEmail(storedEmail); + } + }, []); + + const requestFCMToken = useCallback(async (): Promise => { + try { + const app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig); + const messaging = getMessaging(app); + const permission = await Notification.requestPermission(); + registerServiceWorker(); + + if (permission !== 'granted') { + alert('알림 권한 허용이 필요합니다!'); + return null; + } + + const forFcmtoken = await getToken(messaging, { + vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY, + }); + + if (forFcmtoken) { + return forFcmtoken; + } + + alert('토큰 생성 실패. 다시 시도해주세요.'); + return null; + } catch { + alert('알림 설정 중 오류가 발생했습니다. 다시 시도해주세요.'); + return null; + } + }, []); + + useEffect(() => { + const ua = navigator.userAgent.toLowerCase(); + if (ua.includes('mac os') || ua.includes('iphone') || ua.includes('ipad')) { + setIsMac(true); + } + + (async () => { + const token = await requestFCMToken(); + if (token) { + setFcmToken(token); + localStorage.setItem('FcmToken', token); + } else { + alert('푸시 알람 설정 에러'); + } + })(); + }, [requestFCMToken]); + + const nextStep = useCallback(async () => { + const next = stepOrder[currentIndex + 1]; + const isAlarmStep = step === Step.ALARM; + const isFinalStep = step === Step.FINAL; + const isMacStep = next === Step.MAC; + const shouldSkipMacStep = isMacStep && !isMac; + + if (isAlarmStep) { + if (alarmSelected === 1) setRemindTime('09:00'); + else if (alarmSelected === 2) setRemindTime('20:00'); + else { + const raw = AlarmsType[alarmSelected - 1].time; + setRemindTime(normalizeTime(raw)); + } + } + + if (shouldSkipMacStep) { + setDirection(1); + setStep(Step.FINAL); + return; + } + + if (isFinalStep) { + postSignData( + { email: userEmail, remindDefault: remindTime, fcmToken }, + { + onSuccess: () => (window.location.href = '/'), + onError: () => { + const savedEmail = localStorage.getItem('email'); + if (savedEmail) window.location.href = '/'; + }, + } + ); + return; + } + + setDirection(1); + goNext(); + sendGAEvent( + `onboard-step-${currentIndex + 1}`, + `onboard-step-${currentIndex + 1}`, + `onboard-step-${currentIndex + 1}` + ); + }, [ + alarmSelected, + currentIndex, + fcmToken, + goNext, + isMac, + postSignData, + remindTime, + setStep, + step, + userEmail, + ]); + + const prevStep = useCallback(() => { + if (currentIndex > 0) { + setDirection(-1); + goPrev(); + } + }, [currentIndex, goPrev]); + + return { + step, + currentIndex, + direction, + alarmSelected, + jobShareAgree, + setAlarmSelected, + setJobShareAgree, + nextStep, + prevStep, + }; +} diff --git a/apps/client/src/shared/hooks/useFunnel.ts b/apps/client/src/shared/hooks/useFunnel.ts new file mode 100644 index 00000000..3d2912d4 --- /dev/null +++ b/apps/client/src/shared/hooks/useFunnel.ts @@ -0,0 +1,69 @@ +import { useCallback, useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +interface UseFunnelOptions { + steps: readonly TStep[]; + initialStep: TStep; + queryKey?: string; +} + +interface MoveStepOptions { + replace?: boolean; +} + +export function useFunnel({ + steps, + initialStep, + queryKey = 'step', +}: UseFunnelOptions) { + const [searchParams, setSearchParams] = useSearchParams(); + + const currentStep = useMemo(() => { + const value = searchParams.get(queryKey); + return value && steps.includes(value as TStep) + ? (value as TStep) + : initialStep; + }, [searchParams, queryKey, steps, initialStep]); + + const currentIndex = steps.indexOf(currentStep); + + const setStep = useCallback( + (nextStep: TStep, { replace = true }: MoveStepOptions = {}) => { + if (!steps.includes(nextStep)) return; + const nextParams = new URLSearchParams(searchParams); + nextParams.set(queryKey, nextStep); + setSearchParams(nextParams, { replace }); + }, + [steps, searchParams, queryKey, setSearchParams] + ); + + const goNext = useCallback( + (options?: MoveStepOptions) => { + const nextStep = steps[currentIndex + 1]; + if (!nextStep) return null; + setStep(nextStep, options); + return nextStep; + }, + [steps, currentIndex, setStep] + ); + + const goPrev = useCallback( + (options?: MoveStepOptions) => { + const prevStep = steps[currentIndex - 1]; + if (!prevStep) return null; + setStep(prevStep, options); + return prevStep; + }, + [steps, currentIndex, setStep] + ); + + return { + currentStep, + currentIndex, + isFirstStep: currentIndex <= 0, + isLastStep: currentIndex === steps.length - 1, + setStep, + goNext, + goPrev, + }; +}