diff --git a/.github/workflows/deploy_preview.yaml b/.github/workflows/deploy_preview.yaml index 578e1b8b..4b590343 100644 --- a/.github/workflows/deploy_preview.yaml +++ b/.github/workflows/deploy_preview.yaml @@ -7,8 +7,9 @@ on: branches: - "dev" paths: - - "common/**" - - "mainPage/**" + - "src/common/**" + - "src/mainPage/**" + - "public/**" jobs: deploy-preview: runs-on: ubuntu-latest diff --git a/.github/workflows/deploy_production.yaml b/.github/workflows/deploy_production.yaml index 7df4129f..fde35b9e 100644 --- a/.github/workflows/deploy_production.yaml +++ b/.github/workflows/deploy_production.yaml @@ -7,8 +7,9 @@ on: branches: - "main" paths: - - "common/**" - - "mainPage/**" + - "src/common/**" + - "src/mainPage/**" + - "public/**" jobs: deploy-production: runs-on: ubuntu-latest diff --git a/src/adminPage/App.jsx b/src/adminPage/App.jsx index 1ca2d56e..9345f7f7 100644 --- a/src/adminPage/App.jsx +++ b/src/adminPage/App.jsx @@ -2,11 +2,15 @@ import { useEffect } from "react"; import { Route, Routes } from "react-router-dom"; import LoginPage from "./pages/LoginPage.jsx"; import EventsPage from "./pages/EventsPage.jsx"; +import EventsDetailPage from "./pages/EventsDetailPage.jsx"; +import EventsCreatePage from "./pages/EventsCreatePage.jsx"; import ProtectedRoute from "./pages/ProtectedRoute.jsx"; import RootRoute from "./pages/RootRoute.jsx"; import CommentsPage from "./pages/CommentsPage.jsx"; import CommentsIDPage from "./pages/CommentsIDPage.jsx"; +import ServerTimeInitializer from "./shared/serverTime/ServerTimeInitializer.jsx"; +import Modal from "@common/modal/modal.jsx"; import { initLoginState, logout } from "@admin/auth/store.js"; import useLogoutMiddleware from "@common/dataFetch/initLogoutMiddleware"; @@ -20,12 +24,13 @@ function App() { <> }> + } /> event 생성 화면} + path="/events/:id/edit" + element={
이벤트 수정하는 페이지
} /> - event 보는 화면} /> + } /> } /> } /> } /> @@ -33,6 +38,8 @@ function App() { } /> } />
+ + ); } diff --git a/src/adminPage/features/eventEdit/Container.jsx b/src/adminPage/features/eventEdit/Container.jsx new file mode 100644 index 00000000..2653c596 --- /dev/null +++ b/src/adminPage/features/eventEdit/Container.jsx @@ -0,0 +1,22 @@ +import Button from "@common/components/Button.jsx"; + +function EventEditContainer({ title, children }) { + return ( +
+
+
+

{title}

+

*는 필수 입력

+
+
+ + + +
+
+ {children} +
+ ); +} + +export default EventEditContainer; diff --git a/src/adminPage/features/eventEdit/index.jsx b/src/adminPage/features/eventEdit/index.jsx new file mode 100644 index 00000000..176ca6b9 --- /dev/null +++ b/src/adminPage/features/eventEdit/index.jsx @@ -0,0 +1,25 @@ +import Input from "@common/components/Input.jsx"; + +function EventEditor() { + return ( +
+ + +
+ + 이벤트 기간* + +
+
+ ); +} + +export default EventEditor; diff --git a/src/adminPage/features/eventList/DeleteButton.jsx b/src/adminPage/features/eventList/DeleteButton.jsx new file mode 100644 index 00000000..d77e84f1 --- /dev/null +++ b/src/adminPage/features/eventList/DeleteButton.jsx @@ -0,0 +1,55 @@ +import { fetchServer } from "@common/dataFetch/fetchServer.js"; +import { useMutation } from "@common/dataFetch/getQuery.js"; +import ConfirmModal from "@admin/modals/ConfirmModal.jsx"; +import AlertModal from "@admin/modals/AlertModal.jsx"; +import Button from "@common/components/Button.jsx"; +import openModal from "@common/modal/openModal.js"; + +function DeleteButton({ selected, reset }) { + const mutate = useMutation( + "admin-event-list", + () => + fetchServer("/api/v1/admin/events", { + method: "delete", + body: { + eventIds: [...selected], + }, + }), + { + onSuccess: () => { + openModal( + , + ); + reset(); + }, + }, + ); + const deleteConfirmModal = ( + + 이 동작은 다시 돌이킬 수 없습니다. +
+ + {selected.keys().next().value} + {selected.size > 1 && ` 외 ${selected.size - 1} 개의`} 이벤트를 + 삭제하시겠습니까? + + + } + onConfirm={mutate} + /> + ); + + function onClick() { + openModal(deleteConfirmModal); + } + return ( + + ); +} + +export default DeleteButton; diff --git a/src/adminPage/features/eventList/SearchBar.jsx b/src/adminPage/features/eventList/SearchBar.jsx index a608733f..fb3b2dd1 100644 --- a/src/adminPage/features/eventList/SearchBar.jsx +++ b/src/adminPage/features/eventList/SearchBar.jsx @@ -11,7 +11,6 @@ function SearchBar({ onSearch = () => {} }) { onSubmit={(e) => { e.preventDefault(); onSearch(query); - setQuery(""); }} > state.has(key) === false); + if (allFalse) action.keys.forEach((key) => newSet.add(key)); + else action.keys.forEach((key) => newSet.delete(key)); + return newSet; + } + } + throw Error("unknown action."); +} + +export default checkReducer; diff --git a/src/adminPage/features/eventList/index.jsx b/src/adminPage/features/eventList/index.jsx index badef746..ec05408c 100644 --- a/src/adminPage/features/eventList/index.jsx +++ b/src/adminPage/features/eventList/index.jsx @@ -1,23 +1,55 @@ -import { useReducer } from "react"; +import { useReducer, useDeferredValue } from "react"; +import { Link } from "react-router-dom"; -import { searchReducer, setDefaultState } from "./reducer.js"; +import { + searchReducer, + setDefaultState, + searchStateToQuery, +} from "./queryReducer.js"; +import checkReducer from "./checkReducer.js"; import SearchBar from "./SearchBar.jsx"; import Filter from "./Filter.jsx"; -import TableHeader from "./TableHeader.jsx"; +import SearchResult from "./table"; +import DeleteButton from "./DeleteButton.jsx"; import Button from "@common/components/Button.jsx"; +import Suspense from "@common/components/Suspense.jsx"; +import ErrorBoundary from "@common/components/ErrorBoundary.jsx"; function EventList() { const [state, dispatch] = useReducer(searchReducer, null, setDefaultState); + const [checkSet, setCheck] = useReducer(checkReducer, new Set()); + const query = useDeferredValue(searchStateToQuery(state)); + const resetCheck = () => setCheck({ type: "reset" }); return (
- + + +
- + { + dispatch({ type: "set_query", value }); + resetCheck(); + }} + /> - +
+ +
+ Error
}> + login}> + + + ); } diff --git a/src/adminPage/features/eventList/mock.js b/src/adminPage/features/eventList/mock.js new file mode 100644 index 00000000..558def55 --- /dev/null +++ b/src/adminPage/features/eventList/mock.js @@ -0,0 +1,105 @@ +import { http, HttpResponse } from "msw"; +import { makeLorem } from "@common/mock/utils.js"; + +function getEventsMock() { + return Array.from({ length: 100 }, (_, i) => { + const startTime = new Date( + Date.now() - + 86400 * 30 * 1000 + + Math.floor(Math.random() * 86400 * 60 * 1000), + ); + const endTime = new Date( + startTime.getTime() + Math.floor(Math.random() * 86400 * 120) * 1000, + ); + + return { + name: makeLorem(3, 7), + eventType: Math.random() > 0.5 ? "fcfs" : "draw", + startTime, + endTime, + eventId: `HD_240808_${i.toString().padStart(3, "0")}`, + }; + }); +} + +const dummyData = getEventsMock(); + +function filterData(filterParam) { + const filterKey = filterParam.split(","); + return function (data) { + if (filterKey.length === 0) return true; + for (let key of filterKey) { + if (key === "fcfs" && data.eventType === "fcfs") return true; + if (key === "draw" && data.eventType === "draw") return true; + } + return false; + }; +} + +function compareString(a, b) { + if (a < b) return -1; + if (a > b) return 1; + return 0; +} + +function sortData(sortParam) { + const sortKey = sortParam.split(",").map((keyValue) => keyValue.split(":")); + return function (a, b) { + for (let [key, sorter] of sortKey) { + const pm = sorter === "desc" ? -1 : 1; + if (key === "eventId") { + const compared = compareString(a.eventId, b.eventId) * pm; + if (compared !== 0) return compared; + } + if (key === "name") { + const compared = compareString(a.name, b.name) * pm; + if (compared !== 0) return compared; + } + if (key === "startTime") { + const compared = (a.startTime - b.startTime) * pm; + if (compared !== 0) return compared; + } + if (key === "endTime") { + const compared = (a.endTime - b.endTime) * pm; + if (compared !== 0) return compared; + } + if (key === "eventType") { + const compared = compareString(a.eventType, b.eventType) * pm; + if (compared !== 0) return compared; + } + } + return 0; + }; +} + +const handlers = [ + http.get("/api/v1/admin/events", async ({ request }) => { + const url = new URL(request.url); + const search = url.searchParams.get("search"); + const filter = url.searchParams.get("filter"); + const sort = url.searchParams.get("sort"); + const page = +url.searchParams.get("page") ?? 1; + const size = +url.searchParams.get("size") ?? 5; + + const result = dummyData + .filter(({ name }) => (search === null ? true : name.includes(search))) + .filter(filterData(filter)) + .sort(sortData(sort)) + .slice((page - 1) * size, page * size); + + return HttpResponse.json(result); + }), + http.delete("/api/v1/admin/events", async ({ request }) => { + const { eventIds } = await request.json(); + + for (let id of eventIds) { + const index = dummyData.findIndex(({ eventId }) => eventId === id); + if (index === -1) continue; + dummyData.splice(index, 1); + } + + return HttpResponse.json(true); + }), +]; + +export default handlers; diff --git a/src/adminPage/features/eventList/reducer.js b/src/adminPage/features/eventList/queryReducer.js similarity index 88% rename from src/adminPage/features/eventList/reducer.js rename to src/adminPage/features/eventList/queryReducer.js index 06b546ef..04b98192 100644 --- a/src/adminPage/features/eventList/reducer.js +++ b/src/adminPage/features/eventList/queryReducer.js @@ -6,7 +6,7 @@ export function setDefaultState() { draw: true, }, sort: { - eventId: "asc", + eventId: "none", name: "none", startTime: "none", endTime: "none", @@ -37,13 +37,16 @@ export function searchReducer(state, action) { }, }; case "set_page": - return { ...state, page: Math.isNaN(+action.value) ? 1 : +action.value }; + return { + ...state, + page: Number.isNaN(+action.value) ? 1 : +action.value, + }; } throw Error("unknown action."); } export function searchStateToQuery(state) { - const path = "api/v1/admin/events"; + const path = "/api/v1/admin/events"; const paramObj = { search: state.query, filter: Object.entries(state.filter) @@ -55,7 +58,7 @@ export function searchStateToQuery(state) { .map(([key, value]) => `${key}:${value}`) .join(","), page: state.page, - size: 5, + size: 10, }; if (state.query === "") delete paramObj.search; diff --git a/src/adminPage/features/eventList/table/SearchResultBody.jsx b/src/adminPage/features/eventList/table/SearchResultBody.jsx new file mode 100644 index 00000000..7c3a8495 --- /dev/null +++ b/src/adminPage/features/eventList/table/SearchResultBody.jsx @@ -0,0 +1,18 @@ +import SearchResultItem from "./SearchResultItem.jsx"; + +function SearchResultBody({ data, checkState, setCheck }) { + return ( +
+ {data.map((item) => ( + + ))} +
+ ); +} + +export default SearchResultBody; diff --git a/src/adminPage/features/eventList/table/SearchResultItem.jsx b/src/adminPage/features/eventList/table/SearchResultItem.jsx new file mode 100644 index 00000000..eb7be91d --- /dev/null +++ b/src/adminPage/features/eventList/table/SearchResultItem.jsx @@ -0,0 +1,60 @@ +import { Link } from "react-router-dom"; + +import tableTemplateCol from "./tableStyle.js"; +import EventStatus from "@admin/serverTime/EventStatus.js"; +import Button from "@common/components/Button.jsx"; +import Checkbox from "@common/components/Checkbox.jsx"; +import { formatDate } from "@common/utils.js"; + +function SearchResultItem({ + eventId, + name, + startTime, + endTime, + eventType, + checked, + setCheck, +}) { + return ( + + ); +} + +export default SearchResultItem; diff --git a/src/adminPage/features/eventList/TableHeader.jsx b/src/adminPage/features/eventList/table/TableHeader.jsx similarity index 82% rename from src/adminPage/features/eventList/TableHeader.jsx rename to src/adminPage/features/eventList/table/TableHeader.jsx index 088ca8a8..94f3f84f 100644 --- a/src/adminPage/features/eventList/TableHeader.jsx +++ b/src/adminPage/features/eventList/table/TableHeader.jsx @@ -9,7 +9,7 @@ const headerData = [ { key: "eventType", name: "종류" }, ]; -function TableHeader({ state, dispatch }) { +function TableHeader({ state, dispatch, checkSelect }) { function changeSort(target) { return (value) => dispatch({ type: "set_sort", target, value }); } @@ -18,9 +18,13 @@ function TableHeader({ state, dispatch }) {
-
+
+ {headerData.map(({ key, name }) => ( fetchServer(query), { + dependencyArray: [query], + deferred: true, + }); + const page = 10; + + const checkSelect = () => { + const keys = dataList.map(({ eventId }) => eventId); + checkDispatch({ type: "toggle_keys", keys }); + }; + + return ( + <> + + { + return (value) => checkDispatch({ type: "check_key", key, value }); + }} + /> +
+ queryDispatch({ type: "set_page", value })} + maxPage={page} + /> +
+ + ); +} + +export default SearchResult; diff --git a/src/adminPage/features/eventList/table/tableStyle.js b/src/adminPage/features/eventList/table/tableStyle.js new file mode 100644 index 00000000..55d9cba8 --- /dev/null +++ b/src/adminPage/features/eventList/table/tableStyle.js @@ -0,0 +1,3 @@ +const tableTemplateCol = `grid grid-cols-[0.5fr_1.5fr_3fr_2fr_2fr_0.5fr_0.5fr_1fr]`; + +export default tableTemplateCol; diff --git a/src/adminPage/features/eventList/tableStyle.js b/src/adminPage/features/eventList/tableStyle.js deleted file mode 100644 index 2bb96c25..00000000 --- a/src/adminPage/features/eventList/tableStyle.js +++ /dev/null @@ -1,3 +0,0 @@ -const tableTemplateCol = `grid grid-cols-[0.5fr_1fr_3fr_2fr_2fr_1fr_1fr_1fr]`; - -export default tableTemplateCol; diff --git a/src/adminPage/index.css b/src/adminPage/index.css index 5d8fc6ec..ce65b507 100644 --- a/src/adminPage/index.css +++ b/src/adminPage/index.css @@ -29,6 +29,6 @@ body.scrollLocked { position: fixed; width: 100%; - overflow-y: scroll; + overflow-y: auto; } } diff --git a/src/adminPage/mock.js b/src/adminPage/mock.js index f68decce..35538303 100644 --- a/src/adminPage/mock.js +++ b/src/adminPage/mock.js @@ -1,8 +1,15 @@ import { setupWorker } from "msw/browser"; import authHandler from "@admin/auth/mock.js"; import commentHandler from "./features/comment/mock.js"; +import serverTimeHandler from "@admin/serverTime/mock.js"; +import eventSearchHandler from "./features/eventList/mock.js"; // mocking은 기본적으로 각 feature 폴더 내의 mock.js로 정의합니다. // 새로운 feature의 mocking을 추가하셨으면, mock.js의 setupWorker 내부 함수에 인자를 spread 연산자를 이용해 추가해주세요. // 예시 : export default setupWorker(...authHandler, ...questionHandler, ...articleHandler); -export default setupWorker(...authHandler, ...commentHandler); +export default setupWorker( + ...authHandler, + ...eventSearchHandler, + ...serverTimeHandler, + ...commentHandler, +); diff --git a/src/adminPage/pages/EventsCreatePage.jsx b/src/adminPage/pages/EventsCreatePage.jsx new file mode 100644 index 00000000..e9461ea6 --- /dev/null +++ b/src/adminPage/pages/EventsCreatePage.jsx @@ -0,0 +1,15 @@ +import Container from "@admin/components/Container.jsx"; +import EventEditContainer from "../features/eventEdit/Container.jsx"; +import EventEditor from "../features/eventEdit/index.jsx"; + +function EventsCreatePage() { + return ( + + + + + + ); +} + +export default EventsCreatePage; diff --git a/src/adminPage/pages/EventsDetailPage.jsx b/src/adminPage/pages/EventsDetailPage.jsx new file mode 100644 index 00000000..172040bc --- /dev/null +++ b/src/adminPage/pages/EventsDetailPage.jsx @@ -0,0 +1,11 @@ +import Container from "@admin/components/Container.jsx"; + +function EventsDetailPage() { + return ( + +
이벤트 디테일 들어갈 예정
+
+ ); +} + +export default EventsDetailPage; diff --git a/src/adminPage/pages/LoginPage.jsx b/src/adminPage/pages/LoginPage.jsx index 7e2d2975..1b2d9ba9 100644 --- a/src/adminPage/pages/LoginPage.jsx +++ b/src/adminPage/pages/LoginPage.jsx @@ -2,7 +2,7 @@ import Container from "@admin/components/Container.jsx"; import LoginSection from "@admin/auth/LoginSection.jsx"; function LoginPage() { return ( - + ); diff --git a/src/adminPage/shared/components/Container.jsx b/src/adminPage/shared/components/Container.jsx index 5b8a6f78..38e4a8c4 100644 --- a/src/adminPage/shared/components/Container.jsx +++ b/src/adminPage/shared/components/Container.jsx @@ -1,10 +1,12 @@ import NavBar from "./NavBar.jsx"; -function Container({ children }) { +function Container({ children, shouldCenter = false }) { return (
-
+
{children}
diff --git a/src/adminPage/shared/components/Pagination.jsx b/src/adminPage/shared/components/Pagination.jsx new file mode 100644 index 00000000..e187e041 --- /dev/null +++ b/src/adminPage/shared/components/Pagination.jsx @@ -0,0 +1,69 @@ +import { clamp } from "@common/utils.js"; + +function getPaginationItem(currentPage, maxPage, length) { + let prevDelta = length % 2 === 1 ? (length - 1) / 2 : length / 2 - 1; + let postDelta = length % 2 === 1 ? (length - 1) / 2 : length / 2; + + if (currentPage - prevDelta <= 0) + return Array.from({ length }, (_, i) => i + 1); + if (currentPage + postDelta > maxPage) + return Array.from({ length }, (_, i) => maxPage - length + i + 1); + return Array.from({ length }, (_, i) => currentPage - prevDelta + i); +} + +function PaginationButton({ onClick, disabled, highlighted, children }) { + return ( + + ); +} + +function Pagination({ currentPage, setPage: _setPage, maxPage, length = 5 }) { + const setPage = (index) => () => _setPage(clamp(index, 1, maxPage)); + + return ( +
+ + << + + + < + + {getPaginationItem(currentPage, maxPage, length).map((i) => ( + + {i} + + ))} + maxPage} + > + > + + maxPage} + > + >> + +
+ ); +} + +export default Pagination; diff --git a/src/adminPage/shared/modals/AlertModal.jsx b/src/adminPage/shared/modals/AlertModal.jsx new file mode 100644 index 00000000..ecc8e38f --- /dev/null +++ b/src/adminPage/shared/modals/AlertModal.jsx @@ -0,0 +1,28 @@ +import { useContext } from "react"; +import { ModalCloseContext } from "@common/modal/modal.jsx"; +import Button from "@common/components/Button.jsx"; + +function AlertModal({ title, description }) { + const close = useContext(ModalCloseContext); + + return ( +
+
+
+

{title}

+ +
+
{description}
+
+
+ +
+
+ ); +} + +export default AlertModal; diff --git a/src/adminPage/shared/modals/ConfirmModal.jsx b/src/adminPage/shared/modals/ConfirmModal.jsx new file mode 100644 index 00000000..3abc2aa2 --- /dev/null +++ b/src/adminPage/shared/modals/ConfirmModal.jsx @@ -0,0 +1,38 @@ +import { useContext } from "react"; +import { ModalCloseContext } from "@common/modal/modal.jsx"; +import Button from "@common/components/Button.jsx"; + +function ConfirmModal({ title, description, onConfirm }) { + const close = useContext(ModalCloseContext); + + return ( +
+
+
+

{title}

+ +
+
{description}
+
+
+ + +
+
+ ); +} + +export default ConfirmModal; diff --git a/src/adminPage/shared/serverTime/EventStatus.js b/src/adminPage/shared/serverTime/EventStatus.js new file mode 100644 index 00000000..a8551314 --- /dev/null +++ b/src/adminPage/shared/serverTime/EventStatus.js @@ -0,0 +1,14 @@ +import useServerTime from "./store.js"; + +function EventStatus({ startTime: _startTime, endTime: _endTime }) { + const serverTime = useServerTime((store) => store.serverTime); + const startTime = + _startTime instanceof Date ? _startTime : new Date(_startTime); + const endTime = _endTime instanceof Date ? _endTime : new Date(_endTime); + + if (startTime > serverTime) return "예정"; + if (endTime > serverTime) return "진행중"; + return "종료"; +} + +export default EventStatus; diff --git a/src/adminPage/shared/serverTime/ServerTimeInitializer.jsx b/src/adminPage/shared/serverTime/ServerTimeInitializer.jsx new file mode 100644 index 00000000..84e2788b --- /dev/null +++ b/src/adminPage/shared/serverTime/ServerTimeInitializer.jsx @@ -0,0 +1,21 @@ +import useServerTime from "./store.js"; +import Suspense from "@common/components/Suspense.jsx"; +import ErrorBoundary from "@common/components/ErrorBoundary.jsx"; + +function ServerTimeInitializerInternal() { + const getData = useServerTime((store) => store.getData); + getData(); + return null; +} + +function ServerTimeInitializer() { + return ( + + + + + + ); +} + +export default ServerTimeInitializer; diff --git a/src/adminPage/shared/serverTime/mock.js b/src/adminPage/shared/serverTime/mock.js new file mode 100644 index 00000000..5be9026d --- /dev/null +++ b/src/adminPage/shared/serverTime/mock.js @@ -0,0 +1,9 @@ +import { http, HttpResponse } from "msw"; + +const handlers = [ + http.get("/api/serverTime", () => { + return HttpResponse.json({ timestamp: new Date() }); + }), +]; + +export default handlers; diff --git a/src/adminPage/shared/serverTime/store.js b/src/adminPage/shared/serverTime/store.js new file mode 100644 index 00000000..6a69f15c --- /dev/null +++ b/src/adminPage/shared/serverTime/store.js @@ -0,0 +1,17 @@ +import { create } from "zustand"; +import { getQuerySuspense } from "@/common/dataFetch/getQuery.js"; +import { getServerPresiseTime } from "@common/utils.js"; + +const serverTimeStore = create((set) => ({ + serverTime: new Date("2024-08-31 12:00"), // <-- dummy initial data + getData: () => { + const promiseFn = async () => { + const time = await getServerPresiseTime(); + set({ serverTime: new Date(time) }); + return time; + }; + return getQuerySuspense("server-precise-time", promiseFn, [set]); + }, +})); + +export default serverTimeStore; diff --git a/src/common/components/Checkbox.jsx b/src/common/components/Checkbox.jsx index 8aac4e06..54700331 100644 --- a/src/common/components/Checkbox.jsx +++ b/src/common/components/Checkbox.jsx @@ -1,4 +1,10 @@ -function Checkbox({ className, onChange: userOnChange, ...otherProps }) { +function Checkbox({ + className, + checked, + onChange: userOnChange, + defaultChecked, + ...otherProps +}) { const checkboxStyle = `${className} size-4 appearance-none border border-neutral-300 checked:bg-blue-400 checked:border-0 checked:bg-checked bg-center bg-cover`; @@ -7,11 +13,25 @@ function Checkbox({ className, onChange: userOnChange, ...otherProps }) { userOnChange(target.checked); } + if (checked !== null && checked !== undefined) { + return ( + + ); + } + return ( ); diff --git a/src/common/constants.js b/src/common/constants.js index 8f48624b..db149ca1 100644 --- a/src/common/constants.js +++ b/src/common/constants.js @@ -1,4 +1,4 @@ -export const EVENT_FCFS_ID = ""; +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 032a14b8..85c16328 100644 --- a/src/common/dataFetch/getQuery.js +++ b/src/common/dataFetch/getQuery.js @@ -1,8 +1,30 @@ +import { useSyncExternalStore, useDeferredValue } from "react"; import use from "./use.js"; const queryMap = new Map(); +const queryObservers = new Map(); const CACHE_DURATION = 10 * 60 * 1000; +function subscribeQuery(key) { + return (callback) => { + if (!queryObservers.has(key)) queryObservers.set(key, new Set()); + const set = queryObservers.get(key); + set.add(callback); + + return () => { + const set = queryObservers.get(key); + set.delete(callback); + if (set.size === 0) queryObservers.delete(key); + }; + }; +} + +function updateSubscribedQuery(key) { + queryMap.delete(key); + if (!queryObservers.has(key)) return; + queryObservers.get(key).forEach((callback) => callback()); +} + function isSame(arr1, arr2) { if (arr1.length !== arr2.length) return false; return arr1.every((value, i) => value === arr2[i]); @@ -19,8 +41,33 @@ export function getQuery(key, promiseFn, dependencyArray = []) { return promise; } -export function useQuery(key, promiseFn, dependencyArray = []) { - return use(getQuery(key, promiseFn, dependencyArray)); +export function useQuery(key, promiseFn, config = {}) { + let _config = { dependencyArray: [] }; + if (Array.isArray(config)) _config.dependencyArray = config; + else _config = Object.assign(_config, config); + + const query = useSyncExternalStore(subscribeQuery(key), () => + getQuery(key, promiseFn, _config.dependencyArray), + ); + const deferredQuery = useDeferredValue(query); + + if (_config.deferred) return use(deferredQuery); + return use(query); +} + +export function useMutation(key, promiseFn, { onSuccess, onError } = {}) { + return async () => { + try { + const value = await promiseFn(); + updateSubscribedQuery(key); + onSuccess?.(value); + } catch (e) { + onError?.(e); + if (onError === undefined) throw e; + } + }; } -export const getQuerySuspense = useQuery; +export function getQuerySuspense(key, promiseFn, dependencyArray = []) { + return use(getQuery(key, promiseFn, dependencyArray)); +} diff --git a/src/common/mock/utils.js b/src/common/mock/utils.js new file mode 100644 index 00000000..cffa3324 --- /dev/null +++ b/src/common/mock/utils.js @@ -0,0 +1,18 @@ +export function randArr(arr) { + let idx = Math.floor(Math.random() * arr.length); + return arr[idx]; +} + +export function makeLorem(min, max) { + const loremipsum = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.".split( + " ", + ); + + let result = []; + let cnt = Math.floor(Math.random() * (max - min)) + min; + for (let i = 0; i < cnt; i++) { + result.push(randArr(loremipsum)); + } + return result.join(" "); +} diff --git a/src/common/utils.js b/src/common/utils.js index 23a251cb..8f796cff 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -27,6 +27,40 @@ export function convertSecondsToString(time) { return `${days > 0 ? days + " : " : ""}${[hours, minutes, seconds].map(padNumber).join(" : ")}`; } +export function formatDate(rawDate, format) { + const date = new Date(rawDate); + + const components = { + YYYY: date.getFullYear(), + YY: String(date.getFullYear()).slice(-2), + MM: String(date.getMonth() + 1).padStart(2, "0"), + M: date.getMonth() + 1, + DD: String(date.getDate()).padStart(2, "0"), + D: date.getDate(), + hh: String(date.getHours()).padStart(2, "0"), + h: date.getHours(), + mm: String(date.getMinutes()).padStart(2, "0"), + m: date.getMinutes(), + ss: String(date.getSeconds()).padStart(2, "0"), + s: date.getSeconds(), + }; + + return format.replace( + /YYYY|YY|MM|M|DD|D|hh|h|mm|m|ss|s/g, + (match) => components[match], + ); +} + +export async function getServerPresiseTime() { + const startClientTime = performance.now(); + const { timestamp: serverTime } = await fetch("/api/serverTime") + .then((e) => e.json()) + .catch(() => new Date()); + const networkPayloadTime = performance.now() - startClientTime; + + return new Date(serverTime).getTime() + networkPayloadTime; +} + export function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/src/mainPage/features/comment/commentCarousel/CommentCarousel.jsx b/src/mainPage/features/comment/commentCarousel/CommentCarousel.jsx index a0dd79c3..e021f4a9 100644 --- a/src/mainPage/features/comment/commentCarousel/CommentCarousel.jsx +++ b/src/mainPage/features/comment/commentCarousel/CommentCarousel.jsx @@ -1,6 +1,8 @@ import { useQuery } from "@common/dataFetch/getQuery.js"; import { fetchServer } from "@common/dataFetch/fetchServer.js"; import AutoScrollCarousel from "../autoScrollCarousel"; +import { formatDate } from "@common/utils.js"; +import { EVENT_ID } from "@common/constants.js"; function mask(string) { const len = string.length; @@ -9,17 +11,9 @@ function mask(string) { return string[0] + "*".repeat(len - 2) + string[len - 1]; } -function formatDate(dateString) { - const date = new Date(dateString); - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - return `${year}. ${month}. ${day}`; -} - function CommentCarousel() { const { comments } = useQuery("comment-data", () => - fetchServer("/api/v1/comment"), + fetchServer(`/api/v1/comment/${EVENT_ID}`), ); return ( @@ -33,7 +27,9 @@ function CommentCarousel() {

{content}

{mask(userName)} 님

-

{formatDate(createdAt)}

+

+ {formatDate(createdAt, "YYYY. MM. DD")} +

))} diff --git a/src/mainPage/features/comment/mock.js b/src/mainPage/features/comment/mock.js index 5bdf42e4..8992b73e 100644 --- a/src/mainPage/features/comment/mock.js +++ b/src/mainPage/features/comment/mock.js @@ -1,23 +1,5 @@ import { http, HttpResponse } from "msw"; - -function randArr(arr) { - let idx = Math.floor(Math.random() * arr.length); - return arr[idx]; -} - -function makeLorem(min, max) { - const loremipsum = - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.".split( - " ", - ); - - let result = []; - let cnt = Math.floor(Math.random() * (max - min)) + min; - for (let i = 0; i < cnt; i++) { - result.push(randArr(loremipsum)); - } - return result.join(" "); -} +import { makeLorem } from "@common/mock/utils.js"; function getCommentMock() { return Array.from({ length: 20 }, (_, i) => { @@ -35,7 +17,13 @@ function getCommentMock() { const commentSet = new Set(); const handlers = [ - http.get("/api/v1/comment", () => { + http.get("/api/v1/comment/info", ({ request }) => { + const token = request.headers.get("authorization"); + + if (token === null) return HttpResponse.json({ submitted: false }); + return HttpResponse.json({ submitted: false }); + }), + http.get("/api/v1/comment/:eventFrameId", () => { return HttpResponse.json({ comments: getCommentMock() }); }), http.post("/api/v1/comment/:eventFrameId", async ({ request }) => { @@ -52,12 +40,6 @@ const handlers = [ commentSet.add(token); return HttpResponse.json(true, { status: 200 }); }), - http.get("/api/v1/comment/info", ({ request }) => { - const token = request.headers.get("authorization"); - - if (token === null) return HttpResponse.json({ submitted: false }); - return HttpResponse.json({ submitted: false }); - }), ]; export default handlers; diff --git a/src/mainPage/features/fcfs/cardGame/CardGame.jsx b/src/mainPage/features/fcfs/cardGame/CardGame.jsx index 71d03692..509e0f2e 100644 --- a/src/mainPage/features/fcfs/cardGame/CardGame.jsx +++ b/src/mainPage/features/fcfs/cardGame/CardGame.jsx @@ -10,7 +10,7 @@ import AuthModal from "@main/auth/AuthModal.jsx"; import useFcfsStore from "@main/realtimeEvent/store.js"; import * as Status from "@main/realtimeEvent/constants.js"; -import { EVENT_ID } from "@common/constants.js"; +import { EVENT_FCFS_ID } from "@common/constants.js"; import { fetchServer, handleError } from "@common/dataFetch/fetchServer.js"; import ResetButton from "@main/components/ResetButton.jsx"; @@ -57,7 +57,7 @@ function CardGame({ offline }) { const fetchConfig = { method: "post", body: { eventAnswer: index } }; try { const { answerResult, winner } = await fetchServer( - `/api/v1/event/fcfs/${EVENT_ID}`, + `/api/v1/event/fcfs/${EVENT_FCFS_ID}`, fetchConfig, ).catch(handleError(submitCardgameErrorHandle)); if (answerResult) { diff --git a/src/mainPage/features/fcfs/mock.js b/src/mainPage/features/fcfs/mock.js index 3acf7dce..83afb158 100644 --- a/src/mainPage/features/fcfs/mock.js +++ b/src/mainPage/features/fcfs/mock.js @@ -14,15 +14,18 @@ const handlers = [ eventStatus: "countdown", }); }), - http.get("/api/v1/event/fcfs/participated", async ({ request }) => { - const token = request.headers.get("authorization"); - if (token === null) - return HttpResponse.json({ answerResult: false, winner: false }); + http.get( + "/api/v1/event/fcfs/:eventFrameId/participated", + async ({ request }) => { + const token = request.headers.get("authorization"); + if (token === null) + return HttpResponse.json({ answerResult: false, winner: false }); - //await delay(10000); + //await delay(10000); - return HttpResponse.json({ answerResult: false, winner: false }); - }), + return HttpResponse.json({ answerResult: false, winner: false }); + }, + ), http.post("/api/v1/event/fcfs/:eventFrameId", async ({ request }) => { const { eventAnswer } = await request.json(); diff --git a/src/mainPage/shared/auth/InfoInput/index.jsx b/src/mainPage/shared/auth/InfoInput/index.jsx index 390cb97b..c96fc5be 100644 --- a/src/mainPage/shared/auth/InfoInput/index.jsx +++ b/src/mainPage/shared/auth/InfoInput/index.jsx @@ -67,7 +67,7 @@ function AuthFirstSection({ 자세히 보기