Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 선착순 이벤트 일부 구현 #80

Merged
merged 14 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -25,6 +26,7 @@ function App() {
<InteractionPage />
<DetailInformation />
<CommentSection />
<FcfsSection />
<QnA />
<Footer />
<Modal layer="interaction" />
Expand Down
2 changes: 1 addition & 1 deletion src/auth/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const handlers = [
{ error: "응답 내용이 잘못됨" },
{ status: 400 },
);
if (authCode !== "726679")
if (+authCode < 500000 === false)
return HttpResponse.json(
{ error: "인증번호 일치 안 함" },
{ status: 401 },
Expand Down
8 changes: 6 additions & 2 deletions src/comment/commentCarousel/CommentCarousel.jsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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 (
<div className="w-full h-[29rem]">
Expand Down
5 changes: 1 addition & 4 deletions src/comment/commentCarousel/index.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<ErrorBoundary fallback={<CommentCarouselError />}>
<Suspense fallback={<CommentCarouselSkeleton />}>
<CommentCarousel resource={resource} />
<CommentCarousel />
</Suspense>
</ErrorBoundary>
);
Expand Down
39 changes: 18 additions & 21 deletions src/common/ClientOnly.jsx
Original file line number Diff line number Diff line change
@@ -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 래퍼 입니다.
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/common/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
19 changes: 1 addition & 18 deletions src/common/dataFetch/fetchServer.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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 };
Expand Down Expand Up @@ -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) {
Expand All @@ -95,4 +78,4 @@ function handleError(errorDescriptor) {
};
}

export { fetchServer, fetchResource, handleError, HTTPError, ServerCloseError };
export { fetchServer, handleError, HTTPError, ServerCloseError };
26 changes: 26 additions & 0 deletions src/common/dataFetch/getQuery.js
Original file line number Diff line number Diff line change
@@ -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;
19 changes: 19 additions & 0 deletions src/common/dataFetch/use.js
Original file line number Diff line number Diff line change
@@ -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;
}
23 changes: 0 additions & 23 deletions src/common/dataFetch/wrapPromise.js

This file was deleted.

19 changes: 19 additions & 0 deletions src/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(" : ")}`;
}
58 changes: 58 additions & 0 deletions src/eventDescription/EventDetail.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import makeHighlight from "./makeHighlight.jsx";

export default function EventDetail({
durationYear,
duration,
announceDate,
announceDateCaption,
howto,
}) {
return (
<div className="flex flex-col">
<span className="text-body-l pb-10 text-neutral-50 font-bold">
상세 안내
</span>

<div className="flex gap-5">
<div className="bg-neutral-900 p-6 flex flex-col font-bold">
<span className="text-body-m text-neutral-300">이벤트 기간</span>

<span className="pt-6 text-body-m text-blue-100">{durationYear}</span>

<span className="text-body-l text-blue-400">{duration}</span>
</div>

<div className="bg-neutral-900 p-6 flex flex-col">
<span className="text-body-m text-neutral-300 font-bold">
당첨자 발표
</span>

<span className="pt-6 text-body-l text-white font-bold">
{makeHighlight(announceDate, "font-normal text-neutral-300")}
</span>

<span className="pt-2 text-body-s text-neutral-300">
{announceDateCaption}
</span>
</div>
</div>

<div className="mt-5 p-6 bg-neutral-900 flex flex-col font-bold">
<span className="pb-6 text-body-m text-neutral-300">참여방법</span>

<ul className="flex flex-col gap-2">
{howto.map((description, index) => (
<li key={`howTo-${index}`} className="flex gap-2">
<span className="size-6 flex justify-center items-center bg-neutral-100 text-neutral-900 text-body-s px-2 py-0.5 rounded flex-shrink-0">
{index + 1}
</span>
<p className="text-neutral-400 text-body-m">
{makeHighlight(description, "text-white")}
</p>
</li>
))}
</ul>
</div>
</div>
);
}
17 changes: 17 additions & 0 deletions src/eventDescription/makeHighlight.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<span key={content + index} className={highlightClass}>
{content}
</span>
);
});
}

export default makeHighlight;
12 changes: 12 additions & 0 deletions src/fcfs/FcfsDescription.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import EventDetail from "@/eventDescription/EventDetail.jsx";
import content from "./content.json";

function FcfsDescription() {
return (
<div>
<EventDetail {...content} />
</div>
);
}

export default FcfsDescription;
Loading