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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 Down Expand 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 number Diff line number Diff 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 Down Expand 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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