diff --git a/src/App.jsx b/src/App.jsx
index ca022617..27b936e1 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -4,6 +4,7 @@ import Header from "./header";
import SimpleInformation from "./simpleInformation";
import DetailInformation from "./detailInformation";
import CommentSection from "./comment";
+import FcfsSection from "./fcfs";
import QnA from "./qna";
import Footer from "./footer";
import Modal from "./modal/modal.jsx";
@@ -25,6 +26,7 @@ function App() {
+
diff --git a/src/auth/mock.js b/src/auth/mock.js
index 778d58f1..aed8b1c0 100644
--- a/src/auth/mock.js
+++ b/src/auth/mock.js
@@ -33,7 +33,7 @@ const handlers = [
{ error: "응답 내용이 잘못됨" },
{ status: 400 },
);
- if (authCode !== "726679")
+ if (+authCode < 500000 === false)
return HttpResponse.json(
{ error: "인증번호 일치 안 함" },
{ status: 401 },
diff --git a/src/comment/commentCarousel/CommentCarousel.jsx b/src/comment/commentCarousel/CommentCarousel.jsx
index a38f73e5..9e9e84b9 100644
--- a/src/comment/commentCarousel/CommentCarousel.jsx
+++ b/src/comment/commentCarousel/CommentCarousel.jsx
@@ -1,3 +1,5 @@
+import { useQuery } from "@/common/dataFetch/getQuery.js";
+import { fetchServer } from "@/common/dataFetch/fetchServer.js";
import AutoScrollCarousel from "../autoScrollCarousel";
function mask(string) {
@@ -15,8 +17,10 @@ function formatDate(dateString) {
return `${year}. ${month}. ${day}`;
}
-function CommentCarousel({ resource }) {
- const comments = resource().comments;
+function CommentCarousel() {
+ const { comments } = useQuery("comment-data", () =>
+ fetchServer("/api/v1/comment"),
+ );
return (
diff --git a/src/comment/commentCarousel/index.jsx b/src/comment/commentCarousel/index.jsx
index 86a2d69c..3b78aae1 100644
--- a/src/comment/commentCarousel/index.jsx
+++ b/src/comment/commentCarousel/index.jsx
@@ -1,17 +1,14 @@
-import { useMemo } from "react";
import Suspense from "@/common/Suspense.jsx";
import ErrorBoundary from "@/common/ErrorBoundary.jsx";
-import { fetchResource } from "@/common/dataFetch/fetchServer.js";
import CommentCarousel from "./CommentCarousel.jsx";
import CommentCarouselSkeleton from "./CommentCarouselSkeleton.jsx";
import CommentCarouselError from "./CommentCarouselError.jsx";
function CommentCarouselView() {
- const resource = useMemo(() => fetchResource("/api/v1/comment"), []);
return (
}>
}>
-
+
);
diff --git a/src/common/ClientOnly.jsx b/src/common/ClientOnly.jsx
index 48f0ae32..4de0cc2c 100644
--- a/src/common/ClientOnly.jsx
+++ b/src/common/ClientOnly.jsx
@@ -1,20 +1,20 @@
-import { useSyncExternalStore, useEffect } from "react";
+import { useSyncExternalStore } from "react";
-const mountedStore = {
- mounted: false,
- listeners: new Set(),
- mount() {
- mountedStore.mounted = true;
- mountedStore.listeners.forEach((listener) => listener());
- },
- subscribe(listener) {
- mountedStore.listeners.add(listener);
- return () => mountedStore.listeners.delete(listener);
- },
- getSnapshot() {
- return mountedStore.mounted;
- },
-};
+// const mountedStore = {
+// mounted: false,
+// listeners: new Set(),
+// mount() {
+// mountedStore.mounted = true;
+// mountedStore.listeners.forEach((listener) => listener());
+// },
+// subscribe(listener) {
+// mountedStore.listeners.add(listener);
+// return () => mountedStore.listeners.delete(listener);
+// },
+// getSnapshot() {
+// return mountedStore.mounted;
+// },
+// };
/**
* react 클라이언트 only 래퍼 입니다.
@@ -23,13 +23,10 @@ const mountedStore = {
*/
export default function ClientOnly({ children, fallback }) {
const mounted = useSyncExternalStore(
- mountedStore.subscribe,
- mountedStore.getSnapshot,
+ () => {},
+ () => true,
() => false,
);
- useEffect(() => {
- mountedStore.mount();
- }, []);
if (!mounted) return fallback;
return children;
diff --git a/src/common/constants.js b/src/common/constants.js
index 0c012c18..afb037f3 100644
--- a/src/common/constants.js
+++ b/src/common/constants.js
@@ -3,3 +3,5 @@ export const TOKEN_ID = "AWESOME_ORANGE_ACCESS_TOKEN";
// scroll section constants
export const INTERACTION_SECTION = 1;
+export const COMMENT_SECTION = 3;
+export const FCFS_SECTION = 4;
diff --git a/src/common/dataFetch/fetchServer.js b/src/common/dataFetch/fetchServer.js
index 541a9941..244a5fb8 100644
--- a/src/common/dataFetch/fetchServer.js
+++ b/src/common/dataFetch/fetchServer.js
@@ -1,9 +1,5 @@
-import wrapPromise from "./wrapPromise.js";
import tokenSaver from "@/auth/tokenSaver.js";
-const cacheMap = new Map();
-const CACHE_DURATION = 0.2 * 1000;
-
class HTTPError extends Error {
constructor(response) {
super(response.status + " " + response.statusText);
@@ -20,12 +16,6 @@ class ServerCloseError extends Error {
}
function fetchServer(url, options = {}) {
- const key = JSON.stringify({ url, options });
- if (cacheMap.has(key)) {
- const { promise, date } = cacheMap.get(key);
- if (Date.now() - date < CACHE_DURATION) return promise;
- }
-
// 기본적으로 옵션을 그대로 가져오지만, body가 존재하고 header.content-type을 설정하지 않는다면
// json으로 간주하여 option을 생성합니다.
const fetchOptions = { ...options };
@@ -65,17 +55,10 @@ function fetchServer(url, options = {}) {
}
throw e;
});
- cacheMap.set(key, { promise, date: Date.now() });
return promise;
}
-function fetchResource(url, loginStatus = false) {
- return wrapPromise(
- fetchServer(url, { credentials: loginStatus ? "include" : "same-origin" }),
- );
-}
-
function handleError(errorDescriptor) {
return (error) => {
if (error instanceof HTTPError) {
@@ -95,4 +78,4 @@ function handleError(errorDescriptor) {
};
}
-export { fetchServer, fetchResource, handleError, HTTPError, ServerCloseError };
+export { fetchServer, handleError, HTTPError, ServerCloseError };
diff --git a/src/common/dataFetch/getQuery.js b/src/common/dataFetch/getQuery.js
new file mode 100644
index 00000000..032a14b8
--- /dev/null
+++ b/src/common/dataFetch/getQuery.js
@@ -0,0 +1,26 @@
+import use from "./use.js";
+
+const queryMap = new Map();
+const CACHE_DURATION = 10 * 60 * 1000;
+
+function isSame(arr1, arr2) {
+ if (arr1.length !== arr2.length) return false;
+ return arr1.every((value, i) => value === arr2[i]);
+}
+
+export function getQuery(key, promiseFn, dependencyArray = []) {
+ if (queryMap.has(key)) {
+ const { promise, depArr } = queryMap.get(key);
+ if (isSame(depArr, dependencyArray)) return promise;
+ }
+ const promise = promiseFn();
+ queryMap.set(key, { promise, depArr: dependencyArray });
+ setTimeout(() => queryMap.delete(key), CACHE_DURATION);
+ return promise;
+}
+
+export function useQuery(key, promiseFn, dependencyArray = []) {
+ return use(getQuery(key, promiseFn, dependencyArray));
+}
+
+export const getQuerySuspense = useQuery;
diff --git a/src/common/dataFetch/use.js b/src/common/dataFetch/use.js
new file mode 100644
index 00000000..70da6cac
--- /dev/null
+++ b/src/common/dataFetch/use.js
@@ -0,0 +1,19 @@
+export default function use(promise) {
+ if (promise.status === "resolved") return promise.value;
+ if (promise.status === "rejected") throw promise.error;
+ if (promise.status === "pending") throw promise;
+
+ promise.status = "pending";
+ promise
+ .then((e) => {
+ promise.status = "resolved";
+ promise.value = e;
+ return e;
+ })
+ .catch((e) => {
+ promise.status = "rejected";
+ promise.error = e;
+ });
+
+ throw promise;
+}
diff --git a/src/common/dataFetch/wrapPromise.js b/src/common/dataFetch/wrapPromise.js
deleted file mode 100644
index 7172fa02..00000000
--- a/src/common/dataFetch/wrapPromise.js
+++ /dev/null
@@ -1,23 +0,0 @@
-export default function wrapPromise(promise) {
- let state = "pending";
- let result = null;
- promise
- .then((res) => {
- state = "complete";
- result = res;
- })
- .catch((err) => {
- state = "error";
- result = err;
- });
- return () => {
- switch (state) {
- case "complete":
- return result;
- case "error":
- throw result;
- default:
- throw promise;
- }
- };
-}
diff --git a/src/common/utils.js b/src/common/utils.js
index 590043d8..2be99fe6 100644
--- a/src/common/utils.js
+++ b/src/common/utils.js
@@ -7,3 +7,22 @@ export function clamp(target, min, max) {
export function linearMap(ratio, min, max) {
return ratio * (max - min) + min;
}
+
+const HOURS = 24;
+const MINUTES = 60;
+const SECONDS = 60;
+
+export function padNumber(number) {
+ return number.toString().padStart(2, "0");
+}
+
+export function convertSecondsToString(time) {
+ if (time < 0) return "00 : 00 : 00";
+
+ const days = Math.floor(time / (HOURS * MINUTES * SECONDS));
+ const hours = Math.floor(time / (MINUTES * SECONDS)) % HOURS;
+ const minutes = Math.floor(time / SECONDS) % MINUTES;
+ const seconds = time % SECONDS;
+
+ return `${days > 0 ? days + " : " : ""}${[hours, minutes, seconds].map(padNumber).join(" : ")}`;
+}
diff --git a/src/eventDescription/EventDetail.jsx b/src/eventDescription/EventDetail.jsx
new file mode 100644
index 00000000..1f5a12a3
--- /dev/null
+++ b/src/eventDescription/EventDetail.jsx
@@ -0,0 +1,58 @@
+import makeHighlight from "./makeHighlight.jsx";
+
+export default function EventDetail({
+ durationYear,
+ duration,
+ announceDate,
+ announceDateCaption,
+ howto,
+}) {
+ return (
+
+
+ 상세 안내
+
+
+
+
+ 이벤트 기간
+
+ {durationYear}
+
+ {duration}
+
+
+
+
+ 당첨자 발표
+
+
+
+ {makeHighlight(announceDate, "font-normal text-neutral-300")}
+
+
+
+ {announceDateCaption}
+
+
+
+
+
+
+ );
+}
diff --git a/src/eventDescription/makeHighlight.jsx b/src/eventDescription/makeHighlight.jsx
new file mode 100644
index 00000000..6286a017
--- /dev/null
+++ b/src/eventDescription/makeHighlight.jsx
@@ -0,0 +1,17 @@
+// **매일매일 공개되는** 더 뉴 아이오닉과 관련된 **인터랙션을 수행한다.**
+// **...**로 감싸진 것은 강조입니다.
+
+function makeHighlight(plainText, highlightClass) {
+ const tokened = plainText.split(/\*\*(.*?)\*\*/gm);
+
+ return tokened.map((content, index) => {
+ if (index % 2 === 0) return content;
+ return (
+
+ {content}
+
+ );
+ });
+}
+
+export default makeHighlight;
diff --git a/src/fcfs/FcfsDescription.jsx b/src/fcfs/FcfsDescription.jsx
new file mode 100644
index 00000000..64f0afdf
--- /dev/null
+++ b/src/fcfs/FcfsDescription.jsx
@@ -0,0 +1,12 @@
+import EventDetail from "@/eventDescription/EventDetail.jsx";
+import content from "./content.json";
+
+function FcfsDescription() {
+ return (
+
+
+
+ );
+}
+
+export default FcfsDescription;
diff --git a/src/fcfs/cardGame/Card.jsx b/src/fcfs/cardGame/Card.jsx
new file mode 100644
index 00000000..41b82db1
--- /dev/null
+++ b/src/fcfs/cardGame/Card.jsx
@@ -0,0 +1,58 @@
+import { useState } from "react";
+import style from "./style.module.css";
+import correct1x from "./assets/correct@1x.png";
+import correct2x from "./assets/correct@2x.png";
+import failed1x from "./assets/failed@1x.png";
+import failed2x from "./assets/failed@2x.png";
+import hidden1x from "./assets/hidden@1x.png";
+import hidden2x from "./assets/hidden@2x.png";
+
+// 빠진 것: index props, setPending, setCorrect state
+function Card({ offline, locked, fliped, setGlobalLock }) {
+ const [isFlipped, setFlipped] = useState(fliped);
+ const [isPending] = useState(false);
+ const [isCorrect] = useState(false);
+ const cardFaceBaseStyle = "absolute top-0 left-0 w-full h-full";
+
+ function flip() {
+ setGlobalLock(true);
+ if (offline) return setFlipped(true);
+
+ setFlipped(true);
+ }
+
+ const answer1x = isCorrect ? correct1x : failed1x;
+ const answer2x = isCorrect ? correct2x : failed2x;
+
+ return (
+
+ );
+}
+
+export default Card;
diff --git a/src/fcfs/cardGame/CardGame.jsx b/src/fcfs/cardGame/CardGame.jsx
new file mode 100644
index 00000000..4aa0ca92
--- /dev/null
+++ b/src/fcfs/cardGame/CardGame.jsx
@@ -0,0 +1,51 @@
+import { useState } from "react";
+import useFcfsStore from "../store.js";
+import * as Status from "../constants.js";
+import CardGameTitle from "./CardGameTitle.jsx";
+import Card from "./Card.jsx";
+
+function getLocked(eventStatus, isParticipated, offline) {
+ if (offline) return false;
+ if (isParticipated) return true;
+ if (eventStatus === Status.PROGRESS || eventStatus === Status.OFFLINE)
+ return false;
+ return true;
+}
+
+function CardGame({ offline }) {
+ const [transLocked, setTransLocked] = useState(false);
+ const eventStatus = useFcfsStore((store) => store.eventStatus);
+ const isParticipated = useFcfsStore((store) => store.isParticipated);
+ const isOffline = offline || eventStatus === Status.OFFLINE;
+ const isLocked = getLocked(eventStatus, isParticipated, offline);
+ const cardProps = {
+ offline: isOffline,
+ locked: isLocked || transLocked,
+ fliped: isParticipated,
+ setGlobalLock: setTransLocked,
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default CardGame;
diff --git a/src/fcfs/cardGame/CardGameTitle.jsx b/src/fcfs/cardGame/CardGameTitle.jsx
new file mode 100644
index 00000000..aed89a6c
--- /dev/null
+++ b/src/fcfs/cardGame/CardGameTitle.jsx
@@ -0,0 +1,55 @@
+import useFcfsStore from "../store.js";
+import * as Status from "../constants.js";
+import { convertSecondsToString } from "@/common/utils.js";
+
+function CardGameCountdown() {
+ const countdown = useFcfsStore((store) => store.countdown);
+ return (
+
+ {convertSecondsToString(countdown)}
+
+ );
+}
+
+function CardGameTitle({ status }) {
+ const commonStyle = "text-head-l md:text-7xl font-bold text-center";
+
+ if (status === Status.PROGRESS)
+ return (
+
+ 카드를 뒤집어 주세요!
+
+ );
+ if (status === Status.COUNTDOWN) return
;
+ if (status === Status.WAITING)
+ return (
+
+ 오후
+
+ {" "}
+ 05 : 00
+
+ 에 다시 만나요!
+
+ );
+ if (status === Status.ALREADY)
+ return (
+
이미 참여하셨습니다
+ );
+ return (
+
+
+ 카드를 뒤집어 주세요!
+
+
+ * 본 이벤트는 마감된 이벤트입니다. 이벤트의 열기를 느껴보세요!
+
+
+ );
+}
+
+export default CardGameTitle;
diff --git a/src/fcfs/cardGame/assets/correct@1x.png b/src/fcfs/cardGame/assets/correct@1x.png
new file mode 100644
index 00000000..a3488bcd
Binary files /dev/null and b/src/fcfs/cardGame/assets/correct@1x.png differ
diff --git a/src/fcfs/cardGame/assets/correct@2x.png b/src/fcfs/cardGame/assets/correct@2x.png
new file mode 100644
index 00000000..7e98931c
Binary files /dev/null and b/src/fcfs/cardGame/assets/correct@2x.png differ
diff --git a/src/fcfs/cardGame/assets/failed@1x.png b/src/fcfs/cardGame/assets/failed@1x.png
new file mode 100644
index 00000000..476e770c
Binary files /dev/null and b/src/fcfs/cardGame/assets/failed@1x.png differ
diff --git a/src/fcfs/cardGame/assets/failed@2x.png b/src/fcfs/cardGame/assets/failed@2x.png
new file mode 100644
index 00000000..3148c099
Binary files /dev/null and b/src/fcfs/cardGame/assets/failed@2x.png differ
diff --git a/src/fcfs/cardGame/assets/hidden@1x.png b/src/fcfs/cardGame/assets/hidden@1x.png
new file mode 100644
index 00000000..fe0f4463
Binary files /dev/null and b/src/fcfs/cardGame/assets/hidden@1x.png differ
diff --git a/src/fcfs/cardGame/assets/hidden@2x.png b/src/fcfs/cardGame/assets/hidden@2x.png
new file mode 100644
index 00000000..1cd3319b
Binary files /dev/null and b/src/fcfs/cardGame/assets/hidden@2x.png differ
diff --git a/src/fcfs/cardGame/index.jsx b/src/fcfs/cardGame/index.jsx
new file mode 100644
index 00000000..772d81b1
--- /dev/null
+++ b/src/fcfs/cardGame/index.jsx
@@ -0,0 +1,24 @@
+import Suspense from "@/common/Suspense.jsx";
+import ErrorBoundary from "@/common/ErrorBoundary.jsx";
+import useFcfsStore from "../store.js";
+import CardGame from "./CardGame.jsx";
+
+function CardGameInitializer() {
+ const getData = useFcfsStore((store) => store.getData);
+
+ getData();
+
+ return
;
+}
+
+function CardGameSection() {
+ return (
+
에러남 }>
+