diff --git a/public/assets/icon16/arrow_square_16.svg b/public/assets/icon16/arrow_square_16.svg new file mode 100644 index 00000000..e5f1521d --- /dev/null +++ b/public/assets/icon16/arrow_square_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/icon24/right_arrow_24.svg b/public/assets/icon24/right_arrow_24.svg new file mode 100644 index 00000000..25cf4e08 --- /dev/null +++ b/public/assets/icon24/right_arrow_24.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/api/api-config.ts b/src/api/api-config.ts index 083c4b1d..596cf7fd 100644 --- a/src/api/api-config.ts +++ b/src/api/api-config.ts @@ -1,8 +1,8 @@ import axios from 'axios'; import Cookies from 'js-cookie'; -import { TOKEN_REFRESH_URL } from '@constants/endpoint'; -import { getTokenRefresh } from '@utils/getTokenRefresh'; +import { TOKEN_REFRESH_URL, LOGOUT_URL } from '@constants/endpoint'; +import { getTokenRefresh, logout } from '@utils/auth'; type Method = 'get' | 'post' | 'put' | 'delete' | 'patch'; @@ -62,6 +62,11 @@ axiosInstance.interceptors.request.use( delete config.headers['Authorization']; } + // NOTE: 로그아웃 요청 시 헤더 변경 + if (config.url === LOGOUT_URL) { + config.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + return config; }, function (error) { @@ -88,12 +93,25 @@ axiosInstance.interceptors.response.use( const refreshToken = Cookies.get('refreshToken'); // NOTE: 토큰 재발급 요청 - if (response.data.code === 401 && refreshToken) { + if ( + config.url !== TOKEN_REFRESH_URL && + response.data.code === 401 && + refreshToken + ) { const { data } = await getTokenRefresh(); Cookies.set('accessToken', data.accessToken); config.headers['Authorization'] = data.accessToken; + return axios(config); + } + + // NOTE: 토큰 재발급 요청이 유효하지 않으면, 쿠키의 토큰을 삭제하고 로그아웃 처리. 로그인 페이지로 이동 + if (config.url === TOKEN_REFRESH_URL && response.data.code === 401) { + Cookies.remove('accessToken'); + Cookies.remove('refreshToken'); + logout(); + + window.location.href = '/login'; } - return axios(config); }, ); diff --git a/src/app/settings/layout.tsx b/src/app/settings/layout.tsx new file mode 100644 index 00000000..a7797995 --- /dev/null +++ b/src/app/settings/layout.tsx @@ -0,0 +1,16 @@ +import Header from '@components/common/Header'; + +interface TermsLayoutProps { + children: React.ReactNode; +} + +export default function layout({ children }: TermsLayoutProps) { + return ( +
+
+

설정

+
+ {children} +
+ ); +} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx new file mode 100644 index 00000000..86c9d30d --- /dev/null +++ b/src/app/settings/page.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { useRouter } from 'next/navigation'; + +import { SETTINGS, Setting } from '@constants/settings'; +import RightArrowIcon from 'public/assets/icon24/right_arrow_24.svg'; +import ArrowSquareIcon from 'public/assets/icon16/arrow_square_16.svg'; +import { useLogout } from '@hooks/api/useLogout'; + +export default function Page() { + const { push } = useRouter(); + const { mutate: logout } = useLogout(); + + const handleClickSettingItem = (setting: Setting) => () => { + if (setting.url) { + push(setting.url); + } + + if (setting.title === '로그아웃') { + logout(); + } + }; + + return ( + + ); +} diff --git a/src/constants/endpoint.ts b/src/constants/endpoint.ts index da246a12..5d6b986a 100644 --- a/src/constants/endpoint.ts +++ b/src/constants/endpoint.ts @@ -1 +1,2 @@ export const TOKEN_REFRESH_URL = '/api/v1/auth/token/reissue'; +export const LOGOUT_URL = '/api/v1/auth/logout'; diff --git a/src/constants/settings.ts b/src/constants/settings.ts new file mode 100644 index 00000000..705b53f0 --- /dev/null +++ b/src/constants/settings.ts @@ -0,0 +1,25 @@ +export interface Setting { + url?: string; + title: string; +} + +export const SETTINGS: Setting[] = [ + { + url: '/service-terms', + title: '이용약관', + }, + { + url: '/privacy-policy', + title: '개인정보처리방침', + }, + { + url: '/location-terms', + title: '위치기반 서비스 이용약관', + }, + { + title: '로그아웃', + }, + { + title: '회원탈퇴', + }, +]; diff --git a/src/hooks/api/useLogout.ts b/src/hooks/api/useLogout.ts new file mode 100644 index 00000000..38830a7a --- /dev/null +++ b/src/hooks/api/useLogout.ts @@ -0,0 +1,19 @@ +import { useMutation, UseMutationResult } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import Cookies from 'js-cookie'; +import { useRouter } from 'next/navigation'; + +import { logout } from '@utils/auth'; + +export const useLogout = (): UseMutationResult => { + const { push } = useRouter(); + return useMutation({ + mutationKey: ['logout'], + mutationFn: logout, + onSuccess: () => { + Cookies.remove('accessToken'); + Cookies.remove('refreshToken'); + push('/login'); + }, + }); +}; diff --git a/src/middleware.ts b/src/middleware.ts index e600fabf..877c0ef6 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -17,6 +17,8 @@ export function middleware(request: NextRequest) { url.pathname = '/login'; } + url.search = ''; + const response = NextResponse.redirect(url); return response; diff --git a/src/utils/getTokenRefresh.ts b/src/utils/auth.ts similarity index 73% rename from src/utils/getTokenRefresh.ts rename to src/utils/auth.ts index e0aee678..7cf90b5b 100644 --- a/src/utils/getTokenRefresh.ts +++ b/src/utils/auth.ts @@ -10,3 +10,7 @@ export const getTokenRefresh = async (): Promise< > => { return await axiosRequest('post', '/api/v1/auth/token/reissue'); }; + +export const logout = async (): Promise => { + return axiosRequest('post', '/api/v1/auth/logout'); +};