diff --git a/admin/src/App.jsx b/admin/src/App.jsx index 0620511..ba5dcf4 100644 --- a/admin/src/App.jsx +++ b/admin/src/App.jsx @@ -1,15 +1,18 @@ import React from 'react'; -import { Outlet } from 'react-router-dom'; +import { Outlet, useLocation } from 'react-router-dom'; import AdminHeader from '@/components/header/AdminHeader'; import TabHeader from '@/components/header/TabHeader'; import { DateProvider } from '@/context/dateContext'; function App() { + const location = useLocation(); + const hideHeader = ['/login', '/error'].includes(location.pathname); + return (
- - + {!hideHeader && } + {!hideHeader && }
diff --git a/admin/src/api/auth/index.js b/admin/src/api/auth/index.js new file mode 100644 index 0000000..387dd1a --- /dev/null +++ b/admin/src/api/auth/index.js @@ -0,0 +1,16 @@ +import { post, get, patch, put, del } from '@/api/index'; + +const loginPhone = phoneNumber => { + return post('/login/phone', { + phoneNumber: phoneNumber, + }); +}; + +const loginCode = (phoneNumber, validateCode) => { + return post('/login/code', { + phoneNumber: phoneNumber, + code: validateCode, + }); +}; + +export { loginPhone, loginCode }; diff --git a/admin/src/api/index.js b/admin/src/api/index.js index b61a926..5987066 100644 --- a/admin/src/api/index.js +++ b/admin/src/api/index.js @@ -1,5 +1,5 @@ const ApiRequest = async (url, method, body) => { - const accessToken = localStorage.getItem('userInfo'); + const accessToken = sessionStorage.getItem('userInfo'); try { const options = { diff --git a/admin/src/api/miniQuiz/index.js b/admin/src/api/miniQuiz/index.js index d42c2dc..3a4e5b4 100644 --- a/admin/src/api/miniQuiz/index.js +++ b/admin/src/api/miniQuiz/index.js @@ -2,8 +2,8 @@ import { post, get, patch, put, del } from '@/api/index'; const getAdminMiniQuiz = day => get(`/admin/quiz/${day}`); -const putAdminMiniQuiz = body => - put(`/admin/quiz`, { +const putAdminMiniQuiz = (day, body) => + put(`/admin/quiz/${day}`, { quizId: body.quizId, quizDescription: body.quizDescription, quizQuestions: { diff --git a/admin/src/components/buttons/BlackButton.jsx b/admin/src/components/buttons/BlackButton.jsx index 352b9e0..1991d20 100644 --- a/admin/src/components/buttons/BlackButton.jsx +++ b/admin/src/components/buttons/BlackButton.jsx @@ -1,11 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -function BlackButton({ value, onClickFunc }) { +function BlackButton({ value, onClickFunc, disabled = false }) { return ( @@ -15,6 +16,7 @@ function BlackButton({ value, onClickFunc }) { BlackButton.propTypes = { value: PropTypes.string.isRequired, onClickFunc: PropTypes.func.isRequired, + disabled: PropTypes.bool, }; //memo를 이용하여 rerender 방지 diff --git a/admin/src/hooks/useFetch.js b/admin/src/hooks/useFetch.js index be41cb0..725d0ce 100644 --- a/admin/src/hooks/useFetch.js +++ b/admin/src/hooks/useFetch.js @@ -1,6 +1,8 @@ import { useState, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; function useFetch(api, params) { + const navigate = useNavigate(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -8,9 +10,12 @@ function useFetch(api, params) { const fetchData = useCallback(async () => { try { const response = await api(params); - if (typeof response === 'object') { - setData(response); + if (response.code === 'UNAUTHORIZED') { + navigate('/error'); + } else { + setData(response); + } } else if (typeof response === 'string') { setData({ message: response }); } else { diff --git a/admin/src/pages/ErrorPage.jsx b/admin/src/pages/ErrorPage.jsx new file mode 100644 index 0000000..39242b1 --- /dev/null +++ b/admin/src/pages/ErrorPage.jsx @@ -0,0 +1,10 @@ +import React from 'react'; + +function ErrorPage() { + return ( +
+ 접근 권한이 없습니다!! +
+ ); +} +export default ErrorPage; diff --git a/admin/src/pages/ProtectedRoute.jsx b/admin/src/pages/ProtectedRoute.jsx new file mode 100644 index 0000000..cbc2288 --- /dev/null +++ b/admin/src/pages/ProtectedRoute.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Navigate, Outlet } from 'react-router-dom'; + +const ProtectedRoute = () => { + const accessToken = sessionStorage.getItem('userInfo'); + + if (!accessToken) { + return ; + } + return ; +}; + +export default ProtectedRoute; diff --git a/admin/src/pages/login/Login.jsx b/admin/src/pages/login/Login.jsx new file mode 100644 index 0000000..d1abb73 --- /dev/null +++ b/admin/src/pages/login/Login.jsx @@ -0,0 +1,112 @@ +import React, { useState } from 'react'; +import isValidPhoneNumber from '@/utils/isValidPhoneNumber'; +import { loginPhone, loginCode } from '@/api/auth/index'; +import phoneNumberFormatting from '@/utils/phoneNumberFormatting'; +import BlackButton from '@/components/buttons/BlackButton'; +import { useNavigate } from 'react-router-dom'; + +function Login() { + const navigate = useNavigate(); + const [inputPhone, setInputPhone] = useState(''); + const [phoneValid, setPhoneValid] = useState(false); + const [firstPhoneClick, setFirstPhoneClick] = useState(false); + const [showAuth, setShowAuth] = useState(false); + + const [validateCode, setValidateCode] = useState(''); + const [codeValid, setCodeValid] = useState(false); + const [firstCodeClick, setFirstCodeClick] = useState(false); + const [alertText, setAlertText] = useState('인증번호 형식이 맞지 않습니다!'); + + const handlePhoneInput = e => { + setInputPhone(e.target.value); + setPhoneValid(isValidPhoneNumber(e.target.value)); + }; + + const handleCodeInput = e => { + setValidateCode(e.target.value); + setCodeValid(e.target.value.length === 6); + }; + + const handlePhoneAuth = async () => { + try { + const formattedPhone = phoneNumberFormatting(inputPhone); + await loginPhone(formattedPhone); + setInputPhone(formattedPhone); + setShowAuth(true); + } catch (error) { + console.error('loginPhone API 통신 실패:', error); + } + }; + + const handleCodeAuth = async () => { + if (validateCode === '111111') { + try { + const response = await loginCode(inputPhone, validateCode); + sessionStorage.setItem('userInfo', response.accessToken); + navigate('/'); + } catch (error) { + console.error('API 통신 실패:', error); + } + } else { + setAlertText('인증번호를 다시 확인해주세요!'); + setCodeValid(false); + } + }; + + const handleFirstPhoneClick = () => setFirstPhoneClick(true); + const handleFirstCodeClick = () => setFirstCodeClick(true); + + return ( +
+ {showAuth ? ( + <> +

+ 인증번호를 입력해주세요 +

+ + {!codeValid && firstCodeClick && ( + + {alertText} + + )} + + + ) : ( + <> +

+ 핸드폰 번호를 입력해주세요 +

+ + {!phoneValid && firstPhoneClick && ( + + 전화번호 형식이 맞지 않습니다! + + )} + + + )} +
+ ); +} + +export default Login; diff --git a/admin/src/pages/miniQuiz/MiniQuiz.jsx b/admin/src/pages/miniQuiz/MiniQuiz.jsx index 4e28843..61fac10 100644 --- a/admin/src/pages/miniQuiz/MiniQuiz.jsx +++ b/admin/src/pages/miniQuiz/MiniQuiz.jsx @@ -1,6 +1,6 @@ import React, { useState, useContext, useEffect } from 'react'; import AdminEditHeader from '@/components/header/AdminEditHeader'; -import AdminEditMiniQuizContent from './MiniQuizContent'; +import MiniQuizContent from './MiniQuizContent'; import BlackButton from '@/components/buttons/BlackButton'; import { getAdminMiniQuiz, putAdminMiniQuiz } from '@/api/miniQuiz/index'; import { DateContext } from '@/context/dateContext'; @@ -20,19 +20,42 @@ function MiniQuiz() { useEffect(() => { if (initialData) { - setQuizData(initialData); + if (initialData.code === 'NO_QUIZ_INFO') { + setQuizData({ + quizId: '0', + quizDescription: '', + quizQuestions: { + 1: '', + 2: '', + 3: '', + 4: '', + }, + }); + } else { + setQuizData(initialData); + } } }, [initialData]); - const handleChange = (field, value) => { - setQuizData(prevState => ({ - ...prevState, - [field]: value, - })); + const handleChange = (key, value) => { + if (key === 'quizDescription') { + setQuizData(prevState => ({ + ...prevState, + [key]: value, + })); + } else { + setQuizData(prevState => ({ + ...prevState, + quizQuestions: { + ...prevState.quizQuestions, + [key]: value, + }, + })); + } }; const handleSubmit = async () => { - const response = await putAdminMiniQuiz(quizData); + const response = await putAdminMiniQuiz(dateInfo, quizData); if (response.status === 200) { await refetch(); } else { @@ -57,7 +80,7 @@ function MiniQuiz() {
- + setOpenModal(true)} />
{openModal && ( diff --git a/admin/src/pages/miniQuiz/MiniQuizContent.jsx b/admin/src/pages/miniQuiz/MiniQuizContent.jsx index 1d03a58..9ba55cb 100644 --- a/admin/src/pages/miniQuiz/MiniQuizContent.jsx +++ b/admin/src/pages/miniQuiz/MiniQuizContent.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import InputForm from '@/components/form/InputForm'; function MiniQuizContent({ response, onChange }) { + // console.log(response); return ( <> onChange(`quizQuestions.${key}`, value)} + onChange={value => onChange(key, value)} /> ))} diff --git a/admin/src/router.jsx b/admin/src/router.jsx index 0e0b0d2..8fbb225 100644 --- a/admin/src/router.jsx +++ b/admin/src/router.jsx @@ -8,40 +8,52 @@ import Reward from '@/pages/reward/Reward'; import AdminEventStatus from '@/pages/AdminEventStatus/AdminEventStatus'; import UploadReward from '@/pages/UploadReward/UploadReward'; import UploadPrize from '@/pages/UploadPrize/UploadPrize'; +import Login from '@/pages/login/Login'; +import ProtectedRoute from '@/pages/ProtectedRoute'; +import ErrorPage from '@/pages/ErrorPage'; const router = createBrowserRouter([ + { + path: '/login', + element: , // Login 페이지를 루트로 설정 + }, { path: '/', element: , children: [ - { index: true, element: }, - { - path: 'miniQuizAnswer', - element: , - }, - { - path: 'draw', - element: , - }, - { - path: 'reward', - element: , - }, - { - path: 'adminEventStatus', - element: , - }, - { - path: 'adminEventStatus', - element: , - }, - { - path: 'uploadReward', - element: , - }, { - path: 'uploadPrize', - element: , + element: , + children: [ + { index: true, element: }, + { + path: 'miniQuizAnswer', + element: , + }, + { + path: 'draw', + element: , + }, + { + path: 'reward', + element: , + }, + { + path: 'adminEventStatus', + element: , + }, + { + path: 'uploadReward', + element: , + }, + { + path: 'uploadPrize', + element: , + }, + { + path: 'error', + element: , + }, + ], }, ], }, diff --git a/admin/src/utils/isValidPhoneNumber.js b/admin/src/utils/isValidPhoneNumber.js new file mode 100644 index 0000000..0dc90b8 --- /dev/null +++ b/admin/src/utils/isValidPhoneNumber.js @@ -0,0 +1,9 @@ +function isValidPhoneNumber(number) { + if (number.length < 9 || number.length > 11) { + return false; + } + const result = /^(01[016789]{1})[0-9]{3,4}[0-9]{4}$/; + return result.test(number); +} + +export default isValidPhoneNumber; diff --git a/admin/src/utils/phoneNumberFormatting.js b/admin/src/utils/phoneNumberFormatting.js new file mode 100644 index 0000000..3c7c728 --- /dev/null +++ b/admin/src/utils/phoneNumberFormatting.js @@ -0,0 +1,5 @@ +function phoneNumberFormatting(phoneNumber) { + return phoneNumber.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'); +} + +export default phoneNumberFormatting; diff --git a/service/src/components/buttons/BlackButton.jsx b/service/src/components/buttons/BlackButton.jsx deleted file mode 100644 index 3861fc9..0000000 --- a/service/src/components/buttons/BlackButton.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -function BlackButton({ onClickFunc }) { - return ( - - ); -} - -BlackButton.propTypes = { - onClickFunc: PropTypes.func.isRequired, -}; - -//memo를 이용하여 rerender 방지 -export default React.memo(BlackButton);