From 26fc9793157c049e8eadfc6f08f2600c3d993ab4 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Fri, 22 Aug 2025 07:13:11 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20getTicketApi=EC=9D=98=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=A0=95=EC=9D=98=EB=A5=BC=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=9D=91=EB=8B=B5=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EC=9D=84=20=EB=AA=85=EC=8B=9C=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/auth/apis/get-ticket.api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, }, From c60de91ba9da0c627414cee90004426eb4014971 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Fri, 22 Aug 2025 07:13:24 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=9B=85=20=EB=B0=8F=20=EC=BD=9C?= =?UTF-8?q?=EB=B0=B1=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/auth/hooks/index.ts | 1 + src/entities/auth/hooks/useKakaoLogin.ts | 38 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/entities/auth/hooks/useKakaoLogin.ts 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, + }); +}; From 1887b7084ad0b3b18eadb88d60b826e64e68facc Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Fri, 22 Aug 2025 07:13:35 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=86=8C=20=EB=AA=A8=EB=93=88=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9C=A0=ED=8B=B8=EB=A6=AC=ED=8B=B0=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/utils/auth-storage/auth-storage.ts | 31 +++++++++++++++++++ src/shared/utils/auth-storage/index.ts | 1 + src/shared/utils/index.ts | 1 + 3 files changed, 33 insertions(+) create mode 100644 src/shared/utils/auth-storage/auth-storage.ts create mode 100644 src/shared/utils/auth-storage/index.ts 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'; From fc11de6d01598b770c9de4291987dbbf7d4aac32 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Fri, 22 Aug 2025 07:13:44 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84=ED=8A=BC=EC=97=90?= =?UTF-8?q?=EC=84=9C=20OAuth=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89?= =?UTF-8?q?=ED=8A=B8=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=ED=9B=85=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98=EA=B3=A0=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EC=83=81=ED=83=9C=20=ED=91=9C=EC=8B=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/buttons/KakaoLoginButton.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) 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 ( ); }; From 8f8873eeef91197c4fd045c60fb88e602dfbf509 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Fri, 22 Aug 2025 07:13:55 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20OAuth=20=EB=A6=AC=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EB=A0=89=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=B0=8F=20API=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0,=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/oauth/OAuthRedirectPage.tsx | 103 ++++---------------------- 1 file changed, 16 insertions(+), 87 deletions(-) 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
로그인 중입니다...
; }