diff --git a/.github/workflows/admin-cicd.yml b/.github/workflows/admin-cicd.yml index a34f8f7..754d0b3 100644 --- a/.github/workflows/admin-cicd.yml +++ b/.github/workflows/admin-cicd.yml @@ -1,4 +1,4 @@ -name: Deploy to S3 and CloudFront +name: admin Deploy to S3 and CloudFront on: push: diff --git a/.github/workflows/service-cicd.yml b/.github/workflows/service-cicd.yml index 0c9d153..400e8dc 100644 --- a/.github/workflows/service-cicd.yml +++ b/.github/workflows/service-cicd.yml @@ -1,4 +1,4 @@ -name: Deploy to S3 and CloudFront +name: admin Deploy to S3 and CloudFront on: push: diff --git a/admin/src/components/header/AdminHeader.jsx b/admin/src/components/header/AdminHeader.jsx index 69eb657..b0046a1 100644 --- a/admin/src/components/header/AdminHeader.jsx +++ b/admin/src/components/header/AdminHeader.jsx @@ -15,54 +15,63 @@ function AdminHeader() { const [isPreviousDayDisabled, setIsPreviousDayDisabled] = useState(false); const [selectedDate, setSelectedDate] = useState(''); - useEffect(() => { + const checkAuthorization = () => { const token = sessionStorage.getItem('userInfo'); - if (token) { - try { - const decodedToken = jwtDecode(token); + if (!token) { + navigate('/login'); + return; + } - if (decodedToken && decodedToken.role === 'ADMIN') { - // Do Nothing - } else { - navigate('/error'); - } - } catch (error) { - console.error(error); + try { + const decodedToken = jwtDecode(token); + if (decodedToken?.role !== 'ADMIN') { navigate('/error'); } - } else { - navigate('/login'); + } catch (error) { + navigate('/error'); } - }, []); + }; useEffect(() => { - const getDate = async () => { + checkAuthorization(); + }, []); + + const fetchEventSchedules = async () => { + try { const response = await getEventSchedules(); if (response.code === 'UNAUTHORIZED') { navigate('/error'); - } else { - const startDate = new Date(response[0].date); - const finishDate = new Date(response[13].date); - const currentDate = new Date(dateInfo); - setIsPreviousDayDisabled(currentDate.getTime() === startDate.getTime()); - setIsNextDayDisabled(currentDate.getTime() === finishDate.getTime()); + return; } - }; - getDate(); - }, [dateInfo]); - const handleDateChange = event => { - setSelectedDate(event.target.value); + const startDate = new Date(response[0]?.date); + const finishDate = new Date(response[response.length - 1]?.date); + const currentDate = new Date(dateInfo); + + setIsPreviousDayDisabled(currentDate.getTime() === startDate.getTime()); + setIsNextDayDisabled(currentDate.getTime() === finishDate.getTime()); + } catch (error) { + navigate('/error'); + } + }; + + useEffect(() => { + fetchEventSchedules(); + }, []); + + const handleDateChange = event => setSelectedDate(event.target.value); + + const navigateToDate = newDate => { + const [, , tabName] = location.pathname.split('/'); + const newPath = `/${dateFormatting(newDate)}${tabName ? `/${tabName}` : ''}`; + navigate(newPath); }; const handlePreviousDay = () => { if (!isPreviousDayDisabled) { const previousDay = new Date(dateInfo); previousDay.setDate(previousDay.getDate() - 1); - const [, , tabName] = location.pathname.split('/'); - navigate( - `/${dateFormatting(previousDay)}${tabName !== undefined ? `/${tabName}` : ''}`, - ); + navigateToDate(previousDay); } }; @@ -70,15 +79,16 @@ function AdminHeader() { if (!isNextDayDisabled) { const nextDay = new Date(dateInfo); nextDay.setDate(nextDay.getDate() + 1); - const [, , tabName] = location.pathname.split('/'); - navigate( - `/${dateFormatting(nextDay)}${tabName !== undefined ? `/${tabName}` : ''}`, - ); + navigateToDate(nextDay); } }; - const handleSubmit = () => { - putEventSchedules(selectedDate); + const handleSubmit = async () => { + try { + await putEventSchedules(selectedDate); + } catch (error) { + console.error(error); + } }; return ( diff --git a/admin/src/pages/AdminEventStatus/EntryTable.jsx b/admin/src/pages/AdminEventStatus/EntryTable.jsx index 40e895e..c68eb86 100644 --- a/admin/src/pages/AdminEventStatus/EntryTable.jsx +++ b/admin/src/pages/AdminEventStatus/EntryTable.jsx @@ -1,39 +1,82 @@ import React, { useState, useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; import EntryRow from '@/pages/AdminEventStatus/EntryRow'; import RadioButton from '@/pages/AdminEventStatus/RadioButton'; import PageButton from '@/pages/AdminEventStatus/PageButton'; import { getDrawHistory } from '@/api/AdminEventStatus'; +const RADIO_BUTTON_VALUES = [10, 30, 50]; //버튼의 경우의 수가 3개라 상수로 선언 + +const EntryHeader = ({ totalRows, rowsPerPage, handleRowsPerPageChange }) => ( +
+
전체 {totalRows}
+
+ {RADIO_BUTTON_VALUES.map(value => ( + + ))} +
+
+); + +EntryHeader.propTypes = { + totalRows: PropTypes.number.isRequired, + rowsPerPage: PropTypes.number.isRequired, + handleRowsPerPageChange: PropTypes.func.isRequired, +}; + +const TableHeader = ({ desc, asc }) => ( +
+
+ 순번 + + +
+
전화번호
+
응모 시간
+
응모 결과
+
+); + +TableHeader.propTypes = { + desc: PropTypes.func.isRequired, + asc: PropTypes.func.isRequired, +}; + const EntryTable = () => { - const [rowsPerPage, setRowsPerPage] = useState(10); - const [totalRows, setTotalRows] = useState(0); - const [currentPage, setCurrentPage] = useState(1); - const [pageData, setPageData] = useState([]); - const [totalPages, setTotalPages] = useState(0); - const [sort, setSort] = useState('desc'); - const table = useRef(null); + const [rowsPerPage, setRowsPerPage] = useState(RADIO_BUTTON_VALUES[0]); //초기는 10개 보기 + const [totalRows, setTotalRows] = useState(0); //전체 데이터 수(표의 행) 설정 + const [currentPage, setCurrentPage] = useState(1); //현재 페이지 설정 + const [pageData, setPageData] = useState([]); //해당 페이지에 있는 데이터 저장하는 배열 + const [totalPages, setTotalPages] = useState(0); //총 페이지 수를 설정 + const [sort, setSort] = useState('desc'); //내림차순, 오름차순 설정 + const tableRef = useRef(null); //page 변경 시 맨 위로 돌아가기 위한 table DOM을 저장 useEffect(() => { - const getData = async () => { + const fetchData = async () => { try { - const response = await getDrawHistory( - currentPage - 1, + const { drawHistories, totalPages, totalItems } = await getDrawHistory( + currentPage - 1, //api는 0 페이지부터 시작이기에 -1해서 가져옴 rowsPerPage, sort, - ); - const { drawHistories, totalPages, totalItems } = response; + ); //응모 내역 가져오기 setPageData(drawHistories); setTotalPages(totalPages); setTotalRows(totalItems); } catch (error) { - console.error(error); + console.error('Failed to fetch data:', error); } }; - getData(); + fetchData(); }, [rowsPerPage, currentPage, sort]); - const startPage = Math.floor((currentPage - 1) / 10) * 10 + 1; - const endPage = Math.min(startPage + 9, totalPages); + //10페이지 단위로 페이지 버튼 생성 + const startPage = Math.floor((currentPage - 1) / 10) * 10 + 1; // 1이상 10이하의 페이지면 페이지 버튼 1부터 시작, 11이상 20 이하 페이지면 페이지 버튼 11부터 시작 + const endPage = Math.min(startPage + 9, totalPages); //끝 버튼 설정 const handleRowsPerPageChange = event => { setRowsPerPage(Number(event.target.value)); @@ -42,48 +85,24 @@ const EntryTable = () => { const handlePageChange = newPage => { setCurrentPage(newPage); - table.current.scrollIntoView(); + tableRef.current.scrollIntoView(); //table 위로 스크롤 }; return ( -
-
-
전체 {totalRows}
-
- - - -
-
-
-
- 순번 - - -
-
전화번호
-
응모 시간
-
응모 결과
-
+
+ + setSort('desc')} asc={() => setSort('asc')} /> {pageData.map(item => ( ))} { endPage={endPage} currentPage={currentPage} totalPages={totalPages} - handlePageChange={handlePageChange} + onPageChange={handlePageChange} />
); diff --git a/admin/src/pages/AdminEventStatus/PageButton.jsx b/admin/src/pages/AdminEventStatus/PageButton.jsx index 02076c6..17faf05 100644 --- a/admin/src/pages/AdminEventStatus/PageButton.jsx +++ b/admin/src/pages/AdminEventStatus/PageButton.jsx @@ -1,60 +1,65 @@ import React from 'react'; -import Proptypes from 'prop-types'; +import PropTypes from 'prop-types'; function PageButton({ startPage, endPage, currentPage, totalPages, - handlePageChange, + onPageChange, }) { + const createPageButtons = () => + Array.from({ length: endPage - startPage + 1 }).map((_, index) => { + const pageIndex = startPage + index; + return ( + + ); + }); + return (
{/* 10칸씩 이동하는 버튼*/} - {Array.from({ length: endPage - startPage + 1 }).map((_, index) => ( - - ))} + {createPageButtons()}
); } PageButton.propTypes = { - startPage: Proptypes.number.isRequired, - endPage: Proptypes.number.isRequired, - currentPage: Proptypes.number.isRequired, - totalPages: Proptypes.number.isRequired, - handlePageChange: Proptypes.func.isRequired, + startPage: PropTypes.number.isRequired, + endPage: PropTypes.number.isRequired, + currentPage: PropTypes.number.isRequired, + totalPages: PropTypes.number.isRequired, + onPageChange: PropTypes.func.isRequired, }; + export default PageButton; diff --git a/admin/src/pages/AdminEventStatus/RadioButton.jsx b/admin/src/pages/AdminEventStatus/RadioButton.jsx index ec8f2ad..b500f88 100644 --- a/admin/src/pages/AdminEventStatus/RadioButton.jsx +++ b/admin/src/pages/AdminEventStatus/RadioButton.jsx @@ -1,13 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -function RadioButton({ value, rowsPerPage, onChange }) { +function RadioButton({ value, checked, onChange }) { return (
))} @@ -22,7 +21,7 @@ function Dau({ data }) { } Dau.propTypes = { - data: PropTypes.array.isRequired, + data: PropTypes.arrayOf(PropTypes.number).isRequired, }; export default Dau; diff --git a/admin/src/pages/Indicator/TableRow.jsx b/admin/src/pages/Indicator/TableRow.jsx index 928fd5c..c98741c 100644 --- a/admin/src/pages/Indicator/TableRow.jsx +++ b/admin/src/pages/Indicator/TableRow.jsx @@ -2,35 +2,52 @@ import React from 'react'; import PropTypes from 'prop-types'; function TableRow({ data, day }) { - let null_box = 0; + /* + * 전체 데이터는 우하향으로 들어옴 + *[ 2, 50, 50, 0, 0] + *[13, -1, 8.33, 0, 0] + *[ 0, -1, -1, 0, 0] + *[ 0, -1, -1, -1, 0] + *[ 0, -1, -1, -1, -1] + * 해당 데이터를 + * [2, 50, 50, 0, 0] + * [13, 8.33, 0, 0] + * [0, 0, 0,] + * [0, 0] + * [0] + * 으로 수정할 필요가 있음 + * + * TableRow는 한 행을 받는 친구이기에 각각의 행에서 -1을 제거해서 배열을 재생성함 + */ + const renderedData = data.filter(value => value !== -1); + const nullBoxCount = data.length - renderedData.length; //빈박스 생성으로 표의 구조 유지 + const initialUsers = renderedData.shift(); + return (
{day} 일차
- {data.shift()} + {initialUsers} +
+
+ {initialUsers === 0 ? 'N/A' : '100%'} + {/* 유인된 유저가 0명이면 'N/A' 표시 */}
-
100%
- {data.map((value, index) => { - if (value === -1) { - null_box++; - return null; - } - return ( -
- {value.toFixed(2)}% -
- ); - })} - {Array.from({ length: null_box }).map((_, index) => ( + {renderedData.map((value, index) => ( +
+ {value.toFixed(2)}% +
+ ))} + {Array.from({ length: nullBoxCount }).map((_, index) => (
+ /> ))}
); @@ -38,7 +55,7 @@ function TableRow({ data, day }) { TableRow.propTypes = { day: PropTypes.number.isRequired, - data: PropTypes.array.isRequired, + data: PropTypes.arrayOf(PropTypes.number).isRequired, }; export default TableRow; diff --git a/admin/src/pages/UploadPrize/UploadPrize.jsx b/admin/src/pages/UploadPrize/UploadPrize.jsx index 4fa89c4..262b228 100644 --- a/admin/src/pages/UploadPrize/UploadPrize.jsx +++ b/admin/src/pages/UploadPrize/UploadPrize.jsx @@ -6,8 +6,10 @@ import useNavigationBlocker from '@/hooks/useNavigationBlocker'; import useFormData from '@/hooks/useFormData'; import JSZip from 'jszip'; +const PRIZE_VALUES = [2, 3, 4, 5]; + function UploadPrize() { - const [rank, setRank] = useState(2); + const [rank, setRank] = useState(PRIZE_VALUES[0]); const tempRank = useRef(null); const [errorMessage, setErrorMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); @@ -19,7 +21,6 @@ function UploadPrize() { const [totalPrize, setTotalPrize] = useState({}); const [openChangeModal, setOpenChangeModal] = useState(false); const [openSubmitModal, setOpenSubmitModal] = useState(false); - const [modified, setModified] = useState(false); const createFormData = useFormData(); @@ -44,7 +45,7 @@ function UploadPrize() { }, []); const handleRank = rank => { - if (!modified) { + if (!selectedFile) { setRank(rank); } else { tempRank.current = rank; @@ -99,7 +100,6 @@ function UploadPrize() { const handleFileChange = async files => { if (!files.length) return; - setModified(true); setErrorMessage(''); setIsLoading(true); @@ -190,7 +190,7 @@ function UploadPrize() {
경품 등록은 날짜와 상관이 없습니다.
- {[2, 3, 4, 5].map(item => ( + {PRIZE_VALUES.map(item => (
handleRank(item)} diff --git a/admin/src/pages/UploadReward/UploadReward.jsx b/admin/src/pages/UploadReward/UploadReward.jsx index d6661bc..ba602aa 100644 --- a/admin/src/pages/UploadReward/UploadReward.jsx +++ b/admin/src/pages/UploadReward/UploadReward.jsx @@ -43,11 +43,9 @@ function UploadReward() { file: selectedFile, quizDate: dateInfo, }); - console.log(body); try { setIsLoading(true); const response = await postQuizReward(body); - console.log(response); setOpenModal(false); if (response.message === 'success') { setProcessMessage('파일 업로드를 완료했습니다.'); @@ -64,7 +62,6 @@ function UploadReward() { const handleFileChange = async files => { if (!files.length) return; - setModified(true); setErrorMessage(''); setIsLoading(true); diff --git a/admin/src/pages/drawCasper/DrawCasper.jsx b/admin/src/pages/drawCasper/DrawCasper.jsx index f8f17a2..83dd1ff 100644 --- a/admin/src/pages/drawCasper/DrawCasper.jsx +++ b/admin/src/pages/drawCasper/DrawCasper.jsx @@ -4,7 +4,7 @@ import BlackButton from '@/components/buttons/BlackButton'; import ModalFrame from '@/components/modal/ModalFrame'; import { draw } from '@/api/DrawCasper/index'; -function DrawCasper() { +const useDrawCasper = () => { const [openSubmitModal, setOpenSubmitModal] = useState(false); const [winner, setWinner] = useState(''); @@ -19,24 +19,36 @@ function DrawCasper() { } }; + return { + openSubmitModal, + winner, + setOpenSubmitModal, + handleSubmit, + }; +}; + +function DrawCasper() { + const { openSubmitModal, winner, setOpenSubmitModal, handleSubmit } = + useDrawCasper(); + return (
- {winner === '' ? ( + {winner ? ( +
{winner}
+ ) : ( setOpenSubmitModal(true)} /> - ) : ( -
{winner}
)}
{openSubmitModal && ( setOpenSubmitModal(false)} - onClickYes={() => handleSubmit()} + onClickYes={handleSubmit} /> )}
diff --git a/admin/yarn.lock b/admin/yarn.lock index 8901578..586d48b 100644 --- a/admin/yarn.lock +++ b/admin/yarn.lock @@ -2900,6 +2900,7 @@ fast-glob@^3.3.0: "@nodelib/fs.walk" "^1.2.3" glob-parent "^5.1.2" merge2 "^1.3.0" + micromatch "^4.0.4" fast-json-stable-stringify@^2.0.0: version "2.1.0" @@ -3671,10 +3672,13 @@ merge2@^1.3.0: resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micro-slider@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micro-slider/-/micro-slider-1.1.0.tgz#f664f9fe6e0cf569ff224fd9c07dd95d609716d3" - integrity sha512-/PQSZMZdjJ8/UCG2B27Jf/WomKxfldZVxb1nr6FJBe+nOU0jt59Z9udu02TtND9/dj/GvxMxS27oGZPAthUMJA== +micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.7" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" milliparsec@^2.3.0: version "2.3.0" @@ -4119,6 +4123,7 @@ react-js-pagination@^3.0.3: paginator "^1.0.0" prop-types "15.x.x - 16.x.x" react "15.x.x - 16.x.x" + tar "7.4.3" react-router-dom@^6.25.1: version "6.25.1" @@ -4666,6 +4671,7 @@ tailwindcss@^3.4.6: is-glob "^4.0.3" jiti "^1.21.0" lilconfig "^2.1.0" + micromatch "^4.0.5" normalize-path "^3.0.0" object-hash "^3.0.0" picocolors "^1.0.0" diff --git a/service/package.json b/service/package.json index d751c87..1e1affd 100644 --- a/service/package.json +++ b/service/package.json @@ -15,6 +15,7 @@ "@prerenderer/rollup-plugin": "^0.3.12", "@svgr/webpack": "^8.1.0", "autoprefixer": "^10.4.19", + "classnames": "^2.5.1", "dotenv": "^16.4.5", "eslint-config-airbnb": "^19.0.4", "eslint-import-resolver-node": "^0.3.9", @@ -23,7 +24,6 @@ "framer-motion": "^11.3.28", "js-confetti": "^0.12.0", "json-server": "^1.0.0-beta.1", - "micro-slider": "^1.1.0", "micromatch": "^4.0.7", "postcss": "^8.4.39", "prop-types": "^15.8.1", @@ -33,7 +33,6 @@ "react-helmet-async": "^2.0.5", "react-js-pagination": "^3.0.3", "react-router-dom": "^6.25.1", - "swiper": "^11.1.7", "tailwindcss": "^3.4.6", "tar": "^7.4.3", "vite-plugin-svgr": "^4.2.0" diff --git a/service/src/components/buttons/BaseButton.jsx b/service/src/components/buttons/BaseButton.jsx deleted file mode 100644 index 75efd14..0000000 --- a/service/src/components/buttons/BaseButton.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -function BaseButton({ - value, - onClickFunc, - styles = '', - disabled = false, - bgColor, - textColor, -}) { - return ( - - ); -} - -BaseButton.propTypes = { - value: PropTypes.string.isRequired, - onClickFunc: PropTypes.func.isRequired, - styles: PropTypes.string, - disabled: PropTypes.bool, - bgColor: PropTypes.string.isRequired, - textColor: PropTypes.string.isRequired, -}; - -export default React.memo(BaseButton); diff --git a/service/src/components/buttons/BlueButton.jsx b/service/src/components/buttons/BlueButton.jsx index 681c682..85c731d 100644 --- a/service/src/components/buttons/BlueButton.jsx +++ b/service/src/components/buttons/BlueButton.jsx @@ -1,14 +1,23 @@ import React from 'react'; -import BaseButton from '@/components/buttons/BaseButton'; +import PropTypes from 'prop-types'; -function BlueButton(props) { +function BlueButton({ value, styles, onClickFunc, disabled = false }) { return ( - + ); } +BlueButton.propTypes = { + value: PropTypes.string.isRequired, + onClickFunc: PropTypes.func.isRequired, + styles: PropTypes.string.isRequired, + disabled: PropTypes.bool, +}; + +//memo를 이용하여 rerender 방지 export default React.memo(BlueButton); diff --git a/service/src/components/buttons/BluePurpleButton.jsx b/service/src/components/buttons/BluePurpleButton.jsx index 422bd34..5dfc9c3 100644 --- a/service/src/components/buttons/BluePurpleButton.jsx +++ b/service/src/components/buttons/BluePurpleButton.jsx @@ -1,14 +1,23 @@ import React from 'react'; -import BaseButton from '@/components/buttons/BaseButton'; +import PropTypes from 'prop-types'; -function BluePurpleButton(props) { +function BluePurpleButton({ value, onClickFunc, styles, disabled = false }) { return ( - + ); } +BluePurpleButton.propTypes = { + value: PropTypes.string.isRequired, + onClickFunc: PropTypes.func.isRequired, + styles: PropTypes.string.isRequired, + disabled: PropTypes.bool, +}; + export default React.memo(BluePurpleButton); diff --git a/service/src/components/buttons/WhiteButton.jsx b/service/src/components/buttons/WhiteButton.jsx index 9536b84..418d00b 100644 --- a/service/src/components/buttons/WhiteButton.jsx +++ b/service/src/components/buttons/WhiteButton.jsx @@ -1,14 +1,23 @@ import React from 'react'; -import BaseButton from '@/components/buttons/BaseButton'; +import PropTypes from 'prop-types'; -function WhiteButton(props) { +function WhiteButton({ value, onClickFunc, styles, disabled = false }) { return ( - + ); } +WhiteButton.propTypes = { + value: PropTypes.string.isRequired, + onClickFunc: PropTypes.func.isRequired, + styles: PropTypes.string.isRequired, + disabled: PropTypes.bool, +}; + export default React.memo(WhiteButton); diff --git a/service/src/pages/eventIntro/EventIntro.jsx b/service/src/pages/eventIntro/EventIntro.jsx index 0f37176..9aad71c 100644 --- a/service/src/pages/eventIntro/EventIntro.jsx +++ b/service/src/pages/eventIntro/EventIntro.jsx @@ -5,25 +5,26 @@ import EventIntroRewards from '@/pages/eventIntro/EventIntroRewards'; import SlideUpMotion from '@/components/SlideUpMotion/SlideUpMotion'; import { Helmet } from 'react-helmet-async'; +const TITLE = '캐스퍼 이벤트 소개'; +const DESCRIPTION = + '캐스퍼 EV를 받을 수 있는 이벤트에 대해 자세히 알아보세요. 이벤트 참여 방법과 다양한 보상을 소개합니다.'; +const OG_IMAGE_URL = + 'https://softeer4-team8.s3.ap-northeast-2.amazonaws.com/%E1%84%86%E1%85%B5%E1%84%82%E1%85%B5+%E1%84%8F%E1%85%B1%E1%84%8C%E1%85%B3+7.svg'; + +const HelmetMeta = () => ( + + {TITLE} + + + + + +); + function EventIntro() { return (
- - 캐스퍼 이벤트 소개 - - - - - +
diff --git a/service/src/pages/joinEvent/JoinEventIntro.jsx b/service/src/pages/joinEvent/JoinEventIntro.jsx index 85b2ec3..d79202e 100644 --- a/service/src/pages/joinEvent/JoinEventIntro.jsx +++ b/service/src/pages/joinEvent/JoinEventIntro.jsx @@ -7,28 +7,30 @@ import MiniQuiz from '@/pages/joinEvent/MiniQuiz'; import useScroll from '@/hooks/useScroll'; import { Helmet } from 'react-helmet-async'; +const TITLE = '캐스퍼 이벤트 참여'; +const DESCRIPTION = + '다양한 게임을 즐기고 응모권을 모아 캐스퍼 EV를 받을 수 있는 기회를 잡아보세요!'; +const OG_IMAGE_URL = + 'https://softeer4-team8.s3.ap-northeast-2.amazonaws.com/OGImage.png'; +const OG_URL = 'https://casper-event.store/event'; + +const HelmetMeta = () => ( + + {TITLE} + + + + + + +); + function JoinEventIntro() { const { refs } = useScroll(); return ( <> - - 캐스퍼 이벤트 참여 - - - - - - +
diff --git a/service/src/pages/joinEvent/WorldCup.jsx b/service/src/pages/joinEvent/WorldCup.jsx index 1f02396..f552f77 100644 --- a/service/src/pages/joinEvent/WorldCup.jsx +++ b/service/src/pages/joinEvent/WorldCup.jsx @@ -39,20 +39,20 @@ const WorldCupImages = () => ( className="absolute top-[51px] left-[6px] z-0" src={worldCupIntro1} alt="출차 시 고급승용차에 둘러 싸이기" - style={{ clipPath: 'polygon(0 10%, 100% 0%, 100% 90%, 10% 100%)' }} //clipPath를 이용한 밑줄 삭제 + style={{ clipPath: 'polygon(0% 18%, 78% 0%, 100% 79%, 22% 100%)' }} //clipPath를 이용한 밑줄 삭제 loading="lazy" /> 산 중턱에서 전기차 배터리 방전되기 골목에서 천천히 걸어가는 보행자 만나기
@@ -65,7 +65,7 @@ function WorldCup() { return (
-
+
diff --git a/service/src/pages/miniquiz/ButtonCases.jsx b/service/src/pages/miniquiz/ButtonCases.jsx index d86bea0..93882bd 100644 --- a/service/src/pages/miniquiz/ButtonCases.jsx +++ b/service/src/pages/miniquiz/ButtonCases.jsx @@ -13,6 +13,8 @@ function ButtonCases({ openOrderModal, }) { const { userInfo } = useContext(AuthContext); + //showOrderButton은 선착순 수령 대상자인지에 대한 검사(500명 안에 들면 participantId가 defined) + //userGotPrize는 선착순 수령 여부 이를 통해 선착순 수령 대상자가 선착순을 수령하면 선착순 버튼을 삭제시키기 위함 const showOrderButton = participantId !== undefined && !userGotPrize; const showToolBoxButton = isCorrect && !(userInfo.quizParticipated === true); //userInfo.quizParticipated는 undefined일 수 있음 const buttons = []; diff --git a/service/src/pages/miniquiz/ClickBox.jsx b/service/src/pages/miniquiz/ClickBox.jsx index d174ee5..73a793b 100644 --- a/service/src/pages/miniquiz/ClickBox.jsx +++ b/service/src/pages/miniquiz/ClickBox.jsx @@ -1,26 +1,37 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; +import classnames from 'classnames'; -function ClickBox({ id, value, isChosen, onClick }) { - let className = - 'hover-scale-ani set-center text-body-3-semibold min-w-[586px] min-h-[120px] rounded-[15px] border-2 overflow-hidden '; - if (id === isChosen) { - className = className + 'bg-background-lightblue gradient-border'; - } else { - className = className + 'border-op-30-blue bg-neutral-white'; - } +const ClickBox = ({ id, value, isChosen, onClick }) => { + const baseClass = + 'hover-scale-ani set-center text-body-3-semibold min-w-[586px] min-h-[120px] rounded-[15px] border-2 overflow-hidden'; + + const className = classnames(baseClass, { + ' bg-background-lightblue gradient-border': isChosen, + ' border-op-30-blue bg-neutral-white': !isChosen, + }); //classnames 를 사용하여 가독성 개선 + + //이전 코드 + /* let className = + * 'hover-scale-ani set-center text-body-3-semibold min-w-[586px] min-h-[120px] rounded-[15px] border-2 overflow-hidden '; + * if (id === isChosen) { + * className = className + 'bg-background-lightblue gradient-border'; + * } else { + * className = className + 'border-op-30-blue bg-neutral-white'; + * } + */ return ( ); -} +}; ClickBox.propTypes = { - value: PropTypes.string.isRequired, - isChosen: PropTypes.number.isRequired, id: PropTypes.number.isRequired, + value: PropTypes.string.isRequired, + isChosen: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, }; diff --git a/service/src/pages/miniquiz/LoadingQuiz.jsx b/service/src/pages/miniquiz/LoadingQuiz.jsx index e0eb906..febabd5 100644 --- a/service/src/pages/miniquiz/LoadingQuiz.jsx +++ b/service/src/pages/miniquiz/LoadingQuiz.jsx @@ -1,36 +1,43 @@ -import React, { useEffect, useCallback, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import BlueButton from '@/components/buttons/BlueButton'; import WhiteButton from '@/components/buttons/WhiteButton'; import { useNavigate } from 'react-router-dom'; -function LoadingQuiz() { - const navigate = useNavigate(); - - const handleExit = () => { - navigate('/event'); - }; - - const handleRefresh = useCallback(() => { - window.location.reload(); - }, []); - - const [loadingText, setLoadingText] = useState('퀴즈 정보를 가져오는 중..!'); +const useLoadingText = (initialText, texts, interval) => { + const [loadingText, setLoadingText] = useState(initialText); useEffect(() => { - const texts = [ - '퀴즈 정보를 가져오는 중.', - '퀴즈 정보를 가져오는 중..', - '퀴즈 정보를 가져오는 중...', - '퀴즈 정보를 가져오는 중..!', - ]; let index = 0; - const interval = setInterval(() => { + const textInterval = setInterval(() => { setLoadingText(texts[index]); index = (index + 1) % texts.length; - }, 500); + }, interval); + + return () => clearInterval(textInterval); + }, [texts, interval]); - return () => clearInterval(interval); - }, []); + return loadingText; +}; //loading 시 문자 바뀜 + +function LoadingQuiz() { + const navigate = useNavigate(); + + const handleExit = () => navigate('/event'); + + const handleRefresh = () => window.location.reload(); + + const loadingTexts = [ + '퀴즈 정보를 가져오는 중.', + '퀴즈 정보를 가져오는 중..', + '퀴즈 정보를 가져오는 중...', + '퀴즈 정보를 가져오는 중..!', + ]; + + const loadingText = useLoadingText( + '퀴즈 정보를 가져오는 중..!', + loadingTexts, + 500, + ); return (
@@ -46,9 +53,8 @@ function LoadingQuiz() { onClickFunc={handleExit} styles="px-3000 py-500 text-bold-3-regular" /> - diff --git a/service/src/pages/miniquiz/MiniQuiz.jsx b/service/src/pages/miniquiz/MiniQuiz.jsx index 9f3f36b..3c942b4 100644 --- a/service/src/pages/miniquiz/MiniQuiz.jsx +++ b/service/src/pages/miniquiz/MiniQuiz.jsx @@ -4,6 +4,23 @@ import ExitModal from '@/components/modal/ExitModal'; import MiniQuizMain from '@/pages/miniquiz/MiniQuizMain'; import { Helmet } from 'react-helmet-async'; +const TITLE = '캐스퍼 미니퀴즈'; +const DESCRIPTION = '미니퀴즈을 통해 선착순 경품과 툴박스 아이템을 획득하세요!'; +const OG_IMAGE_URL = + 'https://softeer4-team8.s3.ap-northeast-2.amazonaws.com/OGImage.png'; +const OG_URL = 'https://casper-event.store/event/miniquiz'; + +const HelmetMeta = () => ( + + {TITLE} + + + + + + +); + function MiniQuiz() { const [openExitModal, setOpenExitModal] = useState(false); @@ -12,26 +29,7 @@ function MiniQuiz() { return ( <>
- - 캐스퍼 미니퀴즈 - - - - - - + ( +
{description}
+); + +QuizDescription.propTypes = { + description: PropTypes.string.isRequired, +}; + +const QuizQuestions = ({ questions, selectedId, onSelect }) => ( +
+ {questions.map(([id, value]) => ( + onSelect(Number(id))} + key={id} + /> + ))} +
+); + +QuizQuestions.propTypes = { + questions: PropTypes.arrayOf(PropTypes.array).isRequired, + selectedId: PropTypes.number, + onSelect: PropTypes.func.isRequired, +}; function MiniQuizMain() { const navigate = useNavigate(); const { code, loading, error, data, shuffledQuizQuestion } = useMiniQuiz(); - const { quizDescription, quizId } = data; - const [isChosen, setIsChosen] = useState(0); - const [disabled, setDisabled] = useState(true); + const [selectedId, setSelectedId] = useState(0); + const [isSubmitDisabled, setSubmitDisabled] = useState(true); useEffect(() => { if (code === 'NO_QUIZ_CONTENT') { navigate('/event/noQuiz'); } - }, [code]); + }, [code, navigate]); - const handleClickBox = id => { - setIsChosen(id); - setDisabled(false); // ClickBox가 클릭될 때 SubmitButton 활성화 - }; + const handleSelect = useCallback(id => { + setSelectedId(id); + setSubmitDisabled(false); + }, []); - if (error) { - return
Error: {error.message}
; - } else if (loading) { - return ; - } + if (loading) return ; + if (error) return
Error: {error.message}
; + + const { quizDescription, quizId } = data; return (
월드컵 일일 미니퀴즈
-
- {quizDescription} -
-
- {shuffledQuizQuestion.map(item => { - const id = Number(item[0]); - const value = item[1]; - return ( - handleClickBox(id)} - key={id} - /> - ); - })} -
+ +
); diff --git a/service/src/pages/miniquiz/NoQuizMain.jsx b/service/src/pages/miniquiz/NoQuizMain.jsx index e92c172..bd9d2c2 100644 --- a/service/src/pages/miniquiz/NoQuizMain.jsx +++ b/service/src/pages/miniquiz/NoQuizMain.jsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import WhiteButton from '@/components/buttons/WhiteButton'; import { useNavigate } from 'react-router-dom'; @@ -14,7 +14,7 @@ function NoQuizMain() { */ //이런 식으로 하면 되긴 하지만 이 페이지에서는 불필요한 최적화임 - const handleExit = () => navigate('/event'); + const handleNavigateToEvent = () => navigate('/event'); return (
@@ -26,7 +26,7 @@ function NoQuizMain() {
diff --git a/service/src/pages/miniquiz/miniquizhooks/useMiniQuiz.js b/service/src/pages/miniquiz/miniquizhooks/useMiniQuiz.js index b28034c..37aa08f 100644 --- a/service/src/pages/miniquiz/miniquizhooks/useMiniQuiz.js +++ b/service/src/pages/miniquiz/miniquizhooks/useMiniQuiz.js @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { getMiniQuiz } from '@/api/miniQuiz'; import shuffleArr from '@/utils/shuffleArr'; @@ -13,25 +13,25 @@ const useMiniQuiz = () => { const fetchMiniQuiz = async () => { try { setLoading(true); - const [data] = await Promise.all([ + setError(''); + const [quizData] = await Promise.all([ getMiniQuiz(), - new Promise(resolve => setTimeout(resolve, 500)), - ]); //Promise.all을 이용하여 둘 중 오래 걸리는 시간 동안 로딩 화면 보여줌 - const { code } = data; - if (code === 'NO_QUIZ_CONTENT') { - setCode(code); + new Promise(resolve => setTimeout(resolve, 300)), // 사용자의 경험을 방해하지 않는 선에서 로딩 화면을 보여주기 위한 0.3초 + ]); + + const { quizCode, quizQuestions } = quizData; + if (quizCode === 'NO_QUIZ_CONTENT') { + setCode(quizCode); return; } - setData(data); - setShuffledQuizQuestion(shuffleArr(Object.entries(data.quizQuestions))); + setData(quizData); + setShuffledQuizQuestion(shuffleArr(Object.entries(quizQuestions))); } catch (err) { - setError(err); - return; + setError('퀴즈 로딩에 실패했습니다. 다시 시도 부탁드립니다.'); } finally { setLoading(false); } }; - fetchMiniQuiz(); }, []); diff --git a/service/src/pages/newCarIntro/NewCarIntro.jsx b/service/src/pages/newCarIntro/NewCarIntro.jsx index c0f1483..d05074a 100644 --- a/service/src/pages/newCarIntro/NewCarIntro.jsx +++ b/service/src/pages/newCarIntro/NewCarIntro.jsx @@ -4,23 +4,27 @@ import NewCarCarousel from '@/pages/newCarIntro/NewCarCarousel'; import NewCarDetail from '@/pages/newCarIntro/NewCarDetail'; import { Helmet } from 'react-helmet-async'; +const TITLE = '캐스퍼 EV 소개'; +const DESCRIPTION = '캐스퍼 EV에 대해 알아보세요!'; +const OG_IMAGE_URL = + 'https://softeer4-team8.s3.ap-northeast-2.amazonaws.com/OGImage.png'; +const OG_URL = 'https://casper-event.store/introd'; + +const HelmetMeta = () => ( + + {TITLE} + + + + + + +); + function NewCarIntro() { return ( <> - - 캐스퍼 EV 소개 - - - - - - +
diff --git a/service/src/pages/worldCup/WorldCupMain.jsx b/service/src/pages/worldCup/WorldCupMain.jsx index 73eda63..2da0477 100644 --- a/service/src/pages/worldCup/WorldCupMain.jsx +++ b/service/src/pages/worldCup/WorldCupMain.jsx @@ -6,6 +6,23 @@ import shuffleArr from '@/utils/shuffleArr'; import { postWorldCupResult } from '@/api/worldCup/index'; import { Helmet } from 'react-helmet-async'; +const TITLE = '캐스퍼 상황 월드컵'; +const DESCRIPTION = '월드컵 게임을 통해 자동차 아이템을 획득하세요!'; +const OG_IMAGE_URL = + 'https://softeer4-team8.s3.ap-northeast-2.amazonaws.com/OGImage.png'; +const OG_URL = 'https://casper-event.store/event/worldcup'; + +const HelmetMeta = () => ( + + {TITLE} + + + + + + +); + const WorldCupMain = () => { const navigate = useNavigate(); const [totalData, setTotalData] = useState(shuffleArr(worldCupData)); @@ -66,26 +83,7 @@ const WorldCupMain = () => { return (
- - 캐스퍼 상황 월드컵 - - - - - - +