diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 46a4c4f..2d70d7a 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -9,29 +9,45 @@ import type { SignUpRequest, SocialLoginRequest, } from '@/types/auth'; +import { useCallback } from 'react'; import { useNavigate } from 'react-router'; export default function useAuth() { const navigate = useNavigate(); - const { user, isLoggedIn, login, logout, updateUser } = useAuthStore( - (state) => state - ); + const user = useAuthStore((state) => state.user); + const isLoggedIn = useAuthStore((state) => state.isLoggedIn); + const login = useAuthStore((state) => state.login); + const logout = useAuthStore((state) => state.logout); + const updateUser = useAuthStore((state) => state.updateUser); - // 1. 이메일 로그인 로직 - const handleLogin = async (data: LoginRequest) => { + // 5. 로그아웃 로직 (handleLogout을 먼저 정의함) + const handleLogout = useCallback(async () => { try { - const { token } = await loginApi(data); - const user = await getMeApi(token); - login(user, token); // Zustand 스토어 업데이트 - navigate('/'); // 메인 페이지로 이동 - } catch (error) { - console.error('Login failed:', error); + await logoutApi(); + } finally { + logout(); // Zustand 상태 초기화 + navigate('/login'); } - }; + }, [logout, navigate]); + + // 1. 이메일 로그인 로직 + const handleLogin = useCallback( + async (data: LoginRequest) => { + try { + const { token } = await loginApi(data); + const user = await getMeApi(token); + login(user, token); // Zustand 스토어 업데이트 + navigate('/'); // 메인 페이지로 이동 + } catch (error) { + console.error('Login failed:', error); + } + }, + [login, navigate] + ); // 2. 이메일 회원가입 로직 - const handleSignUp = async (data: SignUpRequest) => { + const handleSignUp = useCallback(async (data: SignUpRequest) => { try { const response = await signupApi(data); @@ -44,22 +60,25 @@ export default function useAuth() { console.error('Signup failed:', error); } return false; - }; + }, []); // 3. 소셜 로그인 로직 - const handleSocialLogin = async (data: SocialLoginRequest) => { - try { - const { token } = await socialApi(data); - const user = await getMeApi(token); - login(user, token); - navigate('/'); - } catch (error) { - console.error('Social login failed:', error); - } - }; + const handleSocialLogin = useCallback( + async (data: SocialLoginRequest) => { + try { + const { token } = await socialApi(data); + const user = await getMeApi(token); + login(user, token); + navigate('/'); + } catch (error) { + console.error('Social login failed:', error); + } + }, + [login, navigate] + ); // 4. 내 정보 동기화 - const refreshUser = async () => { + const refreshUser = useCallback(async () => { if (!isLoggedIn) return; try { const userData = await getMeApi(); @@ -69,14 +88,7 @@ export default function useAuth() { console.error('Refresh user failed:', error); handleLogout(); // 토큰이 유효하지 않으면 로그아웃 처리 } - }; - - // 5. 로그아웃 로직 - const handleLogout = async () => { - await logoutApi(); - logout(); // Zustand 상태 초기화 - navigate('/login'); - }; + }, [isLoggedIn, updateUser, handleLogout]); return { user, diff --git a/src/mocks/handlers/auth.ts b/src/mocks/handlers/auth.ts index 61bba17..aa8957f 100644 --- a/src/mocks/handlers/auth.ts +++ b/src/mocks/handlers/auth.ts @@ -4,6 +4,8 @@ import type { LogoutResponse, SignUpRequest, SignUpResponse, + SocialLoginRequest, + SocialLoginResponse, } from '@/types/auth'; import { http, HttpResponse, delay } from 'msw'; import { userDB } from '../db/user.db'; @@ -80,4 +82,42 @@ export const authHandlers = [ }); } ), + + // 5. 소셜 로그인 + http.post( + path('/auth/social'), + async ({ request }) => { + const { provider, code } = await request.json(); + + // 실제 서버처럼 이미 사용된 코드는 401을 반환하도록 시뮬레이션 + if (usedSocialCodes.has(code)) { + await delay(500); + return new HttpResponse(null, { + status: 401, + statusText: 'Unauthorized', + }); + } + + // 테스트를 위해 provider와 code가 있으면 무조건 성공하는 시나리오 (준엽 유저로 로그인) + if (provider && code) { + const user = userDB.find((u) => u.id === 2); + + if (user) { + usedSocialCodes.add(code); // 코드 사용 처리 + await delay(500); + return HttpResponse.json({ + token: user.token, + }); + } + } + + return new HttpResponse(null, { + status: 401, + statusText: 'Unauthorized', + }); + } + ), ]; + +// 코드 사용 여부를 추적하기 위한 메모리 저장소 +const usedSocialCodes = new Set(); diff --git a/src/routes/SocialCallback.tsx b/src/routes/SocialCallback.tsx index a81fb0c..cd0a792 100644 --- a/src/routes/SocialCallback.tsx +++ b/src/routes/SocialCallback.tsx @@ -1,5 +1,5 @@ import type { AuthProvider } from '@/types/auth'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router'; import useAuth from '../hooks/useAuth'; @@ -13,23 +13,30 @@ export default function SocialCallback() { ? SOCIAL_PROVIDER_MAP[providerParam.toLowerCase()] : undefined; const [searchParams] = useSearchParams(); - const { handleSocialLogin } = useAuth(); + const { handleSocialLogin, isLoggedIn } = useAuth(); const navigate = useNavigate(); + const loginAttempted = useRef(false); useEffect(() => { + // 1. 이미 로그인되어 있거나 이미 요청을 보냈다면 중단 + if (isLoggedIn || loginAttempted.current) { + return; + } + const code = searchParams.get('code'); if (code && provider) { + loginAttempted.current = true; // 요청 시작을 표시 handleSocialLogin({ provider, code }); } else { alert('로그인에 실패했습니다.'); navigate('/login'); } - }, [provider, searchParams, handleSocialLogin, navigate]); + }, [provider, searchParams, handleSocialLogin, navigate, isLoggedIn]); return (
-
+

소셜 계정 정보를 확인 중입니다...

);