Skip to content

Commit 7dca323

Browse files
authored
Merge pull request #177 from kakao-tech-campus-3rd-step3/feat/oauth-login
[FEAT] OAuth 로그인 인가 코드 전송 후 회원가입/로그인 성공 분기 작업
2 parents 573e8d1 + 42b0842 commit 7dca323

File tree

9 files changed

+156
-57
lines changed

9 files changed

+156
-57
lines changed

src/pages/admin/ClubDetailEdit/Page.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,8 @@ export const ClubDetailEditPage = () => {
4848
color: 'white',
4949
},
5050
duration: 1000,
51+
onAutoClose: () => navigate(`/clubs/${clubId}`),
5152
});
52-
53-
setTimeout(() => {
54-
navigate(`/clubs/${clubId}`);
55-
}, 1000);
5653
})
5754
.catch(() => {
5855
toast.error('수정 실패!', {

src/pages/admin/Login/KakaoCallback.tsx

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,54 @@
1-
import axios from 'axios';
21
import { useEffect } from 'react';
32
import { useNavigate } from 'react-router-dom';
43
import { LoadingSpinner } from '@/shared/components/LoadingSpinner';
4+
import { postAuthCode } from './api/postAuthCode';
5+
import { setAccessToken, setTemporaryToken } from '../Signup/utils/token';
6+
import type { ErrorResponse } from '../Signup/type/error';
7+
import type { AxiosError } from 'axios';
8+
9+
interface LoginSuccessResponse {
10+
status: 'LOGIN_SUCCESS';
11+
accessToken: string;
12+
refreshToken: string;
13+
}
14+
15+
interface RegistrationRequiredResponse {
16+
status: 'REGISTRATION_REQUIRED';
17+
temporaryToken: string;
18+
}
19+
20+
type LoginResponse = LoginSuccessResponse | RegistrationRequiredResponse;
521

622
export const KakaoCallback = () => {
723
const navigate = useNavigate();
824

925
useEffect(() => {
1026
const code = new URL(window.location.href).searchParams.get('code');
11-
if (!code) return;
1227

13-
console.log(code);
28+
if (!code) {
29+
navigate('/login');
30+
return;
31+
}
32+
1433
const fetchToken = async () => {
1534
try {
16-
const res = axios.post(`${import.meta.env.VITE_API_BASE_URL}/auth/kakao/login`, {
17-
authorizationCode: code,
18-
});
19-
console.log('응답res ', res);
20-
21-
// CASE 1) 기존 회원
22-
23-
// 1-1. accessToken, refreshToken 발급
24-
// localStorage.setItem('accessToken', res.data.accessToken);
25-
// localStorage.setItem('refreshToken ', res.data.refreshToken)- (수정전)
26-
// refreshToken은 httpOnly 관리(수정후)
27-
// ------------------------------------------------------------
28-
// 2-2 main 페이지 이동
29-
// navigate('/'); // 로그인 후 홈으로 이동
30-
31-
// CASE 2) 기존 회원
32-
// 2-1. 임시 토큰
33-
// 2-2. navigate('/signup')
34-
} catch (error) {
35-
console.log('error:', error);
35+
const response: LoginResponse = await postAuthCode(code);
36+
37+
switch (response.status) {
38+
case 'LOGIN_SUCCESS':
39+
setAccessToken(response.accessToken);
40+
navigate('/');
41+
break;
42+
case 'REGISTRATION_REQUIRED':
43+
setTemporaryToken(response.temporaryToken);
44+
navigate('/signup');
45+
break;
46+
}
47+
} catch (e) {
48+
const error = e as AxiosError<ErrorResponse>;
49+
return new Error(error.response?.data.message);
3650
}
3751
};
38-
3952
fetchToken();
4053
}, [navigate]);
4154

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import axios from 'axios';
2+
import type { AxiosInstance, CreateAxiosDefaults } from 'axios';
3+
4+
const initInstance = (config: CreateAxiosDefaults): AxiosInstance => {
5+
const instance = axios.create({
6+
timeout: 10000,
7+
headers: {
8+
'Content-Type': 'application/json',
9+
...config.headers,
10+
},
11+
// TODO 0. interceptor 적용지점(동아리 운영자)
12+
...config,
13+
});
14+
15+
return instance;
16+
};
17+
18+
export const apiInstance = initInstance({
19+
baseURL: import.meta.env.VITE_API_BASE_URL,
20+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { apiInstance } from './initInstance';
2+
import type { AxiosResponse } from 'axios';
3+
4+
interface LoginSuccessResponse {
5+
status: 'LOGIN_SUCCESS';
6+
accessToken: string;
7+
refreshToken: string;
8+
}
9+
10+
interface RegistrationRequiredResponse {
11+
status: 'REGISTRATION_REQUIRED';
12+
temporaryToken: string;
13+
}
14+
15+
type LoginResponse = LoginSuccessResponse | RegistrationRequiredResponse;
16+
17+
export const postAuthCode = async (code: string): Promise<LoginResponse> => {
18+
const response: AxiosResponse<LoginResponse> = await apiInstance.post('/auth/kakao/login', {
19+
authorizationCode: code,
20+
});
21+
return response.data;
22+
};
Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,43 @@
1+
import axios, { AxiosError, type AxiosResponse } from 'axios';
2+
import { apiInstance } from '../../Login/api/initInstance';
3+
import type { ErrorResponse } from '../type/error';
14
import type { SignupFormInputs } from '../type/signup';
25

3-
export const postSignupForm = async (formData: SignupFormInputs): Promise<SignupFormInputs> => {
4-
const url = `${import.meta.env.VITE_API_BASE_URL}/auth/register`;
5-
const response = await fetch(url, {
6-
method: 'POST',
7-
headers: {
8-
'Content-Type': 'application/json',
9-
},
10-
body: JSON.stringify(formData),
11-
});
6+
export interface RegisterSuccessResponse {
7+
status: 'REGISTER_SUCCESS';
8+
accessToken: string;
9+
refreshToken: string;
10+
}
1211

13-
if (!response.ok) throw new Error('회원 가입 양식을 제출하지 못했습니다.');
14-
return await response.json();
12+
export const postSignupForm = async (
13+
formData: SignupFormInputs,
14+
tempToken: string,
15+
): Promise<RegisterSuccessResponse> => {
16+
try {
17+
const response: AxiosResponse<RegisterSuccessResponse> = await apiInstance.post(
18+
'/auth/register',
19+
formData,
20+
{
21+
headers: { Authorization: `Bearer ${tempToken}` },
22+
},
23+
);
24+
return response.data;
25+
} catch (e: unknown) {
26+
if (axios.isAxiosError(e)) {
27+
const error = e as AxiosError<ErrorResponse>;
28+
const status = error.response?.status;
29+
const detailMsg = error.response?.data.detail;
30+
switch (status) {
31+
case 400:
32+
throw new Error(`입력 오류: ${detailMsg}`);
33+
case 401:
34+
throw new Error(`권한 오류: ${detailMsg}`);
35+
case 409:
36+
throw new Error(`중복 오류: ${detailMsg}`);
37+
default:
38+
throw new Error(`알 수 없는 오류: ${e.message}`);
39+
}
40+
}
41+
throw e;
42+
}
1543
};

src/pages/admin/Signup/components/SignupForm/index.tsx

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { useForm, FormProvider } from 'react-hook-form';
22
import { useNavigate } from 'react-router-dom';
33
import { toast } from 'sonner';
4-
import { postSignupForm } from '@/pages/admin/Signup/api/signup';
4+
import { postSignupForm, type RegisterSuccessResponse } from '@/pages/admin/Signup/api/signup';
55
import * as S from '@/pages/admin/Signup/components/SignupForm/index.styled';
66
import { Button } from '@/shared/components/Button';
77
import { OutlineInputField } from '@/shared/components/Form/InputField/OutlineInputField';
88
import { theme } from '@/styles/theme';
9+
import { getTemporaryToken, setAccessToken } from '../../utils/token';
910
import type { SignupFormInputs } from '@/pages/admin/Signup/type/signup';
1011

1112
export const SignupForm = () => {
@@ -20,28 +21,38 @@ export const SignupForm = () => {
2021
phoneNumber: '',
2122
},
2223
});
23-
2424
const { errors, isSubmitting } = methods.formState;
2525

2626
const onSubmit = async (signupFormValue: SignupFormInputs) => {
27+
const temporaryToken = getTemporaryToken();
28+
29+
if (!temporaryToken) {
30+
toast.error('회원가입을 위한 토큰이 존재하지 않습니다.');
31+
return;
32+
}
33+
2734
try {
28-
await postSignupForm(signupFormValue);
35+
const response: RegisterSuccessResponse = await postSignupForm(
36+
signupFormValue,
37+
temporaryToken,
38+
);
39+
40+
setAccessToken(response.accessToken);
2941
toast.success('회원가입 완료!', {
3042
style: { backgroundColor: theme.colors.primary, color: 'white' },
3143
duration: 1000,
44+
onAutoClose: () => navigate('/'),
3245
});
33-
setTimeout(() => {
34-
navigate(`/login`);
35-
}, 1000);
36-
} catch (e) {
37-
console.error(e);
38-
toast.error('회원가입 실패!', {
39-
duration: 1000,
40-
style: {
41-
backgroundColor: 'white',
42-
color: theme.colors.error,
43-
},
44-
});
46+
} catch (e: unknown) {
47+
if (e instanceof Error) {
48+
toast.error(e.message, {
49+
duration: 1000,
50+
style: {
51+
backgroundColor: 'white',
52+
color: theme.colors.error,
53+
},
54+
});
55+
}
4556
}
4657
};
4758

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface ErrorResponse {
2+
status: number;
3+
message: string;
4+
detail: string;
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const setAccessToken = (token: string) => localStorage.setItem('accessToken', token);
2+
export const getAccessToken = () => localStorage.getItem('accessToken');
3+
export const getRefreshToken = () => localStorage.getItem('refreshToken');
4+
export const setTemporaryToken = (token: string) => sessionStorage.setItem('temporaryToken', token);
5+
export const getTemporaryToken = () => sessionStorage.getItem('temporaryToken');

src/pages/user/Apply/components/ApplicationForm/index.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,8 @@ export const ApplicationForm = ({ questions }: Props) => {
4545
color: 'white',
4646
},
4747
duration: 1000,
48+
onAutoClose: () => navigate(`/clubs/${clubIdNumber}`),
4849
});
49-
setTimeout(() => {
50-
navigate(`/clubs/${clubIdNumber}`);
51-
}, 1000);
5250
})
5351
.catch(() => {
5452
toast.error('제출 실패!', {

0 commit comments

Comments
 (0)