diff --git a/Caecae/src/components/FindingGame/FindingGame.tsx b/Caecae/src/components/FindingGame/FindingGame.tsx index 24bfb1b4..72add31c 100644 --- a/Caecae/src/components/FindingGame/FindingGame.tsx +++ b/Caecae/src/components/FindingGame/FindingGame.tsx @@ -3,37 +3,40 @@ import { action, initFindingGameState, } from "../../jobs/FindingGame/FindingGameWork.tsx"; -import { useEffect, useRef } from "react"; +import { useEffect } from "react"; import LottieContainer from "../common/LottieContainer/index.tsx"; import correctLottie from "@assets/animationCorrect.json"; import wrongLottie from "@assets/animationIncorrect.json"; import { store, useExistState } from "../../shared/Hyundux"; -import HintSpot from "./Hint/HintSpot.tsx"; +//import HintSpot from "./Hint/HintSpot.tsx"; import SmileBadge from "../common/SmileBadge/index.tsx"; +import { createStory } from "../../shared/Hyundux-saga/Story.tsx"; +import useSaga from "../../shared/Hyundux-saga/useSaga.tsx"; +import { getFindGameStory } from "../../stories/getFindingGame.tsx"; const FindingGame = () => { const state = useExistState(initFindingGameState); - const timerId = useRef(null); + // const timerId = useRef(null); + const [status, teller] = useSaga(); + status; useEffect(() => { - store.dispatch(action.init()); - timerId.current = setTimeout(() => { - store.dispatch(action.showHint()); - }, 40000); + const getFindGameRunStory = createStory(getFindGameStory, {}); + teller(action.init, [getFindGameRunStory]); + // timerId.current = setTimeout(() => { + // store.dispatch(action.showHint()); + // }, 40000); }, []); - useEffect(() => { - if (state.showingHint.length == 0) { - if (timerId.current != null) { - clearInterval(timerId.current); - } - timerId.current = setTimeout(() => { - store.dispatch(action.showHint()); - }, 40000); - } - }, [state.showingHint]); - - const imgURL = - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202109/01/b57fdda5-3996-430f-8bf2-65052b1d12b2.jpg"; + // useEffect(() => { + // if (state.showingHint.length == 0) { + // if (timerId.current != null) { + // clearInterval(timerId.current); + // } + // timerId.current = setTimeout(() => { + // store.dispatch(action.showHint()); + // }, 40000); + // } + // }, [state.showingHint]); const onClickAction = ( width: number, @@ -44,7 +47,7 @@ const FindingGame = () => { width; heigjht; if (state.gameStatus == "Gaming") { - store.dispatch(action.click(y, x)); + store.dispatch(action.click(y, x, width, heigjht)); } }; @@ -55,9 +58,8 @@ const FindingGame = () => { if (state.gameStatus == "Gaming") { return ( { return <>; }); - const answerElement = state.answers.map((answer, index) => { - if (state.gameStatus == "Done") { - const left = answer.x - 50; - const top = answer.y - 50; + const answerElement = state.showingAnswers.map((answer, index) => { + if (state.gameStatus == "DoneSuccess" || state.gameStatus == "DoneFail") { + const left = answer.positionX - 50; + const top = answer.positionY - 50; const rotateRadian = index == 0 ? "-13" : "8"; const bageType = index == 0 ? "orange_line" : "yellow_line"; return ( @@ -109,18 +111,19 @@ const FindingGame = () => { /> ); }); - const showingHintElement = state.showingHint.map((hintAnswer) => { - return ; - }); + + // const showingHintElement = state.showingHint.map((hintAnswer) => { + // return ; + // }); return (
{ setIsTooltipShowing((prev) => !prev); } - const modeData = modeDependency("pixel"); + const modeData = modeDependency(state.gameType); const tooltip = isTooltipShowing ? ( <> @@ -53,13 +53,9 @@ const FindingGameInfo = () => { <> ); - const badges = state.answers.map((answer, index) => { + const badges = new Array(2).fill(0).map((answer, index) => { const badgeType = index == 0 ? "blue" : "orange"; - if ( - state.showingAnswers.filter( - (showingAnswer) => showingAnswer.id == answer.id - ).length !== 0 - ) { + if (state.showingAnswers.length > index) { return ( { const state = useExistState(initFindingGameState); - const currentAnswer = state.answers[state.answerIndex]; - + const currentAnswer = state.showingAnswers[state.answerIndex]; return ( <>
@@ -24,8 +23,8 @@ const FindingGameResult = () => { className="bg-[#000000] bg-opacity-50 w-[50px] h-[50px] flex justify-center items-center" onClick={() => { const index = - (state.answers.length + state.answerIndex - 1) % - state.answers.length; + (state.showingAnswers.length + state.answerIndex - 1) % + state.showingAnswers.length; store.dispatch(action.changeShowingAnswer(index)); }} > @@ -34,7 +33,8 @@ const FindingGameResult = () => {
{ - const index = (state.answerIndex + 1) % state.answers.length; + const index = + (state.answerIndex + 1) % state.showingAnswers.length; store.dispatch(action.changeShowingAnswer(index)); }} > @@ -42,13 +42,13 @@ const FindingGameResult = () => {

{currentAnswer.title}

-

{currentAnswer.info}

+

{currentAnswer.content}

diff --git a/Caecae/src/components/PhoneNumberOverlay/PhoneNumberOverlay.tsx b/Caecae/src/components/PhoneNumberOverlay/PhoneNumberOverlay.tsx index 5ac33c58..4522f5be 100644 --- a/Caecae/src/components/PhoneNumberOverlay/PhoneNumberOverlay.tsx +++ b/Caecae/src/components/PhoneNumberOverlay/PhoneNumberOverlay.tsx @@ -4,9 +4,13 @@ import { store } from "../../shared/Hyundux"; interface PhoneNumberOverlayProps { type: "findCasper" | "raceCasper"; + onClick?: (phoneNumber: string) => void; } -const PhoneNumberOverlay = ({ type }: PhoneNumberOverlayProps) => { +const PhoneNumberOverlay = ({ + type, + onClick = () => {}, +}: PhoneNumberOverlayProps) => { const [timeLeft, setTimeLeft] = useState(3 * 60); // 3분을 초 단위로 변환 const [phoneNumber, setPhoneNumber] = useState(""); const [check, setCheck] = useState(false); @@ -98,13 +102,15 @@ const PhoneNumberOverlay = ({ type }: PhoneNumberOverlayProps) => {
)}
-

전화번호

+

+ 전화번호 +

@@ -112,7 +118,7 @@ const PhoneNumberOverlay = ({ type }: PhoneNumberOverlayProps) => { 개인정보 동의

-
+

1. 개인정보의 처리 목적
@@ -152,6 +158,8 @@ const PhoneNumberOverlay = ({ type }: PhoneNumberOverlayProps) => { {enterable === true ? (

{ + const parameter = phoneNumber.replace(/-/g, ""); + onClick(parameter); store.dispatch(action.nextPage()); }} className="bg-[#002C5F] h-[12%] flex items-center justify-center hover:cursor-pointer" @@ -160,7 +168,9 @@ const PhoneNumberOverlay = ({ type }: PhoneNumberOverlayProps) => {
) : (
-

개인정보를 입력해주세요

+

+ 개인정보를 입력해주세요 +

)}
diff --git a/Caecae/src/components/common/InfoSection/InfoSection.tsx b/Caecae/src/components/common/InfoSection/InfoSection.tsx index d48815ce..f667af7c 100644 --- a/Caecae/src/components/common/InfoSection/InfoSection.tsx +++ b/Caecae/src/components/common/InfoSection/InfoSection.tsx @@ -1,5 +1,6 @@ import { ReactElement, ReactNode } from "react"; interface InfoSectionProps { + width?: number; type?: "Default" | "Header"; title?: string; children: ReactNode; @@ -9,6 +10,7 @@ const InfoSection = ({ type = "Header", title = "", children, + width = 100, }: InfoSectionProps) => { let header: ReactElement | null = null; @@ -27,10 +29,12 @@ const InfoSection = ({ header = topLine; break; } - + const style = { + width: `${width}%`, + }; return ( <> -
+
{header}
{children} @@ -52,12 +56,7 @@ interface InfoSectionDotProps { right?: number; } -const InfoSectionDot = ({ - top, - bottom, - left, - right, -}: InfoSectionDotProps) => { +const InfoSectionDot = ({ top, bottom, left, right }: InfoSectionDotProps) => { const style: React.CSSProperties = { width: "20px", position: "absolute", diff --git a/Caecae/src/components/common/PictureGameBoard/PictureGameBoard.tsx b/Caecae/src/components/common/PictureGameBoard/PictureGameBoard.tsx index ab47507d..d5774b8d 100644 --- a/Caecae/src/components/common/PictureGameBoard/PictureGameBoard.tsx +++ b/Caecae/src/components/common/PictureGameBoard/PictureGameBoard.tsx @@ -31,7 +31,7 @@ const PictureGameBoard = ({ Finding Picture
diff --git a/Caecae/src/features/FindingGameLanding/EventPeriod.tsx b/Caecae/src/features/FindingGameLanding/EventPeriod.tsx index 0ab97871..9d5c5db1 100644 --- a/Caecae/src/features/FindingGameLanding/EventPeriod.tsx +++ b/Caecae/src/features/FindingGameLanding/EventPeriod.tsx @@ -6,16 +6,16 @@ const EventPeriod = () => { return (
-

+

이벤트 기간

-
- +
+
-

+

참여 기간: 7.15 (월) - 7.21 (일){" "} 오후 3시 15분
diff --git a/Caecae/src/features/FindingGameLanding/HowToEvent.tsx b/Caecae/src/features/FindingGameLanding/HowToEvent.tsx index 589b1ecd..5940c91d 100644 --- a/Caecae/src/features/FindingGameLanding/HowToEvent.tsx +++ b/Caecae/src/features/FindingGameLanding/HowToEvent.tsx @@ -4,13 +4,13 @@ const HowToEvent = () => { return (

-

+

이벤트 참여 방법

-
- +
+
-

+

매일 새롭게 제공되는 캐스퍼 일렉트릭의 사진에서 숨겨진 로봇
뱃지와 픽셀 디자인을 찾아보세요. @@ -51,12 +51,12 @@ const HowToEvent = () => { howToEventLeft howToEventLeft

diff --git a/Caecae/src/features/FindingGameLanding/LadingPageTitle.tsx b/Caecae/src/features/FindingGameLanding/LadingPageTitle.tsx index dabb87d4..fe024c40 100644 --- a/Caecae/src/features/FindingGameLanding/LadingPageTitle.tsx +++ b/Caecae/src/features/FindingGameLanding/LadingPageTitle.tsx @@ -1,11 +1,46 @@ +import { useState } from "react"; + interface LadingPageTitleProps { onClick: () => void; } const LadingPageTitle = ({ onClick }: LadingPageTitleProps) => { + const [showMessage, setShowMessage] = useState(false); + const [animate, setAnimate] = useState(false); + const [isAnimating, setIsAnimating] = useState(false); + + const shareEvent = () => { + if (isAnimating) return; + setIsAnimating(true); + const url: string = window.location.href; + + navigator.clipboard.writeText(url) + .then(() => { + setShowMessage(true); + + setTimeout(() => { + setAnimate(true); + + setTimeout(() => { + setAnimate(false); + + setTimeout(() => { + setShowMessage(false); + setIsAnimating(false); + }, 500); + + }, 3000); + + }, 10); + }) + .catch((err: Error) => { + console.error('URL 복사에 실패했습니다.', err); + setIsAnimating(false); + }); + }; return ( <> -
+

CASPER Electric 신차 @@ -23,7 +58,7 @@ const LadingPageTitle = ({ onClick }: LadingPageTitleProps) => {

캐스퍼 일렉트릭에 숨겨진 로봇 뱃지 -
찾고 1만 원 커피 쿠폰 +
찾고 1만 원 커피 쿠폰 받아가자!

{ className="h-[300px] mt-[50px]" />
-
+
sharedButton -

공유하기

+

+ 공유하기 +

{ findGameLeftBlocks findGameLeftBlocks findGameLeftBlocks
+ {showMessage && ( +
+
+ URL이 복사되었습니다! +
+
+ )}
); diff --git a/Caecae/src/features/FindingGameLanding/OpenEvent.tsx b/Caecae/src/features/FindingGameLanding/OpenEvent.tsx index f38b55f4..a25d4ace 100644 --- a/Caecae/src/features/FindingGameLanding/OpenEvent.tsx +++ b/Caecae/src/features/FindingGameLanding/OpenEvent.tsx @@ -1,7 +1,7 @@ -import { forwardRef, ReactElement, useRef, useState } from "react"; +import { forwardRef, ReactElement, useEffect, useState } from "react"; import SmileBadge from "../../components/common/SmileBadge/index"; -import { useSpecificTimeEffect } from "../../hooks"; import { Link } from "../../shared/Hyunouter"; +import getFindingGameStartTime from "../../stories/getFindingGameStartTime"; // props 타입 정의 interface OpenEventProps {} @@ -14,26 +14,50 @@ interface StatusInfo { } const OpenEvent = forwardRef((props, ref) => { - const eventOpenStatus = useRef("none"); - const [leftTime, setLeftTime] = useState(0); props; + const [eventStatus, setEventStatus] = useState("none"); + const [leftTime, setLeftTime] = useState(0); const targetDate = new Date(); - targetDate.setHours(16, 0, 0, 0); - useSpecificTimeEffect(targetDate, (leftSeconds) => { - if (leftSeconds <= 3600 && leftSeconds > 0) { - eventOpenStatus.current = "soon"; - setLeftTime(leftSeconds); - } else if (leftSeconds < 0) { - eventOpenStatus.current = "opened"; - setLeftTime(leftSeconds); - } else { - eventOpenStatus.current = "none"; - } - }); + useEffect(() => { + const fetchData = async () => { + const reponse = await getFindingGameStartTime(); + + let answer: EventOpenStatus = "none"; // 명시적으로 타입 지정 + reponse.data.findingGameInfos.forEach((info) => { + const tempAnswer: EventOpenStatus = chechCurrentStuts( + info.startTime, + info.endTime + ); + + if (tempAnswer !== "none") { + answer = tempAnswer; + } + if (tempAnswer === "soon") { + targetDate.setHours(info.startTime[3], info.startTime[4], 0, 0); + } + }); + setEventStatus(answer); + + // @ts-expect-error: 진짜 ts 병신 + if (answer === "soon") { + const checkTime = () => { + const now = new Date(); + setLeftTime( + Math.floor((targetDate.getTime() - now.getTime()) / 1000) + ); + }; + checkTime(); + const intervalId = setInterval(checkTime, 1000); + return () => clearInterval(intervalId); + } + }; + + fetchData(); + }, []); let data: StatusInfo | null = null; - if (eventOpenStatus.current == "none") { + if (eventStatus == "none") { data = { badgeTitle: "이벤트 오픈 예정", title: "7월 15일 오후 3시 15분", @@ -51,7 +75,7 @@ const OpenEvent = forwardRef((props, ref) => { ), isButtonOpen: false, }; - } else if (eventOpenStatus.current == "soon") { + } else if (eventStatus == "soon") { data = { badgeTitle: "이벤트 오픈 예정", title: "이벤트 오픈이 얼마 남지 않았어요!", @@ -160,6 +184,33 @@ const OpenEvent = forwardRef((props, ref) => { ); }); +function chechCurrentStuts(startTime: number[], endTime: number[]) { + const currentDate = new Date(); + const currentTime = [ + currentDate.getFullYear(), + currentDate.getMonth() + 1, + currentDate.getDate(), + currentDate.getHours(), + currentDate.getMinutes(), + ]; + const soonTime = [...startTime]; + soonTime[3] = soonTime[3] - 1; + + function isIn(startTime: number[], targetTime: number[], endTime: number[]) { + for (let i = 0; i < startTime.length; i++) { + if (startTime[i] === endTime[i] && startTime[i] === targetTime[i]) + continue; + else if (startTime[i] <= targetTime[i] && targetTime[i] <= endTime[i]) + return true; + else return false; + } + return false; + } + if (isIn(soonTime, currentTime, startTime)) return "soon"; + else if (isIn(startTime, currentTime, endTime)) return "opened"; + return "none"; +} + const WhiteTimerRectangle = (props: { num: number }) => { return ( <> diff --git a/Caecae/src/features/FindingGameLanding/SelectionMethod.tsx b/Caecae/src/features/FindingGameLanding/SelectionMethod.tsx index 4cf43beb..e0e2dd97 100644 --- a/Caecae/src/features/FindingGameLanding/SelectionMethod.tsx +++ b/Caecae/src/features/FindingGameLanding/SelectionMethod.tsx @@ -6,16 +6,16 @@ const SelectionMethod = () => { return (
-

+

참여 혜택 및 선정 방식

-
- +
+
-

+

매일 정해진 선착순 인원에게{" "} 스타벅스 1만 원 쿠폰을 드려요. @@ -23,7 +23,7 @@ const SelectionMethod = () => { 당첨 후 3분 내로 전화 번호를 기입하면 응모 완료!

-

+

* 하루에 한 번 응모 가능합니다.

diff --git a/Caecae/src/features/RacingGameLanding/EventIntro.tsx b/Caecae/src/features/RacingGameLanding/EventIntro.tsx index d7b4dca4..761a8c95 100644 --- a/Caecae/src/features/RacingGameLanding/EventIntro.tsx +++ b/Caecae/src/features/RacingGameLanding/EventIntro.tsx @@ -1,18 +1,63 @@ +import { useState } from "react"; import { Link } from "../../shared/Hyunouter"; -const EventIntro = () => { +interface EventIntroProps { + isEventOpen: boolean; +} + +const EventIntro: React.FC = ({isEventOpen}) => { + const [showMessage, setShowMessage] = useState(false); + const [animate, setAnimate] = useState(false); + const [isAnimating, setIsAnimating] = useState(false); + + const shareEvent = () => { + if (isAnimating) return; + setIsAnimating(true); + const url: string = window.location.href; + + navigator.clipboard.writeText(url) + .then(() => { + setShowMessage(true); + + setTimeout(() => { + setAnimate(true); + + setTimeout(() => { + setAnimate(false); + + setTimeout(() => { + setShowMessage(false); + setIsAnimating(false); + }, 500); + + }, 3000); + + }, 10); + }) + .catch((err: Error) => { + console.error('URL 복사에 실패했습니다.', err); + setIsAnimating(false); + }); + }; + + const checkEventOpen = () => { + if(!isEventOpen) { + alert("지금은 이벤트 기간이 아닙니다!"); + } + }; + return ( <>
-

- CASPER Electric +

+ CASPER Electric 신차 출시 추첨 이벤트

-

+

전력으로...!
- + 315Km { />
sharedButton - 공유하기 + 공유하기 +
+
+ +
+ 전력 질주하러 가기 + rightShevron +
+
- -
- 전력 질주하러 가기 - rightShevron -
-
@@ -66,12 +113,16 @@ const EventIntro = () => { className="opacity-[70%] w-[230px] right-0 top-96 absolute z-10" />
+ {showMessage && ( +
+
+ URL이 복사되었습니다! +
+
+ )}

); }; -const shareEvent = () => { - console.log("이벤트를 공유합니다!"); -}; export default EventIntro; diff --git a/Caecae/src/features/RacingGameLanding/EventPeriod.tsx b/Caecae/src/features/RacingGameLanding/EventPeriod.tsx index 54b3812a..f8555e75 100644 --- a/Caecae/src/features/RacingGameLanding/EventPeriod.tsx +++ b/Caecae/src/features/RacingGameLanding/EventPeriod.tsx @@ -1,7 +1,18 @@ import InfoSection from "../../components/common/InfoSection/index"; import { Link } from "../../shared/Hyunouter"; -const EventPeriod = () => { +interface EventPeriodProps { + isEventOpen: boolean; +} + +const EventPeriod:React.FC = ({isEventOpen}) => { + + const checkEventOpen = () => { + if(!isEventOpen) { + alert("지금은 이벤트 기간이 아닙니다!"); + } + }; + return ( <>
@@ -9,13 +20,13 @@ const EventPeriod = () => {

이벤트 기간

-
+
-

- 참여기간: 7.15(월) - 7.21(일) +

+ 참여 기간: 7.15 (월) - 7.21 (일)
- 당첨자 발표 날짜: 7.29(월)부터 주말ㆍ공휴일 제외 순차적으로 + 당첨자 발표 날짜: 7.29 (월)부터 주말ㆍ공휴일 제외 순차적으로 발송

@@ -24,14 +35,16 @@ const EventPeriod = () => { alt="eventPeriodBackground" />
- -
- play315GameButton -
- +
+ +
+ play315GameButton +
+ +
diff --git a/Caecae/src/features/RacingGameLanding/GiftInfo.tsx b/Caecae/src/features/RacingGameLanding/GiftInfo.tsx index d9e90b10..ca778d9c 100644 --- a/Caecae/src/features/RacingGameLanding/GiftInfo.tsx +++ b/Caecae/src/features/RacingGameLanding/GiftInfo.tsx @@ -6,10 +6,10 @@ const GiftInfo = () => {

참여 혜택

-
+
-

+

캐스퍼 일렉트릭과 함께 압도적 캐미를 보여준 분께
딱 맞는 아이템을 diff --git a/Caecae/src/features/RacingGameLanding/HowToEvent.tsx b/Caecae/src/features/RacingGameLanding/HowToEvent.tsx index 75b57d5f..54ff041b 100644 --- a/Caecae/src/features/RacingGameLanding/HowToEvent.tsx +++ b/Caecae/src/features/RacingGameLanding/HowToEvent.tsx @@ -8,10 +8,10 @@ const HowToEvent = () => {

이벤트 참여 방법

-
+
-

+

캐스퍼 일렉트릭과 전력으로 315Km를 질주해보아요!
@@ -19,7 +19,7 @@ const HowToEvent = () => { 에게 추첨하여 경품 증정!

-

+

* 중복 응모 시 가장 높은 점수 1건만 추첨에 반영됩니다.

@@ -91,10 +91,10 @@ const HowToEvent = () => {
-
+
-

+

캐스퍼 일렉트릭은 다양한 옵션 선택이 가능해요.
가장 기대되는 커스터마이징 옵션을 고르면 diff --git a/Caecae/src/index.tsx b/Caecae/src/index.tsx index f62b68d1..3ed5ef86 100644 --- a/Caecae/src/index.tsx +++ b/Caecae/src/index.tsx @@ -6,6 +6,7 @@ import FindingGameLandingPage from "./pages/FindingGameLanding/FindingGameLandin import RacingGameLandingPage from "./pages/RacingGameLanding/RacingGameLandingPage.tsx"; import FindingGamePage from "./pages/FindingGame/FindingGamePage.tsx"; import RacingGamePage from "./pages/RacingGame/RacingGamePage.tsx"; +import AdminPage from "./pages/Admin/AdminPage.tsx"; // 임시 React component const App = () => { @@ -21,6 +22,7 @@ const App = () => { element={} /> } /> + } />

diff --git a/Caecae/src/jobs/FindingGame/FindingGameWork.tsx b/Caecae/src/jobs/FindingGame/FindingGameWork.tsx index 0033155c..76ed32ab 100644 --- a/Caecae/src/jobs/FindingGame/FindingGameWork.tsx +++ b/Caecae/src/jobs/FindingGame/FindingGameWork.tsx @@ -4,25 +4,37 @@ import { Action, Reducer, } from "../../shared/Hyundux"; +import { + _Position, + GetFindFAmeIsAnswerBodyParameter, + getFindGameIsAnswerDTO, +} from "../../stories/getFindGameIsAnswer"; +import { FindGame } from "../../stories/getFindingGame"; +import Response from "../../utils/Response"; -import FindingGameAnswer from "../../types/FindingGameAnswer"; +import { CorrectAnswer } from "../../stories/getFindGameIsAnswer"; +import huynxios from "../../shared/Hyunxios"; const WORK_NAME = "FindingGame"; // state type interface FindingGamePayLoad { - gameStatus: "Gaming" | "Done"; + imageURL: ""; + gameType: "PIXEL" | "BADGE"; + gameStatus: "Gaming" | "DoneSuccess" | "DoneFail"; answerIndex: number; - answers: FindingGameAnswer[]; - showingAnswers: FindingGameAnswer[]; + ticketId: string; + showingAnswers: CorrectAnswer[]; wrongAnswers: { id: number; y: number; x: number }[]; - showingHint: FindingGameAnswer[]; + showingHint: CorrectAnswer[]; } const initFindingGameState = createState(WORK_NAME, { + imageURL: "", + gameType: "PIXEL", gameStatus: "Gaming", answerIndex: 0, - answers: [], + ticketId: "-1", showingAnswers: [], wrongAnswers: [], showingHint: [], @@ -35,66 +47,66 @@ const findingGameReducer: Reducer = { const payLoad = state.payload; switch (action.actionName) { case "init": { - // 실제로는 여기서 비동기로 answer fetch해야함 - const fetchedAnswers: FindingGameAnswer[] = [ - { - id: Math.random(), - y: 100, - x: 100, - imageURL: - "https://cdn.newautopost.co.kr/newautopost/2024/07/09121310/%EC%BA%90%EC%8A%A4%ED%8D%BC-%EC%9D%BC%EB%A0%89%ED%8A%B8%EB%A6%AD-1.jpg", - info: "알로이 휠은 강도가 높으면서도 무게가 가벼워 주행 성능과 연비를 개선하는 데 큰 도움을 줍니다. 픽셀 디자인의 휠은 캐스퍼 일렉트릭의 스타일을 돋보이게 합니다.", - title: "17인치 알로이 휠 & 타이어", - }, - { - id: Math.random(), - y: 500, - x: 500, - imageURL: - "https://www.hyundai.co.kr/image/upload/asset_library/MDA00000000000052243/d91508780929423b9999a699bafe60aa.jpg", - title: "전자식 공조 시스템", - info: "풀 오토 에어컨이 적용된 전자식 공조 시스템을 통해 자동으로 오너가 원하는 온도로 풍량을 조절하여 쾌적한 실내를 유지합니다.", - }, - ]; - - return makePayLoad(state, { answers: fetchedAnswers }); + const actionPayload = action.payload as Response; + return makePayLoad(state, { + imageURL: actionPayload.data.info.questionImageUrl, + gameType: actionPayload.data.info.answerType, + }); } case "click": { - const actionPayLoad = (action.payload || {}) as { - id: number; + const actionPayLoad = action.payload as { y: number; x: number; + width: number; + heght: number; }; - const showingAnswers: FindingGameAnswer[] = [...payLoad.showingAnswers]; - let isCorrect = false; - payLoad.answers.forEach((answer) => { - if ( - calculateRange(answer.y, answer.x, actionPayLoad.y, actionPayLoad.x) - ) { - isCorrect = true; - showingAnswers.push({ - id: answer.id, - y: answer.y, - x: answer.x, - imageURL: "", - title: "", - info: "", + + const parameter = { + answerList: [ + ...payLoad.showingAnswers.map((answer) => { + return { + positionX: answer.positionX, + positionY: answer.positionY, + } as _Position; + }), + { + positionX: actionPayLoad.x / actionPayLoad.width, + positionY: actionPayLoad.y / actionPayLoad.heght, + } as _Position, + ], + } as GetFindFAmeIsAnswerBodyParameter; + const response = await huynxios.post>( + "/api/finding/answer", + parameter + ); + + if ( + state.payload.showingAnswers.length != + response.data.correctAnswerList.length + ) { + if (response.data.correctAnswerList.length == 2) { + return makePayLoad(state, { + ticketId: response.data.ticketId, + gameStatus: + response.data.ticketId === "-1" ? "DoneFail" : "DoneSuccess", + showingAnswers: response.data.correctAnswerList, }); } - }); - if (isCorrect) { return makePayLoad(state, { - gameStatus: showingAnswers.length == 2 ? "Done" : "Gaming", - showingAnswers: showingAnswers, - showingHint: [], + showingAnswers: response.data.correctAnswerList, }); - } else { - const _wrongAnswers: { id: number; y: number; x: number }[] = [ - ...payLoad.wrongAnswers, - ]; - _wrongAnswers.push(actionPayLoad); - return makePayLoad(state, { wrongAnswers: _wrongAnswers }); } + + return makePayLoad(state, { + wrongAnswers: [ + ...state.payload.wrongAnswers, + { + id: Math.round(Math.random() * 1000), + y: actionPayLoad.y, + x: actionPayLoad.x, + }, + ], + }); } case "removeWrongAnswer": { const actionPayLoad = (action.payload || {}) as { @@ -105,23 +117,24 @@ const findingGameReducer: Reducer = { ); return makePayLoad(state, { wrongAnswers: _wrongAnswers }); } - case "showHint": { - if (state.payload.showingAnswers.length < 2) { - const idsInAnswers = new Set( - state.payload.showingAnswers.map((item) => item.id) - ); - const newHints = state.payload.answers.filter( - (item) => !idsInAnswers.has(item.id) - ); - const newShowingHints = [newHints[0]]; - return makePayLoad(state, { showingHint: newShowingHints }); - } - return state; - } + // case "showHint": { + // if (state.payload.showingAnswers.length < 2) { + // const idsInAnswers = new Set( + // state.payload.showingAnswers.map((item) => item.id) + // ); + // const newHints = state.payload.answers.filter( + // (item) => !idsInAnswers.has(item.id) + // ); + // const newShowingHints = [newHints[0]]; + // return makePayLoad(state, { showingHint: newShowingHints }); + // } + // return state; + // } case "changeShowingAnswer": { const actionPayLoad = (action.payload || {}) as { answerIndex: number; }; + return makePayLoad(state, { answerIndex: actionPayLoad.answerIndex }); } default: @@ -132,21 +145,33 @@ const findingGameReducer: Reducer = { // actions const action = { - init: (): Action => { + init: (object: object): Action => { + const payLoad = object as Response; return { type: WORK_NAME, actionName: "init", + payload: payLoad, }; }, - click: (y: number, x: number): Action => { + click: (y: number, x: number, width: number, height: number): Action => { + const payLoad = { y: y, x: x, width: width, heght: height } as { + y: number; + x: number; + width: number; + heght: number; + }; return { type: WORK_NAME, actionName: "click", - payload: { - id: Math.random(), - y: y, - x: x, - }, + payload: payLoad, + }; + }, + checkAnswer: (object: object): Action => { + const payLoad = object as Response; + return { + type: WORK_NAME, + actionName: "checkAnswer", + payload: payLoad, }; }, removeWrongAnswer: (id: number): Action => { @@ -175,17 +200,4 @@ const action = { }, }; -const calculateRange = ( - answerY: number, - answerX: number, - clickedY: number, - clickedX: number -) => { - return ( - Math.sqrt( - Math.pow(answerY - clickedY, 2) + Math.pow(answerX - clickedX, 2) - ) <= 100 - ); -}; - export { action, initFindingGameState, findingGameReducer }; diff --git a/Caecae/src/pages/Admin/AdminPage.tsx b/Caecae/src/pages/Admin/AdminPage.tsx new file mode 100644 index 00000000..50a05fab --- /dev/null +++ b/Caecae/src/pages/Admin/AdminPage.tsx @@ -0,0 +1,434 @@ +import { ChangeEvent, useState } from "react"; +import PictureGameBoard from "../../components/common/PictureGameBoard"; +import huynxios from "../../shared/Hyunxios"; +import findMeToDTO from "./FindMeToDTO"; + +export interface FindMe { + day: number; + winner: number; + startTime: string; + endTime: string; + gameType: string; + questionImageURL: string; + answers: FindMeAnswer[]; +} + +interface FindMeAnswer { + id: number; + title: string; + explain: string; + imageURL: string; + x: number; + y: number; +} + +const defaultFindMe: FindMe = { + day: 0, + winner: 0, + startTime: "", + endTime: "", + gameType: "PICXEL", + questionImageURL: "", + answers: [], +}; + +const defaultFindMeAnswer: FindMeAnswer = { + id: 0, + title: "", + explain: "", + imageURL: "", + x: 0, + y: 0, +}; + +const days = [ + "첫째 날", + "둘째 날", + "셋째 날", + "넷째 날", + "다섯째 날", + "여섯째 날", + "일곱째 날", +]; + +const AdminPage = () => { + const [findmes, setFindMes] = useState( + new Array(7).fill(0).map((_, index) => { + return { ...defaultFindMe, day: index }; + }) + ); + const [day, setDay] = useState(0); + const [answer, setAnswer] = useState(defaultFindMeAnswer); + const [mode, setMode] = useState("findme"); + + function changeQuestionURL(url: string) { + const newFindme = [...findmes]; + newFindme[day].questionImageURL = url; + setFindMes(newFindme); + } + + function changeMode(mode: string) { + setMode(mode); + } + + function changeAnswer(answer: FindMeAnswer) { + setAnswer(() => { + return { ...answer }; + }); + } + + const tableData = findmes[day].answers.map((eachAnswer) => { + return ( + + + { +

+ {eachAnswer.y}
{eachAnswer.x} +

+ } + + {eachAnswer.title} + {eachAnswer.explain} + {eachAnswer.imageURL} + + ); + }); + + const handleImageChange = ( + event: ChangeEvent, + isQuestion: boolean = false + ) => { + const file = event.target.files?.[0]; + if (file && file.type === "image/jpeg") { + const reader = new FileReader(); + reader.onloadend = async () => { + const url = await imageToURL(file, isQuestion); + alert(url); + if (isQuestion) { + changeQuestionURL(url); + } + }; + reader.readAsDataURL(file); + } else { + alert("Please select a JPG file."); + } + }; + + const content = + mode === "findme" ? ( +
+
+
+ {days.map((_day, index) => ( + { + setDay(day); + }} + /> + ))} +
+
+
+

당첨 인원

+
+ { + const newFindme = [...findmes]; + newFindme[day].winner = Number(str.target.value); + setFindMes(newFindme); + }} + className="border border-black p-1" + placeholder="Enter text here" + /> +

+
+
+

시작 시간

+
+ { + const newFindme = [...findmes]; + newFindme[day].startTime = str.target.value; + setFindMes(newFindme); + }} + className="border border-black p-1" + placeholder="Enter text here" + /> +
+
+

종료 시간

+
+ { + const newFindme = [...findmes]; + newFindme[day].endTime = str.target.value; + setFindMes(newFindme); + }} + className="border border-black p-1" + placeholder="Enter text here" + /> +
+
+

정답 유형

+
+ { + const newFindme = [...findmes]; + newFindme[day].gameType = str.target.value; + setFindMes(newFindme); + }} + className="border border-black p-1" + placeholder="Enter text here" + /> +
+
+
+ { + width; + height; + const newAnswer = { ...answer }; + newAnswer.y = y / 400; + newAnswer.x = x / 473.7036; + setAnswer(newAnswer); + }} + /> +
+
+ + + + + + + + {tableData} + + + + + + +
no정답설명이미지 소스
+ + + { + const before = { ...answer }; + before.title = str.target.value; + changeAnswer(before); + }} + className="border border-black p-1" + placeholder="Enter text here" + /> + + { + const before = { ...answer }; + before.explain = str.target.value; + changeAnswer(before); + }} + className="border border-black p-1" + placeholder="Enter text here" + /> + + { + const before = { ...answer }; + before.imageURL = str.target.value; + changeAnswer(before); + }} + className="border border-black p-1" + placeholder="Enter text here" + /> +
+
+
{ + const newFindMe = [...findmes]; + + newFindMe[day].answers = [...newFindMe[day].answers, answer]; + setFindMes(newFindMe); + }} + > +

저장하기

+
+
+
+ ) : ( +
+
+

이벤트 상태

+
+ +
+
+

let's 당첨

+
+
+
+ ); +
+

당첨 인원

+
+ +

+
; + return ( +
+
+

이벤트 설정

+
+ handleImageChange(event, true)} + /> + +
{ + await huynxios.post( + "/api/admin/finding/answer", + findMeToDTO(findmes[day]) + ); + }} + > +

저장하기

+
+
+
+

기본 설정

+
+

이벤트 기간

+
+ +
+ +
+

상세 이벤트 설정

+
+

{ + changeMode("findme"); + }} + > + 나를 찾아봐 +

+
+

{ + changeMode(""); + }} + > + 전력으로 513km +

+
+ {content} +
+ ); +}; + +const DayBtn = ({ + day, + data, + onclick, + isOn = false, +}: { + day: number; + data: string; + isOn?: boolean; + onclick: (day: number) => void; +}) => { + return ( +
onclick(day)} + > + {isOn ? ( +

{data}

+ ) : ( +

{data}

+ )} +
+ ); +}; + +async function imageToURL(data: Blob, isQuestion = false) { + const formdata = new FormData(); + formdata.append("file", data); + formdata.append("directory", isQuestion ? "question" : "answer"); + + const requestOptions: RequestInit = { + method: "POST", + body: formdata, + headers: new Headers(), + credentials: "include", + redirect: "follow", + }; + + const response = await await fetch( + "http://43.201.185.99:8080/api/admin/s3", + requestOptions + ); + const result = (await response.json()) as { + responseCode: number; + message: string; + data: { + imageUrl: string; + }; + }; + return result.data.imageUrl; +} + +export default AdminPage; diff --git a/Caecae/src/pages/Admin/FindMeToDTO.tsx b/Caecae/src/pages/Admin/FindMeToDTO.tsx new file mode 100644 index 00000000..dcaae8cf --- /dev/null +++ b/Caecae/src/pages/Admin/FindMeToDTO.tsx @@ -0,0 +1,40 @@ +import { FindMe } from "./AdminPage"; + +interface Answer { + coordX: number; + coordY: number; + descriptionImageUrl: string; + title: string; + content: string; +} + +interface FindMeDTO { + dayOfEvent: number; + numberOfWinner: number; + questionImageUrl: string; + startTime: string; + endTime: string; + answerType: string; //BADGE + answerInfoList: Answer[]; +} + +export default function findMeToDTO(data: FindMe): FindMeDTO { + const answers = data.answers.map((answer): Answer => { + return { + coordX: answer.x, + coordY: answer.y, + descriptionImageUrl: answer.imageURL, + title: answer.title, + content: answer.explain, + }; + }); + return { + dayOfEvent: data.day + 1, + numberOfWinner: 315, + questionImageUrl: data.questionImageURL, + startTime: "15:15:00", + endTime: "14:15:00", + answerType: data.gameType, + answerInfoList: answers, + }; +} diff --git a/Caecae/src/pages/FindingGame/FindingGamePage.tsx b/Caecae/src/pages/FindingGame/FindingGamePage.tsx index 52665834..d103095d 100644 --- a/Caecae/src/pages/FindingGame/FindingGamePage.tsx +++ b/Caecae/src/pages/FindingGame/FindingGamePage.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { ReactNode, useEffect } from "react"; import { FindingGame, FindingGameInfo } from "../../components/FindingGame"; import { findingGameReducer, @@ -12,6 +12,7 @@ import FailContent from "./Enter/FailContent"; import SuccessEnterContent from "./Enter/SuccessEnterContent"; import FindingGameResult from "../../components/FindingGame/FindingGameResult"; import PhoneNumberOverlay from "../../components/PhoneNumberOverlay/PhoneNumberOverlay"; +import huynxios from "../../shared/Hyunxios"; const FindingGamePage = () => { const [gameState, dispatch] = useWork( @@ -27,14 +28,43 @@ const FindingGamePage = () => { } }, [gameState.showingAnswers.length]); - return ( -
+ let content: ReactNode | null = null; + if (gameState.gameStatus == "DoneSuccess") { + console.log(gameState.gameStatus); + content = ( } /> - } /> + { + const response = await huynxios.post("/api/finding/register", { + ticketId: gameState.ticketId, + phone: phoneNumber, + }); + console.log(response); + }} + /> + } + /> } /> - } /> + ); + } else if (gameState.gameStatus == "DoneFail") { + console.log(gameState.gameStatus); + content = ( + + } /> + + ); + } + console.log(gameState.gameStatus); + + return ( +
+ {content}
diff --git a/Caecae/src/pages/RacingGameLanding/RacingGameLandingPage.tsx b/Caecae/src/pages/RacingGameLanding/RacingGameLandingPage.tsx index 814d01e8..02c35d6b 100644 --- a/Caecae/src/pages/RacingGameLanding/RacingGameLandingPage.tsx +++ b/Caecae/src/pages/RacingGameLanding/RacingGameLandingPage.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react"; import Navigation from "../../components/common/Navigation"; import { EventIntro, @@ -7,19 +8,37 @@ import { EventPrecaution, } from "../../features/RacingGameLanding"; import Footer from "../../components/common/Footer"; +import getRacingGameAvailable from "../../stories/getRacingGameAvailable"; const RacingGameLandingPage = () => { - return ( - <> - - - - - - -