Skip to content

Commit c3c8aa9

Browse files
authored
feat: 비밀번호 재설정 페이지 및 토큰 재발급 기능 구현 (#39)
* feat: 로그아웃 API 연동 및 엑세스 토큰 자동 갱신 로직 추가 * feat: 비밀번호 변경 페이지 구현 * fix: 백엔드 요청 필드 추가 * refactor: useTimer 관련 경로 수정 * refactor: smsSent 로직 개선 * fix: 페이지 의존성 수정 및 경로 재구성 * fix: logoutSession 함수 독립성 강화 * refactor: refresh 폭주 방지 로직 설정 * fix: email 정규식 경로 수정
1 parent d07a776 commit c3c8aa9

21 files changed

Lines changed: 751 additions & 21 deletions

src/app/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ManagerWorkerScheduleLegacyEntryRedirect } from '@/pages/manager/worker
1212
import { SocialPage } from '@/pages/manager/social'
1313
import { SocialChatPage } from '@/pages/manager/social-chat'
1414
import { LoginPage } from '@/pages/login'
15+
import { FindPasswordPage } from '@/pages/find-password'
1516
import { KakaoCallbackPage } from '@/pages/oauth/KakaoCallbackPage'
1617
import { JobLookupMapPage } from '@/pages/user/job-lookup-map'
1718
import { JobLookupMapApplyPage } from '@/pages/user/job-lookup-map-apply'
@@ -67,6 +68,10 @@ export function App() {
6768
<Routes>
6869
<Route element={<MobileRouteLayoutWithoutDocbar />}>
6970
<Route path={ROUTES.AUTH.LOGIN} element={<LoginPage />} />
71+
<Route
72+
path={ROUTES.AUTH.FIND_PASSWORD}
73+
element={<FindPasswordPage />}
74+
/>
7075
<Route
7176
path={ROUTES.OAUTH.KAKAO_CALLBACK}
7277
element={<KakaoCallbackPage />}

src/features/auth/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export { KakaoLoginButton } from './ui/KakaoLoginButton'
22
export { AppleLoginButton } from './ui/AppleLoginButton'
3+
export { PhoneVerification } from './ui/PhoneVerification'
4+
export { VerifyActionButton } from './ui/VerifyActionButton'

src/pages/signup/components/PhoneVerification.tsx renamed to src/features/auth/ui/PhoneVerification.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
11
import { AuthInput } from '@/shared/ui/common/AuthInput'
22
import { VerifyActionButton } from './VerifyActionButton'
3-
import type { usePhoneVerification } from '../hooks/usePhoneVerification'
43

5-
type Props = ReturnType<typeof usePhoneVerification>
4+
type PhoneVerificationProps = {
5+
phone: string
6+
smsSent: boolean
7+
smsCode: string
8+
setSmsCode: (value: string) => void
9+
verified: boolean
10+
message: string
11+
isSending: boolean
12+
isVerifying: boolean
13+
resendCooldown: number
14+
handlePhoneChange: (value: string) => void
15+
sendSms: () => void | Promise<void>
16+
verifySms: () => void | Promise<void>
17+
}
18+
19+
type Props = PhoneVerificationProps
620

721
/**
822
* 전화번호 SMS 인증 UI
File renamed without changes.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { useState, useRef, useEffect } from 'react'
2+
import type { ConfirmationResult } from 'firebase/auth'
3+
import { createPasswordResetSession } from '@/shared/api/auth'
4+
import {
5+
sendPhoneVerification,
6+
toInternationalPhone,
7+
clearRecaptcha,
8+
getFreshFirebaseIdToken,
9+
} from '@/shared/lib/firebase'
10+
import {
11+
normalizePhone,
12+
formatPhone,
13+
} from '@/shared/lib/utils/signupValidation'
14+
import { useTimer } from '@/shared/hooks/useTimer'
15+
16+
export const FIND_PASSWORD_RECAPTCHA_ID = 'find-password-recaptcha-container'
17+
const RESEND_COOLDOWN = 30
18+
19+
/**
20+
* 비밀번호 찾기 2단계 — Firebase SMS 인증 후 재설정 세션 생성
21+
*/
22+
export function useFindPasswordPhoneVerification(email: string) {
23+
const [phone, setPhone] = useState('')
24+
const [smsSent, setSmsSent] = useState(false)
25+
const [smsCode, setSmsCode] = useState('')
26+
const [verified, setVerified] = useState(false)
27+
const [message, setMessage] = useState('')
28+
const [isSending, setIsSending] = useState(false)
29+
const [isVerifying, setIsVerifying] = useState(false)
30+
const [sessionId, setSessionId] = useState('')
31+
32+
const smsConfirmationRef = useRef<ConfirmationResult | null>(null)
33+
const {
34+
countdown: resendCooldown,
35+
start: startCooldown,
36+
clear: clearCooldown,
37+
} = useTimer()
38+
39+
useEffect(() => {
40+
return () => clearRecaptcha()
41+
}, [])
42+
43+
const handlePhoneChange = (value: string) => {
44+
setPhone(formatPhone(value))
45+
smsConfirmationRef.current = null
46+
setSmsSent(false)
47+
setSmsCode('')
48+
setVerified(false)
49+
setSessionId('')
50+
setMessage('')
51+
clearCooldown()
52+
}
53+
54+
const sendSms = async () => {
55+
const normalized = normalizePhone(phone)
56+
if (normalized.length !== 11 || isSending || resendCooldown > 0) {
57+
if (normalized.length !== 11) {
58+
setMessage('전화번호 11자리를 입력해주세요.')
59+
}
60+
return
61+
}
62+
63+
try {
64+
setIsSending(true)
65+
setMessage('')
66+
clearRecaptcha()
67+
const intlPhone = toInternationalPhone(normalized)
68+
const confirmation = await sendPhoneVerification(
69+
intlPhone,
70+
FIND_PASSWORD_RECAPTCHA_ID
71+
)
72+
smsConfirmationRef.current = confirmation
73+
setSmsSent(true)
74+
setMessage('인증번호가 발송됐어요. 문자를 확인해주세요.')
75+
startCooldown(RESEND_COOLDOWN)
76+
} catch (err) {
77+
const e = err as { message?: string }
78+
clearRecaptcha()
79+
setMessage(
80+
e.message || 'SMS 발송에 실패했습니다. 잠시 후 다시 시도해주세요.'
81+
)
82+
} finally {
83+
setIsSending(false)
84+
}
85+
}
86+
87+
const verifySms = async () => {
88+
const confirmation = smsConfirmationRef.current
89+
if (!smsSent || !smsCode.trim() || isVerifying || !confirmation) return
90+
try {
91+
setIsVerifying(true)
92+
setMessage('')
93+
await confirmation.confirm(smsCode)
94+
const firebaseIdToken = await getFreshFirebaseIdToken()
95+
if (!firebaseIdToken) {
96+
setMessage('전화번호 인증이 만료되었습니다. 다시 인증해 주세요.')
97+
return
98+
}
99+
const newSessionId = await createPasswordResetSession(
100+
email.trim(),
101+
normalizePhone(phone),
102+
firebaseIdToken
103+
)
104+
setSessionId(newSessionId)
105+
setVerified(true)
106+
setMessage('휴대폰 인증이 완료됐어요!')
107+
clearCooldown()
108+
} catch (error) {
109+
setVerified(false)
110+
const e = error as { message?: string }
111+
setMessage(
112+
e.message || '인증번호가 올바르지 않습니다. 다시 확인해주세요.'
113+
)
114+
} finally {
115+
setIsVerifying(false)
116+
}
117+
}
118+
119+
return {
120+
phone,
121+
smsSent,
122+
smsCode,
123+
setSmsCode,
124+
verified,
125+
message,
126+
isSending,
127+
isVerifying,
128+
resendCooldown,
129+
sessionId,
130+
handlePhoneChange,
131+
sendSms,
132+
verifySms,
133+
}
134+
}

0 commit comments

Comments
 (0)