diff --git a/src/entities/auth/apis/get-ticket.api.ts b/src/entities/auth/apis/get-ticket.api.ts index 5e7d8e7..60f27c3 100644 --- a/src/entities/auth/apis/get-ticket.api.ts +++ b/src/entities/auth/apis/get-ticket.api.ts @@ -7,7 +7,7 @@ interface GetTicketApiResponse { } export const getTicketApi = async (ticket: string): Promise => { - const response = await fetchInstance.get(GET_TICKET_API_PATH, { + const response = await fetchInstance.get(GET_TICKET_API_PATH, { params: { ticket, }, diff --git a/src/entities/auth/hooks/index.ts b/src/entities/auth/hooks/index.ts index df60f1b..83322f7 100644 --- a/src/entities/auth/hooks/index.ts +++ b/src/entities/auth/hooks/index.ts @@ -1 +1,2 @@ export * from './getAuthTicket'; +export * from './useKakaoLogin'; diff --git a/src/entities/auth/hooks/useKakaoLogin.ts b/src/entities/auth/hooks/useKakaoLogin.ts new file mode 100644 index 0000000..24dc889 --- /dev/null +++ b/src/entities/auth/hooks/useKakaoLogin.ts @@ -0,0 +1,38 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import { toast } from 'sonner'; + +import { BASE_URL } from '@/shared'; + +import { getTicketApi } from '../apis'; + +// --- 1. "로그인 시작"을 위한 useMutation --- +// 이 훅은 API를 호출하지 않아. 오직 카카오 로그인 페이지로 '이동'시키는 액션만 책임져. +export const useKakaoLogin = () => { + return useMutation({ + mutationFn: () => { + // 실제로는 Promise를 반환할 필요는 없지만, mutationFn은 Promise를 기대해. + window.location.href = `${BASE_URL}/oauth2/authorization/kakao +`; + return Promise.resolve(); + }, + onError: () => { + toast.error('카카오 로그인 페이지로 이동하는 데 실패했습니다.'); + }, + }); +}; + +// --- 2. "콜백 처리"를 위한 useQuery --- +export const KakaoCallbackQueryKey = { + callback: (ticket: string) => ['kakaoCallback', ticket], +}; + +export const useKakaoCallback = (ticket: string) => { + return useQuery({ + queryKey: KakaoCallbackQueryKey.callback(ticket), + // code가 있을 때만 이 쿼리를 실행해. + queryFn: () => getTicketApi(ticket), + // 이 쿼리는 한 번만 실행되면 되므로, 재시도나 캐시 관련 옵션을 조정할 수 있어. + retry: 0, + staleTime: Infinity, + }); +}; diff --git a/src/features/login/components/common/buttons/KakaoLoginButton.tsx b/src/features/login/components/common/buttons/KakaoLoginButton.tsx index 67aefd7..9f5688e 100644 --- a/src/features/login/components/common/buttons/KakaoLoginButton.tsx +++ b/src/features/login/components/common/buttons/KakaoLoginButton.tsx @@ -1,21 +1,19 @@ -import { BASE_URL, Button } from '@/shared'; +import { useKakaoLogin } from '@/entities'; +import { Button } from '@/shared'; import KakaoSymbol from '../../../_assets/kakao-symbol.webp'; export const KakaoLoginButton = () => { - const kakaoLogin = () => { - // OAuth 서버로 리다이렉트하되, 리다이렉트 URL을 명시적으로 지정 - const redirectUri = encodeURIComponent(`${window.location.origin}/oauth/redirect`); - window.location.href = `${BASE_URL}/oauth2/authorization/kakao?redirect_uri=${redirectUri}`; - }; - + const { mutate: startKakaoLogin, isPending } = useKakaoLogin(); return ( ); }; diff --git a/src/pages/oauth/OAuthRedirectPage.tsx b/src/pages/oauth/OAuthRedirectPage.tsx index 0569a70..08bf675 100644 --- a/src/pages/oauth/OAuthRedirectPage.tsx +++ b/src/pages/oauth/OAuthRedirectPage.tsx @@ -1,104 +1,33 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { useGetAuthTicket } from '@/entities/auth'; +import { toast } from 'sonner'; -import { ROUTER_PATH } from '@/shared'; +import { useKakaoCallback } from '@/entities'; +import { ROUTER_PATH, authStorage } from '@/shared'; export default function OAuthRedirectPage() { const [searchParams] = useSearchParams(); + const navigate = useNavigate(); - const [error, setError] = useState(null); - // URL에서 ticket 파라미터 추출 const ticket = searchParams.get('ticket'); - const oauthError = searchParams.get('error'); - const errorDescription = searchParams.get('error_description'); - - useEffect(() => { - if (oauthError) { - setError(errorDescription || `OAuth 인증 오류: ${oauthError}`); - } else if (!ticket) { - setError('인증 토큰이 없습니다. 다시 로그인해주세요.'); - } - }, [oauthError, errorDescription, ticket]); - - // ticket이 있고 OAuth 에러가 없을 때만 API 호출 - const shouldCallApi = !!ticket && !oauthError; - const { - data: authData, - error: apiError, - isLoading, - } = useGetAuthTicket(shouldCallApi ? ticket : ''); - // API 에러 처리 - useEffect(() => { - if (apiError) { - console.error('API 호출 중 오류:', apiError); - setError('인증 검증에 실패했습니다. 다시 시도해주세요.'); - } - }, [apiError]); + const { data, isSuccess, isError } = useKakaoCallback(ticket ?? ''); - // 성공 시 처리 useEffect(() => { - if (authData && !isLoading && !apiError && shouldCallApi) { - console.log('OAuth 인증 성공!', authData); - - // refreshToken을 localStorage에 저장 - localStorage.setItem('refreshToken', authData.refreshToken); + if (isSuccess && data) { + authStorage.refreshToken.set(data.refreshToken); - // 메인 페이지로 이동 - navigate(ROUTER_PATH.MAIN, { replace: true }); + navigate(ROUTER_PATH.ROOT); } - }, [authData, isLoading, apiError, navigate, shouldCallApi]); - if (isLoading && shouldCallApi) { - return ( -
-
-
-
-
-

인증 처리 중...

-

잠시만 기다려주세요.

-
-
- ); - } - - if (error) { - return ( -
-
-
-
- - - -
-
-

인증 실패

-

{error}

- -
-
- ); - } + if (isError) { + // 실패 시, 에러 처리 후 로그인 페이지로 이동 + toast.error('카카오 로그인에 실패했습니다.'); + navigate('/login'); + } + }, [isSuccess, isError, data, navigate]); - return null; + return
로그인 중입니다...
; } diff --git a/src/shared/utils/auth-storage/auth-storage.ts b/src/shared/utils/auth-storage/auth-storage.ts new file mode 100644 index 0000000..1db7db0 --- /dev/null +++ b/src/shared/utils/auth-storage/auth-storage.ts @@ -0,0 +1,31 @@ +type AuthStorageKey = { + accessToken: string; + refreshToken: string; +}; + +const initStorage = (key: T, storage: Storage) => { + const storageKey = `${key}`; + + const get = (): AuthStorageKey[T] => { + const value = storage.getItem(storageKey); + + return JSON.parse(value as string); + }; + + const set = (value: AuthStorageKey[T]) => { + if (value === undefined || value === null) { + return storage.removeItem(storageKey); + } + + const stringifiedValue = JSON.stringify(value); + + storage.setItem(storageKey, stringifiedValue); + }; + + return { get, set }; +}; + +export const authStorage = { + accessToken: initStorage('accessToken', localStorage), + refreshToken: initStorage('refreshToken', localStorage), +}; diff --git a/src/shared/utils/auth-storage/index.ts b/src/shared/utils/auth-storage/index.ts new file mode 100644 index 0000000..56c4430 --- /dev/null +++ b/src/shared/utils/auth-storage/index.ts @@ -0,0 +1 @@ +export * from './auth-storage'; diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts index 3932761..24a34be 100644 --- a/src/shared/utils/index.ts +++ b/src/shared/utils/index.ts @@ -2,3 +2,4 @@ export * from './scroll-to-top'; export * from './class-value'; export * from './image-cache'; export * from './chart'; +export * from './auth-storage';