Skip to content

Commit

Permalink
refactor: 로그인 관련 기능 개선 (#279)
Browse files Browse the repository at this point in the history
* feat: 이미 가입한 이메일로 소셜 로그인할 경우 리다이렉트 구현

* refactor: 토스트 모달 메세지 상수로 수정

* feat: 소셜 로그인 회원의 경우 비밀번호 검증 없이 회원 정보 수정이 가능하도록 구현

* refactor: member 타입의 signupType 값 수정

* refactor: 리프레시 토큰을 이용한 토큰 재발급으로 로그인 유지

* refactor: 로그아웃 알림을 토스트 모달로 처리

* refactor: 리덕스에 토큰이 담겨 있으면 토큰 재발급을 실행하지 않도록 수정

* refactor: interceptor에서 토큰 재발급 실패 시 로그인 페이지로 이동

* feat: 토큰 재발급 함수를 위한 axios instance 선언

* refactor: 토큰 재발급 함수 주석 수정

* refactor: id 대신 토큰으로 단일 회원 정보를 가져오도록 api 훅 수정 및 적용

* refactor: 로그인 및 회원 정보 수정 시 바로 헤더에 반영되도록 쿼리 키 무효화

* feat: 리프레시 토큰 만료 시 alert 후 로그인 페이지로 이동

* refactor: 로그인 관련 메세지를 상수로 처리

* refactor: 불필요한 주석 제거

* refactor: useIsMobile 임포트 위치 수정

* refactor: 불필요한 주석 제거

* refactor: 비회원 여부 처리 오타 수정

* refactor: 로그인 만료 메시지 상수 오타 수정

* refactor: Header 컴포넌트에서 사용하지 않는 import문 삭제

* refactor: 쿠키에 리프레시 토큰이 존재하면 액세스 토큰 재발급으로 로그인을 유지하도록 로컬 스토리지 로직 삭제

* refactor: 액세스 토큰 재발급 함수를 auth 파일로 분리

* refactor: 헤더에 직접적으로 토큰을 담는 로직 제거 및 회원 쿼리키 무효화

* refactor: auth 파일의 getRefreshToken 함수를 interceptor 파일로 이동
  • Loading branch information
areumH authored Jan 19, 2025
1 parent 8ae5fe3 commit db11ff8
Show file tree
Hide file tree
Showing 24 changed files with 221 additions and 225 deletions.
31 changes: 13 additions & 18 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useEffect } from 'react';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import 'dayjs/locale/ko';
import { Route, Routes } from 'react-router-dom';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import MyPage from '@/pages/MyPage/MyPage';
import SearchGamePage from '@/pages/SearchResultsPage/SearchGamePage';
import SearchTalkPickPage from '@/pages/SearchResultsPage/SearchTalkPickPage';
Expand All @@ -23,6 +23,9 @@ import SignUpPage from './pages/SignUpPage/SignUpPage';
import BalanceGamePage from './pages/BalanceGamePage/BalanceGamePage';
import BalanceGameMobilePage from './pages/mobile/BalanceGameMobilePage/BalanceGameMobilePage';
import BalanceGameCreationPage from './pages/BalanceGameCreationPage/BalanceGameCreationPage';
import { useNewSelector } from './store';
import { selectAccessToken } from './store/auth';
import useIsMobile from './hooks/common/useIsMobile';
// import NotAuthRoutes from './components/Routes/NotAuthRoutes';
// import { useMemberQuery } from './hooks/api/member/useMemberQuery';
// import { useParseJwt } from './hooks/common/useParseJwt';
Expand All @@ -40,30 +43,22 @@ import BalanceGameCreationPage from './pages/BalanceGameCreationPage/BalanceGame
// import PostPage from './pages/PostPage/PostPage';
// import SearchResultPage from './pages/SearchResultPage/SearchResultPage';
// import SignUpPage from './pages/SignUpPage/SignUpPage';
import { getCookie } from './utils/cookie';
import { axiosInstance } from './api/interceptor';
import { useNewDispatch } from './store';
import { tokenActions } from './store/auth';
import useIsMobile from './hooks/common/useIsMobile';

const App: React.FC = () => {
const dispatch = useNewDispatch();
const location = useLocation();
const navigate = useNavigate();

const isMobile = useIsMobile();
const isLoggedIn = !!useNewSelector(selectAccessToken);

useEffect(() => {
const token = getCookie('accessToken');
if (token) {
localStorage.setItem('accessToken', token);
localStorage.setItem('refreshToken', 'refreshToken');
const searchParams = new URLSearchParams(location.search);
const status = searchParams.get('status');

dispatch(tokenActions.setToken(token));
axiosInstance.defaults.headers.Authorization = `Bearer ${token}`;
if (status === 'already_registered') {
navigate(`/${PATH.LOGIN}`, { state: { status } });
}
}, [dispatch]);

// const accessToken = useNewSelector(selectAccessToken);
// const { member } = useMemberQuery(useParseJwt(accessToken).memberId);
const isLoggedIn = !!localStorage.getItem('accessToken');
}, [location.search, navigate]);
useTokenRefresh();

return (
Expand Down
78 changes: 45 additions & 33 deletions src/api/interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import store from '@/store';
/* eslint-disable no-alert */
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { AXIOS, END_POINT } from '../constants/api';
import { PATH } from '@/constants/path';
import { NOTICE } from '@/constants/message';
import store from '@/store';
import { ServerResponse } from '@/types/api';
import { tokenActions } from '@/store/auth';
import { AXIOS, END_POINT, HTTP_STATUS_CODE } from '../constants/api';
import { HTTPError } from './HttpError';

export interface AxiosErrorResponse {
Expand All @@ -13,8 +18,6 @@ const baseURL =
process.env.NODE_ENV === 'production' ? process.env.API_URL : '/api';

export const axiosInstance = axios.create({
// baseURL: process.env.API_URL,
// baseURL: '/api',
baseURL,
headers: {
'Content-Type': 'application/json',
Expand All @@ -23,61 +26,70 @@ export const axiosInstance = axios.create({
timeout: AXIOS.TIMEOUT,
});

export const axiosRefreshInstance = axios.create({
baseURL,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
timeout: AXIOS.TIMEOUT,
});

export const getRefreshToken = async () => {
const { data } = await axiosRefreshInstance.get<ServerResponse>(
`${END_POINT.REFRESH}`,
);
return data;
};

// request interceptor (before request)
axiosInstance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
if (config.headers.Authorization) return config;

const { accessToken } = store.getState().token;
const newConfig = { ...config };
const { accessToken } = store.getState().token;

if (newConfig.url === END_POINT.FILE_UPLOAD) {
newConfig.headers['Content-Type'] = 'multipart/form-data';
}
if (accessToken) {
newConfig.headers.Authorization = `Bearer ${accessToken}`;
}

// console.log('요청 전 config', newConfig);
return newConfig;
},
(error: AxiosError<AxiosErrorResponse>) => {
// console.log('요청 전 config 에러');

return Promise.reject(error);
},
);

// response interceptor (after request)
axiosInstance.interceptors.response.use(
(response) => {
// console.log('요청 후 response');
return response;
},
(error: AxiosError<AxiosErrorResponse>) => {
// console.log('요청 후 response 에러');
async (error: AxiosError<AxiosErrorResponse>) => {
const originalRequest = error.config;
if (!error.response || !originalRequest) throw error;

const { data, status } = error.response;
// const refreshToken = localStorage.getItem('rtk');

// if (refreshToken) {
// if (status === HTTP_STATUS_CODE.UNAUTHORIZED) {
// const accessToken = getRefreshToken();
// console.log('new accessToken: ', accessToken);
// localStorage.setItem('accessToken', accessToken);
// store.dispatch({ type: 'token/setAccessToken', payload: accessToken });
// originalRequest.headers.Authorization = `Bearer ${accessToken}`;
// return axiosInstance(originalRequest);
// console.log('토큰 재발급');
// }
// if (status === HTTP_STATUS_CODE.BAD_REQUEST) {
// localStorage.removeItem('accessToken');
// localStorage.removeItem('rtk');
// window.location.href = '/';
// }
// }

if (status === HTTP_STATUS_CODE.UNAUTHORIZED) {
try {
const newAccessToken = await getRefreshToken();

store.dispatch(tokenActions.setToken(newAccessToken));
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;

return await axiosInstance(originalRequest);
} catch (err) {
delete axiosInstance.defaults.headers.Authorization;
store.dispatch(tokenActions.deleteToken());

alert(NOTICE.LOGIN.EXPIRED);
window.location.href = `/${PATH.LOGIN}`;

return Promise.reject(err);
}
}

throw new HTTPError(status, data.httpStatus, data.message);
},
);
6 changes: 2 additions & 4 deletions src/api/member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import {
} from '@/types/member';
import { axiosInstance } from './interceptor';

export const getMember = async (memberId: number) => {
const { data } = await axiosInstance.get<Member>(
`${END_POINT.MEMBER(memberId)}`,
);
export const getMember = async () => {
const { data } = await axiosInstance.get<Member>(`${END_POINT.MEMBER}`);
return data;
};

Expand Down
6 changes: 1 addition & 5 deletions src/components/molecules/CommentItem/CommentItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Comment } from '@/types/comment';
import { ArrowDown, ArrowUp } from '@/assets';
import { useNewSelector } from '@/store';
import { selectAccessToken } from '@/store/auth';
import { useParseJwt } from '@/hooks/common/useParseJwt';
import { useMemberQuery } from '@/hooks/api/member/useMemberQuery';
import { formatDateFromISOWithTime } from '@/utils/formatData';
import { useCommentActions } from '@/hooks/comment/useCommentActions';
Expand Down Expand Up @@ -35,8 +32,7 @@ const CommentItem = ({
selectedPage,
talkPickWriter,
}: CommentItemProps) => {
const accessToken = useNewSelector(selectAccessToken);
const { member } = useMemberQuery(useParseJwt(accessToken).memberId);
const { member } = useMemberQuery();

const isMyComment = useMemo(() => {
return comment?.nickname === member?.nickname;
Expand Down
8 changes: 0 additions & 8 deletions src/components/molecules/LoginForm/LoginForm.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,3 @@ export const loginButtonStyling = css({
outline: `1px solid ${color.GY[2]}`,
cursor: 'pointer',
});

export const toastModalStyling = css({
position: 'fixed',
top: '110px',
left: '50%',
transform: 'translate(-50%)',
zIndex: '1000',
});
27 changes: 10 additions & 17 deletions src/components/molecules/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,36 @@ import { PATH } from '@/constants/path';
import Button from '@/components/atoms/Button/Button';
import Input from '@/components/atoms/Input/Input';
import Divider from '@/components/atoms/Divider/Divider';
import ToastModal from '@/components/atoms/ToastModal/ToastModal';
import SocialLoginButton from '@/components/atoms/SocialLoginButton/SocialLoginButton';
import { useLoginForm } from '@/hooks/login/useLoginForm';
import type { State } from '@/pages/LoginPage/LoginPage';
import * as S from './LoginForm.style';

export interface LoginFormProps {
showToastModal?: (message: string, callback?: () => void) => void;
withSignInText?: boolean;
pathTalkPickId?: number;
loginState?: State;
onModalLoginSuccess?: () => void;
}

const LoginForm = ({
showToastModal,
withSignInText,
pathTalkPickId,
loginState,
onModalLoginSuccess,
}: LoginFormProps) => {
const {
form,
onChange,
isError,
errorMessage,
handleSubmit,
isVisible,
modalText,
} = useLoginForm(pathTalkPickId, onModalLoginSuccess);
const { form, onChange, isError, errorMessage, handleSubmit } = useLoginForm(
showToastModal,
loginState?.talkPickId,
onModalLoginSuccess,
);

const handleSocialLogin = (social: string) => {
window.location.href = `${process.env.API_URL}/oauth2/authorization/${social}`;
};

return (
<form onSubmit={handleSubmit} css={S.loginFormStyling}>
{isVisible && (
<div css={S.toastModalStyling}>
<ToastModal>{modalText}</ToastModal>
</div>
)}
<div css={S.loginTextStyling}>LOGIN</div>
<div css={S.loginFormWrapper}>
<Input
Expand Down
6 changes: 1 addition & 5 deletions src/components/molecules/ReplyItem/ReplyItem.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Comment } from '@/types/comment';
import { useNewSelector } from '@/store';
import { selectAccessToken } from '@/store/auth';
import { useParseJwt } from '@/hooks/common/useParseJwt';
import { useMemberQuery } from '@/hooks/api/member/useMemberQuery';
import { formatDateFromISOWithTime } from '@/utils/formatData';
import { useCommentActions } from '@/hooks/comment/useCommentActions';
Expand Down Expand Up @@ -30,8 +27,7 @@ const ReplyItem = ({
talkPickWriter,
parentId,
}: ReplyItemProps) => {
const accessToken = useNewSelector(selectAccessToken);
const { member } = useMemberQuery(useParseJwt(accessToken).memberId);
const { member } = useMemberQuery();

const isMyReply = useMemo(() => {
return reply?.nickname === member?.nickname;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { BookmarkDF, BookmarkPR, NextArrow, PrevArrow, Share } from '@/assets';
import { VoteRecord } from '@/types/vote';
import { SUCCESS } from '@/constants/message';
import { GameDetail, GameSet } from '@/types/game';
import { useNewSelector } from '@/store';
import { selectAccessToken } from '@/store/auth';
import { formatDateFromISO } from '@/utils/formatData';
import Chips from '@/components/atoms/Chips/Chips';
import Divider from '@/components/atoms/Divider/Divider';
Expand Down Expand Up @@ -53,7 +55,7 @@ const BalanceGameSection = ({

const gameStages: GameDetail[] =
game?.gameDetailResponses ?? gameDefaultDetail;
const isGuest = !localStorage.getItem('accessToken');
const isGuest = !useNewSelector(selectAccessToken);

const [guestVotedList, setGuestVotedList] = useState<VoteRecord[]>([]);

Expand Down
6 changes: 1 addition & 5 deletions src/components/organisms/CommentsSection/CommentsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import React, { useState } from 'react';
import { useNewSelector } from '@/store';
import { selectAccessToken } from '@/store/auth';
import { useParseJwt } from '@/hooks/common/useParseJwt';
import { useMemberQuery } from '@/hooks/api/member/useMemberQuery';
import { CommentsPagination } from '@/types/comment';
import { generatePageNumbers } from '@/utils/pagination';
Expand Down Expand Up @@ -39,8 +36,7 @@ const CommentsSection = ({
handlePageChange,
voted,
}: CommentsSectionProps) => {
const accessToken = useNewSelector(selectAccessToken);
const { member } = useMemberQuery(useParseJwt(accessToken).memberId);
const { member } = useMemberQuery();
const isMyTalkPick: boolean = talkPickWriter === member?.nickname;

const totalPages = commentList?.totalPages ?? 0;
Expand Down
3 changes: 1 addition & 2 deletions src/components/organisms/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useLogoutMutation } from '@/hooks/api/member/useLogoutMutation';
import { useNewSelector } from '@/store';
import { useParseJwt } from '@/hooks/common/useParseJwt';
import { useMemberQuery } from '@/hooks/api/member/useMemberQuery';
import { selectAccessToken } from '@/store/auth';
import { Logo, DefaultProfile, ListIcon, LogoSmall } from '@/assets';
Expand All @@ -30,7 +29,7 @@ const Header = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const accessToken = useNewSelector(selectAccessToken) ?? '';
const logout = useLogoutMutation();
const { member } = useMemberQuery(useParseJwt(accessToken)?.memberId);
const { member } = useMemberQuery();

const handleMenuToggle = () => {
setIsMenuOpen((prev) => !prev);
Expand Down
2 changes: 1 addition & 1 deletion src/constants/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const END_POINT = {
REFRESH: '/members/reissue',
ALL_MEMBERS: '/members',
EDIT_MEMBERS: '/members',
MEMBER: (id: number) => `/members/${id}`,
MEMBER: '/members/info',
MEMBER_PROFILE: (id: number) => `/members/${id}/profile`,
MEMBER_IMAGE: '/members/image',
MEMBER_NICKNAME: '/members/nickname',
Expand Down
4 changes: 4 additions & 0 deletions src/constants/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const SUCCESS = {
},
SIGN_UP: '회원가입 완료!',
LOGIN: '로그인 완료!',
LOGOUT: '로그아웃되었습니다.',
COPY: {
LINK: '복사 완료!',
},
Expand Down Expand Up @@ -110,6 +111,9 @@ export const NOTICE = {
STATUS: {
NOT_READY: '아직 준비 중인 서비스입니다!',
},
LOGIN: {
EXPIRED: '로그인 시간이 만료되었습니다. 다시 로그인해주세요.',
},
} as const;

export const NULL = {
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/api/member/useChangeUserInfoMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const useChangeUserInfoMutation = (

if (memberId) {
await queryClient.invalidateQueries({
queryKey: ['members', memberId],
queryKey: ['members'],
});
}
},
Expand Down
Loading

0 comments on commit db11ff8

Please sign in to comment.