-
Notifications
You must be signed in to change notification settings - Fork 0
[기능 구현] 카카오 OAuth 리다이렉트 페이지 및 인증 API 연동 구현 #45
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
d3ddfd3
e81e8f7
95738b0
d59c1c5
c08eb98
0c4335d
674f780
af756a8
8151831
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,16 @@ | ||
| import { fetchInstance } from '@/shared'; | ||
|
|
||
| export const GET_TICKET_API_PATH = '/api/auth/ticket'; | ||
|
|
||
| interface GetTicketApiResponse { | ||
| refreshToken: string; | ||
| } | ||
|
|
||
| export const getTicketApi = async (ticket: string): Promise<GetTicketApiResponse> => { | ||
| const response = await fetchInstance.get(GET_TICKET_API_PATH, { | ||
| params: { | ||
| ticket, | ||
| }, | ||
| }); | ||
| return response.data; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './get-ticket.api'; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,14 @@ | ||||||||||||||||||||||||||||||||||
| import { useQuery } from '@tanstack/react-query'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import { getTicketApi } from '../apis'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export const GetAuthTicketQueryKey = { | ||||||||||||||||||||||||||||||||||
| ticket: (ticket: string) => ['ticket', ticket], | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export const useGetAuthTicket = (ticket: string) => { | ||||||||||||||||||||||||||||||||||
| return useQuery({ | ||||||||||||||||||||||||||||||||||
| queryKey: GetAuthTicketQueryKey.ticket(ticket), | ||||||||||||||||||||||||||||||||||
| queryFn: () => getTicketApi(ticket), | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+9
to
+14
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.
Suggested change
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './getAuthTicket'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './apis'; | ||
| export * from './hooks'; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,10 +1,19 @@ | ||||||
| import { Button } from '@/shared'; | ||||||
| import { BASE_URL, 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`); | ||||||
|
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. 리다이렉트 URL 경로가 하드코딩되어 있습니다.
Suggested change
|
||||||
| window.location.href = `${BASE_URL}/oauth2/authorization/kakao?redirect_uri=${redirectUri}`; | ||||||
| }; | ||||||
|
|
||||||
| return ( | ||||||
| <Button className='flex h-11 w-90 items-center gap-3 rounded-lg bg-kakao text-black hover:bg-kakao/80 active:bg-kakao/80'> | ||||||
| <Button | ||||||
| onClick={kakaoLogin} | ||||||
| className='flex h-11 w-90 items-center gap-3 rounded-lg bg-kakao text-black hover:bg-kakao/80 active:bg-kakao/80' | ||||||
| > | ||||||
| <img src={KakaoSymbol} className='size-4' width={16} height={16} alt='kakao' /> | ||||||
| <span className='text-sm font-semibold'>카카오 계정으로 계속하기</span> | ||||||
| </Button> | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { useEffect, useState } from 'react'; | ||
| import { useNavigate, useSearchParams } from 'react-router-dom'; | ||
|
|
||
| import { useGetAuthTicket } from '@/entities/auth'; | ||
|
|
||
| import { ROUTER_PATH } from '@/shared'; | ||
|
|
||
| export default function OAuthRedirectPage() { | ||
| const [searchParams] = useSearchParams(); | ||
| const navigate = useNavigate(); | ||
| const [error, setError] = useState<string | null>(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 : ''); | ||
|
Comment on lines
+27
to
+32
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.
|
||
|
|
||
| // API 에러 처리 | ||
| useEffect(() => { | ||
| if (apiError) { | ||
| console.error('API 호출 중 오류:', apiError); | ||
|
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. |
||
| setError('인증 검증에 실패했습니다. 다시 시도해주세요.'); | ||
| } | ||
| }, [apiError]); | ||
|
|
||
| // 성공 시 처리 | ||
| useEffect(() => { | ||
| if (authData && !isLoading && !apiError && shouldCallApi) { | ||
| console.log('OAuth 인증 성공!', authData); | ||
|
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. |
||
|
|
||
| // refreshToken을 localStorage에 저장 | ||
| localStorage.setItem('refreshToken', authData.refreshToken); | ||
|
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. |
||
|
|
||
| // 메인 페이지로 이동 | ||
| navigate(ROUTER_PATH.MAIN, { replace: true }); | ||
| } | ||
| }, [authData, isLoading, apiError, navigate, shouldCallApi]); | ||
|
Comment on lines
+43
to
+53
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. 성공 처리 |
||
|
|
||
| if (isLoading && shouldCallApi) { | ||
| return ( | ||
| <div className='flex min-h-screen items-center justify-center'> | ||
| <div className='text-center'> | ||
| <div className='mb-4'> | ||
| <div className='mx-auto h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent'></div> | ||
| </div> | ||
| <p className='text-lg font-medium text-gray-700'>인증 처리 중...</p> | ||
| <p className='mt-2 text-sm text-gray-500'>잠시만 기다려주세요.</p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (error) { | ||
| return ( | ||
| <div className='flex min-h-screen items-center justify-center'> | ||
| <div className='text-center'> | ||
| <div className='mb-4'> | ||
| <div className='mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100'> | ||
| <svg | ||
| className='h-6 w-6 text-red-600' | ||
| fill='none' | ||
| viewBox='0 0 24 24' | ||
| stroke='currentColor' | ||
| > | ||
| <path | ||
| strokeLinecap='round' | ||
| strokeLinejoin='round' | ||
| strokeWidth={2} | ||
| d='M6 18L18 6M6 6l12 12' | ||
| /> | ||
| </svg> | ||
| </div> | ||
| </div> | ||
| <h2 className='mb-2 text-xl font-semibold text-gray-900'>인증 실패</h2> | ||
| <p className='mb-4 text-gray-600'>{error}</p> | ||
| <button | ||
| onClick={() => navigate(ROUTER_PATH.LOGIN)} | ||
| className='rounded-lg bg-blue-500 px-4 py-2 text-white transition-colors hover:bg-blue-600' | ||
| > | ||
| 로그인 페이지로 돌아가기 | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default as OAuthRedirectPage } from './OAuthRedirectPage'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,9 @@ | ||
| import { initInstance } from './axios-instance'; | ||
|
|
||
| export const BASE_URL = 'http://localhost:8080'; | ||
| export const BASE_URL = 'http://43.200.101.253'; | ||
|
|
||
| export const fetchInstance = initInstance({ | ||
| baseURL: BASE_URL, | ||
| withCredentials: true, | ||
|
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. |
||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
|
|
||
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.
GetTicketApiResponse타입을 export하여 다른 파일(특히 custom hook)에서 재사용할 수 있도록 하는 것이 좋습니다. 이렇게 하면 타입 안정성을 높일 수 있습니다.