Skip to content

Commit 7e9ff5c

Browse files
authored
[기능 구현] 카카오 OAuth 리다이렉트 페이지 및 인증 API 연동 구현 (#45)
2 parents 3f1b6ca + 8151831 commit 7e9ff5c

13 files changed

Lines changed: 163 additions & 5 deletions

File tree

src/app/routes/components/auth.routes.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LoginPage, RealtorCertificationPage, RealtorLoginPage } from '@/pages';
1+
import { LoginPage, OAuthRedirectPage, RealtorCertificationPage, RealtorLoginPage } from '@/pages';
22
import { ROUTER_PATH } from '@/shared';
33
import { Layout } from '@/widgets';
44

@@ -14,6 +14,11 @@ export const authRoutes = [
1414
element: <LoginPage />,
1515
handle: { layout: ROUTE_CONFIG.LOGIN.layout },
1616
},
17+
{
18+
path: ROUTE_CONFIG.OAUTH_REDIRECT.path,
19+
element: <OAuthRedirectPage />,
20+
handle: { layout: ROUTE_CONFIG.OAUTH_REDIRECT.layout },
21+
},
1722
],
1823
},
1924
{

src/app/routes/config/route-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export const ROUTE_CONFIG: Record<string, RouteConfig> = {
3131
path: ROUTER_PATH.REALTOR_CERTIFICATION,
3232
layout: 'Auth',
3333
},
34+
OAUTH_REDIRECT: {
35+
path: ROUTER_PATH.OAUTH_REDIRECT,
36+
layout: 'Auth',
37+
},
3438
REALTOR: {
3539
path: ROUTER_PATH.REALTOR,
3640
requiresRealtor: true, // 공인중개사만 접근 가능
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { fetchInstance } from '@/shared';
2+
3+
export const GET_TICKET_API_PATH = '/api/auth/ticket';
4+
5+
interface GetTicketApiResponse {
6+
refreshToken: string;
7+
}
8+
9+
export const getTicketApi = async (ticket: string): Promise<GetTicketApiResponse> => {
10+
const response = await fetchInstance.get(GET_TICKET_API_PATH, {
11+
params: {
12+
ticket,
13+
},
14+
});
15+
return response.data;
16+
};

src/entities/auth/apis/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './get-ticket.api';
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
3+
import { getTicketApi } from '../apis';
4+
5+
export const GetAuthTicketQueryKey = {
6+
ticket: (ticket: string) => ['ticket', ticket],
7+
};
8+
9+
export const useGetAuthTicket = (ticket: string) => {
10+
return useQuery({
11+
queryKey: GetAuthTicketQueryKey.ticket(ticket),
12+
queryFn: () => getTicketApi(ticket),
13+
});
14+
};

src/entities/auth/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './getAuthTicket';

src/entities/auth/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './apis';
2+
export * from './hooks';

src/features/login/components/common/buttons/KakaoLoginButton.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1-
import { Button } from '@/shared';
1+
import { BASE_URL, Button } from '@/shared';
22

33
import KakaoSymbol from '../../../_assets/kakao-symbol.webp';
44

55
export const KakaoLoginButton = () => {
6+
const kakaoLogin = () => {
7+
// OAuth 서버로 리다이렉트하되, 리다이렉트 URL을 명시적으로 지정
8+
const redirectUri = encodeURIComponent(`${window.location.origin}/oauth/redirect`);
9+
window.location.href = `${BASE_URL}/oauth2/authorization/kakao?redirect_uri=${redirectUri}`;
10+
};
11+
612
return (
7-
<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'>
13+
<Button
14+
onClick={kakaoLogin}
15+
className='flex h-11 w-90 items-center gap-3 rounded-lg bg-kakao text-black hover:bg-kakao/80 active:bg-kakao/80'
16+
>
817
<img src={KakaoSymbol} className='size-4' width={16} height={16} alt='kakao' />
918
<span className='text-sm font-semibold'>카카오 계정으로 계속하기</span>
1019
</Button>

src/pages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './main';
22
export * from './realtor';
33
export * from './login';
44
export * from './diagnostics';
5+
export * from './oauth';
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { useEffect, useState } from 'react';
2+
import { useNavigate, useSearchParams } from 'react-router-dom';
3+
4+
import { useGetAuthTicket } from '@/entities/auth';
5+
6+
import { ROUTER_PATH } from '@/shared';
7+
8+
export default function OAuthRedirectPage() {
9+
const [searchParams] = useSearchParams();
10+
const navigate = useNavigate();
11+
const [error, setError] = useState<string | null>(null);
12+
13+
// URL에서 ticket 파라미터 추출
14+
const ticket = searchParams.get('ticket');
15+
const oauthError = searchParams.get('error');
16+
const errorDescription = searchParams.get('error_description');
17+
18+
useEffect(() => {
19+
if (oauthError) {
20+
setError(errorDescription || `OAuth 인증 오류: ${oauthError}`);
21+
} else if (!ticket) {
22+
setError('인증 토큰이 없습니다. 다시 로그인해주세요.');
23+
}
24+
}, [oauthError, errorDescription, ticket]);
25+
26+
// ticket이 있고 OAuth 에러가 없을 때만 API 호출
27+
const shouldCallApi = !!ticket && !oauthError;
28+
const {
29+
data: authData,
30+
error: apiError,
31+
isLoading,
32+
} = useGetAuthTicket(shouldCallApi ? ticket : '');
33+
34+
// API 에러 처리
35+
useEffect(() => {
36+
if (apiError) {
37+
console.error('API 호출 중 오류:', apiError);
38+
setError('인증 검증에 실패했습니다. 다시 시도해주세요.');
39+
}
40+
}, [apiError]);
41+
42+
// 성공 시 처리
43+
useEffect(() => {
44+
if (authData && !isLoading && !apiError && shouldCallApi) {
45+
console.log('OAuth 인증 성공!', authData);
46+
47+
// refreshToken을 localStorage에 저장
48+
localStorage.setItem('refreshToken', authData.refreshToken);
49+
50+
// 메인 페이지로 이동
51+
navigate(ROUTER_PATH.MAIN, { replace: true });
52+
}
53+
}, [authData, isLoading, apiError, navigate, shouldCallApi]);
54+
55+
if (isLoading && shouldCallApi) {
56+
return (
57+
<div className='flex min-h-screen items-center justify-center'>
58+
<div className='text-center'>
59+
<div className='mb-4'>
60+
<div className='mx-auto h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent'></div>
61+
</div>
62+
<p className='text-lg font-medium text-gray-700'>인증 처리 중...</p>
63+
<p className='mt-2 text-sm text-gray-500'>잠시만 기다려주세요.</p>
64+
</div>
65+
</div>
66+
);
67+
}
68+
69+
if (error) {
70+
return (
71+
<div className='flex min-h-screen items-center justify-center'>
72+
<div className='text-center'>
73+
<div className='mb-4'>
74+
<div className='mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100'>
75+
<svg
76+
className='h-6 w-6 text-red-600'
77+
fill='none'
78+
viewBox='0 0 24 24'
79+
stroke='currentColor'
80+
>
81+
<path
82+
strokeLinecap='round'
83+
strokeLinejoin='round'
84+
strokeWidth={2}
85+
d='M6 18L18 6M6 6l12 12'
86+
/>
87+
</svg>
88+
</div>
89+
</div>
90+
<h2 className='mb-2 text-xl font-semibold text-gray-900'>인증 실패</h2>
91+
<p className='mb-4 text-gray-600'>{error}</p>
92+
<button
93+
onClick={() => navigate(ROUTER_PATH.LOGIN)}
94+
className='rounded-lg bg-blue-500 px-4 py-2 text-white transition-colors hover:bg-blue-600'
95+
>
96+
로그인 페이지로 돌아가기
97+
</button>
98+
</div>
99+
</div>
100+
);
101+
}
102+
103+
return null;
104+
}

0 commit comments

Comments
 (0)