Skip to content

Commit

Permalink
Merge pull request #98 from softeerbootcamp4th/feature/26-admin-events
Browse files Browse the repository at this point in the history
[feat] 어드민 페이지 이벤트 목록 구현
  • Loading branch information
darkdulgi authored Aug 14, 2024
2 parents 98400df + 8369277 commit 602259b
Show file tree
Hide file tree
Showing 43 changed files with 831 additions and 99 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/deploy_preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ on:
branches:
- "dev"
paths:
- "common/**"
- "mainPage/**"
- "src/common/**"
- "src/mainPage/**"
- "public/**"
jobs:
deploy-preview:
runs-on: ubuntu-latest
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/deploy_production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ on:
branches:
- "main"
paths:
- "common/**"
- "mainPage/**"
- "src/common/**"
- "src/mainPage/**"
- "public/**"
jobs:
deploy-production:
runs-on: ubuntu-latest
Expand Down
13 changes: 10 additions & 3 deletions src/adminPage/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ 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 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";

Expand All @@ -19,19 +23,22 @@ function App() {
<>
<Routes>
<Route element={<ProtectedRoute />}>
<Route exact path="/events/create" element={<EventsCreatePage />} />
<Route
exact
path="/events/create"
element={<div>event 생성 화면</div>}
path="/events/:id/edit"
element={<div>이벤트 수정하는 페이지</div>}
/>
<Route path="/events/:id" element={<div>event 보는 화면</div>} />
<Route path="/events/:id" element={<EventsDetailPage />} />
<Route path="/events" element={<EventsPage />} />
<Route path="/comments/:id" element={<div>기대평 화면</div>} />
<Route path="/comments" element={<CommentsPage />} />
</Route>
<Route path="/login" element={<LoginPage />} />
<Route path="/" element={<RootRoute />} />
</Routes>
<Modal layer="alert" />
<ServerTimeInitializer />
</>
);
}
Expand Down
22 changes: 22 additions & 0 deletions src/adminPage/features/eventEdit/Container.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Button from "@common/components/Button.jsx";

function EventEditContainer({ title, children }) {
return (
<section className="flex flex-col gap-8">
<div className="flex w-full justify-between">
<div>
<h2 className="text-title-m font-bold">{title}</h2>
<p className="text-detail-l">*는 필수 입력</p>
</div>
<div>
<Button>임시저장 불러오기</Button>
<Button>임시저장</Button>
<Button>등록</Button>
</div>
</div>
{children}
</section>
);
}

export default EventEditContainer;
25 changes: 25 additions & 0 deletions src/adminPage/features/eventEdit/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Input from "@common/components/Input.jsx";

function EventEditor() {
return (
<div>
<label>
<span>
이벤트 명<sup>*</sup>
</span>
<Input />
</label>
<label>
<span>이벤트 ID</span>
<span>(대충 이벤트 id 들어감)</span>
</label>
<div>
<span>
이벤트 기간<sup>*</sup>
</span>
</div>
</div>
);
}

export default EventEditor;
55 changes: 55 additions & 0 deletions src/adminPage/features/eventList/DeleteButton.jsx
Original file line number Diff line number Diff line change
@@ -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(
<AlertModal title="삭제" description="기대평이 삭제되었습니다." />,
);
reset();
},
},
);
const deleteConfirmModal = (
<ConfirmModal
title="삭제"
description={
<>
<span>이 동작은 다시 돌이킬 수 없습니다.</span>
<br />
<span>
{selected.keys().next().value}
{selected.size > 1 && ` 외 ${selected.size - 1} 개의`} 이벤트를
삭제하시겠습니까?
</span>
</>
}
onConfirm={mutate}
/>
);

function onClick() {
openModal(deleteConfirmModal);
}
return (
<Button onClick={onClick} disabled={selected.size === 0}>
삭제
</Button>
);
}

export default DeleteButton;
1 change: 0 additions & 1 deletion src/adminPage/features/eventList/SearchBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ function SearchBar({ onSearch = () => {} }) {
onSubmit={(e) => {
e.preventDefault();
onSearch(query);
setQuery("");
}}
>
<Input
Expand Down
24 changes: 24 additions & 0 deletions src/adminPage/features/eventList/checkReducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function checkReducer(state, action) {
switch (action.type) {
case "reset":
return new Set();
case "check_key": {
const newSet = new Set(state);
if (action.value === true) newSet.add(action.key);
else if (action.value === false) newSet.delete(action.key);
else if (state.has(action.key)) newSet.delete(action.key);
else newSet.add(action.key);
return newSet;
}
case "toggle_keys": {
const newSet = new Set(state);
let allFalse = action.keys.every((key) => 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;
44 changes: 38 additions & 6 deletions src/adminPage/features/eventList/index.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-full h-full flex flex-col gap-4">
<div className="flex justify-end">
<Button>+ 이벤트 등록</Button>
<Link to="./create">
<Button>+ 이벤트 등록</Button>
</Link>
</div>
<SearchBar />
<SearchBar
onSearch={(value) => {
dispatch({ type: "set_query", value });
resetCheck();
}}
/>
<Filter state={state.filter} dispatch={dispatch} />
<TableHeader state={state.sort} dispatch={dispatch} />
<div className="flex justify-end">
<DeleteButton selected={checkSet} reset={resetCheck} />
</div>
<ErrorBoundary fallback={<div>Error</div>}>
<Suspense fallback={<div>login</div>}>
<SearchResult
query={query}
queryState={state}
queryDispatch={dispatch}
checkState={checkSet}
checkDispatch={setCheck}
/>
</Suspense>
</ErrorBoundary>
</div>
);
}
Expand Down
105 changes: 105 additions & 0 deletions src/adminPage/features/eventList/mock.js
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 602259b

Please sign in to comment.