diff --git a/src/apis/authAxois.ts b/src/apis/authAxois.ts new file mode 100644 index 0000000..3ae91e1 --- /dev/null +++ b/src/apis/authAxois.ts @@ -0,0 +1,101 @@ +import axios from 'axios'; +import { getNewToken } from './login'; +import LocalStorage from '@/utils/localStorage'; + +const baseURL = process.env.NEXT_PUBLIC_SERVER_URL; + +const parseJwt = (token: string | null) => { + if (token) return JSON.parse(atob(token.split('.')[1])); +}; + +// 토큰 유효성 검사 및 재발급 +export const checkAndRefreshToken = async () => { + const accessToken = LocalStorage.getItem('access'); + const refreshToken = LocalStorage.getItem('refresh'); + + if (!accessToken || !refreshToken) { + // console.log('필요 토큰 미존재'); + return null; + } + + const decodedAccess = parseJwt(accessToken); + const decodedRefresh = parseJwt(refreshToken); + + if (decodedAccess && decodedAccess.exp * 1000 < Date.now()) { + // console.log('access 토큰 만료'); + if (decodedRefresh && decodedRefresh.exp * 1000 >= Date.now()) { + try { + // console.log('자동 재연장') + const { accessToken: newAccessToken } = await getNewToken(); + return newAccessToken; + } catch (error) { + LocalStorage.removeItem('access'); + LocalStorage.removeItem('refresh'); + return null; + } + } else { + // console.log('Refresh 토큰 만료'); + alert('토큰 만료로 자동 로그아웃되었습니다. 다시 로그인해주세요.'); + LocalStorage.removeItem('access'); + LocalStorage.removeItem('refresh'); + return null; + } + } + + return accessToken; +}; + +// 토큰 유효성 검사 함수 +export const AuthVerify = async () => { + const accessToken = await checkAndRefreshToken(); + + if (!accessToken) { + return false; + } + return true; +}; + +// 주어진 토큰을 사용하여 API 요청에 인증된 Axios 인스턴스를 초기화 +export const getAuthAxios = (token: string | null) => { + // Axios 인스턴스 생성 + const authAxios = axios.create({ + baseURL, + timeout: 8000, + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + // 응답 인터셉터 설정 + authAxios.interceptors.response.use( + (response) => { + return response; + }, + async (error) => { + const originalRequest = error.config; + + try { + // 토큰 갱신 시도 + const { accessToken } = await getNewToken(); + + if (!accessToken) { + throw new Error('토큰 갱신 실패'); + } + + // 갱신된 토큰으로 원래 요청 재시도 + originalRequest.headers.Authorization = `Bearer ${accessToken}`; + LocalStorage.setItem('access', accessToken); + + return authAxios(originalRequest); // 재시도된 요청 반환 + } catch (refreshError) { + alert('토큰이 만료되었습니다. 재로그인 후 시도해주세요!'); + LocalStorage.removeItem('access'); + LocalStorage.removeItem('refresh'); + window.location.href = '/login'; // 로그인 페이지로 리다이렉트 + return Promise.reject(refreshError); // 에러 전달 + } + }, + ); + + return authAxios; +}; diff --git a/src/apis/docs.ts b/src/apis/docs.ts index 597f9dd..e61b994 100644 --- a/src/apis/docs.ts +++ b/src/apis/docs.ts @@ -3,6 +3,7 @@ import axios, { AxiosResponse } from 'axios'; import { Server } from './settings'; import LocalStorage from '@/utils/localStorage'; import { ISearchResult, ISearchResultList, SearchResult } from '@/types/search'; +import { getAuthAxios } from './authAxois'; const baseURL = process.env.NEXT_PUBLIC_SERVER_URL; @@ -22,15 +23,13 @@ export const getSearchResult = async (keyword: string): Promise { try { - const token = LocalStorage.getItem('access'); - const result = await axios.post(`${baseURL}docs/`, body, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - return result.data; + const access = LocalStorage.getItem('access'); + + const authAxios = getAuthAxios(access); + const response = await authAxios.post(`${baseURL}docs/`, body); + + return response.data; } catch (error) { - alert('로그아웃 후 다시 시도해주세요!'); throw error; } }; diff --git a/src/apis/login.ts b/src/apis/login.ts index e8560cc..988e8b7 100644 --- a/src/apis/login.ts +++ b/src/apis/login.ts @@ -18,71 +18,36 @@ export const postCode = async (body: postCodeBody) => { } return response.data; } catch (error) { - console.error('에러 발생', error); + // console.error('에러 발생', error); throw error; } }; // 토큰 재발급 -// export const getNewRefreshToken = async () => { -// const accessToken = localStorage.getItem("access"); -// const refreshToken = localStorage.getItem("refresh"); -// const result = await axios.post( -// "http://front.cau-likelion.org/refresh", -// { -// // 요청 body -// refreshToken, -// }, -// { -// // headers.Authorization 필수 -// headers: { -// Authorization: accessToken, -// }, -// } -// ); -// console.log(result) -// return result.data; -// }; - -// 회원가입 -export const signUp = async (emailInput: string, nameInput: string) => { - // 구글 로그인 후 context에 이메일 저장하고 있음 +export const getNewToken = async () => { + const refreshToken = localStorage.getItem('refresh'); try { - const response = await axios.post(`${baseURL}users/signup/`, { email: emailInput, name: nameInput }); - if (response.data.status === '201') { - const accessToken = response.data.data.token.access_token; - const refreshToken = response.data.data.token.refresh_token; - - LocalStorage.setItem('access', accessToken); - LocalStorage.setItem('refresh', refreshToken); + const response = await axios.post( + `${baseURL}users/token/refresh/`, + { + refresh: refreshToken, + }, + { + headers: { + Authorization: `Bearer ${refreshToken}`, + }, + }, + ); + + if (response.status === 200) { + const newAccessToken = response.data.access; + return { + accessToken: newAccessToken, + }; + } else { + throw new Error('토큰 갱신 실패'); } - return response.data; - } catch (error) { - console.log(error); - return false; - } -}; - -// 랜덤 닉네임 부여 -export const getRandomNickname = async () => { - try { - const response = await axios.get(`${baseURL}users/rand-name/`); - return response.data; - } catch (error) { - console.log(error); - return false; - } -}; - -// 닉네임 중복여부 확인 -export const checkNickName = async (name: string) => { - var resultString = name.replace(/\s+/g, '+'); - try { - const response = await axios.get(`${baseURL}users/check-name/?name=${resultString}`); - return response.data; } catch (error) { - // 사용할 수 없는 닉네임 - console.log(error); - return false; + throw new Error('토큰 갱신 요청 실패'); } }; diff --git a/src/apis/signup.ts b/src/apis/signup.ts new file mode 100644 index 0000000..f8ab7b3 --- /dev/null +++ b/src/apis/signup.ts @@ -0,0 +1,47 @@ +import axios from 'axios'; +import LocalStorage from '@/utils/localStorage'; + +const baseURL = process.env.NEXT_PUBLIC_SERVER_URL; + +// 회원가입 +export const signUp = async (emailInput: string, nameInput: string) => { + // 구글 로그인 후 context에 이메일 저장하고 있음 + try { + const response = await axios.post(`${baseURL}users/signup/`, { email: emailInput, name: nameInput }); + if (response.data.status === '201') { + const accessToken = response.data.data.token.access_token; + const refreshToken = response.data.data.token.refresh_token; + + LocalStorage.setItem('access', accessToken); + LocalStorage.setItem('refresh', refreshToken); + } + return response.data; + } catch (error) { + console.log(error); + return false; + } +}; + +// 랜덤 닉네임 부여 +export const getRandomNickname = async () => { + try { + const response = await axios.get(`${baseURL}users/rand-name/`); + return response.data; + } catch (error) { + console.log(error); + return false; + } +}; + +// 닉네임 중복여부 확인 +export const checkNickName = async (name: string) => { + let resultString = name.replace(/\s+/g, '+'); + try { + const response = await axios.get(`${baseURL}users/check-name/?name=${resultString}`); + return response.data; + } catch (error) { + // 사용할 수 없는 닉네임 + console.log(error); + return false; + } +}; diff --git a/src/components/common/navbar/NavBar.tsx b/src/components/common/navbar/NavBar.tsx index e058e12..e0780ab 100644 --- a/src/components/common/navbar/NavBar.tsx +++ b/src/components/common/navbar/NavBar.tsx @@ -1,13 +1,12 @@ 'use client'; import Image from 'next/image'; -import { useRouter } from 'next/navigation'; -import { token } from '@/app/recoilContextProvider'; -import { useRecoilValue } from 'recoil'; +import { usePathname, useRouter } from 'next/navigation'; import { getRandomDoc } from '@/apis/viewer'; import { useEffect, useState } from 'react'; import { useMediaQuery } from 'react-responsive'; import SearchHeaderInput from '@/components/search/searchHeaderInput/searchHeaderInput'; +import { AuthVerify } from '@/apis/authAxois'; import * as S from './NavBar.styled'; export interface IMenu { @@ -18,8 +17,7 @@ export interface IMenu { const NavBar = () => { const isMobile = useMediaQuery({ query: '(max-width: 540px)' }); const router = useRouter(); - - const { access: tokenState } = useRecoilValue(token); + const pathname = usePathname(); const [isLogin, setIsLogin] = useState(false); const gotoRandomDoc = async () => { @@ -29,9 +27,24 @@ const NavBar = () => { router.push(`/viewer?title=${encodedTitle}`); }; + const handlePostClick = () => { + if (!isLogin) { + alert('🦁 로그인을 먼저 해주세요 🦁'); + router.push('/login'); + } else { + router.push('/post'); + } + }; + + // 유효한 토큰을 가진 경우에만 상태 변경 useEffect(() => { - if (tokenState) setIsLogin(true); - }, [tokenState]); + const checkLoginStatus = async () => { + const loginStatus = await AuthVerify(); + setIsLogin(loginStatus === true); + }; + + checkLoginStatus(); + }, [pathname]); return ( <> @@ -79,14 +92,11 @@ const NavBar = () => { { if (!isLogin) { - alert('🦁 로그인을 먼저 해주세요 🦁'); router.push('/login'); - } else { - router.push('/post'); } }} - src="/img/newPost.png" - alt={'newPost'} + src={isLogin ? '/img/welcome.png' : '/img/login.png'} + alt={isLogin ? '로그인버튼' : '로그인'} width={44} height={44} style={{ cursor: 'pointer' }} @@ -100,17 +110,13 @@ const NavBar = () => { height={44} style={{ cursor: 'pointer' }} /> - {isLogin ? ( - {'로그인버튼'} - ) : ( + {!isMobile && ( { - router.push(`/login`); - }} + onClick={handlePostClick} + src="/img/newPost.png" + alt={'newPost'} width={44} height={44} - alt={'로그인버튼'} style={{ cursor: 'pointer' }} /> )} diff --git a/src/components/common/viewer/LinkBox.tsx b/src/components/common/viewer/LinkBox.tsx index 402beba..7b3eb55 100644 --- a/src/components/common/viewer/LinkBox.tsx +++ b/src/components/common/viewer/LinkBox.tsx @@ -1,5 +1,6 @@ +import { AuthVerify } from '@/apis/authAxois'; import { useRouter } from 'next/navigation'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; interface LinkBoxProps { width?: string; @@ -11,16 +12,21 @@ interface LinkBoxProps { const LinkBox: React.FC = ({ width = '51', height = '34', text, docTitle = '' }) => { const router = useRouter(); - let token: string | null; - if (typeof window !== 'undefined') { - token = localStorage.getItem('access'); - } + const [isLogin, setIsLogin] = useState(false); + + useEffect(() => { + const checkLoginStatus = async () => { + const loginStatus = await AuthVerify(); + setIsLogin(loginStatus === true); + }; + + checkLoginStatus(); + }, []); const handleClick = () => { if (text === '편집') { - if (!token) { - alert('🦁로그인을 먼저 해주세요🦁'); - router.push('/login'); + if (!isLogin) { + alert('로그인 후 편집이 가능합니다'); } else { let encodedTitle = encodeURIComponent(docTitle); router.push(`/edit?title=${encodedTitle}`); diff --git a/src/components/userName/ConfirmModal.tsx b/src/components/userName/ConfirmModal.tsx index 53c3621..85a2f68 100644 --- a/src/components/userName/ConfirmModal.tsx +++ b/src/components/userName/ConfirmModal.tsx @@ -1,11 +1,11 @@ import styled from 'styled-components'; import React, { useState } from 'react'; import Image from 'next/image'; -import { signUp } from '@/apis/login'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { token, userEmailAtom, userNameAtom } from '@/app/recoilContextProvider'; import { useRouter } from 'next/navigation'; import LocalStorage from '@/utils/localStorage'; +import { signUp } from '@/apis/signup'; const ConfirmModal = (props: { setModalIsOpen: React.Dispatch>; nickname: string }) => { const route = useRouter(); diff --git a/src/components/userName/UserNickNameMain.tsx b/src/components/userName/UserNickNameMain.tsx index 1f6e0df..3fb6624 100644 --- a/src/components/userName/UserNickNameMain.tsx +++ b/src/components/userName/UserNickNameMain.tsx @@ -3,7 +3,7 @@ import Image from 'next/image'; import React, { useState } from 'react'; import styled from 'styled-components'; import ConfirmModal from './ConfirmModal'; -import { checkNickName, getRandomNickname } from '@/apis/login'; +import { checkNickName, getRandomNickname } from '@/apis/signup'; const UserNickNameMain = () => { const [userNickname, setUserNickname] = useState('어쩔사자티비'); diff --git a/src/utils/historyUtils.ts b/src/utils/historyUtils.ts index fdb6416..2628437 100644 --- a/src/utils/historyUtils.ts +++ b/src/utils/historyUtils.ts @@ -1,66 +1,74 @@ -export const renderOldStr = (change: any) => { - var oldStr = change.replace( - /(.*?)<\/modified_to>|(.*?)<\/added>/g, - function ([match, p1, p2]: any) { - return p1 ? ' ' : p2 ? ' ' : ''; - }, - ); +export const cleanImageTag = (str: string): string => { + return str + .replace(/]*?)\s*>/g, (match: string, p1: string) => { + p1 = p1.replace(/(src\s*=\s*["'])(.*?)(["'])/g, (match: string, prefix: string, url: string, suffix: string) => { + return prefix + url.replace(/\s+/g, '') + suffix; + }); + return ``; + }) + .replace(/"\s/g, '"') + .replace(/\s"/g, '"') + .trim(); +}; - oldStr = oldStr.replace(//g, ' ' + ""); - oldStr = oldStr.replace(/<\/added>/g, ''); - oldStr = oldStr.replace(//g, ' ' + ""); - oldStr = oldStr.replace(/<\/modified_to>/g, ''); +export const renderOldStr = (change: string): { __html: string } => { + let oldStr = change.replace(/(.*?)<\/modified_to>|(.*?)<\/added>/g, (match, p1, p2) => + p1 ? ' ' : p2 ? ' ' : '', + ); - oldStr = oldStr.replace(//g, ' ' + ""); - oldStr = oldStr.replace(/<\/deleted>/g, ''); - oldStr = oldStr.replace(//g, ' ' + ""); - oldStr = oldStr.replace(/<\/modified_from>/g, ''); + oldStr = oldStr + .replace(//g, " ") + .replace(/<\/added>/g, '') + .replace(//g, " ") + .replace(/<\/modified_to>/g, '') + .replace(//g, " ") + .replace(/<\/deleted>/g, '') + .replace(//g, " ") + .replace(/<\/modified_from>/g, '') + .replace(/\r\n|\n|\r|\\r\\n|\\n|\\r/g, '
'); - oldStr = oldStr.replace(/\r\n|\n|\r|\\r\\n|\\n|\\r/g, '
'); + oldStr = cleanImageTag(oldStr); - return { __html: oldStr }; + return { __html: oldStr }; }; -export const renderNewStr = (change: any) => { - let newStr = change.replace( - /(.*?)<\/modified_from>|(.*?)<\/deleted>/g, - function (): string { - return ''; - }, - ); - - newStr = newStr.replace(//g, ' ' + ""); - newStr = newStr.replace(/<\/deleted>/g, ''); - newStr = newStr.replace(//g, ' ' + ""); - newStr = newStr.replace(/<\/modified_from>/g, ''); +export const renderNewStr = (change: string): { __html: string } => { + let newStr = change.replace(/(.*?)<\/modified_from>|(.*?)<\/deleted>/g, () => ''); - newStr = newStr.replace(//g, ' ' + ""); - newStr = newStr.replace(/<\/added>/g, ''); - newStr = newStr.replace(//g, ' ' + ""); - newStr = newStr.replace(/<\/modified_to>/g, ''); + newStr = newStr + .replace(//g, " ") + .replace(/<\/deleted>/g, '') + .replace(//g, " ") + .replace(/<\/modified_from>/g, '') + .replace(//g, " ") + .replace(/<\/added>/g, '') + .replace(//g, " ") + .replace(/<\/modified_to>/g, '') + .replace(/\r\n|\n|\r|\\r\\n|\\n|\\r/g, '
'); - newStr = newStr.replace(/\r\n|\n|\r|\\r\\n|\\n|\\r/g, '
'); + newStr = cleanImageTag(newStr); - return { __html: newStr }; + return { __html: newStr }; }; -export const renderFirstStr = (content: any) => { - let firstStr = content; - firstStr = firstStr.replace(/\r\n|\n|\r|\\r\\n|\\n|\\r/g, '
'); - firstStr = "" + firstStr + ''; +export const renderFirstStr = (content: string): { __html: string } => { + let firstStr = content.replace(/\r\n|\n|\r|\\r\\n|\\n|\\r/g, '
'); + firstStr = `${firstStr}`; - return { __html: firstStr }; + firstStr = cleanImageTag(firstStr); + + return { __html: firstStr }; }; -export const parseAndFormatDate = (dateString: string) => { - const date = new Date(dateString); - const options: Intl.DateTimeFormatOptions = { - year: 'numeric', - month: 'numeric', - day: 'numeric', - weekday: 'short', - hour: 'numeric', - minute: 'numeric', - }; - return date.toLocaleString('ko-KR', options); -}; \ No newline at end of file +export const parseAndFormatDate = (dateString: string): string => { + const date = new Date(dateString); + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'numeric', + day: 'numeric', + weekday: 'short', + hour: 'numeric', + minute: 'numeric', + }; + return date.toLocaleString('ko-KR', options); +};