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"
/>
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 (
-
+
{
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 (
+ <>
+
+
+ >
+ );
+}
+
+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
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}
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 (
-