Skip to content
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

[Fix/igw 65/145] JWT 토큰 재발급/회원가입 리디자인 반영 #150

Merged
merged 9 commits into from
Jan 10, 2025
21 changes: 19 additions & 2 deletions src/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { api } from '@/api/index.ts';
import { apiWithoutAuth } from '@/api/index.ts';
import { RESTYPE } from '@/types/api/common';
import axios from 'axios';

/**
* 사용자 로그인을 처리하는 함수
Expand Down Expand Up @@ -50,8 +51,24 @@ export const logout = async (): Promise<RESTYPE<null>> => {
};

// 1.3 JWT 재발급
export const reIssueToken = async (): Promise<RESTYPE<SignInResponse>> => {
const response = await api.post('/auth/reissue/token');
// (1) refresh token을 헤더에 포함하여 axios instance 생성
const apiWithRefreshToken = axios.create({
baseURL: import.meta.env.VITE_APP_API_GIGGLE_API_BASE_URL, // 기본 URL을 api와 동일하게 설정
});
// (2) 재발급 api 호출
export const reIssueToken = async (
refreshToken: string,
): Promise<RESTYPE<SignInResponse>> => {
//
const response = await apiWithRefreshToken.post(
'/auth/reissue/token',
{},
{
headers: {
Authorization: `Bearer ${refreshToken}`,
},
},
);
return response.data;
};

Expand Down
92 changes: 91 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
import { getAccessToken } from '@/utils/auth';
import {
getAccessToken,
getRefreshToken,
setAccessToken,
setRefreshToken,
} from '@/utils/auth';
import axios, { AxiosInstance } from 'axios';
import { reIssueToken } from '@/api/auth';

// 리다이렉션 처리 함수 (전역)
let redirectToSignin: () => void = () => {
console.warn('리다이렉션 함수가 설정되지 않았습니다.');
};

// 리다이렉션 함수 설정
export function setRedirectToLogin(callback: () => void) {
redirectToSignin = callback;
}

/**
* Axios 인스턴스에 인터셉터를 설정하는 함수
Expand Down Expand Up @@ -31,6 +47,80 @@ function setInterceptors(instance: AxiosInstance, type: string) {
},
);

/**
* api instance의 반환값에 대한 처리
*
* 에러코드 40101이 반환되었을 경우,
* access token 만료되어 refresh token으로 토큰 재발행이 필요합니다.
*
* 반환되는 에러 참고 - error: {code: 40101, message: "만료된 토큰입니다."}
*/
let isTokenRefreshing = false;
let failedRequestsQueue: any[] = [];

instance.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response && error.response.status === 401) {
if (error.response.data.error.code === 40101) {
console.log('401');

const refreshToken = getRefreshToken();

if (!refreshToken) {
console.error('Refresh Token이 없습니다.');
setAccessToken(null);
setRefreshToken(null);
redirectToSignin();
return Promise.reject('Refresh Token이 없습니다.');
}

// 토큰 재발급 중인 상태일 때
if (isTokenRefreshing) {
// 재발급이 완료될 때까지 대기하고, 완료 후 요청을 재시도
return new Promise((resolve, reject) => {
failedRequestsQueue.push({ resolve, reject });
});
}

// JWT 재발급
try {
// 토큰 재발급 시작
isTokenRefreshing = true;

// 1. Refresh Token을 사용하여 새로운 Access Token 발급
const response = await reIssueToken(refreshToken);
const access_token = response.data.access_token;
const refresh_token = response.data.refresh_token;

// 2. 새로운 Access Token, Refresh Token 저장
setAccessToken(access_token);
setRefreshToken(refresh_token);

// 3. 모든 대기 중인 요청을 새로운 Access Token과 함께 처리
failedRequestsQueue.forEach((request: any) => request.resolve()); // 요청 재시도
failedRequestsQueue = []; // 큐 비우기

// 4. 원래 요청을 새로운 Access Token과 함께 재시도
error.config.headers['Authorization'] = `Bearer ${access_token}`;
return instance.request(error.config); // 재시도
} catch (e) {
// 재발급 실패 시
console.error('토큰 재발급 실패:', e);
// 토큰 초기화 및 로그인 페이지로 이동
setAccessToken(null);
setRefreshToken(null);
redirectToSignin(); // 로그인 페이지로 이동
// 실패한 요청 큐 에러 처리
failedRequestsQueue.forEach((request: any) => request.reject(e)); // 실패한 요청들에 대해 에러 처리
failedRequestsQueue = [];
return Promise.reject(e);
}
}
}
return Promise.reject(error);
},
);
return instance;
}

Expand Down
Empty file added src/components/Common/Header/B
Empty file.
49 changes: 26 additions & 23 deletions src/components/Signin/SigninInputSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { validateId, validatePassword } from '@/utils/signin';
import { useSignIn } from '@/hooks/api/useAuth';
import { useUserInfoforSigninStore } from '@/store/signup';
import BottomButtonPanel from '../Common/BottomButtonPanel';

const SigninInputSection = () => {
const navigate = useNavigate();
Expand Down Expand Up @@ -57,8 +58,8 @@ const SigninInputSection = () => {
}, [idValue, passwordValue]);

return (
<div className="flex flex-col gap-2">
<div className="w-[20.5rem] flex flex-col gap-4">
<div className="w-full px-6 flex flex-col gap-2">
<div className="flex flex-col gap-4">
<div>
<p className="py-2 px-1 text-sm font-normal text-[#171719]">ID</p>
<Input
Expand Down Expand Up @@ -94,28 +95,30 @@ const SigninInputSection = () => {
</button>
*/}
</div>
<div className="py-6 flex flex-col items-center gap-2">
<Button
type="large"
bgColor={isValid ? 'bg-[#FEF387]' : 'bg-[#F4F4F9]'}
fontColor={isValid ? 'text-[#1E1926]' : 'text-[#BDBDBD]'}
isBorder={false}
title="Sign In"
onClick={isValid ? handleSubmit : undefined}
/>
<div className="flex items-center justify-center gap-2">
<p className="text-[#7D8A95] text-sm font-normal">
Don't have an account?
</p>
{/* 회원가입 화면 이동 */}
<button
className="text-[#7872ED] text-sm font-semibold"
onClick={() => navigate('/signup')}
>
Create Account
</button>
<BottomButtonPanel>
<div className="w-full flex flex-col items-center gap-6">
<div className="flex items-center justify-center gap-2">
<p className="text-[#7D8A95] text-sm font-normal">
Don't have an account?
</p>
{/* 회원가입 화면 이동 */}
<button
className="text-[#000] text-sm font-semibold"
onClick={() => navigate('/signup')}
>
Create Account
</button>
</div>
<Button
type="large"
bgColor={isValid ? 'bg-[#000]' : 'bg-[#F4F4F9]'}
fontColor={isValid ? 'text-[#FEF387]' : 'text-[#BDBDBD]'}
isBorder={false}
title="Sign In"
onClick={isValid ? handleSubmit : undefined}
/>
</div>
</div>
</BottomButtonPanel>
</div>
);
};
Expand Down
50 changes: 18 additions & 32 deletions src/components/Signup/EmailInput.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useEffect, useState } from 'react';
import Input from '@/components/Common/Input';
import Button from '@/components/Common/Button';
import { useLocation, useNavigate } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { validateEmail } from '@/utils/signin';
import { signInputTranclation } from '@/constants/translation';
import { isEmployer } from '@/utils/signup';
import { useGetEmailValidation } from '@/hooks/api/useAuth';
import BottomButtonPanel from '../Common/BottomButtonPanel';

type EmailInputProps = {
email: string;
Expand All @@ -14,7 +15,6 @@ type EmailInputProps = {
};

const EmailInput = ({ email, onEmailChange, onSubmit }: EmailInputProps) => {
const navigate = useNavigate();
const { pathname } = useLocation();
const [emailError, setEmailError] = useState<string | null>(null);
const [isValid, setIsValid] = useState<boolean>(false);
Expand Down Expand Up @@ -56,11 +56,11 @@ const EmailInput = ({ email, onEmailChange, onSubmit }: EmailInputProps) => {
};

return (
<>
<div className="title-1 text-center py-6">
{signInputTranclation.signup[isEmployer(pathname)]}
<div className="w-full">
<div className="title-1 pb-12">
{signInputTranclation.enterEmail[isEmployer(pathname)]}
</div>
<div className="w-[20.5rem] flex flex-col py-6">
<div className="flex flex-col py-6">
<div>
<p className="py-2 px-1 text-sm font-normal text-[#171719]">
{signInputTranclation.email[isEmployer(pathname)]}
Expand All @@ -78,33 +78,19 @@ const EmailInput = ({ email, onEmailChange, onSubmit }: EmailInputProps) => {
)}
</div>
</div>
<div className="py-6 flex flex-col items-center gap-2 absolute bottom-[16%]">
<Button
type="large"
bgColor={isValid ? 'bg-[#FEF387]' : 'bg-[#F4F4F9]'}
fontColor={isValid ? 'text-[#1E1926]' : 'text-[#BDBDBD]'}
isBorder={false}
title={signInputTranclation.continue[isEmployer(pathname)]}
onClick={isValid ? handleSignupClick : undefined}
/>
<div className="flex items-center justify-center gap-2 pb-2">
<p className="text-[#7D8A95] text-sm font-normal">
{signInputTranclation.haveAccount[isEmployer(pathname)]}
</p>
{/* 로그인 화면 이동 */}
<button
className="text-[#7872ED] text-sm font-semibold"
onClick={() => navigate('/signin')}
>
{signInputTranclation.signin[isEmployer(pathname)]}
</button>
<BottomButtonPanel>
<div className="w-full">
<Button
type="large"
bgColor={isValid ? 'bg-[#1E1926]' : 'bg-[#F4F4F9]'}
fontColor={isValid ? 'text-[#FEF387]' : 'text-[#BDBDBD]'}
isBorder={false}
title={signInputTranclation.continue[isEmployer(pathname)]}
onClick={isValid ? handleSignupClick : undefined}
/>
</div>
{/* 소셜은 잠깐 제외 */}
{/*
<SigninSocialButtons />
*/}
</div>
</>
</BottomButtonPanel>
</div>
);
};

Expand Down
36 changes: 21 additions & 15 deletions src/components/Signup/FindJourney.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Button from '@/components/Common/Button';
import { cardData, UserType } from '@/constants/user';
import BottomButtonPanel from '../Common/BottomButtonPanel';

type findJourneyProps = {
onSignUpClick: () => void;
Expand All @@ -17,20 +18,23 @@ const FindJourney = ({
};

return (
<div className="w-full h-full flex flex-col items-center justify-center">
<div className="w-full">
<div className="title-1">Find Your Journey</div>
<div className="w-full h-full flex flex-col items-center">
<div className="flex flex-col w-full gap-1">
<div className="title-1">
기글에 오신 것을 <br />
환영해요!
</div>
<div className="body-1">당신에게 알맞는 정보를 드릴게요!</div>
</div>
<div className="flex gap-2.5 py-6">
<div className="w-full flex flex-col gap-2.5 py-6">
{/* 유학생, 고용주 타입 선택 카드 */}
{cardData.map((card) => (
<div
key={card.accountType}
className={`flex flex-col justify-end gap-1.5 h-[12.5rem] w-[10rem] p-[1.125rem] bg-cover bg-yellow-100 border-[0.5px] border-[#f2f2f2] rounded-[1.25rem] ${
className={`w-full py-6 px-[1.125rem] flex flex-col justify-end gap-1.5 rounded-[1.25rem] ${
accountType === card.accountType
? 'bg-[url("/src/assets/images/yellowCard.png")] shadow-yellowShadow '
: ''
? 'bg-[#FEF387] shadow-md'
: 'bg-yellow-100'
}`}
onClick={() => handleClick(card.accountType)}
>
Expand All @@ -41,14 +45,16 @@ const FindJourney = ({
</div>
<div className="py-6 w-full">
{/* 타입 선택 후에 Sign Up 가능 */}
<Button
type="large"
bgColor={accountType ? 'bg-[#1E1926]' : 'bg-[#F4F4F9]'}
fontColor={accountType ? 'text-[#FEF387]' : 'text-[#BDBDBD]'}
isBorder={false}
title="Sign Up"
onClick={accountType ? onSignUpClick : undefined}
/>
<BottomButtonPanel>
<Button
type="large"
bgColor={accountType ? 'bg-[#1E1926]' : 'bg-[#F4F4F9]'}
fontColor={accountType ? 'text-[#FEF387]' : 'text-[#BDBDBD]'}
isBorder={false}
title="Sign Up"
onClick={accountType ? onSignUpClick : undefined}
/>
</BottomButtonPanel>
</div>
</div>
);
Expand Down
Loading