Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 8 additions & 45 deletions src/api/axios.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,16 @@
import axios, { AxiosError, AxiosInstance } from 'axios'
import axios, { AxiosInstance } from 'axios'

const authInterceptors = (instance: AxiosInstance): AxiosInstance => {
instance.interceptors.request.use(
config => {
// 로컬스토리지에 저장 되어 있는 AccessToken을 가져온다.
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
},
(error: AxiosError): Promise<AxiosError> => {
return Promise.reject(error)
}
)

return instance
}
axios.defaults.baseURL = import.meta.env.VITE_BASE_URL
axios.defaults.headers.common['apikey'] = import.meta.env.VITE_APIKEY
axios.defaults.headers.common['username'] = import.meta.env.VITE_USERNAME

// Authorization 설정이 없는 일반 사용자 API용 Instance
export const baseInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME
}
})
export const baseInstance: AxiosInstance = axios.create()

// Authorization 설정이 추가된 로그인한 사용자 API용 Instance
export const authInstance: AxiosInstance = authInterceptors(baseInstance)
export const authInstance: AxiosInstance = baseInstance

// 관리자 API용 Instance
export const adminInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME,
masterKey: true
}
headers: { masterKey: true }
})

////////////////////////////////
// * Axios Instance 기본 사용법
// * 1. 필요한 instance import
// * 2. instance.method({API_PATH}, {필요한 경우 Body})
// adminInstance.get('/products');
// authInstance.post('/auth/me');
// baseInstance.post('/auth/login', { email: '', password: ''});
////////////////////////////////
42 changes: 40 additions & 2 deletions src/components/App.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { Outlet } from 'react-router-dom'
import { Header, Badge } from 'components/index'
import { Product } from 'types/index'
import { Header, Badge, Modal } from 'components/index'
import { CommonError, Product, ModalProps } from 'types/index'
import {
LoginContext,
RecentlyContext,
Expand All@@ -13,6 +14,7 @@ import {
useSessionStorage,
useCartLocalStorage
} from 'hooks/index'
import { useAxiosInterceptor } from 'hooks/index'

//App은 Outlet을 통해 슬래시로 페이지 경로 이동시의 최상위 컴포넌트로 설정했습니다
export const App = () => {
Expand All@@ -28,6 +30,31 @@ export const App = () => {
isLogined
)

const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return (
<>
<LoginContext.Provider value={{ isLogined, setIsLogined }}>
Expand All@@ -39,6 +66,17 @@ export const App = () => {
<Header />
<Badge />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</WishListContext.Provider>
</RecentlyContext.Provider>
</CartContext.Provider>
Expand Down
32 changes: 20 additions & 12 deletions src/components/Header.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { LoginedUserContext, LoginContext, CartContext } from 'contexts/index'
import React, { useState, useRef, useEffect, useContext } from 'react'
import { logOut } from 'api/signApi'
import styles from 'styles/layout/header.module.scss'
import { CommonError } from '@/types'
//import { MyPageNav } from 'components/mypage'

export const Header: React.FC = () => {
Expand DownExpand Up@@ -55,19 +56,26 @@ export const Header: React.FC = () => {
}
}, [searchRef, hideInput])

const handleLogout = () => {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
}

const logOutId = () => {
// event.preventDefault() // 통합 테스트 때 확인 필요
logOut().then(isSuccess => {
if (isSuccess) {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
} else {
// 예외처리
}
})
logOut()
.then(isSuccess => {
if (isSuccess) {
handleLogout()
}
})
.catch((error: CommonError) => {
if (error.status === 401) {
navigate('/')
}
})
}

const onSearchEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
Expand Down
46 changes: 44 additions & 2 deletions src/components/admin/AdminPrivateRoute.tsx
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState, useContext } from 'react'
import { Outlet, useNavigate } from 'react-router-dom'
import { checkIsAdmin } from 'api/index'
import { AdminNav } from 'components/index'
import { AdminNav, Modal } from 'components/index'
import { CommonError, ModalProps } from 'types/index'
import { LoginContext, LoginedUserContext, CartContext } from 'contexts/index'
import { useAxiosInterceptor } from 'hooks/index'
import styled from 'styles/pages/admin.module.scss'

export const AdminPrivateRoute = () => {
Expand DownExpand Up@@ -33,10 +36,49 @@ export const AdminPrivateRoute = () => {
})
}, [moveSignIn, moveHome])

const { setIsLogined } = useContext(LoginContext)
const { setUserEmail } = useContext(LoginedUserContext)
const { setUserCart } = useContext(CartContext)
const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return isAdmin ? (
<div className={styled.admin}>
<AdminNav />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</div>
) : null
}
1 change: 0 additions & 1 deletion src/components/mypage/MyWishItem.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { calculateDiscountedPrice } from 'utils/index'
import styled from 'styles/components/mypage/myWishItem.module.scss'
import { Link } from 'react-router-dom'
import { WishListContext } from 'contexts/index'
import { on } from 'events'

export const MyWishrItem = React.memo(
({ product, isLast, isChecked, onChange }: MyWishItemProps) => {
Expand Down
12 changes: 12 additions & 0 deletions src/constants/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
export const networkErrors = {
EXPIRE_TOKEN: {
status: 401,
message: 'AccessToken 만료',
isShowModal: false
},
SERVER_ERROR: {
status: 500,
message: '서버에 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
isShowModal: true
}
}
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
export * from 'constants/tag'
export * from 'constants/errors'
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,4 @@ export * from 'hooks/useOnClickOutside'
export * from 'hooks/useLocalStorage'
export * from 'hooks/useSessionStorage'
export * from 'hooks/useCartLocalStorage'
export * from 'hooks/useAxiosInterceptor'
127 changes: 127 additions & 0 deletions src/hooks/useAxiosInterceptor.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
import { useEffect } from 'react'
import { AxiosError, InternalAxiosRequestConfig } from 'axios'
import { baseInstance, adminInstance } from 'api/index'
import { CommonError } from 'types/index'
import { networkErrors } from 'constants/index'

export const useAxiosInterceptor = (
handleLogout: () => void,
setErrorModal: (error: CommonError) => void
) => {
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)

const apiErrorInterceptor = (error: AxiosError): Promise<CommonError> => {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
setErrorModal(errorObj)
return Promise.reject(errorObj)
}

const adminConfig = (config: InternalAxiosRequestConfig<any>) => {
config.headers['masterKey'] = true
return config
}

const authConfig = (config: InternalAxiosRequestConfig<any>) => {
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
}

const responseErrorInterceptor = (
error: AxiosError
): Promise<CommonError> | void => {
if (!accessToken) {
if (error?.response?.status === 500) {
return Promise.reject(networkErrors.SERVER_ERROR)
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}
return Promise.reject(errorObj)
}
} else {
const showModal = !error.request?.responseURL.includes(
'/auth/login',
'/auth/signup'
)

if (
error.request?.responseURL &&
error.request?.responseURL.includes('logout') &&
error?.response?.status === 401
) {
handleLogout()
return Promise.reject(networkErrors.EXPIRE_TOKEN)
} else if (error?.response?.status === 401) {
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
alert('로그인 세션이 만료되었습니다. 다시 로그인해주세요.')
location.replace('/signin')
handleLogout()
return
} else if (error?.response?.status === 500) {
setErrorModal(networkErrors.SERVER_ERROR)
return
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: showModal
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}

return Promise.reject(errorObj)
}
}
}

const adminRequestInterceptor = adminInstance.interceptors.request.use(
adminConfig,
apiErrorInterceptor
)

const requestInterceptor = baseInstance.interceptors.request.use(
authConfig,
apiErrorInterceptor
)

const adminResponseInterceptor = adminInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

const responseInterceptor = baseInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

useEffect(() => {
return () => {
// interceptor 해제
baseInstance.interceptors.request.eject(requestInterceptor)
baseInstance.interceptors.response.eject(responseInterceptor)
adminInstance.interceptors.request.eject(adminRequestInterceptor)
adminInstance.interceptors.response.eject(adminResponseInterceptor)
}
}, [
responseInterceptor,
requestInterceptor,
adminRequestInterceptor,
adminResponseInterceptor
])
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
[공통] API 공통 에러 핸들링 및 토큰 만료 예외처리 by EungBug · Pull Request #84 · ColleyProjectGroup/ColleyProjectGroup2 · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 8 additions & 45 deletions src/api/axios.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,16 @@
import axios, { AxiosError, AxiosInstance } from 'axios'
import axios, { AxiosInstance } from 'axios'

const authInterceptors = (instance: AxiosInstance): AxiosInstance => {
instance.interceptors.request.use(
config => {
// 로컬스토리지에 저장 되어 있는 AccessToken을 가져온다.
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
},
(error: AxiosError): Promise<AxiosError> => {
return Promise.reject(error)
}
)

return instance
}
axios.defaults.baseURL = import.meta.env.VITE_BASE_URL
axios.defaults.headers.common['apikey'] = import.meta.env.VITE_APIKEY
axios.defaults.headers.common['username'] = import.meta.env.VITE_USERNAME

// Authorization 설정이 없는 일반 사용자 API용 Instance
export const baseInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME
}
})
export const baseInstance: AxiosInstance = axios.create()

// Authorization 설정이 추가된 로그인한 사용자 API용 Instance
export const authInstance: AxiosInstance = authInterceptors(baseInstance)
export const authInstance: AxiosInstance = baseInstance

// 관리자 API용 Instance
export const adminInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME,
masterKey: true
}
headers: { masterKey: true }
})

////////////////////////////////
// * Axios Instance 기본 사용법
// * 1. 필요한 instance import
// * 2. instance.method({API_PATH}, {필요한 경우 Body})
// adminInstance.get('/products');
// authInstance.post('/auth/me');
// baseInstance.post('/auth/login', { email: '', password: ''});
////////////////////////////////
42 changes: 40 additions & 2 deletions src/components/App.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { Outlet } from 'react-router-dom'
import { Header, Badge } from 'components/index'
import { Product } from 'types/index'
import { Header, Badge, Modal } from 'components/index'
import { CommonError, Product, ModalProps } from 'types/index'
import {
LoginContext,
RecentlyContext,
Expand All@@ -13,6 +14,7 @@ import {
useSessionStorage,
useCartLocalStorage
} from 'hooks/index'
import { useAxiosInterceptor } from 'hooks/index'

//App은 Outlet을 통해 슬래시로 페이지 경로 이동시의 최상위 컴포넌트로 설정했습니다
export const App = () => {
Expand All@@ -28,6 +30,31 @@ export const App = () => {
isLogined
)

const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return (
<>
<LoginContext.Provider value={{ isLogined, setIsLogined }}>
Expand All@@ -39,6 +66,17 @@ export const App = () => {
<Header />
<Badge />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</WishListContext.Provider>
</RecentlyContext.Provider>
</CartContext.Provider>
Expand Down
32 changes: 20 additions & 12 deletions src/components/Header.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { LoginedUserContext, LoginContext, CartContext } from 'contexts/index'
import React, { useState, useRef, useEffect, useContext } from 'react'
import { logOut } from 'api/signApi'
import styles from 'styles/layout/header.module.scss'
import { CommonError } from '@/types'
//import { MyPageNav } from 'components/mypage'

export const Header: React.FC = () => {
Expand DownExpand Up@@ -55,19 +56,26 @@ export const Header: React.FC = () => {
}
}, [searchRef, hideInput])

const handleLogout = () => {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
}

const logOutId = () => {
// event.preventDefault() // 통합 테스트 때 확인 필요
logOut().then(isSuccess => {
if (isSuccess) {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
} else {
// 예외처리
}
})
logOut()
.then(isSuccess => {
if (isSuccess) {
handleLogout()
}
})
.catch((error: CommonError) => {
if (error.status === 401) {
navigate('/')
}
})
}

const onSearchEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
Expand Down
46 changes: 44 additions & 2 deletions src/components/admin/AdminPrivateRoute.tsx
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState, useContext } from 'react'
import { Outlet, useNavigate } from 'react-router-dom'
import { checkIsAdmin } from 'api/index'
import { AdminNav } from 'components/index'
import { AdminNav, Modal } from 'components/index'
import { CommonError, ModalProps } from 'types/index'
import { LoginContext, LoginedUserContext, CartContext } from 'contexts/index'
import { useAxiosInterceptor } from 'hooks/index'
import styled from 'styles/pages/admin.module.scss'

export const AdminPrivateRoute = () => {
Expand DownExpand Up@@ -33,10 +36,49 @@ export const AdminPrivateRoute = () => {
})
}, [moveSignIn, moveHome])

const { setIsLogined } = useContext(LoginContext)
const { setUserEmail } = useContext(LoginedUserContext)
const { setUserCart } = useContext(CartContext)
const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return isAdmin ? (
<div className={styled.admin}>
<AdminNav />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</div>
) : null
}
1 change: 0 additions & 1 deletion src/components/mypage/MyWishItem.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { calculateDiscountedPrice } from 'utils/index'
import styled from 'styles/components/mypage/myWishItem.module.scss'
import { Link } from 'react-router-dom'
import { WishListContext } from 'contexts/index'
import { on } from 'events'

export const MyWishrItem = React.memo(
({ product, isLast, isChecked, onChange }: MyWishItemProps) => {
Expand Down
12 changes: 12 additions & 0 deletions src/constants/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
export const networkErrors = {
EXPIRE_TOKEN: {
status: 401,
message: 'AccessToken 만료',
isShowModal: false
},
SERVER_ERROR: {
status: 500,
message: '서버에 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
isShowModal: true
}
}
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
export * from 'constants/tag'
export * from 'constants/errors'
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,4 @@ export * from 'hooks/useOnClickOutside'
export * from 'hooks/useLocalStorage'
export * from 'hooks/useSessionStorage'
export * from 'hooks/useCartLocalStorage'
export * from 'hooks/useAxiosInterceptor'
127 changes: 127 additions & 0 deletions src/hooks/useAxiosInterceptor.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
import { useEffect } from 'react'
import { AxiosError, InternalAxiosRequestConfig } from 'axios'
import { baseInstance, adminInstance } from 'api/index'
import { CommonError } from 'types/index'
import { networkErrors } from 'constants/index'

export const useAxiosInterceptor = (
handleLogout: () => void,
setErrorModal: (error: CommonError) => void
) => {
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)

const apiErrorInterceptor = (error: AxiosError): Promise<CommonError> => {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
setErrorModal(errorObj)
return Promise.reject(errorObj)
}

const adminConfig = (config: InternalAxiosRequestConfig<any>) => {
config.headers['masterKey'] = true
return config
}

const authConfig = (config: InternalAxiosRequestConfig<any>) => {
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
}

const responseErrorInterceptor = (
error: AxiosError
): Promise<CommonError> | void => {
if (!accessToken) {
if (error?.response?.status === 500) {
return Promise.reject(networkErrors.SERVER_ERROR)
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}
return Promise.reject(errorObj)
}
} else {
const showModal = !error.request?.responseURL.includes(
'/auth/login',
'/auth/signup'
)

if (
error.request?.responseURL &&
error.request?.responseURL.includes('logout') &&
error?.response?.status === 401
) {
handleLogout()
return Promise.reject(networkErrors.EXPIRE_TOKEN)
} else if (error?.response?.status === 401) {
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
alert('로그인 세션이 만료되었습니다. 다시 로그인해주세요.')
location.replace('/signin')
handleLogout()
return
} else if (error?.response?.status === 500) {
setErrorModal(networkErrors.SERVER_ERROR)
return
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: showModal
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}

return Promise.reject(errorObj)
}
}
}

const adminRequestInterceptor = adminInstance.interceptors.request.use(
adminConfig,
apiErrorInterceptor
)

const requestInterceptor = baseInstance.interceptors.request.use(
authConfig,
apiErrorInterceptor
)

const adminResponseInterceptor = adminInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

const responseInterceptor = baseInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

useEffect(() => {
return () => {
// interceptor 해제
baseInstance.interceptors.request.eject(requestInterceptor)
baseInstance.interceptors.response.eject(responseInterceptor)
adminInstance.interceptors.request.eject(adminRequestInterceptor)
adminInstance.interceptors.response.eject(adminResponseInterceptor)
}
}, [
responseInterceptor,
requestInterceptor,
adminRequestInterceptor,
adminResponseInterceptor
])
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [공통] API 공통 에러 핸들링 및 토큰 만료 예외처리 by EungBug · Pull Request #84 · ColleyProjectGroup/ColleyProjectGroup2 · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 8 additions & 45 deletions src/api/axios.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,16 @@
import axios, { AxiosError, AxiosInstance } from 'axios'
import axios, { AxiosInstance } from 'axios'

const authInterceptors = (instance: AxiosInstance): AxiosInstance => {
instance.interceptors.request.use(
config => {
// 로컬스토리지에 저장 되어 있는 AccessToken을 가져온다.
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
},
(error: AxiosError): Promise<AxiosError> => {
return Promise.reject(error)
}
)

return instance
}
axios.defaults.baseURL = import.meta.env.VITE_BASE_URL
axios.defaults.headers.common['apikey'] = import.meta.env.VITE_APIKEY
axios.defaults.headers.common['username'] = import.meta.env.VITE_USERNAME

// Authorization 설정이 없는 일반 사용자 API용 Instance
export const baseInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME
}
})
export const baseInstance: AxiosInstance = axios.create()

// Authorization 설정이 추가된 로그인한 사용자 API용 Instance
export const authInstance: AxiosInstance = authInterceptors(baseInstance)
export const authInstance: AxiosInstance = baseInstance

// 관리자 API용 Instance
export const adminInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME,
masterKey: true
}
headers: { masterKey: true }
})

////////////////////////////////
// * Axios Instance 기본 사용법
// * 1. 필요한 instance import
// * 2. instance.method({API_PATH}, {필요한 경우 Body})
// adminInstance.get('/products');
// authInstance.post('/auth/me');
// baseInstance.post('/auth/login', { email: '', password: ''});
////////////////////////////////
42 changes: 40 additions & 2 deletions src/components/App.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { Outlet } from 'react-router-dom'
import { Header, Badge } from 'components/index'
import { Product } from 'types/index'
import { Header, Badge, Modal } from 'components/index'
import { CommonError, Product, ModalProps } from 'types/index'
import {
LoginContext,
RecentlyContext,
Expand All@@ -13,6 +14,7 @@ import {
useSessionStorage,
useCartLocalStorage
} from 'hooks/index'
import { useAxiosInterceptor } from 'hooks/index'

//App은 Outlet을 통해 슬래시로 페이지 경로 이동시의 최상위 컴포넌트로 설정했습니다
export const App = () => {
Expand All@@ -28,6 +30,31 @@ export const App = () => {
isLogined
)

const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return (
<>
<LoginContext.Provider value={{ isLogined, setIsLogined }}>
Expand All@@ -39,6 +66,17 @@ export const App = () => {
<Header />
<Badge />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</WishListContext.Provider>
</RecentlyContext.Provider>
</CartContext.Provider>
Expand Down
32 changes: 20 additions & 12 deletions src/components/Header.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { LoginedUserContext, LoginContext, CartContext } from 'contexts/index'
import React, { useState, useRef, useEffect, useContext } from 'react'
import { logOut } from 'api/signApi'
import styles from 'styles/layout/header.module.scss'
import { CommonError } from '@/types'
//import { MyPageNav } from 'components/mypage'

export const Header: React.FC = () => {
Expand DownExpand Up@@ -55,19 +56,26 @@ export const Header: React.FC = () => {
}
}, [searchRef, hideInput])

const handleLogout = () => {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
}

const logOutId = () => {
// event.preventDefault() // 통합 테스트 때 확인 필요
logOut().then(isSuccess => {
if (isSuccess) {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
} else {
// 예외처리
}
})
logOut()
.then(isSuccess => {
if (isSuccess) {
handleLogout()
}
})
.catch((error: CommonError) => {
if (error.status === 401) {
navigate('/')
}
})
}

const onSearchEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
Expand Down
46 changes: 44 additions & 2 deletions src/components/admin/AdminPrivateRoute.tsx
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState, useContext } from 'react'
import { Outlet, useNavigate } from 'react-router-dom'
import { checkIsAdmin } from 'api/index'
import { AdminNav } from 'components/index'
import { AdminNav, Modal } from 'components/index'
import { CommonError, ModalProps } from 'types/index'
import { LoginContext, LoginedUserContext, CartContext } from 'contexts/index'
import { useAxiosInterceptor } from 'hooks/index'
import styled from 'styles/pages/admin.module.scss'

export const AdminPrivateRoute = () => {
Expand DownExpand Up@@ -33,10 +36,49 @@ export const AdminPrivateRoute = () => {
})
}, [moveSignIn, moveHome])

const { setIsLogined } = useContext(LoginContext)
const { setUserEmail } = useContext(LoginedUserContext)
const { setUserCart } = useContext(CartContext)
const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return isAdmin ? (
<div className={styled.admin}>
<AdminNav />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</div>
) : null
}
1 change: 0 additions & 1 deletion src/components/mypage/MyWishItem.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { calculateDiscountedPrice } from 'utils/index'
import styled from 'styles/components/mypage/myWishItem.module.scss'
import { Link } from 'react-router-dom'
import { WishListContext } from 'contexts/index'
import { on } from 'events'

export const MyWishrItem = React.memo(
({ product, isLast, isChecked, onChange }: MyWishItemProps) => {
Expand Down
12 changes: 12 additions & 0 deletions src/constants/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
export const networkErrors = {
EXPIRE_TOKEN: {
status: 401,
message: 'AccessToken 만료',
isShowModal: false
},
SERVER_ERROR: {
status: 500,
message: '서버에 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
isShowModal: true
}
}
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
export * from 'constants/tag'
export * from 'constants/errors'
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,4 @@ export * from 'hooks/useOnClickOutside'
export * from 'hooks/useLocalStorage'
export * from 'hooks/useSessionStorage'
export * from 'hooks/useCartLocalStorage'
export * from 'hooks/useAxiosInterceptor'
127 changes: 127 additions & 0 deletions src/hooks/useAxiosInterceptor.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
import { useEffect } from 'react'
import { AxiosError, InternalAxiosRequestConfig } from 'axios'
import { baseInstance, adminInstance } from 'api/index'
import { CommonError } from 'types/index'
import { networkErrors } from 'constants/index'

export const useAxiosInterceptor = (
handleLogout: () => void,
setErrorModal: (error: CommonError) => void
) => {
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)

const apiErrorInterceptor = (error: AxiosError): Promise<CommonError> => {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
setErrorModal(errorObj)
return Promise.reject(errorObj)
}

const adminConfig = (config: InternalAxiosRequestConfig<any>) => {
config.headers['masterKey'] = true
return config
}

const authConfig = (config: InternalAxiosRequestConfig<any>) => {
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
}

const responseErrorInterceptor = (
error: AxiosError
): Promise<CommonError> | void => {
if (!accessToken) {
if (error?.response?.status === 500) {
return Promise.reject(networkErrors.SERVER_ERROR)
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}
return Promise.reject(errorObj)
}
} else {
const showModal = !error.request?.responseURL.includes(
'/auth/login',
'/auth/signup'
)

if (
error.request?.responseURL &&
error.request?.responseURL.includes('logout') &&
error?.response?.status === 401
) {
handleLogout()
return Promise.reject(networkErrors.EXPIRE_TOKEN)
} else if (error?.response?.status === 401) {
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
alert('로그인 세션이 만료되었습니다. 다시 로그인해주세요.')
location.replace('/signin')
handleLogout()
return
} else if (error?.response?.status === 500) {
setErrorModal(networkErrors.SERVER_ERROR)
return
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: showModal
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}

return Promise.reject(errorObj)
}
}
}

const adminRequestInterceptor = adminInstance.interceptors.request.use(
adminConfig,
apiErrorInterceptor
)

const requestInterceptor = baseInstance.interceptors.request.use(
authConfig,
apiErrorInterceptor
)

const adminResponseInterceptor = adminInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

const responseInterceptor = baseInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

useEffect(() => {
return () => {
// interceptor 해제
baseInstance.interceptors.request.eject(requestInterceptor)
baseInstance.interceptors.response.eject(responseInterceptor)
adminInstance.interceptors.request.eject(adminRequestInterceptor)
adminInstance.interceptors.response.eject(adminResponseInterceptor)
}
}, [
responseInterceptor,
requestInterceptor,
adminRequestInterceptor,
adminResponseInterceptor
])
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [공통] API 공통 에러 핸들링 및 토큰 만료 예외처리 by EungBug · Pull Request #84 · ColleyProjectGroup/ColleyProjectGroup2 · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 8 additions & 45 deletions src/api/axios.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,16 @@
import axios, { AxiosError, AxiosInstance } from 'axios'
import axios, { AxiosInstance } from 'axios'

const authInterceptors = (instance: AxiosInstance): AxiosInstance => {
instance.interceptors.request.use(
config => {
// 로컬스토리지에 저장 되어 있는 AccessToken을 가져온다.
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
},
(error: AxiosError): Promise<AxiosError> => {
return Promise.reject(error)
}
)

return instance
}
axios.defaults.baseURL = import.meta.env.VITE_BASE_URL
axios.defaults.headers.common['apikey'] = import.meta.env.VITE_APIKEY
axios.defaults.headers.common['username'] = import.meta.env.VITE_USERNAME

// Authorization 설정이 없는 일반 사용자 API용 Instance
export const baseInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME
}
})
export const baseInstance: AxiosInstance = axios.create()

// Authorization 설정이 추가된 로그인한 사용자 API용 Instance
export const authInstance: AxiosInstance = authInterceptors(baseInstance)
export const authInstance: AxiosInstance = baseInstance

// 관리자 API용 Instance
export const adminInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME,
masterKey: true
}
headers: { masterKey: true }
})

////////////////////////////////
// * Axios Instance 기본 사용법
// * 1. 필요한 instance import
// * 2. instance.method({API_PATH}, {필요한 경우 Body})
// adminInstance.get('/products');
// authInstance.post('/auth/me');
// baseInstance.post('/auth/login', { email: '', password: ''});
////////////////////////////////
42 changes: 40 additions & 2 deletions src/components/App.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { Outlet } from 'react-router-dom'
import { Header, Badge } from 'components/index'
import { Product } from 'types/index'
import { Header, Badge, Modal } from 'components/index'
import { CommonError, Product, ModalProps } from 'types/index'
import {
LoginContext,
RecentlyContext,
Expand All@@ -13,6 +14,7 @@ import {
useSessionStorage,
useCartLocalStorage
} from 'hooks/index'
import { useAxiosInterceptor } from 'hooks/index'

//App은 Outlet을 통해 슬래시로 페이지 경로 이동시의 최상위 컴포넌트로 설정했습니다
export const App = () => {
Expand All@@ -28,6 +30,31 @@ export const App = () => {
isLogined
)

const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return (
<>
<LoginContext.Provider value={{ isLogined, setIsLogined }}>
Expand All@@ -39,6 +66,17 @@ export const App = () => {
<Header />
<Badge />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</WishListContext.Provider>
</RecentlyContext.Provider>
</CartContext.Provider>
Expand Down
32 changes: 20 additions & 12 deletions src/components/Header.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { LoginedUserContext, LoginContext, CartContext } from 'contexts/index'
import React, { useState, useRef, useEffect, useContext } from 'react'
import { logOut } from 'api/signApi'
import styles from 'styles/layout/header.module.scss'
import { CommonError } from '@/types'
//import { MyPageNav } from 'components/mypage'

export const Header: React.FC = () => {
Expand DownExpand Up@@ -55,19 +56,26 @@ export const Header: React.FC = () => {
}
}, [searchRef, hideInput])

const handleLogout = () => {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
}

const logOutId = () => {
// event.preventDefault() // 통합 테스트 때 확인 필요
logOut().then(isSuccess => {
if (isSuccess) {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
} else {
// 예외처리
}
})
logOut()
.then(isSuccess => {
if (isSuccess) {
handleLogout()
}
})
.catch((error: CommonError) => {
if (error.status === 401) {
navigate('/')
}
})
}

const onSearchEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
Expand Down
46 changes: 44 additions & 2 deletions src/components/admin/AdminPrivateRoute.tsx
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState, useContext } from 'react'
import { Outlet, useNavigate } from 'react-router-dom'
import { checkIsAdmin } from 'api/index'
import { AdminNav } from 'components/index'
import { AdminNav, Modal } from 'components/index'
import { CommonError, ModalProps } from 'types/index'
import { LoginContext, LoginedUserContext, CartContext } from 'contexts/index'
import { useAxiosInterceptor } from 'hooks/index'
import styled from 'styles/pages/admin.module.scss'

export const AdminPrivateRoute = () => {
Expand DownExpand Up@@ -33,10 +36,49 @@ export const AdminPrivateRoute = () => {
})
}, [moveSignIn, moveHome])

const { setIsLogined } = useContext(LoginContext)
const { setUserEmail } = useContext(LoginedUserContext)
const { setUserCart } = useContext(CartContext)
const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return isAdmin ? (
<div className={styled.admin}>
<AdminNav />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</div>
) : null
}
1 change: 0 additions & 1 deletion src/components/mypage/MyWishItem.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { calculateDiscountedPrice } from 'utils/index'
import styled from 'styles/components/mypage/myWishItem.module.scss'
import { Link } from 'react-router-dom'
import { WishListContext } from 'contexts/index'
import { on } from 'events'

export const MyWishrItem = React.memo(
({ product, isLast, isChecked, onChange }: MyWishItemProps) => {
Expand Down
12 changes: 12 additions & 0 deletions src/constants/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
export const networkErrors = {
EXPIRE_TOKEN: {
status: 401,
message: 'AccessToken 만료',
isShowModal: false
},
SERVER_ERROR: {
status: 500,
message: '서버에 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
isShowModal: true
}
}
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
export * from 'constants/tag'
export * from 'constants/errors'
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,4 @@ export * from 'hooks/useOnClickOutside'
export * from 'hooks/useLocalStorage'
export * from 'hooks/useSessionStorage'
export * from 'hooks/useCartLocalStorage'
export * from 'hooks/useAxiosInterceptor'
127 changes: 127 additions & 0 deletions src/hooks/useAxiosInterceptor.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
import { useEffect } from 'react'
import { AxiosError, InternalAxiosRequestConfig } from 'axios'
import { baseInstance, adminInstance } from 'api/index'
import { CommonError } from 'types/index'
import { networkErrors } from 'constants/index'

export const useAxiosInterceptor = (
handleLogout: () => void,
setErrorModal: (error: CommonError) => void
) => {
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)

const apiErrorInterceptor = (error: AxiosError): Promise<CommonError> => {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
setErrorModal(errorObj)
return Promise.reject(errorObj)
}

const adminConfig = (config: InternalAxiosRequestConfig<any>) => {
config.headers['masterKey'] = true
return config
}

const authConfig = (config: InternalAxiosRequestConfig<any>) => {
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
}

const responseErrorInterceptor = (
error: AxiosError
): Promise<CommonError> | void => {
if (!accessToken) {
if (error?.response?.status === 500) {
return Promise.reject(networkErrors.SERVER_ERROR)
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}
return Promise.reject(errorObj)
}
} else {
const showModal = !error.request?.responseURL.includes(
'/auth/login',
'/auth/signup'
)

if (
error.request?.responseURL &&
error.request?.responseURL.includes('logout') &&
error?.response?.status === 401
) {
handleLogout()
return Promise.reject(networkErrors.EXPIRE_TOKEN)
} else if (error?.response?.status === 401) {
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
alert('로그인 세션이 만료되었습니다. 다시 로그인해주세요.')
location.replace('/signin')
handleLogout()
return
} else if (error?.response?.status === 500) {
setErrorModal(networkErrors.SERVER_ERROR)
return
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: showModal
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}

return Promise.reject(errorObj)
}
}
}

const adminRequestInterceptor = adminInstance.interceptors.request.use(
adminConfig,
apiErrorInterceptor
)

const requestInterceptor = baseInstance.interceptors.request.use(
authConfig,
apiErrorInterceptor
)

const adminResponseInterceptor = adminInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

const responseInterceptor = baseInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

useEffect(() => {
return () => {
// interceptor 해제
baseInstance.interceptors.request.eject(requestInterceptor)
baseInstance.interceptors.response.eject(responseInterceptor)
adminInstance.interceptors.request.eject(adminRequestInterceptor)
adminInstance.interceptors.response.eject(adminResponseInterceptor)
}
}, [
responseInterceptor,
requestInterceptor,
adminRequestInterceptor,
adminResponseInterceptor
])
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [공통] API 공통 에러 핸들링 및 토큰 만료 예외처리 by EungBug · Pull Request #84 · ColleyProjectGroup/ColleyProjectGroup2 · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 8 additions & 45 deletions src/api/axios.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,16 @@
import axios, { AxiosError, AxiosInstance } from 'axios'
import axios, { AxiosInstance } from 'axios'

const authInterceptors = (instance: AxiosInstance): AxiosInstance => {
instance.interceptors.request.use(
config => {
// 로컬스토리지에 저장 되어 있는 AccessToken을 가져온다.
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
},
(error: AxiosError): Promise<AxiosError> => {
return Promise.reject(error)
}
)

return instance
}
axios.defaults.baseURL = import.meta.env.VITE_BASE_URL
axios.defaults.headers.common['apikey'] = import.meta.env.VITE_APIKEY
axios.defaults.headers.common['username'] = import.meta.env.VITE_USERNAME

// Authorization 설정이 없는 일반 사용자 API용 Instance
export const baseInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME
}
})
export const baseInstance: AxiosInstance = axios.create()

// Authorization 설정이 추가된 로그인한 사용자 API용 Instance
export const authInstance: AxiosInstance = authInterceptors(baseInstance)
export const authInstance: AxiosInstance = baseInstance

// 관리자 API용 Instance
export const adminInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME,
masterKey: true
}
headers: { masterKey: true }
})

////////////////////////////////
// * Axios Instance 기본 사용법
// * 1. 필요한 instance import
// * 2. instance.method({API_PATH}, {필요한 경우 Body})
// adminInstance.get('/products');
// authInstance.post('/auth/me');
// baseInstance.post('/auth/login', { email: '', password: ''});
////////////////////////////////
42 changes: 40 additions & 2 deletions src/components/App.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { Outlet } from 'react-router-dom'
import { Header, Badge } from 'components/index'
import { Product } from 'types/index'
import { Header, Badge, Modal } from 'components/index'
import { CommonError, Product, ModalProps } from 'types/index'
import {
LoginContext,
RecentlyContext,
Expand All@@ -13,6 +14,7 @@ import {
useSessionStorage,
useCartLocalStorage
} from 'hooks/index'
import { useAxiosInterceptor } from 'hooks/index'

//App은 Outlet을 통해 슬래시로 페이지 경로 이동시의 최상위 컴포넌트로 설정했습니다
export const App = () => {
Expand All@@ -28,6 +30,31 @@ export const App = () => {
isLogined
)

const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return (
<>
<LoginContext.Provider value={{ isLogined, setIsLogined }}>
Expand All@@ -39,6 +66,17 @@ export const App = () => {
<Header />
<Badge />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</WishListContext.Provider>
</RecentlyContext.Provider>
</CartContext.Provider>
Expand Down
32 changes: 20 additions & 12 deletions src/components/Header.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { LoginedUserContext, LoginContext, CartContext } from 'contexts/index'
import React, { useState, useRef, useEffect, useContext } from 'react'
import { logOut } from 'api/signApi'
import styles from 'styles/layout/header.module.scss'
import { CommonError } from '@/types'
//import { MyPageNav } from 'components/mypage'

export const Header: React.FC = () => {
Expand DownExpand Up@@ -55,19 +56,26 @@ export const Header: React.FC = () => {
}
}, [searchRef, hideInput])

const handleLogout = () => {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
}

const logOutId = () => {
// event.preventDefault() // 통합 테스트 때 확인 필요
logOut().then(isSuccess => {
if (isSuccess) {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
} else {
// 예외처리
}
})
logOut()
.then(isSuccess => {
if (isSuccess) {
handleLogout()
}
})
.catch((error: CommonError) => {
if (error.status === 401) {
navigate('/')
}
})
}

const onSearchEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
Expand Down
46 changes: 44 additions & 2 deletions src/components/admin/AdminPrivateRoute.tsx
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState, useContext } from 'react'
import { Outlet, useNavigate } from 'react-router-dom'
import { checkIsAdmin } from 'api/index'
import { AdminNav } from 'components/index'
import { AdminNav, Modal } from 'components/index'
import { CommonError, ModalProps } from 'types/index'
import { LoginContext, LoginedUserContext, CartContext } from 'contexts/index'
import { useAxiosInterceptor } from 'hooks/index'
import styled from 'styles/pages/admin.module.scss'

export const AdminPrivateRoute = () => {
Expand DownExpand Up@@ -33,10 +36,49 @@ export const AdminPrivateRoute = () => {
})
}, [moveSignIn, moveHome])

const { setIsLogined } = useContext(LoginContext)
const { setUserEmail } = useContext(LoginedUserContext)
const { setUserCart } = useContext(CartContext)
const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return isAdmin ? (
<div className={styled.admin}>
<AdminNav />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</div>
) : null
}
1 change: 0 additions & 1 deletion src/components/mypage/MyWishItem.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { calculateDiscountedPrice } from 'utils/index'
import styled from 'styles/components/mypage/myWishItem.module.scss'
import { Link } from 'react-router-dom'
import { WishListContext } from 'contexts/index'
import { on } from 'events'

export const MyWishrItem = React.memo(
({ product, isLast, isChecked, onChange }: MyWishItemProps) => {
Expand Down
12 changes: 12 additions & 0 deletions src/constants/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
export const networkErrors = {
EXPIRE_TOKEN: {
status: 401,
message: 'AccessToken 만료',
isShowModal: false
},
SERVER_ERROR: {
status: 500,
message: '서버에 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
isShowModal: true
}
}
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
export * from 'constants/tag'
export * from 'constants/errors'
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,4 @@ export * from 'hooks/useOnClickOutside'
export * from 'hooks/useLocalStorage'
export * from 'hooks/useSessionStorage'
export * from 'hooks/useCartLocalStorage'
export * from 'hooks/useAxiosInterceptor'
127 changes: 127 additions & 0 deletions src/hooks/useAxiosInterceptor.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
import { useEffect } from 'react'
import { AxiosError, InternalAxiosRequestConfig } from 'axios'
import { baseInstance, adminInstance } from 'api/index'
import { CommonError } from 'types/index'
import { networkErrors } from 'constants/index'

export const useAxiosInterceptor = (
handleLogout: () => void,
setErrorModal: (error: CommonError) => void
) => {
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)

const apiErrorInterceptor = (error: AxiosError): Promise<CommonError> => {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
setErrorModal(errorObj)
return Promise.reject(errorObj)
}

const adminConfig = (config: InternalAxiosRequestConfig<any>) => {
config.headers['masterKey'] = true
return config
}

const authConfig = (config: InternalAxiosRequestConfig<any>) => {
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
}

const responseErrorInterceptor = (
error: AxiosError
): Promise<CommonError> | void => {
if (!accessToken) {
if (error?.response?.status === 500) {
return Promise.reject(networkErrors.SERVER_ERROR)
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}
return Promise.reject(errorObj)
}
} else {
const showModal = !error.request?.responseURL.includes(
'/auth/login',
'/auth/signup'
)

if (
error.request?.responseURL &&
error.request?.responseURL.includes('logout') &&
error?.response?.status === 401
) {
handleLogout()
return Promise.reject(networkErrors.EXPIRE_TOKEN)
} else if (error?.response?.status === 401) {
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
alert('로그인 세션이 만료되었습니다. 다시 로그인해주세요.')
location.replace('/signin')
handleLogout()
return
} else if (error?.response?.status === 500) {
setErrorModal(networkErrors.SERVER_ERROR)
return
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: showModal
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}

return Promise.reject(errorObj)
}
}
}

const adminRequestInterceptor = adminInstance.interceptors.request.use(
adminConfig,
apiErrorInterceptor
)

const requestInterceptor = baseInstance.interceptors.request.use(
authConfig,
apiErrorInterceptor
)

const adminResponseInterceptor = adminInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

const responseInterceptor = baseInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

useEffect(() => {
return () => {
// interceptor 해제
baseInstance.interceptors.request.eject(requestInterceptor)
baseInstance.interceptors.response.eject(responseInterceptor)
adminInstance.interceptors.request.eject(adminRequestInterceptor)
adminInstance.interceptors.response.eject(adminResponseInterceptor)
}
}, [
responseInterceptor,
requestInterceptor,
adminRequestInterceptor,
adminResponseInterceptor
])
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [공통] API 공통 에러 핸들링 및 토큰 만료 예외처리 by EungBug · Pull Request #84 · ColleyProjectGroup/ColleyProjectGroup2 · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 8 additions & 45 deletions src/api/axios.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,16 @@
import axios, { AxiosError, AxiosInstance } from 'axios'
import axios, { AxiosInstance } from 'axios'

const authInterceptors = (instance: AxiosInstance): AxiosInstance => {
instance.interceptors.request.use(
config => {
// 로컬스토리지에 저장 되어 있는 AccessToken을 가져온다.
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
},
(error: AxiosError): Promise<AxiosError> => {
return Promise.reject(error)
}
)

return instance
}
axios.defaults.baseURL = import.meta.env.VITE_BASE_URL
axios.defaults.headers.common['apikey'] = import.meta.env.VITE_APIKEY
axios.defaults.headers.common['username'] = import.meta.env.VITE_USERNAME

// Authorization 설정이 없는 일반 사용자 API용 Instance
export const baseInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME
}
})
export const baseInstance: AxiosInstance = axios.create()

// Authorization 설정이 추가된 로그인한 사용자 API용 Instance
export const authInstance: AxiosInstance = authInterceptors(baseInstance)
export const authInstance: AxiosInstance = baseInstance

// 관리자 API용 Instance
export const adminInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME,
masterKey: true
}
headers: { masterKey: true }
})

////////////////////////////////
// * Axios Instance 기본 사용법
// * 1. 필요한 instance import
// * 2. instance.method({API_PATH}, {필요한 경우 Body})
// adminInstance.get('/products');
// authInstance.post('/auth/me');
// baseInstance.post('/auth/login', { email: '', password: ''});
////////////////////////////////
42 changes: 40 additions & 2 deletions src/components/App.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { Outlet } from 'react-router-dom'
import { Header, Badge } from 'components/index'
import { Product } from 'types/index'
import { Header, Badge, Modal } from 'components/index'
import { CommonError, Product, ModalProps } from 'types/index'
import {
LoginContext,
RecentlyContext,
Expand All@@ -13,6 +14,7 @@ import {
useSessionStorage,
useCartLocalStorage
} from 'hooks/index'
import { useAxiosInterceptor } from 'hooks/index'

//App은 Outlet을 통해 슬래시로 페이지 경로 이동시의 최상위 컴포넌트로 설정했습니다
export const App = () => {
Expand All@@ -28,6 +30,31 @@ export const App = () => {
isLogined
)

const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return (
<>
<LoginContext.Provider value={{ isLogined, setIsLogined }}>
Expand All@@ -39,6 +66,17 @@ export const App = () => {
<Header />
<Badge />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</WishListContext.Provider>
</RecentlyContext.Provider>
</CartContext.Provider>
Expand Down
32 changes: 20 additions & 12 deletions src/components/Header.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { LoginedUserContext, LoginContext, CartContext } from 'contexts/index'
import React, { useState, useRef, useEffect, useContext } from 'react'
import { logOut } from 'api/signApi'
import styles from 'styles/layout/header.module.scss'
import { CommonError } from '@/types'
//import { MyPageNav } from 'components/mypage'

export const Header: React.FC = () => {
Expand DownExpand Up@@ -55,19 +56,26 @@ export const Header: React.FC = () => {
}
}, [searchRef, hideInput])

const handleLogout = () => {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
}

const logOutId = () => {
// event.preventDefault() // 통합 테스트 때 확인 필요
logOut().then(isSuccess => {
if (isSuccess) {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
} else {
// 예외처리
}
})
logOut()
.then(isSuccess => {
if (isSuccess) {
handleLogout()
}
})
.catch((error: CommonError) => {
if (error.status === 401) {
navigate('/')
}
})
}

const onSearchEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
Expand Down
46 changes: 44 additions & 2 deletions src/components/admin/AdminPrivateRoute.tsx
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState, useContext } from 'react'
import { Outlet, useNavigate } from 'react-router-dom'
import { checkIsAdmin } from 'api/index'
import { AdminNav } from 'components/index'
import { AdminNav, Modal } from 'components/index'
import { CommonError, ModalProps } from 'types/index'
import { LoginContext, LoginedUserContext, CartContext } from 'contexts/index'
import { useAxiosInterceptor } from 'hooks/index'
import styled from 'styles/pages/admin.module.scss'

export const AdminPrivateRoute = () => {
Expand DownExpand Up@@ -33,10 +36,49 @@ export const AdminPrivateRoute = () => {
})
}, [moveSignIn, moveHome])

const { setIsLogined } = useContext(LoginContext)
const { setUserEmail } = useContext(LoginedUserContext)
const { setUserCart } = useContext(CartContext)
const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return isAdmin ? (
<div className={styled.admin}>
<AdminNav />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</div>
) : null
}
1 change: 0 additions & 1 deletion src/components/mypage/MyWishItem.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { calculateDiscountedPrice } from 'utils/index'
import styled from 'styles/components/mypage/myWishItem.module.scss'
import { Link } from 'react-router-dom'
import { WishListContext } from 'contexts/index'
import { on } from 'events'

export const MyWishrItem = React.memo(
({ product, isLast, isChecked, onChange }: MyWishItemProps) => {
Expand Down
12 changes: 12 additions & 0 deletions src/constants/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
export const networkErrors = {
EXPIRE_TOKEN: {
status: 401,
message: 'AccessToken 만료',
isShowModal: false
},
SERVER_ERROR: {
status: 500,
message: '서버에 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
isShowModal: true
}
}
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
export * from 'constants/tag'
export * from 'constants/errors'
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,4 @@ export * from 'hooks/useOnClickOutside'
export * from 'hooks/useLocalStorage'
export * from 'hooks/useSessionStorage'
export * from 'hooks/useCartLocalStorage'
export * from 'hooks/useAxiosInterceptor'
127 changes: 127 additions & 0 deletions src/hooks/useAxiosInterceptor.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
import { useEffect } from 'react'
import { AxiosError, InternalAxiosRequestConfig } from 'axios'
import { baseInstance, adminInstance } from 'api/index'
import { CommonError } from 'types/index'
import { networkErrors } from 'constants/index'

export const useAxiosInterceptor = (
handleLogout: () => void,
setErrorModal: (error: CommonError) => void
) => {
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)

const apiErrorInterceptor = (error: AxiosError): Promise<CommonError> => {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
setErrorModal(errorObj)
return Promise.reject(errorObj)
}

const adminConfig = (config: InternalAxiosRequestConfig<any>) => {
config.headers['masterKey'] = true
return config
}

const authConfig = (config: InternalAxiosRequestConfig<any>) => {
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
}

const responseErrorInterceptor = (
error: AxiosError
): Promise<CommonError> | void => {
if (!accessToken) {
if (error?.response?.status === 500) {
return Promise.reject(networkErrors.SERVER_ERROR)
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}
return Promise.reject(errorObj)
}
} else {
const showModal = !error.request?.responseURL.includes(
'/auth/login',
'/auth/signup'
)

if (
error.request?.responseURL &&
error.request?.responseURL.includes('logout') &&
error?.response?.status === 401
) {
handleLogout()
return Promise.reject(networkErrors.EXPIRE_TOKEN)
} else if (error?.response?.status === 401) {
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
alert('로그인 세션이 만료되었습니다. 다시 로그인해주세요.')
location.replace('/signin')
handleLogout()
return
} else if (error?.response?.status === 500) {
setErrorModal(networkErrors.SERVER_ERROR)
return
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: showModal
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}

return Promise.reject(errorObj)
}
}
}

const adminRequestInterceptor = adminInstance.interceptors.request.use(
adminConfig,
apiErrorInterceptor
)

const requestInterceptor = baseInstance.interceptors.request.use(
authConfig,
apiErrorInterceptor
)

const adminResponseInterceptor = adminInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

const responseInterceptor = baseInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

useEffect(() => {
return () => {
// interceptor 해제
baseInstance.interceptors.request.eject(requestInterceptor)
baseInstance.interceptors.response.eject(responseInterceptor)
adminInstance.interceptors.request.eject(adminRequestInterceptor)
adminInstance.interceptors.response.eject(adminResponseInterceptor)
}
}, [
responseInterceptor,
requestInterceptor,
adminRequestInterceptor,
adminResponseInterceptor
])
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); [공통] API 공통 에러 핸들링 및 토큰 만료 예외처리 by EungBug · Pull Request #84 · ColleyProjectGroup/ColleyProjectGroup2 · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 8 additions & 45 deletions src/api/axios.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,16 @@
import axios, { AxiosError, AxiosInstance } from 'axios'
import axios, { AxiosInstance } from 'axios'

const authInterceptors = (instance: AxiosInstance): AxiosInstance => {
instance.interceptors.request.use(
config => {
// 로컬스토리지에 저장 되어 있는 AccessToken을 가져온다.
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
},
(error: AxiosError): Promise<AxiosError> => {
return Promise.reject(error)
}
)

return instance
}
axios.defaults.baseURL = import.meta.env.VITE_BASE_URL
axios.defaults.headers.common['apikey'] = import.meta.env.VITE_APIKEY
axios.defaults.headers.common['username'] = import.meta.env.VITE_USERNAME

// Authorization 설정이 없는 일반 사용자 API용 Instance
export const baseInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME
}
})
export const baseInstance: AxiosInstance = axios.create()

// Authorization 설정이 추가된 로그인한 사용자 API용 Instance
export const authInstance: AxiosInstance = authInterceptors(baseInstance)
export const authInstance: AxiosInstance = baseInstance

// 관리자 API용 Instance
export const adminInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
apikey: import.meta.env.VITE_APIKEY,
username: import.meta.env.VITE_USERNAME,
masterKey: true
}
headers: { masterKey: true }
})

////////////////////////////////
// * Axios Instance 기본 사용법
// * 1. 필요한 instance import
// * 2. instance.method({API_PATH}, {필요한 경우 Body})
// adminInstance.get('/products');
// authInstance.post('/auth/me');
// baseInstance.post('/auth/login', { email: '', password: ''});
////////////////////////////////
42 changes: 40 additions & 2 deletions src/components/App.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { Outlet } from 'react-router-dom'
import { Header, Badge } from 'components/index'
import { Product } from 'types/index'
import { Header, Badge, Modal } from 'components/index'
import { CommonError, Product, ModalProps } from 'types/index'
import {
LoginContext,
RecentlyContext,
Expand All@@ -13,6 +14,7 @@ import {
useSessionStorage,
useCartLocalStorage
} from 'hooks/index'
import { useAxiosInterceptor } from 'hooks/index'

//App은 Outlet을 통해 슬래시로 페이지 경로 이동시의 최상위 컴포넌트로 설정했습니다
export const App = () => {
Expand All@@ -28,6 +30,31 @@ export const App = () => {
isLogined
)

const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return (
<>
<LoginContext.Provider value={{ isLogined, setIsLogined }}>
Expand All@@ -39,6 +66,17 @@ export const App = () => {
<Header />
<Badge />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</WishListContext.Provider>
</RecentlyContext.Provider>
</CartContext.Provider>
Expand Down
32 changes: 20 additions & 12 deletions src/components/Header.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { LoginedUserContext, LoginContext, CartContext } from 'contexts/index'
import React, { useState, useRef, useEffect, useContext } from 'react'
import { logOut } from 'api/signApi'
import styles from 'styles/layout/header.module.scss'
import { CommonError } from '@/types'
//import { MyPageNav } from 'components/mypage'

export const Header: React.FC = () => {
Expand DownExpand Up@@ -55,19 +56,26 @@ export const Header: React.FC = () => {
}
}, [searchRef, hideInput])

const handleLogout = () => {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
}

const logOutId = () => {
// event.preventDefault() // 통합 테스트 때 확인 필요
logOut().then(isSuccess => {
if (isSuccess) {
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
setIsLogined(!isLogined)
navigate('/')
} else {
// 예외처리
}
})
logOut()
.then(isSuccess => {
if (isSuccess) {
handleLogout()
}
})
.catch((error: CommonError) => {
if (error.status === 401) {
navigate('/')
}
})
}

const onSearchEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
Expand Down
46 changes: 44 additions & 2 deletions src/components/admin/AdminPrivateRoute.tsx
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState, useContext } from 'react'
import { Outlet, useNavigate } from 'react-router-dom'
import { checkIsAdmin } from 'api/index'
import { AdminNav } from 'components/index'
import { AdminNav, Modal } from 'components/index'
import { CommonError, ModalProps } from 'types/index'
import { LoginContext, LoginedUserContext, CartContext } from 'contexts/index'
import { useAxiosInterceptor } from 'hooks/index'
import styled from 'styles/pages/admin.module.scss'

export const AdminPrivateRoute = () => {
Expand DownExpand Up@@ -33,10 +36,49 @@ export const AdminPrivateRoute = () => {
})
}, [moveSignIn, moveHome])

const { setIsLogined } = useContext(LoginContext)
const { setUserEmail } = useContext(LoginedUserContext)
const { setUserCart } = useContext(CartContext)
const [isModalShow, setIsModalShow] = useState<boolean>(false)
const [modalProps, setModalProps] = useState<ModalProps | null>(null)

const handleLogout = () => {
setIsLogined(false)
setUserEmail('')
setUserCart([])
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
}

const handleErrorModal = (error: CommonError) => {
setIsModalShow(true)
setModalProps({
title: '오류',
content: error.message,
isTwoButton: false,
okButtonText: '확인',
onClickOkButton: () => {
setIsModalShow(false)
}
})
}

useAxiosInterceptor(handleLogout, handleErrorModal)

return isAdmin ? (
<div className={styled.admin}>
<AdminNav />
<Outlet />
{isModalShow && modalProps ? (
<Modal
isTwoButton={modalProps.isTwoButton}
title={modalProps.title}
content={modalProps.content}
okButtonText={modalProps.okButtonText}
onClickOkButton={modalProps.onClickOkButton}
cancelButtonText={modalProps.cancelButtonText}
onClickCancelButton={modalProps.onClickCancelButton}
/>
) : null}
</div>
) : null
}
1 change: 0 additions & 1 deletion src/components/mypage/MyWishItem.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { calculateDiscountedPrice } from 'utils/index'
import styled from 'styles/components/mypage/myWishItem.module.scss'
import { Link } from 'react-router-dom'
import { WishListContext } from 'contexts/index'
import { on } from 'events'

export const MyWishrItem = React.memo(
({ product, isLast, isChecked, onChange }: MyWishItemProps) => {
Expand Down
12 changes: 12 additions & 0 deletions src/constants/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
export const networkErrors = {
EXPIRE_TOKEN: {
status: 401,
message: 'AccessToken 만료',
isShowModal: false
},
SERVER_ERROR: {
status: 500,
message: '서버에 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
isShowModal: true
}
}
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
export * from 'constants/tag'
export * from 'constants/errors'
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,4 @@ export * from 'hooks/useOnClickOutside'
export * from 'hooks/useLocalStorage'
export * from 'hooks/useSessionStorage'
export * from 'hooks/useCartLocalStorage'
export * from 'hooks/useAxiosInterceptor'
127 changes: 127 additions & 0 deletions src/hooks/useAxiosInterceptor.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
import { useEffect } from 'react'
import { AxiosError, InternalAxiosRequestConfig } from 'axios'
import { baseInstance, adminInstance } from 'api/index'
import { CommonError } from 'types/index'
import { networkErrors } from 'constants/index'

export const useAxiosInterceptor = (
handleLogout: () => void,
setErrorModal: (error: CommonError) => void
) => {
const accessToken = localStorage.getItem(
import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN
)

const apiErrorInterceptor = (error: AxiosError): Promise<CommonError> => {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
setErrorModal(errorObj)
return Promise.reject(errorObj)
}

const adminConfig = (config: InternalAxiosRequestConfig<any>) => {
config.headers['masterKey'] = true
return config
}

const authConfig = (config: InternalAxiosRequestConfig<any>) => {
if (config.headers && accessToken) {
// AccessToken이 정상적으로 저장되어 있으면 headers에 Authorization에 값을 추가해준다.
config.headers.Authorization = `Bearer ${accessToken}`
}
// authorization을 추가한 config 반환
return config
}

const responseErrorInterceptor = (
error: AxiosError
): Promise<CommonError> | void => {
if (!accessToken) {
if (error?.response?.status === 500) {
return Promise.reject(networkErrors.SERVER_ERROR)
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: false
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}
return Promise.reject(errorObj)
}
} else {
const showModal = !error.request?.responseURL.includes(
'/auth/login',
'/auth/signup'
)

if (
error.request?.responseURL &&
error.request?.responseURL.includes('logout') &&
error?.response?.status === 401
) {
handleLogout()
return Promise.reject(networkErrors.EXPIRE_TOKEN)
} else if (error?.response?.status === 401) {
localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN)
alert('로그인 세션이 만료되었습니다. 다시 로그인해주세요.')
location.replace('/signin')
handleLogout()
return
} else if (error?.response?.status === 500) {
setErrorModal(networkErrors.SERVER_ERROR)
return
} else {
const errorObj = {
status: error?.response?.status,
message: (error?.response?.data as string) ?? '',
isShowModal: showModal
}
if (errorObj.isShowModal) {
setErrorModal(errorObj)
}

return Promise.reject(errorObj)
}
}
}

const adminRequestInterceptor = adminInstance.interceptors.request.use(
adminConfig,
apiErrorInterceptor
)

const requestInterceptor = baseInstance.interceptors.request.use(
authConfig,
apiErrorInterceptor
)

const adminResponseInterceptor = adminInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

const responseInterceptor = baseInstance.interceptors.response.use(
response => response,
responseErrorInterceptor
)

useEffect(() => {
return () => {
// interceptor 해제
baseInstance.interceptors.request.eject(requestInterceptor)
baseInstance.interceptors.response.eject(responseInterceptor)
adminInstance.interceptors.request.eject(adminRequestInterceptor)
adminInstance.interceptors.response.eject(adminResponseInterceptor)
}
}, [
responseInterceptor,
requestInterceptor,
adminRequestInterceptor,
adminResponseInterceptor
])
}
Loading