diff --git a/public/icons/ver3/logo_ver3.0.png b/public/icons/ver3/logo_ver3.0.png new file mode 100644 index 00000000..1b78dbde Binary files /dev/null and b/public/icons/ver3/logo_ver3.0.png differ diff --git a/src/app/_api/adminTopics/editAdminTopic.ts b/src/app/_api/adminTopics/editAdminTopic.ts index f4cb1840..96e6ede1 100644 --- a/src/app/_api/adminTopics/editAdminTopic.ts +++ b/src/app/_api/adminTopics/editAdminTopic.ts @@ -1,9 +1,9 @@ -import axiosInstance from '@/lib/axios/axiosInstance'; +import axiosInstanceForAdmin from '@/lib/axios/axiosInstanceForAdmin'; import { editAdminTopicType } from '@/lib/types/requestedTopicType'; const editAdminTopic = async ({ topicId, isExposed, categoryCode, title }: editAdminTopicType) => { - await axiosInstance.put(`/admin/topics/${topicId}`, { + await axiosInstanceForAdmin.put(`/admin/topics/${topicId}`, { isExposed, categoryCode, title, diff --git a/src/app/_api/adminTopics/getAdminTopics.ts b/src/app/_api/adminTopics/getAdminTopics.ts index d7041d24..4a6448f9 100644 --- a/src/app/_api/adminTopics/getAdminTopics.ts +++ b/src/app/_api/adminTopics/getAdminTopics.ts @@ -1,4 +1,4 @@ -import axiosInstance from '@/lib/axios/axiosInstance'; +import axiosInstanceForAdmin from '@/lib/axios/axiosInstanceForAdmin'; interface GetTopicsType { cursorId?: number | null; @@ -13,7 +13,7 @@ const getAdminTopics = async ({ cursorId }: GetTopicsType) => { params.append('cursorId', cursorId.toString()); } - const response = await axiosInstance.get(`/admin/topics?${params.toString()}`); + const response = await axiosInstanceForAdmin.get(`/admin/topics?${params.toString()}`); return response.data; }; diff --git a/src/app/_api/notice/createNotice.ts b/src/app/_api/notice/createNotice.ts index 57bc951e..b2cfe0ad 100644 --- a/src/app/_api/notice/createNotice.ts +++ b/src/app/_api/notice/createNotice.ts @@ -1,4 +1,4 @@ -import axiosInstance from '@/lib/axios/axiosInstance'; +import axiosInstanceForAdmin from '@/lib/axios/axiosInstanceForAdmin'; import { NoticeCreateType } from '@/lib/types/noticeType'; interface ResponseType { @@ -6,7 +6,7 @@ interface ResponseType { } const createNotice = async (data: NoticeCreateType) => { - const response = await axiosInstance.post('/admin/notices', data); + const response = await axiosInstanceForAdmin.post('/admin/notices', data); return response.data; }; diff --git a/src/app/_api/notice/deleteNotice.ts b/src/app/_api/notice/deleteNotice.ts index 9ebcda99..4fbf1134 100644 --- a/src/app/_api/notice/deleteNotice.ts +++ b/src/app/_api/notice/deleteNotice.ts @@ -1,7 +1,7 @@ -import axiosInstance from '@/lib/axios/axiosInstance'; +import axiosInstanceForAdmin from '@/lib/axios/axiosInstanceForAdmin'; const deleteNotice = async (noticeId: number) => { - await axiosInstance.delete(`/admin/notices/${noticeId}`); + await axiosInstanceForAdmin.delete(`/admin/notices/${noticeId}`); }; export default deleteNotice; diff --git a/src/app/_api/notice/getAdminNotices.ts b/src/app/_api/notice/getAdminNotices.ts index 4799f6ed..ac200969 100644 --- a/src/app/_api/notice/getAdminNotices.ts +++ b/src/app/_api/notice/getAdminNotices.ts @@ -1,8 +1,8 @@ -import axiosInstance from '@/lib/axios/axiosInstance'; +import axiosInstanceForAdmin from '@/lib/axios/axiosInstanceForAdmin'; import { AdminNoticeType } from '@/lib/types/noticeType'; const getAdminNotices = async () => { - const result = await axiosInstance.get('/admin/notices'); + const result = await axiosInstanceForAdmin.get('/admin/notices'); return result.data; }; diff --git a/src/app/_api/notice/sendNoticeAlarm.ts b/src/app/_api/notice/sendNoticeAlarm.ts index d4720e5f..23c2a8a6 100644 --- a/src/app/_api/notice/sendNoticeAlarm.ts +++ b/src/app/_api/notice/sendNoticeAlarm.ts @@ -1,7 +1,7 @@ -import axiosInstance from '@/lib/axios/axiosInstance'; +import axiosInstanceForAdmin from '@/lib/axios/axiosInstanceForAdmin'; const sendNoticeAlarm = async (noticeId: number) => { - await axiosInstance.post(`/admin/notices/${noticeId}/alarm`); + await axiosInstanceForAdmin.post(`/admin/notices/${noticeId}/alarm`); }; export default sendNoticeAlarm; diff --git a/src/app/_api/notice/updateNotice.ts b/src/app/_api/notice/updateNotice.ts index d852f168..76014deb 100644 --- a/src/app/_api/notice/updateNotice.ts +++ b/src/app/_api/notice/updateNotice.ts @@ -1,4 +1,4 @@ -import axiosInstance from '@/lib/axios/axiosInstance'; +import axiosInstanceForAdmin from '@/lib/axios/axiosInstanceForAdmin'; import { NoticeCreateType } from '@/lib/types/noticeType'; interface UpdateNoticeRequestType { @@ -7,7 +7,7 @@ interface UpdateNoticeRequestType { } const updateNotice = async ({ noticeData, noticeId }: UpdateNoticeRequestType) => { - await axiosInstance.put(`/admin/notices/${noticeId}`, noticeData); + await axiosInstanceForAdmin.put(`/admin/notices/${noticeId}`, noticeData); }; export default updateNotice; diff --git a/src/app/_api/notice/updateNoticePublic.ts b/src/app/_api/notice/updateNoticePublic.ts index 7e3b656b..d15acd9c 100644 --- a/src/app/_api/notice/updateNoticePublic.ts +++ b/src/app/_api/notice/updateNoticePublic.ts @@ -1,7 +1,7 @@ -import axiosInstance from '@/lib/axios/axiosInstance'; +import axiosInstanceForAdmin from '@/lib/axios/axiosInstanceForAdmin'; const updateNoticePublic = async (noticeId: number) => { - await axiosInstance.patch(`/admin/notices/${noticeId}`); + await axiosInstanceForAdmin.patch(`/admin/notices/${noticeId}`); }; export default updateNoticePublic; diff --git a/src/app/_api/notice/uploadNoticeImages.ts b/src/app/_api/notice/uploadNoticeImages.ts index ec25800b..820738af 100644 --- a/src/app/_api/notice/uploadNoticeImages.ts +++ b/src/app/_api/notice/uploadNoticeImages.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import axiosInstance from '@/lib/axios/axiosInstance'; +import axiosInstanceForAdmin from '@/lib/axios/axiosInstanceForAdmin'; interface UploadImageType { order: number; @@ -19,7 +19,7 @@ interface PresignedResponseType { const uploadNoticeImages = async ({ noticeId, imageFileData, imageExtensionData }: UploadNoticeImagesProps) => { // 1. Presigned url 발급 요청 - const presignedResponse = await axiosInstance.post( + const presignedResponse = await axiosInstanceForAdmin.post( `/admin/notices/${noticeId}/presigned-url`, imageExtensionData ); @@ -34,7 +34,7 @@ const uploadNoticeImages = async ({ noticeId, imageFileData, imageExtensionData }); // 3. 이미지 업로드 완료 서버에 알림 - await axiosInstance.post(`/admin/notices/${noticeId}/upload-complete`, imageExtensionData); + await axiosInstanceForAdmin.post(`/admin/notices/${noticeId}/upload-complete`, imageExtensionData); }; export default uploadNoticeImages; diff --git a/src/app/admin/layout.css.ts b/src/app/admin/layout.css.ts index 53d27a71..89623448 100644 --- a/src/app/admin/layout.css.ts +++ b/src/app/admin/layout.css.ts @@ -1,5 +1,5 @@ import { style } from '@vanilla-extract/css'; -import { Header } from '@/styles/font.css'; +import { Header, Body } from '@/styles/font.css'; import { vars } from '@/styles/theme.css'; export const container = style({ @@ -28,3 +28,24 @@ export const title = style([ export const main = style({ flexGrow: 1, }); + +export const page = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '100vh', +}); + +export const logout = style([ + Body, + { + padding: '1rem 2rem', + backgroundColor: vars.color.bluegray6, + borderRadius: '0.8rem', + opacity: 0.6, + + ':hover': { + opacity: 1, + }, + }, +]); diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 1a08be8a..02f6ae82 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,30 +1,53 @@ +'use client'; + +import { usePathname } from 'next/navigation'; import { ReactNode } from 'react'; import * as styles from './layout.css'; + import NavLinks from './_components/NavLinks'; +import { removeCookie } from '@/lib/utils/cookie'; interface AdminNoticeLayoutProps { children: ReactNode; } +const HIDE_PATH = ['/admin', '/admin/login']; + export default function AdminNoticeLayout({ children }: AdminNoticeLayoutProps) { + const path = usePathname(); + const isHideNav = path && HIDE_PATH.includes(path); + + const handleClickLogout = () => { + removeCookie('admin-accessToken'); + removeCookie('admin-refreshToken'); + + location.href = '/admin'; + }; + return (
-
-

🤍 리스티웨이브 관리

- -
+ {!isHideNav && ( +
+

🤍 리스티웨이브 관리

+ + +
+ )} +
{children}
); diff --git a/src/app/admin/login/page.tsx b/src/app/admin/login/page.tsx new file mode 100644 index 00000000..230972f2 --- /dev/null +++ b/src/app/admin/login/page.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useForm } from 'react-hook-form'; + +import * as styles from './pgae.css'; + +import axiosInstance from '@/lib/axios/axiosInstance'; +import { setCookie } from '@/lib/utils/cookie'; + +interface FormValuesType { + account: string; + password: string; +} + +export default function AdminLogin() { + const router = useRouter(); + const { register, handleSubmit } = useForm({ + defaultValues: { + account: '', + password: '', + }, + }); + + const onSubmit = async (values: FormValuesType) => { + try { + const { data } = await axiosInstance.post('/admin/login', values); + + const adminAccessToken = data.accessToken; + const adminRefreshToken = data.refreshToken; + + setCookie('admin-accessToken', adminAccessToken, 'AT'); + setCookie('admin-refreshToken', adminRefreshToken, 'ADMIN'); + + router.push('/admin/topics'); + } catch (error) { + console.log(error); + } + }; + + return ( +
+
+
+ + +
+
+ + +
+ +
+
+ ); +} diff --git a/src/app/admin/login/pgae.css.ts b/src/app/admin/login/pgae.css.ts new file mode 100644 index 00000000..838c539e --- /dev/null +++ b/src/app/admin/login/pgae.css.ts @@ -0,0 +1,67 @@ +import { style } from '@vanilla-extract/css'; + +import { vars } from '@/styles/theme.css'; +import { BodyRegular } from '@/styles/font.css'; + +export const page = style({ + width: '100%', + minHeight: '100vh', + + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}); + +export const form = style({ + padding: '2rem', + + width: '100%', + maxWidth: '640px', + + display: 'flex', + gap: '2.5rem', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', +}); + +export const field = style({ + width: '100%', + + display: 'flex', + gap: '1rem', + flexDirection: 'column', +}); + +export const label = style({ + fontSize: '2rem', + fontWeight: 500, +}); + +export const input = style([ + BodyRegular, + { + padding: '1.2rem 2.4rem', + borderRadius: '0.8rem', + border: `1px solid ${vars.color.lightblue}`, + + selectors: { + '&:focus': { + border: `1px solid ${vars.color.blue}`, + }, + }, + }, +]); + +export const button = style({ + width: '100%', + padding: '1.5rem', + marginTop: '3rem', + + fontSize: '2rem', + fontWeight: 500, + + borderRadius: '0.8rem', + backgroundColor: vars.color.blue, + color: vars.color.white, +}); diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 00000000..097830a2 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,14 @@ +import Link from 'next/link'; + +import * as styles from './layout.css'; +import Image from 'next/image'; + +export default function AdminPage() { + return ( +
+ + 관리자페이지 로고 + +
+ ); +} diff --git a/src/lib/axios/axiosInstanceForAdmin.ts b/src/lib/axios/axiosInstanceForAdmin.ts new file mode 100644 index 00000000..32639a6e --- /dev/null +++ b/src/lib/axios/axiosInstanceForAdmin.ts @@ -0,0 +1,77 @@ +import axios from 'axios'; +import { getCookie, removeCookie, setCookie } from '../utils/cookie'; + +const axiosInstanceForAdmin = axios.create({ + baseURL: process.env.NEXT_PUBLIC_SERVER_DOMAIN, +}); + +axiosInstanceForAdmin.interceptors.request.use( + (config) => { + const accessToken = getCookie('admin-accessToken'); + + if (accessToken) { + config.headers.Authorization = `Bearer ${accessToken}`; + } + + return config; + }, + (error) => { + console.log(error); + return Promise.reject(error); + } +); + +let isRefreshing = false; + +axiosInstanceForAdmin.interceptors.response.use( + (res) => res, + async (error) => { + const originalRequest = error.config; + const refreshToken = getCookie('admin-refreshToken'); + + if (error.response?.status === 401 && error.response?.data.error === 'UNAUTHORIZED') { + if (!isRefreshing && refreshToken === undefined) { + // accessToken 만료되었는데, refreshToken 없는 경우, storage 비우기 + removeCookie('admin-accessToken'); + removeCookie('admin-refreshToken'); + + isRefreshing = true; + + alert('다시 로그인해주세요.'); + location.href = '/admin'; + } + + if (!isRefreshing) { + isRefreshing = true; + + try { + // instance 대신 axios 요청 + // refreshToken으로 accessToken 재발급 요청 + const { data } = await axios.get(`${process.env.NEXT_PUBLIC_SERVER_DOMAIN}/auth/token`, { + headers: { + Authorization: `Bearer ${refreshToken}`, + }, + }); + + const newAccessToken = data.accessToken; + setCookie('admin-accessToken', newAccessToken, 'AT'); + + originalRequest.headers.authorization = `Bearer ${newAccessToken}`; + return axiosInstanceForAdmin(originalRequest); + } catch (error) { + // refreshToken 생성 실패 시, + removeCookie('admin-accessToken'); + removeCookie('admin-refreshToken'); + + alert('다시 로그인해주세요.'); + location.href = '/admin'; + } finally { + isRefreshing = false; + } + } + } + return Promise.reject(error); + } +); + +export default axiosInstanceForAdmin; diff --git a/src/lib/utils/cookie.ts b/src/lib/utils/cookie.ts index d91cc933..90dd0e3e 100644 --- a/src/lib/utils/cookie.ts +++ b/src/lib/utils/cookie.ts @@ -2,11 +2,11 @@ import { Cookies } from 'react-cookie'; const cookies = new Cookies(); -export const setCookie = (name: string, value: string, type: 'AT' | 'RT') => { +export const setCookie = (name: string, value: string, type: 'AT' | 'RT' | 'ADMIN') => { return cookies.set(name, value, { path: '/', secure: true, - maxAge: type === 'AT' ? 60 * 30 : 60 * 60 * 24 * 14, // AT는 만료 시간 30분, RT는 14일로 설정 + maxAge: type === 'AT' ? 60 * 30 : type === 'RT' ? 60 * 60 * 24 * 14 : 60 * 60 * 2, // AT는 만료 시간 30분, RT는 14일로 설정 }); };