diff --git a/build.js b/build.js index 57d04ea4..17792699 100644 --- a/build.js +++ b/build.js @@ -86,8 +86,9 @@ async function injectSSGToHtml(mode) { await mkdir(dir, { recursive: true }); await writeFile(absolutePath, html); console.log(`pre-rendered : ${path}`); - } catch { + } catch(e) { console.log(`pre-rendered failed : ${path}`); + console.error(e); } } ); await Promise.allSettled(promises); diff --git a/package-lock.json b/package-lock.json index 656e1867..c55b902b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "awesome-orange-project", "version": "0.5.0", "dependencies": { + "jwt-decode": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-lottie-player": "^2.1.0", @@ -4602,6 +4603,15 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/package.json b/package.json index 7ffc8ff7..47cb9cf8 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "preview-admin": "vite preview --outDir dist/admin" }, "dependencies": { + "jwt-decode": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-lottie-player": "^2.1.0", diff --git a/src/adminPage/features/eventEdit/businessLogic/FcfsData.js b/src/adminPage/features/eventEdit/businessLogic/FcfsData.js index 49afbea3..e5c961c0 100644 --- a/src/adminPage/features/eventEdit/businessLogic/FcfsData.js +++ b/src/adminPage/features/eventEdit/businessLogic/FcfsData.js @@ -94,6 +94,15 @@ function verifyItems(map, { startTime, endTime, prevSnapshot = new Map() }) { return result; } +function hasDuplicatedDate(newDate, map) { + if (newDate === undefined) return true; + if (newDate === null) return false; + const dateSet = new Set([...map.values()].map(({ date }) => date?.valueOf() ?? null)); + + if (dateSet.has(newDate.valueOf())) return true; + return false; +} + function getDefaultFcfsArray( startTime, endTime, @@ -194,12 +203,17 @@ class FcfsData { modify(key, data, { startTime, endTime }) { const newData = new FcfsData(this.map); const oldItem = newData.map.get(key); + const verified = verifyItem( { ...oldItem, ...data }, { startTime, endTime, prevSnapshot: oldItem }, ); if (verified === null) newData.map.delete(key); - else newData.map.set(key, verified); + else { + if (hasDuplicatedDate(verified.date, this.map)) verified.date = oldItem.date; + + newData.map.set(key, verified); + } return newData; } modifyAll(data, { startTime, endTime }) { diff --git a/src/mainPage/App.jsx b/src/mainPage/App.jsx index fd7e1720..b6bee566 100644 --- a/src/mainPage/App.jsx +++ b/src/mainPage/App.jsx @@ -11,14 +11,14 @@ import QnA from "./features/qna"; import Footer from "./features/footer"; import Modal from "@common/modal/modal.jsx"; -import { initLoginState, logout } from "@main/auth/store.js"; +import { logout } from "@main/auth/store.js"; import useLogoutMiddleware from "@common/dataFetch/initLogoutMiddleware"; function App() { useEffect(() => { window.scrollTo(0, 0); history.scrollRestoration = "manual"; - initLoginState(); + //initLoginState(); }, []); useLogoutMiddleware(logout); diff --git a/src/mainPage/features/comment/commentForm/index.jsx b/src/mainPage/features/comment/commentForm/index.jsx index d0e65bc9..3c203df8 100644 --- a/src/mainPage/features/comment/commentForm/index.jsx +++ b/src/mainPage/features/comment/commentForm/index.jsx @@ -13,6 +13,7 @@ import openModal from "@common/modal/openModal.js"; const submitCommentErrorHandle = { 400: "negative", 401: "unauthorized", + 404: "no_participated", 409: "하루에 1번만 기대평을 등록할 수 있습니다.", offline: "offline", }; @@ -32,6 +33,10 @@ function CommentForm() { setButtonFetchState(submitted ? "disabled" : "enabled"); }) .catch((e) => { + if (e.status === 401) { + setButtonFetchState("enabled"); + return; + } console.error(e); setButtonFetchState("error"); }) @@ -70,6 +75,7 @@ function CommentForm() { case submitCommentErrorHandle[400]: return openModal(negativeModal); case submitCommentErrorHandle[401]: + case submitCommentErrorHandle[404]: return openModal(noUserModal); case submitCommentErrorHandle["offline"]: return openModal(noServerModal); diff --git a/src/mainPage/features/comment/mock.js b/src/mainPage/features/comment/mock.js index 982645bd..c48122b8 100644 --- a/src/mainPage/features/comment/mock.js +++ b/src/mainPage/features/comment/mock.js @@ -18,7 +18,7 @@ const handlers = [ http.get("/api/v1/comment/info", ({ request }) => { const token = request.headers.get("authorization"); - if (token === null) return HttpResponse.json({ submitted: false }); + if (token === null) return HttpResponse.json({ submitted: false }, { status: 401 }); return HttpResponse.json({ submitted: false }); }), http.get("/api/v1/comment/:eventFrameId", () => { diff --git a/src/mainPage/features/detailInformation/DetailItem.jsx b/src/mainPage/features/detailInformation/DetailItem.jsx index 5a76d766..6fe3ce48 100644 --- a/src/mainPage/features/detailInformation/DetailItem.jsx +++ b/src/mainPage/features/detailInformation/DetailItem.jsx @@ -7,6 +7,7 @@ function DetailItem({ img, title, description }) { className="absolute w-full h-full -z-10 top-0 left-0 object-cover" width="1920" height="1080" + loading="lazy" />

diff --git a/src/mainPage/features/fcfs/cardGame/Card.jsx b/src/mainPage/features/fcfs/cardGame/Card.jsx index 66ce5529..b9d76087 100644 --- a/src/mainPage/features/fcfs/cardGame/Card.jsx +++ b/src/mainPage/features/fcfs/cardGame/Card.jsx @@ -46,6 +46,7 @@ function Card({ index, locked, isFlipped, setFlipped, setGlobalLock, getCardAnsw srcSet={`${hidden1x} 1x, ${hidden2x} 2x`} alt="hidden" draggable="false" + loading="lazy" /> {isCorrect

diff --git a/src/mainPage/features/fcfs/description/DateEventPrize.jsx b/src/mainPage/features/fcfs/description/DateEventPrize.jsx index 81ee0bc9..26136528 100644 --- a/src/mainPage/features/fcfs/description/DateEventPrize.jsx +++ b/src/mainPage/features/fcfs/description/DateEventPrize.jsx @@ -14,7 +14,7 @@ function DateEventPrize({ date, title, capacity, image }) { return (
- {title} + {title}

{ const token = request.headers.get("authorization"); - if (token === null) return HttpResponse.json({ answerResult: false, winner: false }); + if (token === null) return HttpResponse.json(false); //await delay(10000); - return HttpResponse.json({ answerResult: false, winner: false }); + return HttpResponse.json(false); }), http.post("/api/v1/event/fcfs/:eventFrameId", async ({ request }) => { const { eventAnswer } = await request.json(); diff --git a/src/mainPage/features/header/Hamburger/Button.jsx b/src/mainPage/features/header/Hamburger/Button.jsx new file mode 100644 index 00000000..c5fc3b3d --- /dev/null +++ b/src/mainPage/features/header/Hamburger/Button.jsx @@ -0,0 +1,24 @@ +import { useState } from "react"; +import style from "./style.module.css"; + +function HamburgerButton({ children }) { + const [opened, setOpened] = useState(false); + return ( + <> + +

+
{opened && children}
+
+ + ); +} + +export default HamburgerButton; diff --git a/src/mainPage/features/header/Hamburger/style.module.css b/src/mainPage/features/header/Hamburger/style.module.css new file mode 100644 index 00000000..e1d99294 --- /dev/null +++ b/src/mainPage/features/header/Hamburger/style.module.css @@ -0,0 +1,53 @@ +.hamburger { + display: flex; + justify-content: center; + align-items: center; + position: relative; + width: 24px; + height: 20px; +} + +.hamburger > div, +.hamburger > div::before, +.hamburger > div::after { + width: 100%; + height: 2px; + box-sizing: border-box; + background-color: currentColor; + transition: all 0.3s; +} + +.hamburger > div { + position: absolute; + border-color: #24adaf; +} + +.hamburger > div::before { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + transform: translateY(-10px); +} + +.hamburger > div::after { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + transform: translateY(10px); +} + +.hamburger[data-opened="true"] > div { + transform: rotate(135deg); +} + +.hamburger[data-opened="true"] > div::before { + transform: translateY(0px) rotate(180deg); +} + +.hamburger[data-opened="true"] > div::after { + transform: translateY(-0px) rotate(-270deg); +} diff --git a/src/mainPage/features/header/index.jsx b/src/mainPage/features/header/index.jsx index 1743c76f..20f4750c 100644 --- a/src/mainPage/features/header/index.jsx +++ b/src/mainPage/features/header/index.jsx @@ -1,6 +1,7 @@ import scrollTo from "@main/scroll/scrollTo"; import { useSectionStore } from "@main/scroll/store"; import AuthButtonSection from "./AuthButtonSection.jsx"; +import HamburgerButton from "./Hamburger/Button.jsx"; import style from "./index.module.css"; @@ -52,6 +53,22 @@ export default function Header() {
+ +
+
    + {scrollSectionList.map((scrollSection, index) => ( +
  • onClickScrollSection(index + 1)} + className={`flex justify-center items-center w-20 lg:w-24 cursor-pointer ${currentSection - 1 === index ? "text-black" : "text-neutral-300"}`} + > + {scrollSection} +
  • + ))} +
+ +
+
); } diff --git a/src/mainPage/features/interactions/description/GiftDetail.jsx b/src/mainPage/features/interactions/description/GiftDetail.jsx index 5bd64753..a2ba4190 100644 --- a/src/mainPage/features/interactions/description/GiftDetail.jsx +++ b/src/mainPage/features/interactions/description/GiftDetail.jsx @@ -5,7 +5,7 @@ export default function GiftDetail({ contentList }) {
{contentList.map((content, index) => (
- 경품 + 경품
diff --git a/src/mainPage/features/interactions/description/InteractionSlide.jsx b/src/mainPage/features/interactions/description/InteractionSlide.jsx index 9f011ef8..0e8b1450 100644 --- a/src/mainPage/features/interactions/description/InteractionSlide.jsx +++ b/src/mainPage/features/interactions/description/InteractionSlide.jsx @@ -60,11 +60,13 @@ export default function InteractionSlide({ interactionDesc, index, isCurrent, sl inactiveImage activeImage
diff --git a/src/mainPage/features/simpleInformation/contentSection.jsx b/src/mainPage/features/simpleInformation/contentSection.jsx index 7625ba15..748d9560 100644 --- a/src/mainPage/features/simpleInformation/contentSection.jsx +++ b/src/mainPage/features/simpleInformation/contentSection.jsx @@ -45,7 +45,14 @@ export default function ContentSection({ content }) { onAnimationEnd={() => setIsHighlighted(true)} className={`${isVisible ? style.fadeIn : "opacity-0"} z-0 flex flex-col font-bold`} > - {content.title} + {content.title} {content.title} diff --git a/src/mainPage/shared/auth/mock.js b/src/mainPage/shared/auth/mock.js index b29f9233..bb670130 100644 --- a/src/mainPage/shared/auth/mock.js +++ b/src/mainPage/shared/auth/mock.js @@ -4,6 +4,9 @@ function isValidInput(name, phoneNumber) { return name.length >= 2 && phoneNumber.length < 12 && phoneNumber.startsWith("01"); } +const token = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZWFtLW9yYW5nZSIsImlhdCI6MTcyNDA0NDc5MCwiZXhwIjoxNzI0MDQ4MzkwLCJzdWIiOiJldmVudFVzZXIiLCJ1c2VyTmFtZSI6Iuq5gOyCoeu6qSIsInVzZXJJZCI6ImtpbXBpcHB5YXAiLCJyb2xlIjoiZXZlbnRfdXNlciJ9.m5m_PkwmYz5Mt-kjn28435bQtwgph3WO-2J42X82lCg"; + const handlers = [ http.post("/api/v1/event-user/send-auth", async ({ request }) => { const { name, phoneNumber } = await request.json(); @@ -22,7 +25,7 @@ const handlers = [ return HttpResponse.json({ error: "응답 내용이 잘못됨" }, { status: 400 }); if (+authCode < 500000 === false) return HttpResponse.json({ error: "인증번호 일치 안 함" }, { status: 401 }); - return HttpResponse.json({ token: "test_token" }); + return HttpResponse.json({ token }); }), http.post("/api/v1/event-user/login", async ({ request }) => { @@ -32,7 +35,7 @@ const handlers = [ return HttpResponse.json({ error: "응답 내용이 잘못됨" }, { status: 400 }); if (name !== "오렌지" || phoneNumber !== "01019991999") return HttpResponse.json({ error: "사용자 없음" }, { status: 404 }); - return HttpResponse.json({ token: "test_token" }); + return HttpResponse.json({ token }); }), ]; diff --git a/src/mainPage/shared/auth/store.js b/src/mainPage/shared/auth/store.js index 1887f9f7..be118ac2 100644 --- a/src/mainPage/shared/auth/store.js +++ b/src/mainPage/shared/auth/store.js @@ -1,17 +1,56 @@ -import { create } from "zustand"; +import { useSyncExternalStore } from "react"; +import { jwtDecode } from "jwt-decode"; import tokenSaver from "@common/dataFetch/tokenSaver.js"; import { SERVICE_TOKEN_ID } from "@common/constants.js"; -const userStore = create(() => ({ +const defaultUserState = { isLogin: false, userName: "", -})); +}; + +class UserStore { + state; + observers = new Set(); + constructor() { + this.state = createUserStore(); + } + getState(getter) { + return getter(this.state); + } + subscribe(callback) { + this.observers.add(callback); + return () => this.observers.delete(callback); + } + setState(mutateFunc) { + const oldState = this.state; + const newState = typeof mutateFunc === "function" ? mutateFunc(oldState) : mutateFunc; + if (oldState === newState) return; + this.state = newState; + this.observers.forEach((callback) => callback()); + } +} + +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 }; +} function parseTokenToUserName(token) { if (token === null) return ""; - return "사용자"; + try { + const { userName } = jwtDecode(token); + return userName; + } catch { + return "사용자"; + } } +const userStore = new UserStore(); + export function login(token) { tokenSaver.set(token); const userName = parseTokenToUserName(token); @@ -23,12 +62,12 @@ export function logout() { userStore.setState(() => ({ isLogin: false, userName: "" })); } -export function initLoginState() { - tokenSaver.init(SERVICE_TOKEN_ID); - const token = tokenSaver.get(SERVICE_TOKEN_ID); - const userName = parseTokenToUserName(token); - if (token === null) userStore.setState(() => ({ isLogin: false, userName: "" })); - else userStore.setState(() => ({ isLogin: true, userName })); +function useUserStore(func, defaultValue = defaultUserState) { + return useSyncExternalStore( + userStore.subscribe.bind(userStore), + () => userStore.getState(func), + () => func(defaultValue), + ); } -export default userStore; +export default useUserStore; diff --git a/src/mainPage/shared/components/ResetButton.jsx b/src/mainPage/shared/components/ResetButton.jsx index 9185caef..d7ef84e4 100644 --- a/src/mainPage/shared/components/ResetButton.jsx +++ b/src/mainPage/shared/components/ResetButton.jsx @@ -3,7 +3,13 @@ import RefreshIcon from "./refresh.svg?react"; export default function ResetButton({ onClick }) { return ( - ); diff --git a/src/mainPage/shared/drawEvent/store.js b/src/mainPage/shared/drawEvent/store.js new file mode 100644 index 00000000..c6276159 --- /dev/null +++ b/src/mainPage/shared/drawEvent/store.js @@ -0,0 +1,60 @@ +import { create } from "zustand"; +import { fetchServer } from "@common/dataFetch/fetchServer.js"; +import { getQuery, getQuerySuspense } from "@common/dataFetch/getQuery.js"; +import { getServerPresiseTime, getDayDifference } from "@common/utils.js"; +import { EVENT_DRAW_ID, EVENT_START_DATE, DAY_MILLISEC } from "@common/constants.js"; + +function getJoinDataEvent() { + return fetchServer(`/api/v1/event/draw/${EVENT_DRAW_ID}/participation`) + .then(({ dates }) => { + let newJoinedList = [false, false, false, false, false]; + dates.forEach((date) => { + const day = getDayDifference(EVENT_START_DATE, new Date(date)); + newJoinedList[day] = true; + }); + return newJoinedList; + }) + .catch(() => [false, false, false, false, false]); +} + +const drawEventStore = create((set, get) => ({ + joinStatus: [false, false, false, false, false], + openBaseDate: new Date("9999-12-31"), + currentJoined: false, + getJoinData: (logined) => { + async function promiseFn() { + try { + const [serverTime, joinStatus] = Promise.all([ + getQuery("server-time", getServerPresiseTime), + getJoinDataEvent(), + ]); + const currentDay = getDayDifference(EVENT_START_DATE, serverTime); + let currentJoined = false; + if (currentDay >= 0 && currentDay < joinStatus.length) { + currentJoined = joinStatus[currentDay]; + } + set({ joinStatus, openBaseDate: serverTime, currentJoined }); + return joinStatus; + } catch { + set({ + joinStatus: [false, false, false, false, false], + openBaseDate: new Date("9999-12-31"), + currentJoined: false, + }); + return [false, false, false, false, false]; + } + } + return getQuerySuspense(`draw-info-data@${logined}`, promiseFn, [set]); + }, + setCurrentJoin: (value) => { + set({ currentJoined: value }); + }, + getJoinStatus: (index) => { + return get().joinStatus[index]; + }, + getOpenStatus: (index) => { + return get().openBaseDate >= EVENT_START_DATE.getTime() + index * DAY_MILLISEC; + }, +})); + +export default drawEventStore; diff --git a/src/mainPage/shared/realtimeEvent/store.js b/src/mainPage/shared/realtimeEvent/store.js index 8fd9bbad..f78dd4dd 100644 --- a/src/mainPage/shared/realtimeEvent/store.js +++ b/src/mainPage/shared/realtimeEvent/store.js @@ -1,7 +1,7 @@ import { create } from "zustand"; import * as Status from "./constants.js"; import { fetchServer, HTTPError, ServerCloseError } from "@common/dataFetch/fetchServer.js"; -import { getQuerySuspense } from "@common/dataFetch/getQuery.js"; +import { getQuery, getQuerySuspense } from "@common/dataFetch/getQuery.js"; import { getServerPresiseTime } from "@common/utils.js"; import { EVENT_FCFS_ID } from "@common/constants.js"; @@ -55,7 +55,7 @@ const fcfsStore = create((set) => ({ const promiseFn = async function () { // get server time and event info const [serverTime, eventInfo] = await Promise.all([ - getServerPresiseTime(), + getQuery("server-time", getServerPresiseTime), getFcfsEventInfo(), ]); const currentServerTime = serverTime; @@ -75,9 +75,9 @@ const fcfsStore = create((set) => ({ getPariticipatedData: (isLogin) => { const promiseFn = async function () { const participated = await getFcfsParticipated(); - set({ isParticipated: participated.answerResult }); + set({ isParticipated: participated }); }; - return getQuerySuspense(`fcfs-participated-data-${isLogin}`, promiseFn, [set]); + return getQuerySuspense(`fcfs-participated-data@${isLogin}`, promiseFn, [set]); }, setEventStatus: (eventStatus) => set({ eventStatus }), handleCountdown: () => diff --git a/vite.config.js b/vite.config.js index 34fa7ca0..e4e4e440 100644 --- a/vite.config.js +++ b/vite.config.js @@ -26,4 +26,12 @@ export default defineConfig({ { find: "@admin", replacement: resolve(__dirname, "src/adminPage/shared") }, ], }, + preview: { + proxy: { + "/api": { + target: 'http://softeerorange.store', + changeOrigin: true, + } + } + } });