Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…someOrange-FE into feature/122-admin-user
  • Loading branch information
darkdulgi committed Aug 21, 2024
2 parents 70193d5 + 52c07b3 commit e37e0ca
Show file tree
Hide file tree
Showing 57 changed files with 623 additions and 280 deletions.
3 changes: 3 additions & 0 deletions admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="소프티어 부트캠프 4기-팀 어썸 오렌지의 IONIQ 5 어드민 페이지입니다." />
<link rel="preload" href="/font/HyundaiSansTextKROTFBold.otf" as="font" type="font/otf" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFMedium.otf" as="font" type="font/otf" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFRegular.otf" as="font" type="font/otf" crossorigin="anonymous">
<title>Awesome Orange - Admin</title>
</head>
<body>
Expand Down
4 changes: 4 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="HI. Five. The new IONIQ 5. 소프티어 부트캠프 4기-팀 어썸 오렌지의 IONIQ 5 이벤트 페이지입니다." />
<link rel="preload" href="/font/DS-DIGI.TTF" as="font" type="font/ttf" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFBold.otf" as="font" type="font/otf" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFMedium.otf" as="font" type="font/otf" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFRegular.otf" as="font" type="font/otf" crossorigin="anonymous">
<title>Awesome Orange FE</title>
</head>
<body>
Expand Down
2 changes: 1 addition & 1 deletion src/adminPage/features/eventDetail/EventDetailFetcher.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useQuery } from "@common/dataFetch/getQuery.js";
import { fetchServer } from "@common/dataFetch/fetchServer.js";

function EventDetailFetcher({ eventId }) {
const data = useQuery(`event-detail-${eventId}`, () =>
const data = useQuery(`admin-event-list/${eventId}`, () =>
fetchServer(`/api/v1/admin/events/${eventId}`),
);
return <EventDetail data={data} />;
Expand Down
10 changes: 8 additions & 2 deletions src/adminPage/features/eventEdit/EventBaseDataInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,17 @@ function EventBaseDataInput() {
/>
</label>
<label className="grid grid-cols-[6rem_1fr] items-start gap-2">
<span className="text-center font-bold">이벤트 요약</span>
<span className="text-center font-bold">
이벤트 요약<sup className="text-red-500">*</sup>
</span>
<div className="relative">
<TextBox
className="w-full"
text={description}
setText={(value) => dispatch({ type: "set_description", value })}
rows="4"
maxLength="100"
required
/>
<span
className={`absolute right-3 bottom-3 text-detail-l ${description.length >= DESCRIPTION_MAX_LENGTH ? "text-red-500" : "text-neutral-600"}`}
Expand All @@ -88,13 +91,16 @@ function EventBaseDataInput() {
</div>
</label>
<label className={columnsStyle}>
<span className="text-center font-bold">이벤트 URL</span>
<span className="text-center font-bold">
이벤트 URL<sup className="text-red-500">*</sup>
</span>
<Input
className="w-[25rem] h-8"
text={url}
setText={(value) => dispatch({ type: "set_url", value })}
type="url"
pattern="https?://.*"
required
/>
</label>
</>
Expand Down
2 changes: 1 addition & 1 deletion src/adminPage/features/eventEdit/EventEditFetcher.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useQuery } from "@common/dataFetch/getQuery.js";
import { fetchServer } from "@common/dataFetch/fetchServer.js";

function EventEditFetcher({ eventId }) {
const data = useQuery(`event-detail-${eventId}`, () =>
const data = useQuery(`admin-event-list/${eventId}`, () =>
fetchServer(`/api/v1/admin/events/${eventId}`),
);
return <EventEditor initialData={data} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class DrawGradeData {
constructor(rawData) {
if (rawData == null) this.data = [{ grade: 1, count: 0, prizeInfo: "" }];
if (rawData == null) this.data = [];
else this.data = [...rawData].sort((a, b) => a.grade - b.grade);
}
get size() {
Expand Down
9 changes: 8 additions & 1 deletion src/adminPage/features/eventEdit/businessLogic/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ function makeVoidDrawData() {
};
}

function makeDefaultDrawData() {
return {
metadata: new DrawGradeData([{ grade: 1, count: 0, prizeInfo: "" }]),
policies: new DrawPolicyData(),
};
}

function makeDrawData(rawData) {
return {
id: rawData.id,
Expand Down Expand Up @@ -80,7 +87,7 @@ export function eventEditReducer(state, action) {
return { ...state, url: action.value };
case "set_event_type":
if (action.value === "draw") {
return { ...state, eventType: "draw", fcfs: new FcfsData() };
return { ...state, eventType: "draw", draw: makeDefaultDrawData(), fcfs: new FcfsData() };
}
return { ...state, eventType: "fcfs", draw: makeVoidDrawData() };
case "set_event_frame":
Expand Down
79 changes: 59 additions & 20 deletions src/adminPage/features/eventEdit/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,47 +16,86 @@ import AlertModal from "@admin/modals/AlertModal.jsx";
import ConfirmModal from "@admin/modals/ConfirmModal.jsx";

import { useMutation } from "@common/dataFetch/getQuery.js";
import { fetchServer, handleError } from "@common/dataFetch/fetchServer.js";

const submitErrorHandler = {
400: "잘못된 입력으로 이벤트 등록에 실패했습니다.",
401: "인증되지 않은 사용자입니다.",
};

const tempSubmitErrorHandler = {
400: "잘못된 입력으로 임시저장에 실패했습니다.",
401: "인증되지 않은 사용자입니다.",
};
import { fetchServer, handleError, HTTPError } from "@common/dataFetch/fetchServer.js";

const tempLoadErrorHandler = {
401: "인증되지 않은 사용자입니다.",
404: "임시저장된 데이터가 없습니다.",
};

function handleEventSubmitError(e) {
if (e instanceof HTTPError) {
if (e.status === 400) {
e.response.json().then((value) => {
console.log(value);
openModal(
<AlertModal
title="등록 실패"
description={
<>
사용자 입력이 잘못되었습니다.
<br />
<span className="whitespace-pre-wrap">{JSON.stringify(value, null, 2)}</span>
</>
}
/>,
);
});
} else if (e.status === 401) {
openModal(<AlertModal title="등록 실패" description="인증되지 않은 사용자입니다." />);
} else if (e.status < 500) {
openModal(
<AlertModal
title="등록 실패"
description={
<>
클라이언트의 오류가 발생했습니다.
<br />
에러 코드 : {e.status}
</>
}
/>,
);
} else {
openModal(
<AlertModal
title="등록 실패"
description={
<>
서버의 오류가 발생했습니다.
<br />
에러 코드 : {e.status}
</>
}
/>,
);
}
} else {
openModal(<AlertModal title="오류라니!" description="알 수 없는 오류가 발생했습니다." />);
}
}

function EventEditor({ initialData = null } = {}) {
const navigate = useNavigate();
const mode = useContext(EventEditModeContext);
const [state, dispatch] = useReducer(eventEditReducer, initialData, setDefaultState);
const submitMutate = useMutation(
mode === "create" ? "event-detail-created" : `event-detail-${state.eventId}`,
mode === "create" ? "admin-event-list" : `admin-event-list/${state.eventId}`,
() =>
fetchServer(mode === "create" ? "/api/v1/admin/events" : "/api/v1/admin/events/edit", {
method: "post",
body: state,
}).catch(handleError(submitErrorHandler)),
}),
{
onSuccess: () => {
openModal(
<AlertModal
title={`${mode === "create" ? "등록" : "수정"} 완료`}
description={`이벤트가 성공적으로 ${mode === "create" ? "등록" : "수정"}되었습니다!`}
/>,
);
navigate(mode === "create" ? "/events" : `/events/${state.eventId}`);
},
onError: (e) => {
openModal(<AlertModal title="등록 실패" description={e.message} />);
).then(() => navigate(mode === "create" ? "/events" : `/events/${state.eventId}`));
},
onError: handleEventSubmitError,
},
);

Expand All @@ -75,10 +114,10 @@ function EventEditor({ initialData = null } = {}) {
await fetchServer("/api/v1/admin/events/temp", {
method: "post",
body: state,
}).catch(handleError(tempSubmitErrorHandler));
});
openModal(<AlertModal title="완료" description="작성 중인 내용이 임시저장되었습니다." />);
} catch (e) {
openModal(<AlertModal title="저장 실패" description={e.message} />);
handleEventSubmitError(e);
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/adminPage/features/eventEdit/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ const dummyData = getEventsDetailMock();
let tempData = null;

const handlers = [
http.post("/api/v1/admin/events", () => {
http.post("/api/v1/admin/events", async ({ request }) => {
const data = await request.json();
if (data.description === "")
return HttpResponse.json({ description: "디스크립션이 없습니다." }, { status: 400 });
tempData = null;
return new HttpResponse(null, { status: 201 });
}),
Expand Down
6 changes: 3 additions & 3 deletions src/adminPage/features/eventList/Filter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ function Filter({ state, dispatch }) {
const labelStyle = "inline-flex items-center gap-1 text-body-s";

return (
<div className="grid grid-cols-2 gap-4">
<fieldset className={fieldsetStyle}>
<div className="grid grid-cols-1 gap-4">
{/* <fieldset className={fieldsetStyle}>
<legend className="px-2 text-body-l font-medium">상태</legend>
<div className="flex flex-wrap gap-2 px-2">
<label className={labelStyle}>
Expand All @@ -34,7 +34,7 @@ function Filter({ state, dispatch }) {
임시저장
</label>
</div>
</fieldset>
</fieldset>*/}
<fieldset className={fieldsetStyle}>
<legend className="px-2 text-body-l font-medium">종류</legend>
<div className="flex flex-wrap gap-2 px-2">
Expand Down
4 changes: 2 additions & 2 deletions src/adminPage/features/eventList/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ 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 filter = url.searchParams.get("type");
const sort = url.searchParams.get("sort");
const page = +url.searchParams.get("page") ?? 1;
const size = +url.searchParams.get("size") ?? 5;
Expand All @@ -82,7 +82,7 @@ const handlers = [
.filter(filterData(filter))
.sort(sortData(sort));

const contents = filteredData.slice((page - 1) * size, page * size);
const contents = filteredData.slice(page * size, (page + 1) * size);

return HttpResponse.json({
contents,
Expand Down
2 changes: 1 addition & 1 deletion src/adminPage/features/eventList/queryReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function searchStateToQuery(state) {
const path = "/api/v1/admin/events";
const paramObj = {
search: state.query,
filter: Object.entries(state.filter)
type: Object.entries(state.filter)
.filter(([, value]) => value)
.map(([key]) => key)
.join(","),
Expand Down
2 changes: 1 addition & 1 deletion src/adminPage/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

@layer base {
body {
font-family: "hdsans";
font-family: "hdsans", sans-serif;
}
body.scrollLocked {
position: fixed;
Expand Down
4 changes: 1 addition & 3 deletions src/adminPage/shared/auth/LoginSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,9 @@ function LoginSection() {
text={password}
setText={setPassword}
type="password"
placeholder="비밀번호 입력하세요."
placeholder="비밀번호를 입력하세요."
required
minLength="8"
maxLength="16"
pattern="^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,16}$"
/>
</label>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/common/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const EVENT_FCFS_ID = 1;
export const EVENT_DRAW_ID = "HD-19700101-01";
export const EVENT_FCFS_ID = "HD_240808_002";
export const EVENT_DRAW_ID = "HD_240808_001";
export const EVENT_ID = "the-new-ioniq5";
export const EVENT_START_DATE = new Date(2024, 8, 9);

Expand Down
30 changes: 18 additions & 12 deletions src/common/dataFetch/getQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ function updateSubscribedQuery(key) {
queryGroupMap.get(key).forEach((subKey) => queryMap.delete(subKey));
queryGroupMap.deleteKey(key);
}

if (queryObservers.has(key)) queryObservers.get(key).forEach((callback) => callback());

if (key.includes("/")) {
const parent = /^(.*?)(?=\/[^/]+$)/.exec(key)[1];
updateSubscribedQuery(parent);
}
}

function isSame(arr1, arr2) {
Expand Down Expand Up @@ -103,18 +107,20 @@ export function useQuery(key, promiseFn, config = {}) {
*
* @return Function : 호출 시, 실제로 post 요청을 발송하는 함수를 반환합니다.
*/
export async function mutate(key, promiseFn, { onSuccess, onError } = {}) {
try {
const value = await promiseFn();
updateSubscribedQuery(key);
onSuccess?.(value);
return value;
} catch (e) {
onError?.(e);
if (onError === undefined) throw e;
}
}

export function useMutation(key, promiseFn, { onSuccess, onError } = {}) {
return async () => {
try {
const value = await promiseFn();
updateSubscribedQuery(key);
onSuccess?.(value);
return value;
} catch (e) {
onError?.(e);
if (onError === undefined) throw e;
}
};
return () => mutate(key, promiseFn, { onSuccess, onError });
}

export function getQuerySuspense(key, promiseFn, dependencyArray = []) {
Expand Down
16 changes: 16 additions & 0 deletions src/common/modal/modal.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createContext, useCallback, useEffect, useState, useRef } from "react";
import useModalStore, { closeModal } from "./store.js";
import useFocusTrap from "./useFocusTrap.js";

export const ModalCloseContext = createContext(() => {
console.log("모달이 닫힙니다.");
Expand Down Expand Up @@ -29,11 +30,26 @@ function Modal({ layer }) {
}
}, [child]);

useEffect(() => {
if (child === null) return;

function escHatch(e) {
if (e.key !== "Escape") return;
close();
e.preventDefault();
}
document.addEventListener("keydown", escHatch);
return () => document.removeEventListener("keydown", escHatch);
}, [child, close]);

const focusTrapRef = useFocusTrap(child !== null);

return (
<ModalCloseContext.Provider value={close}>
{child !== null ? (
<div
className={`fixed z-[100] top-0 left-0 w-full h-dvh flex justify-center items-center transition-opacity ${opacity === 0 ? "opacity-0" : "opacity-100"}`}
ref={focusTrapRef}
>
{child}
<div
Expand Down
Loading

0 comments on commit e37e0ca

Please sign in to comment.