-
Notifications
You must be signed in to change notification settings - Fork 1
Feat(client): useFunnel 구현 #261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,157 @@ | ||||||||||||||||||||||||||||||||||
| import { sendGAEvent } from '@pinback/design-system/ui'; | ||||||||||||||||||||||||||||||||||
| import { usePostSignUp } from '@shared/apis/queries'; | ||||||||||||||||||||||||||||||||||
| import { useFunnel } from '@shared/hooks/useFunnel'; | ||||||||||||||||||||||||||||||||||
| import { useCallback, useEffect, useState } from 'react'; | ||||||||||||||||||||||||||||||||||
| import { AlarmsType } from '@constants/alarms'; | ||||||||||||||||||||||||||||||||||
| import { normalizeTime } from '@pages/onBoarding/utils/formatRemindTime'; | ||||||||||||||||||||||||||||||||||
| import { registerServiceWorker } from '@pages/onBoarding/utils/registerServiceWorker'; | ||||||||||||||||||||||||||||||||||
| import { Step, stepOrder, StepType } from '@pages/onBoarding/constants/onboardingSteps'; | ||||||||||||||||||||||||||||||||||
| import { firebaseConfig } from '../../../firebase-config'; | ||||||||||||||||||||||||||||||||||
| import { getApp, getApps, initializeApp } from 'firebase/app'; | ||||||||||||||||||||||||||||||||||
| import { getMessaging, getToken } from 'firebase/messaging'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| type AlarmSelection = 1 | 2 | 3; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export function useOnboardingFunnel() { | ||||||||||||||||||||||||||||||||||
| const { mutate: postSignData } = usePostSignUp(); | ||||||||||||||||||||||||||||||||||
| const { currentStep: step, currentIndex, setStep, goNext, goPrev } = | ||||||||||||||||||||||||||||||||||
| useFunnel<StepType>({ | ||||||||||||||||||||||||||||||||||
| steps: stepOrder, | ||||||||||||||||||||||||||||||||||
| initialStep: Step.STORY_0, | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const [direction, setDirection] = useState(0); | ||||||||||||||||||||||||||||||||||
| const [alarmSelected, setAlarmSelected] = useState<AlarmSelection>(1); | ||||||||||||||||||||||||||||||||||
| const [isMac, setIsMac] = useState(false); | ||||||||||||||||||||||||||||||||||
| const [userEmail, setUserEmail] = useState(''); | ||||||||||||||||||||||||||||||||||
| const [remindTime, setRemindTime] = useState('09:00'); | ||||||||||||||||||||||||||||||||||
| const [fcmToken, setFcmToken] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||||
| const [jobShareAgree, setJobShareAgree] = useState(true); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||
| const storedEmail = localStorage.getItem('email'); | ||||||||||||||||||||||||||||||||||
| if (storedEmail) { | ||||||||||||||||||||||||||||||||||
| setUserEmail(storedEmail); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const requestFCMToken = useCallback(async (): Promise<string | null> => { | ||||||||||||||||||||||||||||||||||
| 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)); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+90
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: fd "alarms.ts" --type fRepository: Pinback-Team/pinback-client Length of output: 106 🏁 Script executed: fd "useOnboardingFunnel.ts" --type fRepository: Pinback-Team/pinback-client Length of output: 132 🏁 Script executed: rg "normalizeTime" --type ts --type tsx -A 10Repository: Pinback-Team/pinback-client Length of output: 98 🏁 Script executed: rg "normalizeTime" --type ts -A 10Repository: Pinback-Team/pinback-client Length of output: 5227 🏁 Script executed: cat apps/client/src/constants/alarms.tsRepository: Pinback-Team/pinback-client Length of output: 545 🏁 Script executed: cat -n apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts | sed -n '85,100p'Repository: Pinback-Team/pinback-client Length of output: 682 🏁 Script executed: cat apps/client/src/pages/onBoarding/utils/formatRemindTime.tsRepository: Pinback-Team/pinback-client Length of output: 749 🏁 Script executed: cat apps/client/src/constants/alarms.tsRepository: Pinback-Team/pinback-client Length of output: 545 🏁 Script executed: cat -n apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx | grep -A 50 "select === 3"Repository: Pinback-Team/pinback-client Length of output: 1465 🏁 Script executed: cat -n apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx | sed -n '1,50p'Repository: Pinback-Team/pinback-client Length of output: 1866 🏁 Script executed: rg "getTimePicker" apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx -A 5 -B 5Repository: Pinback-Team/pinback-client Length of output: 567 🏁 Script executed: rg "AlarmStep" --type ts -A 30 | head -80Repository: Pinback-Team/pinback-client Length of output: 7183 🏁 Script executed: cat -n apps/client/src/pages/onBoarding/components/funnel/AlarmStep.tsxRepository: Pinback-Team/pinback-client Length of output: 167 🏁 Script executed: fd "AlarmStep" --type fRepository: Pinback-Team/pinback-client Length of output: 140 🏁 Script executed: rg "AlarmsType\[2\].time =" --type ts -B 5 -A 5Repository: Pinback-Team/pinback-client Length of output: 1076 🏁 Script executed: cat -n apps/client/src/pages/onBoarding/components/funnel/step/AlarmStep.tsxRepository: Pinback-Team/pinback-client Length of output: 1556 🏁 Script executed: rg "remindTime" apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts -B 2 -A 2Repository: Pinback-Team/pinback-client Length of output: 598
시간 선택 검증을 추가하거나, 빈 문자열 처리를 추가해야 합니다. 🐛 빈 문자열 가드 추가 제안 } else {
const raw = AlarmsType[alarmSelected - 1].time;
- setRemindTime(normalizeTime(raw));
+ if (raw) {
+ setRemindTime(normalizeTime(raw));
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+105
to
+117
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for postSignData API type definition and fcmToken usage
rg -n "postSignData|postSignUp" --type ts -C 3 | head -100Repository: Pinback-Team/pinback-client Length of output: 3800 🏁 Script executed: #!/bin/bash
# Find fcmToken initialization around Line 28
rg -n "fcmToken" apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts -C 5Repository: Pinback-Team/pinback-client Length of output: 1214 🏁 Script executed: #!/bin/bash
# Look for the API request type definition
fd -e ts -e tsx | xargs rg -l "fcmToken" | head -10Repository: Pinback-Team/pinback-client Length of output: 202 🏁 Script executed: #!/bin/bash
# Check how fcmToken is set - look for requestFcmToken or similar
rg -n "setFcmToken" apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts -C 10Repository: Pinback-Team/pinback-client Length of output: 1481 🏁 Script executed: #!/bin/bash
# Check if there are any FCM token request functions
rg -n "fcmToken|FCM|firebase" apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts -A 2Repository: Pinback-Team/pinback-client Length of output: 1122 🏁 Script executed: #!/bin/bash
# Check the complete onError handler and context
sed -n '105,120p' apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.tsRepository: Pinback-Team/pinback-client Length of output: 480 🏁 Script executed: #!/bin/bash
# Search for any error notifications or logging in the file
rg -n "toast|console|alert|error" apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.tsRepository: Pinback-Team/pinback-client Length of output: 231
참고: 🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 에러 fallback은 이후에 추가 예정 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이 작업을 추적하기 위해 GitHub 이슈를 생성해 드릴까요? 그러면 나중에 놓치지 않고 처리하실 수 있을 것 같습니다. 🧠 Learnings used |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
registerServiceWorker()가await없이 호출되어getToken과 경쟁 조건 발생 가능.Line 43에서
registerServiceWorker()는 fire-and-forget으로 호출되지만, Line 50의getToken은 서비스 워커가 등록 완료된 상태를 기대합니다. 서비스 워커 등록이 완료되기 전에getToken이 호출되면 토큰 생성이 실패할 수 있습니다.또한 Line 66-80의
useEffect에서 컴포넌트 마운트 즉시 FCM 토큰을 요청하면서 알림 권한 프롬프트가 사용자 의도 없이 표시됩니다. 이는 브라우저의 권한 정책에 의해 이후 요청이 차단될 수 있습니다.🔧 서비스 워커 등록 후 토큰 요청 순서 보장 제안
useEffect내에서 서비스 워커를 먼저 등록한 후 registration 객체를requestFCMToken에 전달하는 방식을 권장합니다.Also applies to: 66-81
🤖 Prompt for AI Agents