diff --git a/src/adminPage/features/eventEdit/index.jsx b/src/adminPage/features/eventEdit/index.jsx index e0625173..79679fd9 100644 --- a/src/adminPage/features/eventEdit/index.jsx +++ b/src/adminPage/features/eventEdit/index.jsx @@ -51,8 +51,7 @@ function EventEditor({ initialData = null } = {}) { title={`${mode === "create" ? "등록" : "수정"} 완료`} description={`이벤트가 성공적으로 ${mode === "create" ? "등록" : "수정"}되었습니다!`} />, - ); - navigate(mode === "create" ? "/events" : `/events/${state.eventId}`); + ).then(() => navigate(mode === "create" ? "/events" : `/events/${state.eventId}`)); }, onError: (e) => { openModal(); diff --git a/src/common/constants.js b/src/common/constants.js index 9a615d47..dfcbbc7c 100644 --- a/src/common/constants.js +++ b/src/common/constants.js @@ -1,4 +1,4 @@ -export const EVENT_FCFS_ID = 1; +export const EVENT_FCFS_ID = "HD_240808_001"; export const EVENT_DRAW_ID = "HD-19700101-01"; export const EVENT_ID = "the-new-ioniq5"; export const EVENT_START_DATE = new Date(2024, 8, 9); diff --git a/src/common/dataFetch/getQuery.js b/src/common/dataFetch/getQuery.js index ec073e85..9b75d721 100644 --- a/src/common/dataFetch/getQuery.js +++ b/src/common/dataFetch/getQuery.js @@ -103,18 +103,20 @@ export function useQuery(key, promiseFn, config = {}) { * * @return Function : 호출 시, 실제로 post 요청을 발송하는 함수를 반환합니다. */ +export async function mutate(key, promiseFn, { onSuccess, onError } = {}) { + try { + const value = await promiseFn(); + updateSubscribedQuery(key); + onSuccess?.(value); + return value; + } catch (e) { + onError?.(e); + if (onError === undefined) throw e; + } +} + export function useMutation(key, promiseFn, { onSuccess, onError } = {}) { - return async () => { - try { - const value = await promiseFn(); - updateSubscribedQuery(key); - onSuccess?.(value); - return value; - } catch (e) { - onError?.(e); - if (onError === undefined) throw e; - } - }; + return () => mutate(key, promiseFn, { onSuccess, onError }); } export function getQuerySuspense(key, promiseFn, dependencyArray = []) { diff --git a/src/common/modal/modal.jsx b/src/common/modal/modal.jsx index 138a8d92..bfc37a42 100644 --- a/src/common/modal/modal.jsx +++ b/src/common/modal/modal.jsx @@ -1,5 +1,6 @@ import { createContext, useCallback, useEffect, useState, useRef } from "react"; import useModalStore, { closeModal } from "./store.js"; +import useFocusTrap from "./useFocusTrap.js"; export const ModalCloseContext = createContext(() => { console.log("모달이 닫힙니다."); @@ -29,11 +30,26 @@ function Modal({ layer }) { } }, [child]); + useEffect(() => { + if (child === null) return; + + function escHatch(e) { + if (e.key !== "Escape") return; + close(); + e.preventDefault(); + } + document.addEventListener("keydown", escHatch); + return () => document.removeEventListener("keydown", escHatch); + }, [child, close]); + + const focusTrapRef = useFocusTrap(child !== null); + return ( {child !== null ? (
{child}
{ + function observe() { + if (modalStore.getSnapshot(layer) !== component) { + resolve(); + clear(); + } + } + const clear = modalStore.subscribe(observe); + }); } diff --git a/src/common/modal/useFocusTrap.js b/src/common/modal/useFocusTrap.js new file mode 100644 index 00000000..3bb9c615 --- /dev/null +++ b/src/common/modal/useFocusTrap.js @@ -0,0 +1,64 @@ +import { useEffect, useRef } from "react"; + +function getEndPointChild(element) { + const focusableElements = [ + ...element.querySelectorAll( + "a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]", + ), + ].filter((elem) => elem.tabIndex >= 0); + if (focusableElements.length === 0) return [null, null]; + return [focusableElements[0], focusableElements[focusableElements.length - 1]]; +} + +function useFocusTrap(active) { + const prevRef = useRef(null); + const ref = useRef(null); + const endPointChild = useRef([null, null]); + useEffect(() => { + if (!active || ref.current === null) return; + + function renewEndPointChild() { + if (ref.current === null) return; + endPointChild.current = getEndPointChild(ref.current); + } + + function handleTabKey(e) { + if (e.key !== "Tab") return; + + const [first, last] = endPointChild.current; + + if (document.activeElement === prevRef.current) { + if (e.shiftKey) last?.focus(); + else first?.focus(); + e.preventDefault(); + return; + } + + if (first === null || last === null) return; + if (document.activeElement === last && !e.shiftKey) { + first.focus(); + e.preventDefault(); + } else if (document.activeElement === first && e.shiftKey) { + last.focus(); + e.preventDefault(); + } + } + + renewEndPointChild(); + prevRef.current = document.activeElement; + document.addEventListener("keydown", handleTabKey); + const config = { subtree: true, childList: true, attributeFilter: ["disabled", "tabindex"] }; + const observer = new MutationObserver(renewEndPointChild); + observer.observe(ref.current, config); + + return () => { + document.removeEventListener("keydown", handleTabKey); + observer.disconnect(); + prevRef.current.focus(); + }; + }, [active]); + + return ref; +} + +export default useFocusTrap; diff --git a/src/mainPage/features/comment/autoScrollCarousel/index.jsx b/src/mainPage/features/comment/autoScrollCarousel/index.jsx index 0a57f695..82bc89cf 100644 --- a/src/mainPage/features/comment/autoScrollCarousel/index.jsx +++ b/src/mainPage/features/comment/autoScrollCarousel/index.jsx @@ -1,9 +1,10 @@ import useAutoCarousel from "./useAutoCarousel.js"; function AutoScrollCarousel({ speed = 1, gap = 0, children }) { - const { position, ref, eventListener } = useAutoCarousel(speed); + const { position, ref, eventListener } = useAutoCarousel(speed, gap); - const flexStyle = "flex [&>div]:flex-shrink-0 gap-[var(--gap,0)] items-center absolute"; + const flexStyle = + "min-w-full flex [&>div]:flex-shrink-0 gap-[var(--gap,0)] justify-around items-center absolute"; return (
-
{children}
+
{children}
-
{children}
+
); diff --git a/src/mainPage/features/comment/autoScrollCarousel/useAutoCarousel.js b/src/mainPage/features/comment/autoScrollCarousel/useAutoCarousel.js index 2b6b89c8..88516fa5 100644 --- a/src/mainPage/features/comment/autoScrollCarousel/useAutoCarousel.js +++ b/src/mainPage/features/comment/autoScrollCarousel/useAutoCarousel.js @@ -5,7 +5,7 @@ const FRICTION_RATE = 0.1; const MOMENTUM_THRESHOLD = 0.6; const MOMENTUM_RATE = 0.3; -function useAutoCarousel(speed = 1) { +function useAutoCarousel(speed = 1, gap = 0) { const childRef = useRef(null); const [position, setPosition] = useState(0); const [isControlled, setIsControlled] = useState(false); @@ -19,7 +19,7 @@ function useAutoCarousel(speed = 1) { (time) => { if (childRef.current === null) return; - const width = childRef.current.clientWidth; + const width = childRef.current.clientWidth + gap; // 마우스 뗐을 때 관성 재계산 const baseSpeed = isHovered ? 0 : speed; @@ -38,7 +38,7 @@ function useAutoCarousel(speed = 1) { // 타임스탬프 저장 timestamp.current = time; }, - [isHovered, speed], + [isHovered, speed, gap], ); useEffect(() => { diff --git a/src/mainPage/features/comment/commentCarousel/CommentCarousel.jsx b/src/mainPage/features/comment/commentCarousel/CommentCarousel.jsx index 8cd7eda1..354037e7 100644 --- a/src/mainPage/features/comment/commentCarousel/CommentCarousel.jsx +++ b/src/mainPage/features/comment/commentCarousel/CommentCarousel.jsx @@ -1,5 +1,6 @@ import { useQuery } from "@common/dataFetch/getQuery.js"; import { fetchServer } from "@common/dataFetch/fetchServer.js"; +import CommentCarouselNoData from "./CommentCarouselNoData.jsx"; import AutoScrollCarousel from "../autoScrollCarousel"; import { formatDate } from "@common/utils.js"; import { EVENT_ID } from "@common/constants.js"; @@ -14,6 +15,8 @@ function mask(string) { function CommentCarousel() { const { comments } = useQuery("comment-data", () => fetchServer(`/api/v1/comment/${EVENT_ID}`)); + if (comments.length === 0) return ; + return (
diff --git a/src/mainPage/features/comment/commentCarousel/CommentCarouselNoData.jsx b/src/mainPage/features/comment/commentCarousel/CommentCarouselNoData.jsx new file mode 100644 index 00000000..2ed1dae0 --- /dev/null +++ b/src/mainPage/features/comment/commentCarousel/CommentCarouselNoData.jsx @@ -0,0 +1,12 @@ +function CommentCarouselNoData() { + return ( +
+
+ 기대평 없음 +

기대평이 없어요!

+
+
+ ); +} + +export default CommentCarouselNoData; diff --git a/src/mainPage/features/comment/modals/CommentNegativeModal.jsx b/src/mainPage/features/comment/modals/CommentNegativeModal.jsx index 2c759ea4..8cad6e46 100644 --- a/src/mainPage/features/comment/modals/CommentNegativeModal.jsx +++ b/src/mainPage/features/comment/modals/CommentNegativeModal.jsx @@ -1,22 +1,20 @@ import { useContext } from "react"; import { ModalCloseContext } from "@common/modal/modal.jsx"; +import AlertModalContainer from "@main/components/AlertModalContainer.jsx"; import Button from "@common/components/Button.jsx"; function CommentNegativeModal() { const close = useContext(ModalCloseContext); return ( -
-
-

해당 기대평을 등록할 수 없습니다

-

- 비속어, 혐오표현 등 타인에게 불쾌감을 줄 수 있는 표현이 포함된 기대평은 작성이 불가합니다 -

-
+ -
+ ); } diff --git a/src/mainPage/features/comment/modals/CommentNoUserModal.jsx b/src/mainPage/features/comment/modals/CommentNoUserModal.jsx index 99a8d1fb..cab0e907 100644 --- a/src/mainPage/features/comment/modals/CommentNoUserModal.jsx +++ b/src/mainPage/features/comment/modals/CommentNoUserModal.jsx @@ -1,5 +1,6 @@ import { useContext } from "react"; import { ModalCloseContext } from "@common/modal/modal.jsx"; +import AlertModalContainer from "@main/components/AlertModalContainer.jsx"; import Button from "@common/components/Button.jsx"; import scrollTo from "@main/scroll/scrollTo.js"; import { INTERACTION_SECTION } from "@main/scroll/constants.js"; @@ -13,14 +14,10 @@ function CommentNoUserModal() { } return ( -
-
-

아직 기대평을 작성할 수 없습니다.

-

- 오늘의 추첨 이벤트에 참여하고 기대평을 작성하세요 -

-
-
+ -
+ } + >
-
+ ); } diff --git a/src/mainPage/features/comment/modals/CommentSuccessModal.jsx b/src/mainPage/features/comment/modals/CommentSuccessModal.jsx index 6fd4becc..56b997f0 100644 --- a/src/mainPage/features/comment/modals/CommentSuccessModal.jsx +++ b/src/mainPage/features/comment/modals/CommentSuccessModal.jsx @@ -1,14 +1,15 @@ import { useContext } from "react"; import { ModalCloseContext } from "@common/modal/modal.jsx"; +import AlertModalContainer from "@main/components/AlertModalContainer.jsx"; import Button from "@common/components/Button.jsx"; function CommentSuccessModal() { const close = useContext(ModalCloseContext); return ( -
-

기대평이 등록되었습니다!

-
+ -
+ } + > -
+ ); } diff --git a/src/mainPage/features/detailInformation/DetailSwiper.jsx b/src/mainPage/features/detailInformation/DetailSwiper.jsx index b5a47750..081cb440 100644 --- a/src/mainPage/features/detailInformation/DetailSwiper.jsx +++ b/src/mainPage/features/detailInformation/DetailSwiper.jsx @@ -8,8 +8,9 @@ function DetailSwiper({ content }) { const isLastPage = page === content.length - 1; const slideClass = "w-[calc(100%-96px)] min-[1024px]:w-full max-w-[1200px] bg-yellow-400"; - const navigationClass = `invisible absolute [--size:3rem] md:[--size:4.5rem] top-[calc(50%-var(--size)*0.5)] size-[var(--size)] p-2 md:p-4 - flex justify-center items-center rounded-full bg-neutral-100 active:bg-neutral-200 z-10 cursor-pointer select-none`; + const navigationClass = `absolute [--size:0px] lg:[--size:4.5rem] top-[calc(50%-var(--size)*0.5)] size-[var(--size)] p-0 lg:p-4 + flex justify-center items-center rounded-full disabled:hidden + bg-neutral-100 active:bg-neutral-200 z-10 cursor-pointer select-none`; return (
@@ -19,6 +20,7 @@ function DetailSwiper({ content }) { slides-per-view="auto" centered-slides="true" space-between="12" + a11y="true" breakpoints='{"1024":{"spaceBetween":400}}' ref={swiperElRef} > @@ -29,7 +31,8 @@ function DetailSwiper({ content }) { ))} ))} diff --git a/src/mainPage/features/fcfs/cardGame/Card.jsx b/src/mainPage/features/fcfs/cardGame/Card.jsx index b9d76087..fec81c0d 100644 --- a/src/mainPage/features/fcfs/cardGame/Card.jsx +++ b/src/mainPage/features/fcfs/cardGame/Card.jsx @@ -44,7 +44,7 @@ function Card({ index, locked, isFlipped, setFlipped, setGlobalLock, getCardAnsw className={`${cardFaceBaseStyle} ${style.front}`} src={hidden1x} srcSet={`${hidden1x} 1x, ${hidden2x} 2x`} - alt="hidden" + alt={isFlipped ? "" : `${index}번 카드를 뒤집으세요!`} draggable="false" loading="lazy" /> @@ -52,7 +52,13 @@ function Card({ index, locked, isFlipped, setFlipped, setGlobalLock, getCardAnsw className={`${cardFaceBaseStyle} ${style.back}`} src={answer1x} srcSet={`${answer1x} 1x, ${answer2x} 2x`} - alt={isCorrect ? "축하합니다, 당첨입니다!" : "아쉽게도 정답이 아니네요!"} + alt={ + isFlipped + ? isCorrect + ? "축하합니다, 당첨입니다!" + : `${index}번 카드는 정답이 아니네요! 다른 카드를 뒤집으세요.` + : "" + } draggable="false" loading="lazy" /> diff --git a/src/mainPage/features/fcfs/cardGame/CardGame.jsx b/src/mainPage/features/fcfs/cardGame/CardGame.jsx index ea89fad8..f7b0c1f6 100644 --- a/src/mainPage/features/fcfs/cardGame/CardGame.jsx +++ b/src/mainPage/features/fcfs/cardGame/CardGame.jsx @@ -70,8 +70,10 @@ function CardGame({ offline }) { openModal(); break; case submitCardgameErrorHandle[401]: - return new Promise((resolve) => { - openModal( resolve(getCardAnswerOnline(index))} />); + return new Promise((resolve, reject) => { + openModal( resolve(getCardAnswerOnline(index))} />).then( + reject, + ); }); case submitCardgameErrorHandle["offline"]: setOfflineMode(true); diff --git a/src/mainPage/features/fcfs/cardGame/index.jsx b/src/mainPage/features/fcfs/cardGame/index.jsx index 177912c6..90094abe 100644 --- a/src/mainPage/features/fcfs/cardGame/index.jsx +++ b/src/mainPage/features/fcfs/cardGame/index.jsx @@ -13,10 +13,10 @@ function CardGameInitializer() { } function CardGamePariticipatedInitializer() { - const isLogin = useAuthStore((state) => state.isLogin); - const defferedLogin = useDeferredValue(isLogin); + const userId = useAuthStore((state) => state.userId); + const deferredUserId = useDeferredValue(userId); const getPariticipatedData = useFcfsStore((store) => store.getPariticipatedData); - getPariticipatedData(defferedLogin); + getPariticipatedData(deferredUserId); return null; } diff --git a/src/mainPage/features/fcfs/modals/FcfsInvalidModal.jsx b/src/mainPage/features/fcfs/modals/FcfsInvalidModal.jsx index b98e1552..09eb637f 100644 --- a/src/mainPage/features/fcfs/modals/FcfsInvalidModal.jsx +++ b/src/mainPage/features/fcfs/modals/FcfsInvalidModal.jsx @@ -1,23 +1,20 @@ import { useContext } from "react"; import { ModalCloseContext } from "@common/modal/modal.jsx"; +import AlertModalContainer from "@main/components/AlertModalContainer.jsx"; import Button from "@common/components/Button.jsx"; function FcfsInvalidModal() { const close = useContext(ModalCloseContext); return ( -
-
-

선착순 이벤트에 참여할 수 없습니다

-

- 아직 선착순 이벤트 진행 중이 아닙니다. 부적절한 방법으로 이벤트를 참여할 경우 향후 - 불이익이 가해질 수 있습니다. -

-
+ -
+ ); } diff --git a/src/mainPage/features/fcfs/modals/FcfsLoseModal.jsx b/src/mainPage/features/fcfs/modals/FcfsLoseModal.jsx index f48cfd63..ae9f7a45 100644 --- a/src/mainPage/features/fcfs/modals/FcfsLoseModal.jsx +++ b/src/mainPage/features/fcfs/modals/FcfsLoseModal.jsx @@ -1,5 +1,6 @@ import { useContext } from "react"; import { ModalCloseContext } from "@common/modal/modal.jsx"; +import AlertModalContainer from "@main/components/AlertModalContainer.jsx"; import Button from "@common/components/Button.jsx"; import scrollTo from "@main/scroll/scrollTo.js"; import { INTERACTION_SECTION } from "@main/scroll/constants.js"; @@ -15,14 +16,10 @@ function FcfsLoseModal() { } return ( -
-
-

이번 이벤트에 당첨되지 않았어요!

-

- 다음 이벤트 일정을 확인하세요! -

-
-
+ -
+ } + >
-
+ ); } diff --git a/src/mainPage/features/fcfs/modals/FcfsWinModal.jsx b/src/mainPage/features/fcfs/modals/FcfsWinModal.jsx index 96859d69..6a5df63d 100644 --- a/src/mainPage/features/fcfs/modals/FcfsWinModal.jsx +++ b/src/mainPage/features/fcfs/modals/FcfsWinModal.jsx @@ -1,19 +1,16 @@ import { useContext } from "react"; import { ModalCloseContext } from "@common/modal/modal.jsx"; +import AlertModalContainer from "@main/components/AlertModalContainer.jsx"; import Button from "@common/components/Button.jsx"; function FcfsWinModal() { const close = useContext(ModalCloseContext); return ( -
-
-

선착순 이벤트에 당첨되었어요!

-

- 경품 수령은 입력하신 연락처로 개별 안내됩니다 -

-
-
+ -
- 벤 + } + > -
+ ); } diff --git a/src/mainPage/features/header/AuthButtonSection.jsx b/src/mainPage/features/header/AuthButtonSection.jsx deleted file mode 100644 index 34077c81..00000000 --- a/src/mainPage/features/header/AuthButtonSection.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import openModal from "@common/modal/openModal.js"; -import AuthModal from "@main/auth/AuthModal.jsx"; -import WelcomeModal from "@main/auth/Welcome"; -import useAuthStore from "@main/auth/store.js"; - -function AuthButtonSection() { - const isLogin = useAuthStore((store) => store.isLogin); - const userName = useAuthStore((store) => store.userName); - - const welcomeModal = ; - const authModal = ( - isFreshMember && openModal(welcomeModal)} /> - ); - - if (isLogin) - return
{userName}님 환영합니다.
; - - return ( - - ); -} - -export default AuthButtonSection; diff --git a/src/mainPage/features/header/Hamburger/Button.jsx b/src/mainPage/features/header/Hamburger/Button.jsx index c5fc3b3d..f947fdb0 100644 --- a/src/mainPage/features/header/Hamburger/Button.jsx +++ b/src/mainPage/features/header/Hamburger/Button.jsx @@ -5,6 +5,9 @@ function HamburgerButton({ children }) { const [opened, setOpened] = useState(false); return ( <> +
+
{opened && children}
+
-
-
{opened && children}
-
); } diff --git a/src/mainPage/features/header/index.jsx b/src/mainPage/features/header/index.jsx index 20f4750c..cda8c08c 100644 --- a/src/mainPage/features/header/index.jsx +++ b/src/mainPage/features/header/index.jsx @@ -1,6 +1,6 @@ import scrollTo from "@main/scroll/scrollTo"; import { useSectionStore } from "@main/scroll/store"; -import AuthButtonSection from "./AuthButtonSection.jsx"; +import AuthButton from "@main/auth/AuthButton.jsx"; import HamburgerButton from "./Hamburger/Button.jsx"; import style from "./index.module.css"; @@ -28,45 +28,42 @@ export default function Header() { }; } + const navItems = scrollSectionList.map((scrollSection, index) => ( +
  • + +
  • + )); + return ( diff --git a/src/mainPage/features/interactions/mock.js b/src/mainPage/features/interactions/mock.js index 2d94f56c..b8a8b1e2 100644 --- a/src/mainPage/features/interactions/mock.js +++ b/src/mainPage/features/interactions/mock.js @@ -5,10 +5,23 @@ const eventParticipationDate = { }; const handlers = [ - http.get("/api/v1/event/draw/:eventId/participation", () => - HttpResponse.json(eventParticipationDate), - ), - http.post("/api/v1/event/draw/:eventId/participation", () => HttpResponse.json(true)), + http.get("/api/v1/event/draw/:eventId/participation", ({ request }) => { + const token = request.headers.get("authorization"); + if (token === null) return HttpResponse.json({ dates: [] }); + + return HttpResponse.json(eventParticipationDate); + }), + http.post("/api/v1/event/draw/:eventId/participation", ({ request }) => { + const token = request.headers.get("authorization"); + if (token === null) return HttpResponse.json(false, { status: 401 }); + + const dummyTodayStatus = "2024-09-10T12:00:00.000Z"; + if (eventParticipationDate.dates.includes(dummyTodayStatus)) + return HttpResponse.json(false, { status: 409 }); + + eventParticipationDate.dates.push("2024-09-10T12:00:00.000Z"); + return HttpResponse.json(true); + }), http.post("/api/v1/url/shorten", () => HttpResponse.json({ shortUrl: "o1PiWwlZZU", diff --git a/src/mainPage/features/interactions/modal/InteractionAnswer.jsx b/src/mainPage/features/interactions/modal/InteractionAnswer.jsx index 90dd1e1b..76fbc24e 100644 --- a/src/mainPage/features/interactions/modal/InteractionAnswer.jsx +++ b/src/mainPage/features/interactions/modal/InteractionAnswer.jsx @@ -5,7 +5,7 @@ import ShareButton from "./buttons/ShareButton.jsx"; import ParticipateButton from "./buttons/ParticipateButton.jsx"; import AnswerDescription from "./AnswerDescription.jsx"; -import useUserStore from "@main/auth/store.js"; +import useAuthStore from "@main/auth/store.js"; import useDrawEventStore from "@main/drawEvent/store.js"; import style from "./InteractionAnswer.module.css"; @@ -14,19 +14,16 @@ import content from "../content.json"; function getParticipantState(index) { return (state) => { if (!state.getOpenStatus(index) || state.fallbackMode) return ""; - if (state.isTodayEvent(index)) { - if (state.currentJoined) return "오늘 응모가 완료되었습니다!"; - else return ""; - } + if (state.isTodayEvent(index) && state.currentJoined) return "오늘 응모가 완료되었습니다!"; if (state.joinStatus[index]) return "이미 응모하셨습니다!"; else return "응모 기간이 지났습니다!"; }; } -export default function InteractionAnswer({ isAnswerUp, setIsAnswerUp }) { +export default function InteractionAnswer({ isAnswerUp, goBack }) { const index = useContext(InteractionContext); - const isLogin = useUserStore((state) => state.isLogin); + const isLogin = useAuthStore((state) => state.isLogin); const isTodayEvent = useDrawEventStore((state) => state.isTodayEvent(index)); const participantState = useDrawEventStore(getParticipantState(index)); const [isAniPlaying, setIsAniPlaying] = useState(false); @@ -44,7 +41,7 @@ export default function InteractionAnswer({ isAnswerUp, setIsAnswerUp }) {
    ); } diff --git a/src/mainPage/features/interactions/modal/joinEvent.js b/src/mainPage/features/interactions/modal/joinEvent.js index 2ef1303a..ad7ed372 100644 --- a/src/mainPage/features/interactions/modal/joinEvent.js +++ b/src/mainPage/features/interactions/modal/joinEvent.js @@ -1,19 +1,45 @@ import { isLogined } from "@main/auth/store.js"; import drawEventStore from "@main/drawEvent/store.js"; -import { fetchServer } from "@common/dataFetch/fetchServer.js"; +import { fetchServer, handleError } from "@common/dataFetch/fetchServer.js"; +import { mutate } from "@common/dataFetch/getQuery.js"; import { EVENT_DRAW_ID } from "@common/constants.js"; +const joinEventErrorHandler = { + 409: "이미 참여했습니다.", + 404: "이벤트가 존재하지 않습니다.", + offline: "오프라인 폴백 모드로 전환합니다.", +}; + export default function joinEvent(index) { const isLogin = isLogined(); - const { isTodayEvent, currentJoined, setCurrentJoin } = drawEventStore.getState(); + const { isTodayEvent, getJoinStatus, setCurrentJoin, readjustJoinStatus, setFallbackMode } = + drawEventStore.getState(); const todayEvent = isTodayEvent(index); - if (!isLogin || !todayEvent || currentJoined) return; + if (!isLogin || !todayEvent || getJoinStatus(index)) return; - fetchServer(`/api/v1/event/draw/${EVENT_DRAW_ID}/participation`, { method: "post" }) - .then(() => setCurrentJoin(true)) - .catch(() => { - alert("이벤트 참여에 실패했습니다."); - }); + mutate( + "draw-info-data", + () => + fetchServer(`/api/v1/event/draw/${EVENT_DRAW_ID}/participation`, { method: "post" }).catch( + handleError(joinEventErrorHandler), + ), + { + onSuccess: () => setCurrentJoin(true), + onError: (e) => { + switch (e.message) { + case joinEventErrorHandler[409]: + readjustJoinStatus(); + break; + case joinEventErrorHandler[404]: + case joinEventErrorHandler["offline"]: + setFallbackMode(); + break; + default: + alert("이벤트 참여에 실패했습니다."); + } + }, + }, + ); } diff --git a/src/mainPage/features/interactions/subsidy/index.jsx b/src/mainPage/features/interactions/subsidy/index.jsx index fd2580c4..95343f6c 100644 --- a/src/mainPage/features/interactions/subsidy/index.jsx +++ b/src/mainPage/features/interactions/subsidy/index.jsx @@ -51,8 +51,9 @@ function SubsidyInteraction({ interactCallback, $ref }) { directive="동전을 클릭하여 예상 금액을 입력해보세요!" />
    -
    -
    +
    {[...lotties].map((id) => ( ); })} diff --git a/src/mainPage/features/interactions/v2l/PuzzlePiece.jsx b/src/mainPage/features/interactions/v2l/PuzzlePiece.jsx index 5f644c55..ebc3d223 100644 --- a/src/mainPage/features/interactions/v2l/PuzzlePiece.jsx +++ b/src/mainPage/features/interactions/v2l/PuzzlePiece.jsx @@ -1,14 +1,14 @@ import { useState } from "react"; import { LINEAR } from "./constants.js"; -function PuzzlePiece({ shape, onClick, fixRotate }) { +function PuzzlePiece({ shape, onClick, fixRotate, ariaLabel }) { const [fixing, setFixing] = useState(false); const style = { transform: `rotate( ${shape.rotate * 90}deg)`, }; return ( -
    { @@ -21,6 +21,7 @@ function PuzzlePiece({ shape, onClick, fixRotate }) { fixRotate(); setTimeout(() => setFixing(false), 60); }} + aria-label={ariaLabel} > -
    + ); } diff --git a/src/mainPage/features/interactions/v2l/utils.js b/src/mainPage/features/interactions/v2l/utils.js index 2f00b664..ad744f53 100644 --- a/src/mainPage/features/interactions/v2l/utils.js +++ b/src/mainPage/features/interactions/v2l/utils.js @@ -47,6 +47,23 @@ class PieceData { newPiece.rotate = this.rotate % 4; return newPiece; } + getLabel() { + if (this.type === LINEAR) { + if (this.rotate % 2) return "위에서 아래로 이어짐."; + else return "왼쪽에서 오른쪽으로 이어짐."; + } else if (this.type === CURVED) { + switch (this.rotate % 4) { + case 0: + return "오른쪽에서 아래로 이어짐."; + case 1: + return "왼쪽에서 아래로 이어짐."; + case 2: + return "왼쪽에서 위로 이어짐."; + case 3: + return "오른쪽에서 위로 이어짐."; + } + } else return "알 수 없는 모양."; + } } export function generatePiece(shapeString) { diff --git a/src/mainPage/features/qna/QnAArticle.jsx b/src/mainPage/features/qna/QnAArticle.jsx index ae9ba551..86c8ea58 100644 --- a/src/mainPage/features/qna/QnAArticle.jsx +++ b/src/mainPage/features/qna/QnAArticle.jsx @@ -24,7 +24,7 @@ function QnAArticle({ question, answer }) { return (
    -
    +
    +

    diff --git a/src/mainPage/shared/auth/AuthButton.jsx b/src/mainPage/shared/auth/AuthButton.jsx new file mode 100644 index 00000000..846a5273 --- /dev/null +++ b/src/mainPage/shared/auth/AuthButton.jsx @@ -0,0 +1,43 @@ +import openModal from "@common/modal/openModal.js"; +import AuthModal from "./AuthModal.jsx"; +import WelcomeModal from "./Welcome"; +import LogoutModal from "./Logout/LogoutConfirmModal.jsx"; +import useAuthStore from "./store.js"; +import useDrawEventStore from "@main/drawEvent/store.js"; + +function AuthButton() { + const isLogin = useAuthStore((store) => store.isLogin); + const userName = useAuthStore((store) => store.userName); + const setCurrentJoin = useDrawEventStore((store) => store.setCurrentJoin); + + const welcomeModal = ; + const authModal = ( + isFreshMember && openModal(welcomeModal)} /> + ); + const logoutModal = setCurrentJoin(false)} />; + + if (isLogin) + return ( + + ); + + return ( + + ); +} + +export default AuthButton; diff --git a/src/mainPage/shared/auth/Logout/LogoutAlertModal.jsx b/src/mainPage/shared/auth/Logout/LogoutAlertModal.jsx new file mode 100644 index 00000000..09cd2ead --- /dev/null +++ b/src/mainPage/shared/auth/Logout/LogoutAlertModal.jsx @@ -0,0 +1,20 @@ +import { useContext } from "react"; +import { ModalCloseContext } from "@common/modal/modal.jsx"; +import AlertModalContainer from "@main/components/AlertModalContainer.jsx"; +import Button from "@common/components/Button.jsx"; + +function LogoutAlertModal() { + const close = useContext(ModalCloseContext); + + return ( + +

    + +
    + + ); +} + +export default LogoutAlertModal; diff --git a/src/mainPage/shared/auth/Logout/LogoutConfirmModal.jsx b/src/mainPage/shared/auth/Logout/LogoutConfirmModal.jsx new file mode 100644 index 00000000..a5de5b2c --- /dev/null +++ b/src/mainPage/shared/auth/Logout/LogoutConfirmModal.jsx @@ -0,0 +1,32 @@ +import { useContext } from "react"; +import { ModalCloseContext } from "@common/modal/modal.jsx"; +import openModal from "@common/modal/openModal.js"; +import AlertModalContainer from "@main/components/AlertModalContainer.jsx"; +import Button from "@common/components/Button.jsx"; + +import { logout } from "@main/auth/store.js"; +import LogoutAlertModal from "./LogoutAlertModal.jsx"; + +function LogoutConfirmModal({ onLogout }) { + const close = useContext(ModalCloseContext); + function clickLogout() { + logout(); + onLogout?.(); + openModal(); + } + + return ( + +
    + + +
    +
    + ); +} + +export default LogoutConfirmModal; diff --git a/src/mainPage/shared/auth/store.js b/src/mainPage/shared/auth/store.js index 44754b5c..04d6ff24 100644 --- a/src/mainPage/shared/auth/store.js +++ b/src/mainPage/shared/auth/store.js @@ -34,18 +34,18 @@ function createUserStore() { if (typeof window === "undefined") return defaultUserState; tokenSaver.init(SERVICE_TOKEN_ID); const token = tokenSaver.get(SERVICE_TOKEN_ID); - const userName = parseTokenToUserName(token); - if (token === null) return { isLogin: false, userName: "" }; - else return { isLogin: true, userName }; + const { userName, userId } = parseTokenAndGetData(token); + if (token === null) return { isLogin: false, userName: "", userId: "" }; + else return { isLogin: true, userName, userId }; } -function parseTokenToUserName(token) { +function parseTokenAndGetData(token) { if (token === null) return ""; try { - const { userName } = jwtDecode(token); - return userName; + const { userName, userId } = jwtDecode(token); + return { userName, userId }; } catch { - return "사용자"; + return { userName: "사용자", userId: "1nvalidU5er" }; } } @@ -53,16 +53,16 @@ const userStore = new UserStore(); export function login(token) { tokenSaver.set(token); - const userName = parseTokenToUserName(token); - userStore.setState(() => ({ isLogin: true, userName })); + const { userName, userId } = parseTokenAndGetData(token); + userStore.setState(() => ({ isLogin: true, userName, userId })); } export function logout() { tokenSaver.remove(); - userStore.setState(() => ({ isLogin: false, userName: "" })); + userStore.setState(() => ({ isLogin: false, userName: "", userId: "" })); } -function useUserStore(func, defaultValue = defaultUserState) { +function useAuthStore(func, defaultValue = defaultUserState) { return useSyncExternalStore( userStore.subscribe.bind(userStore), () => userStore.getState(func), @@ -74,4 +74,4 @@ export function isLogined() { return userStore.state.isLogin; } -export default useUserStore; +export default useAuthStore; diff --git a/src/mainPage/shared/components/AlertModalContainer.jsx b/src/mainPage/shared/components/AlertModalContainer.jsx new file mode 100644 index 00000000..c009d92d --- /dev/null +++ b/src/mainPage/shared/components/AlertModalContainer.jsx @@ -0,0 +1,24 @@ +function AlertModalContainer({ title, description, image, children }) { + const containerStyle = + "w-[calc(100%-1rem)] max-w-[31.25rem] h-[calc(100svh-2rem)] p-10 shadow bg-white relative flex flex-col justify-between items-center"; + return ( +
    +
    +

    {title}

    +

    + {description} +

    +
    + {image && ( +
    + {image} +
    + )} + {children} +
    + ); +} + +export default AlertModalContainer; diff --git a/src/mainPage/shared/components/NoServerModal.jsx b/src/mainPage/shared/components/NoServerModal.jsx index 4b9e62e3..88254fc9 100644 --- a/src/mainPage/shared/components/NoServerModal.jsx +++ b/src/mainPage/shared/components/NoServerModal.jsx @@ -1,25 +1,21 @@ import { useContext } from "react"; import { ModalCloseContext } from "@common/modal/modal.jsx"; +import AlertModalContainer from "@main/components/AlertModalContainer.jsx"; import Button from "@common/components/Button.jsx"; function NoServerModal() { const close = useContext(ModalCloseContext); return ( -
    -
    -

    서버가 닫혔어요!

    -

    - 괜찮아요. 저희는 서버가 닫혀도 일부 동작은 가능하니까요! -

    -
    -
    - 서버 닫힘 -
    + } + > -
    + ); } diff --git a/src/mainPage/shared/components/ResetButton.jsx b/src/mainPage/shared/components/ResetButton.jsx index d7ef84e4..e4a24693 100644 --- a/src/mainPage/shared/components/ResetButton.jsx +++ b/src/mainPage/shared/components/ResetButton.jsx @@ -1,7 +1,7 @@ import Button from "@common/components/Button.jsx"; import RefreshIcon from "./refresh.svg?react"; -export default function ResetButton({ onClick }) { +export default function ResetButton({ onClick, disabled }) { return ( diff --git a/src/mainPage/shared/drawEvent/DrawEventFetcher.jsx b/src/mainPage/shared/drawEvent/DrawEventFetcher.jsx index 6eb3414b..3300919f 100644 --- a/src/mainPage/shared/drawEvent/DrawEventFetcher.jsx +++ b/src/mainPage/shared/drawEvent/DrawEventFetcher.jsx @@ -1,10 +1,11 @@ -import useUserStore from "@main/auth/store.js"; +import useAuthStore from "@main/auth/store.js"; import useDrawStore from "./store.js"; function InteractionEventJoinDataFetcher() { - const isLogin = useUserStore((store) => store.isLogin); + const userId = useAuthStore((store) => store.userId); const getData = useDrawStore((store) => store.getJoinData); - getData(isLogin); + + getData(userId); return null; } diff --git a/src/mainPage/shared/drawEvent/store.js b/src/mainPage/shared/drawEvent/store.js index 35d444cb..1f60d271 100644 --- a/src/mainPage/shared/drawEvent/store.js +++ b/src/mainPage/shared/drawEvent/store.js @@ -20,37 +20,48 @@ const drawEventStore = create((set, get) => ({ openBaseDate: new Date("9999-12-31"), currentJoined: false, fallbackMode: false, - getJoinData: (logined) => { + getJoinData: (userId) => { async function promiseFn() { try { const [serverTime, joinStatus] = await Promise.all([ getQuery("server-time", getServerPresiseTime), getJoinDataEvent(), ]); - const currentDay = getDayDifference(EVENT_START_DATE, serverTime); - - let currentJoined = get().currentJoined; - if (!currentJoined && currentDay >= 0 && currentDay < joinStatus.length) { - currentJoined = joinStatus[currentDay]; - } - - set({ joinStatus, openBaseDate: serverTime, currentJoined, fallbackMode: false }); - return joinStatus; - } catch { - set({ + return { joinStatus, openBaseDate: serverTime, fallbackMode: false }; + } catch (e) { + return { joinStatus: [false, false, false, false, false], openBaseDate: new Date("9999-12-31"), currentJoined: false, fallbackMode: true, - }); - return [false, false, false, false, false]; + }; } } - return getQuerySuspense(`draw-info-data@${logined}`, promiseFn, [set]); + async function setter() { + const newState = await getQuery(`draw-info-data@${userId}`, promiseFn); + set(newState); + return newState; + } + return getQuerySuspense("__zustand__draw-event-store-getData", setter, [userId, set]); }, setCurrentJoin: (value) => { set({ currentJoined: value }); }, + readjustJoinStatus: (index) => { + set(({ joinStatus }) => { + const newJoinStatus = [...joinStatus]; + newJoinStatus[index] = true; + return { joinStatus: newJoinStatus }; + }); + }, + setFallbackMode: () => { + set({ + joinStatus: [false, false, false, false, false], + openBaseDate: new Date("9999-12-31"), + currentJoined: false, + fallbackMode: true, + }); + }, getJoinStatus: (index) => { if (get().isTodayEvent(index)) return get().currentJoined || get().joinStatus[index]; return get().joinStatus[index]; diff --git a/src/mainPage/shared/realtimeEvent/store.js b/src/mainPage/shared/realtimeEvent/store.js index f78dd4dd..f2aa8741 100644 --- a/src/mainPage/shared/realtimeEvent/store.js +++ b/src/mainPage/shared/realtimeEvent/store.js @@ -63,21 +63,27 @@ const fcfsStore = create((set) => ({ // get countdown and syncronize state const countdown = Math.ceil((currentEventTime - currentServerTime) / 1000); - set({ + return { currentServerTime, currentEventTime, countdown, eventStatus: eventInfo.eventStatus, - }); + }; }; - return getQuerySuspense("fcfs-info-data", promiseFn, [set]); + const setter = async function () { + const newState = await getQuery("fcfs-info-data", promiseFn); + set(newState); + return newState; + }; + return getQuerySuspense("fcfs-info-data", setter, [set]); }, - getPariticipatedData: (isLogin) => { - const promiseFn = async function () { - const participated = await getFcfsParticipated(); + getPariticipatedData: (userId) => { + const setter = async function () { + const participated = await getQuery(`fcfs-participated-data@${userId}`, getFcfsParticipated); set({ isParticipated: participated }); + return participated; }; - return getQuerySuspense(`fcfs-participated-data@${isLogin}`, promiseFn, [set]); + return getQuerySuspense(`__zustand__fcfs-participated-getData`, setter, [set, userId]); }, setEventStatus: (eventStatus) => set({ eventStatus }), handleCountdown: () =>