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 (
+
+ );
+}
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 (
+
+ {SETTINGS.map((setting, index) => (
+ -
+
+
{setting.title}
+ {setting.url &&
}
+
+ {setting.url && }
+
+ ))}
+
+ );
+}
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');
+};