From d8a53fe3ed2be080f706234a7ffe6899f27b0235 Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Tue, 6 Aug 2024 14:23:41 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/assets/react.svg | 1 - admin/src/components/Button/index.tsx | 23 ++++-- admin/src/components/Layout/index.tsx | 13 +++ admin/src/index.css | 14 ++-- admin/src/pages/Login/index.tsx | 112 +++++++++----------------- admin/src/router.tsx | 17 +++- 6 files changed, 90 insertions(+), 90 deletions(-) delete mode 100644 admin/src/assets/react.svg create mode 100644 admin/src/components/Layout/index.tsx diff --git a/admin/src/assets/react.svg b/admin/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/admin/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/admin/src/components/Button/index.tsx b/admin/src/components/Button/index.tsx index d425fab9..36b21a6d 100644 --- a/admin/src/components/Button/index.tsx +++ b/admin/src/components/Button/index.tsx @@ -1,9 +1,9 @@ -import { HTMLProps } from "react"; +import { ButtonHTMLAttributes } from "react"; import { cva } from "class-variance-authority"; -interface ButtonProps extends HTMLProps { +interface ButtonProps extends ButtonHTMLAttributes { isValid?: boolean; - type: "lg" | "sm"; + buttonSize: "lg" | "sm"; } const ButtonVariants = cva(`transition-all`, { @@ -12,16 +12,25 @@ const ButtonVariants = cva(`transition-all`, { true: "text-neutral-950 border-neutral-950 bg-white hover:bg-neutral-100", false: "text-neutral-300 border-neutral-300 bg-neutral-100", }, - type: { - lg: "w-[266px] rounded-full py-[16px] border-2", + size: { + lg: "w-[266px] rounded-full py-[16px] border", sm: "inline px-[12px] py-[8px] rounded-xl border", }, }, }); -export default function Button({ isValid = true, type, children, ...restProps }: ButtonProps) { +export default function Button({ + isValid = true, + buttonSize, + children, + ...restProps +}: ButtonProps) { return ( - ); diff --git a/admin/src/components/Layout/index.tsx b/admin/src/components/Layout/index.tsx new file mode 100644 index 00000000..0d4dc98d --- /dev/null +++ b/admin/src/components/Layout/index.tsx @@ -0,0 +1,13 @@ +import { Outlet } from "react-router-dom"; +import Header from "../Header"; + +export default function Layout() { + return ( +
+
+
+ +
+
+ ); +} diff --git a/admin/src/index.css b/admin/src/index.css index 8d716516..f6d56200 100644 --- a/admin/src/index.css +++ b/admin/src/index.css @@ -8,35 +8,35 @@ input:focus { @font-face { font-family: "HyundaiSansHeadOffice-Bold"; - src: url("/public/fonts/hyundai-sans/HyundaiSansHeadKROTFBold.otf") format("opentype"); + src: url("/public/fonts/HyundaiSansHeadKROTFBold.otf") format("opentype"); } @font-face { font-family: "HyundaiSansHeadOffice-Medium"; - src: url("/public/fonts/hyundai-sans/HyundaiSansHeadKROTFMedium.otf") format("opentype"); + src: url("/public/fonts/HyundaiSansHeadKROTFMedium.otf") format("opentype"); } @font-face { font-family: "HyundaiSansHeadOffice-Regular"; - src: url("/public/fonts/hyundai-sans/HyundaiSansHeadKROTFRegular.otf") format("opentype"); + src: url("/public/fonts/HyundaiSansHeadKROTFRegular.otf") format("opentype"); } @font-face { font-family: "HyundaiSansHeadOffice-Light"; - src: url("/public/fonts/hyundai-sans/HyundaiSansHeadKROTFLight.otf") format("opentype"); + src: url("/public/fonts/HyundaiSansHeadKROTFLight.otf") format("opentype"); } @font-face { font-family: "HyundaiSansTextOffice-Bold"; - src: url("/public/fonts/hyundai-sans/HyundaiSansTextKROTFBold.otf") format("opentype"); + src: url("/public/fonts/HyundaiSansTextKROTFBold.otf") format("opentype"); } @font-face { font-family: "HyundaiSansTextOffice-Medium"; - src: url("/public/fonts/hyundai-sans/HyundaiSansTextKROTFMedium.otf") format("opentype"); + src: url("/public/fonts/HyundaiSansTextKROTFMedium.otf") format("opentype"); } @font-face { font-family: "HyundaiSansTextOffice-Regular"; - src: url("/public/fonts/hyundai-sans/HyundaiSansTextKROTFRegular.otf") format("opentype"); + src: url("/public/fonts/HyundaiSansTextKROTFRegular.otf") format("opentype"); } diff --git a/admin/src/pages/Login/index.tsx b/admin/src/pages/Login/index.tsx index 19fa3d5e..66a0124c 100644 --- a/admin/src/pages/Login/index.tsx +++ b/admin/src/pages/Login/index.tsx @@ -1,87 +1,51 @@ -import { ChangeEvent, useState } from "react"; +import { ChangeEvent, FormEvent, useState } from "react"; +import { useNavigate } from "react-router-dom"; import Button from "@/components/Button"; -import Dropdown from "@/components/Dropdown"; -import Header from "@/components/Header"; import Input from "@/components/Input"; -import TabHeader from "@/components/TabHeader"; -import Table from "@/components/Table"; -import useModal from "@/hooks/useModal"; export default function Login() { - const [selectedIdx, setSelectedIdx] = useState(0); - const [value, setValue] = useState(""); + const navigate = useNavigate(); - const { handleOpenModal, ModalComponent } = useModal(); + const [id, setId] = useState(""); + const [password, setPassword] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); - const [selectedDropdownIdx, setSelectedDropdownIdx] = useState(0); + const isValidButton = id.length !== 0 && password.length !== 0; - const headers = [ - "ID", - "이벤트 진행 날짜", - "오픈 시간", - "종료 시간", - "활성화 시간", - "선택지 관리", - "경품 관리", - "선착순 당첨 인원 수", - "진행 상태", - setSelectedDropdownIdx(idx)} - />, - "관리", - ]; - const data = new Array(20).fill(null).map(() => [ - 7, - "2024-07-19", - "22:00:00", - "22:10:00", - "00시간 10분 00초", - , - , -
-

315

-

편집

-
, - "오픈 전", - , - , - ]); + const handleChangeId = (e: ChangeEvent) => { + setId(e.target.value); + }; + + const handleChangePassword = (e: ChangeEvent) => { + setPassword(e.target.value); + }; + + const handleSubmit = (e: FormEvent) => { + // TODO: 로그인 로직 + e.preventDefault(); + navigate("/lottery"); + }; return ( - <> -
- setSelectedIdx(idx)} - /> - - - - - setValue((e as ChangeEvent).target.value)} - /> - setValue((e as ChangeEvent).target.value)} - /> +
+
+ + +
- -
hihi
-
+

{errorMessage}

- - + + ); } diff --git a/admin/src/router.tsx b/admin/src/router.tsx index d500b1c3..8961a7f9 100644 --- a/admin/src/router.tsx +++ b/admin/src/router.tsx @@ -1,9 +1,24 @@ import { createBrowserRouter } from "react-router-dom"; +import Layout from "./components/Layout"; import Login from "./pages/Login"; export const router = createBrowserRouter([ { path: "/", - element: , + element: , + children: [ + { + path: "login/", + element: , + }, + { + path: "lottery/", + element: <>, + }, + { + path: "rush/", + element: <>, + }, + ], }, ]); From 7086073ba1f5c8a303699a5f8286c13678dccc31 Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Tue, 6 Aug 2024 16:04:21 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20=EC=84=A0=EC=B0=A9=EC=88=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=AA=A9=EB=A1=9D=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/index.html | 21 +- admin/src/components/DatePicker/index.tsx | 35 +++ admin/src/components/Layout/index.tsx | 4 +- admin/src/components/TabHeader/index.tsx | 34 ++- admin/src/components/TimePicker/index.tsx | 49 ++++ admin/src/constants/tabOptions.ts | 18 ++ admin/src/contexts/rushEventContext.tsx | 39 +++ admin/src/features/Rush/EventList.tsx | 106 +++++++ .../src/hooks/useRushEventDispatchContext.ts | 13 + admin/src/hooks/useRushEventStateContext.ts | 11 + admin/src/pages/Login/index.tsx | 7 +- admin/src/pages/Lottery/index.tsx | 9 + admin/src/pages/Rush/index.tsx | 15 + admin/src/router.tsx | 6 +- admin/src/types/rush.ts | 26 ++ admin/src/utils/getTimeDifference.ts | 26 ++ admin/tailwind.config.js | 272 +++++++++--------- 17 files changed, 526 insertions(+), 165 deletions(-) create mode 100644 admin/src/components/DatePicker/index.tsx create mode 100644 admin/src/components/TimePicker/index.tsx create mode 100644 admin/src/constants/tabOptions.ts create mode 100644 admin/src/contexts/rushEventContext.tsx create mode 100644 admin/src/features/Rush/EventList.tsx create mode 100644 admin/src/hooks/useRushEventDispatchContext.ts create mode 100644 admin/src/hooks/useRushEventStateContext.ts create mode 100644 admin/src/pages/Lottery/index.tsx create mode 100644 admin/src/pages/Rush/index.tsx create mode 100644 admin/src/types/rush.ts create mode 100644 admin/src/utils/getTimeDifference.ts diff --git a/admin/index.html b/admin/index.html index b12e7963..44b4379a 100644 --- a/admin/index.html +++ b/admin/index.html @@ -1,13 +1,14 @@ - - - - - Hybrid-JGS Admin - - -
- - + + + + + Hybrid-JGS Admin + + +
+ + + diff --git a/admin/src/components/DatePicker/index.tsx b/admin/src/components/DatePicker/index.tsx new file mode 100644 index 00000000..b92dd2e7 --- /dev/null +++ b/admin/src/components/DatePicker/index.tsx @@ -0,0 +1,35 @@ +import { ChangeEvent } from "react"; + +interface DatePickerProps { + date: string; + onChangeDate: (date: string) => void; +} + +export default function DatePicker({ date, onChangeDate }: DatePickerProps) { + const handleChange = (e: ChangeEvent) => { + onChangeDate(e.target.value); + }; + + return ( +
+
+ +
+ +
+ ); +} diff --git a/admin/src/components/Layout/index.tsx b/admin/src/components/Layout/index.tsx index 0d4dc98d..2d599896 100644 --- a/admin/src/components/Layout/index.tsx +++ b/admin/src/components/Layout/index.tsx @@ -3,9 +3,9 @@ import Header from "../Header"; export default function Layout() { return ( -
+
-
+
diff --git a/admin/src/components/TabHeader/index.tsx b/admin/src/components/TabHeader/index.tsx index a38c422c..ec5245d2 100644 --- a/admin/src/components/TabHeader/index.tsx +++ b/admin/src/components/TabHeader/index.tsx @@ -1,12 +1,9 @@ +import { useEffect, useState } from "react"; import { cva } from "class-variance-authority"; +import { Link, useLocation } from "react-router-dom"; +import { TAB_OPTIONS } from "@/constants/tabOptions"; -interface TabHeaderProps { - tabList: string[]; - handleClickTab: (idx: number) => void; - selectedIdx: number; -} - -const TabButtonVariants = cva(`border-b-2`, { +const TabButtonVariants = cva(`border-b-2 flex items-center`, { variants: { selected: { true: "h-body-1-bold border-neutral-950", @@ -15,17 +12,28 @@ const TabButtonVariants = cva(`border-b-2`, { }, }); -export default function TabHeader({ tabList, selectedIdx, handleClickTab }: TabHeaderProps) { +export default function TabHeader() { + const [selectedIdx, setSelectedIdx] = useState(0); + + const location = useLocation(); + + useEffect(() => { + const pathname = location.pathname; + const tabIdx = TAB_OPTIONS.findIndex((tab) => pathname.startsWith(tab.route)); + + setSelectedIdx(tabIdx); + }, [location]); + return (
- {tabList.map((tab, idx) => ( - + {tab.title} + ))}
); diff --git a/admin/src/components/TimePicker/index.tsx b/admin/src/components/TimePicker/index.tsx new file mode 100644 index 00000000..1b6ca04d --- /dev/null +++ b/admin/src/components/TimePicker/index.tsx @@ -0,0 +1,49 @@ +import { ChangeEvent } from "react"; + +interface TimePickerProps { + time: string; + onChangeTime: (time: string) => void; +} + +export default function TimePicker({ time, onChangeTime }: TimePickerProps) { + const handleChange = (e: ChangeEvent) => { + /** + * 시간-분 까지만 선택 가능 + * 초는 0초를 디폴트로 넣는다 + */ + const time = `${e.target.value}:00`; + onChangeTime(time); + }; + + return ( +
+
+
+ +
+ +
+
+ ); +} diff --git a/admin/src/constants/tabOptions.ts b/admin/src/constants/tabOptions.ts new file mode 100644 index 00000000..7d9ed742 --- /dev/null +++ b/admin/src/constants/tabOptions.ts @@ -0,0 +1,18 @@ +export const TAB_OPTIONS = [ + { title: "캐스퍼 일렉트릭 봇 만들기 추첨 이벤트", route: "/lottery" }, + { title: "선착순 밸런스 게임 이벤트", route: "/rush" }, +]; + +export const header = [ + "ID", + "이벤트 진행 날짜", + "오픈 시간", + "종료 시간", + "활성화 시간", + "선택지 관리", + "경품 관리", + "선착순 당첨 인원 수", + "진행 상태", + "참여자 리스트 보기", + "관리", +]; diff --git a/admin/src/contexts/rushEventContext.tsx b/admin/src/contexts/rushEventContext.tsx new file mode 100644 index 00000000..aa671088 --- /dev/null +++ b/admin/src/contexts/rushEventContext.tsx @@ -0,0 +1,39 @@ +import { ReactNode, createContext, useReducer } from "react"; +import { + RUSH_ACTION, + RushEventAction, + RushEventDispatchType, + RushEventStateType, +} from "@/types/rush"; + +export const RushEventStateContext = createContext(null); +export const RushEventDispatchContext = createContext(null); + +const initialState: RushEventStateType = { + rushList: [], +}; + +const casperCustomReducer = ( + state: RushEventStateType, + action: RushEventAction +): RushEventStateType => { + switch (action.type) { + case RUSH_ACTION.SET_EVENT_LIST: + return { ...state, rushList: action.payload }; + + default: + return state; + } +}; + +export const RushEventContext = ({ children }: { children: ReactNode }) => { + const [state, dispatch] = useReducer(casperCustomReducer, initialState); + + return ( + + + {children} + + + ); +}; diff --git a/admin/src/features/Rush/EventList.tsx b/admin/src/features/Rush/EventList.tsx new file mode 100644 index 00000000..f113b0b5 --- /dev/null +++ b/admin/src/features/Rush/EventList.tsx @@ -0,0 +1,106 @@ +import { useEffect } from "react"; +import Button from "@/components/Button"; +import DatePicker from "@/components/DatePicker"; +import Table from "@/components/Table"; +import TimePicker from "@/components/TimePicker"; +import { header } from "@/constants/tabOptions"; +import useRushEventDispatchContext from "@/hooks/useRushEventDispatchContext"; +import useRushEventStateContext from "@/hooks/useRushEventStateContext"; +import { RUSH_ACTION } from "@/types/rush"; +import { getTimeDifference } from "@/utils/getTimeDifference"; + +export default function EventList() { + const { rushList } = useRushEventStateContext(); + const dispatch = useRushEventDispatchContext(); + + useEffect(() => { + dispatch({ + type: RUSH_ACTION.SET_EVENT_LIST, + payload: [ + { + rush_event_id: 1, + event_date: "2024-07-25", + open_time: "20:00:00", + close_time: "20:10:00", + winner_count: 315, + prize_image_url: "prize1.png", + prize_description: "스타벅스 1만원 기프트카드", + }, + { + rush_event_id: 2, + event_date: "2024-07-26", + open_time: "20:00:00", + close_time: "20:10:00", + winner_count: 315, + prize_image_url: "prize2.png", + prize_description: "올리브영 1만원 기프트카드", + }, + { + rush_event_id: 2, + event_date: "2024-07-27", + open_time: "20:00:00", + close_time: "20:10:00", + winner_count: 315, + prize_image_url: "prize3.png", + prize_description: "배달의 민족 1만원 기프트카드", + }, + ], + }); + }, []); + + const handleChangeItem = (key: string, changeIdx: number, date: string) => { + const updatedTableItemList = rushList.map((item, idx) => { + if (idx === changeIdx) { + return { ...item, [key]: date }; + } + return { ...item }; + }); + + dispatch({ type: RUSH_ACTION.SET_EVENT_LIST, payload: updatedTableItemList }); + }; + + const getTableData = () => { + return rushList.map((item, idx) => { + return [ + item.rush_event_id, + handleChangeItem("event_date", idx, date)} + />, + handleChangeItem("open_time", idx, time)} + />, + handleChangeItem("close_time", idx, time)} + />, + getTimeDifference(item.open_time, item.close_time), + , + , +
+

{item.winner_count}

+

편집

+
, + "오픈 전", + , + , + ]; + }); + }; + + return ( +
+
+ +
+ +
+ +
+ + +
+ + ); +} diff --git a/admin/src/hooks/useRushEventDispatchContext.ts b/admin/src/hooks/useRushEventDispatchContext.ts new file mode 100644 index 00000000..b7ec8c2c --- /dev/null +++ b/admin/src/hooks/useRushEventDispatchContext.ts @@ -0,0 +1,13 @@ +import { useContext } from "react"; +import { RushEventDispatchContext } from "@/contexts/rushEventContext"; +import { RushEventDispatchType } from "@/types/rush"; + +export default function useRushEventDispatchContext(): RushEventDispatchType { + const context = useContext(RushEventDispatchContext); + if (context === null) { + throw new Error( + "RushEventDispatchContext must be used within a useRushEventDispatchContext" + ); + } + return context; +} diff --git a/admin/src/hooks/useRushEventStateContext.ts b/admin/src/hooks/useRushEventStateContext.ts new file mode 100644 index 00000000..19296a41 --- /dev/null +++ b/admin/src/hooks/useRushEventStateContext.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +import { RushEventStateContext } from "@/contexts/rushEventContext"; +import { RushEventStateType } from "@/types/rush"; + +export default function useRushEventStateContext(): RushEventStateType { + const context = useContext(RushEventStateContext); + if (context === null) { + throw new Error("RushEventStateContext must be used within a useRushEventStateContext"); + } + return context; +} diff --git a/admin/src/pages/Login/index.tsx b/admin/src/pages/Login/index.tsx index 66a0124c..99c95f9b 100644 --- a/admin/src/pages/Login/index.tsx +++ b/admin/src/pages/Login/index.tsx @@ -21,14 +21,17 @@ export default function Login() { }; const handleSubmit = (e: FormEvent) => { - // TODO: 로그인 로직 e.preventDefault(); + + // TODO: 로그인 로직 + + setErrorMessage(""); navigate("/lottery"); }; return (
diff --git a/admin/src/pages/Lottery/index.tsx b/admin/src/pages/Lottery/index.tsx new file mode 100644 index 00000000..f84cae68 --- /dev/null +++ b/admin/src/pages/Lottery/index.tsx @@ -0,0 +1,9 @@ +import TabHeader from "@/components/TabHeader"; + +export default function Lottery() { + return ( +
+ +
+ ); +} diff --git a/admin/src/pages/Rush/index.tsx b/admin/src/pages/Rush/index.tsx new file mode 100644 index 00000000..bd971be6 --- /dev/null +++ b/admin/src/pages/Rush/index.tsx @@ -0,0 +1,15 @@ +import TabHeader from "@/components/TabHeader"; +import { RushEventContext } from "@/contexts/rushEventContext"; +import EventList from "@/features/Rush/EventList"; + +export default function Rush() { + return ( + +
+ + + +
+
+ ); +} diff --git a/admin/src/router.tsx b/admin/src/router.tsx index 8961a7f9..38e3e0b1 100644 --- a/admin/src/router.tsx +++ b/admin/src/router.tsx @@ -1,6 +1,8 @@ import { createBrowserRouter } from "react-router-dom"; import Layout from "./components/Layout"; import Login from "./pages/Login"; +import Lottery from "./pages/Lottery"; +import Rush from "./pages/Rush"; export const router = createBrowserRouter([ { @@ -13,11 +15,11 @@ export const router = createBrowserRouter([ }, { path: "lottery/", - element: <>, + element: , }, { path: "rush/", - element: <>, + element: , }, ], }, diff --git a/admin/src/types/rush.ts b/admin/src/types/rush.ts new file mode 100644 index 00000000..9d6e23bf --- /dev/null +++ b/admin/src/types/rush.ts @@ -0,0 +1,26 @@ +import { Dispatch } from "react"; + +export interface RushEventType { + rush_event_id: number; + event_date: string; + open_time: string; + close_time: string; + winner_count: number; + prize_image_url: string; + prize_description: string; +} + +export interface RushEventStateType { + rushList: RushEventType[]; +} + +export const RUSH_ACTION = { + SET_EVENT_LIST: "SET_EVENT_LIST", +} as const; + +export type RushEventAction = { + type: typeof RUSH_ACTION.SET_EVENT_LIST; + payload: RushEventType[]; +}; + +export type RushEventDispatchType = Dispatch; diff --git a/admin/src/utils/getTimeDifference.ts b/admin/src/utils/getTimeDifference.ts new file mode 100644 index 00000000..377429d7 --- /dev/null +++ b/admin/src/utils/getTimeDifference.ts @@ -0,0 +1,26 @@ +export function getTimeDifference(openTime: string, closeTime: string) { + const timeRegex = /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/; + + if (!openTime.match(timeRegex) && !closeTime.match(timeRegex)) { + return ""; + } + + const [openHours, openMinutes, openSeconds] = openTime.split(":").map(Number); + const [closeHours, closeMinutes, closeSeconds] = closeTime.split(":").map(Number); + + const openTimeInSeconds = openHours * 3600 + openMinutes * 60 + openSeconds; + const closeTimeInSeconds = closeHours * 3600 + closeMinutes * 60 + closeSeconds; + + let differenceInSeconds = closeTimeInSeconds - openTimeInSeconds; + + if (differenceInSeconds < 0) { + differenceInSeconds += 24 * 3600; + } + + const hours = Math.floor(differenceInSeconds / 3600); + differenceInSeconds %= 3600; + const minutes = Math.floor(differenceInSeconds / 60); + const seconds = differenceInSeconds % 60; + + return `${String(hours).padStart(2, "0")}시 ${String(minutes).padStart(2, "0")}분 ${String(seconds).padStart(2, "0")}초`; +} diff --git a/admin/tailwind.config.js b/admin/tailwind.config.js index 37ab60a7..418904ad 100644 --- a/admin/tailwind.config.js +++ b/admin/tailwind.config.js @@ -1,140 +1,140 @@ /** @type {import('tailwindcss').Config} */ export default { - content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], - theme: { - extend: {}, - }, - plugins: [ - function ({ addUtilities }) { - const fontUtilities = { - ".h-heading-1-bold": { - fontSize: "64px", - lineHeight: "80px", - letterSpacing: "-2.56px", - fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", - }, - ".h-heading-1-medium": { - fontSize: "64px", - lineHeight: "80px", - letterSpacing: "-2.56px", - fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", - }, - ".h-heading-1-regular": { - fontSize: "64px", - lineHeight: "80px", - letterSpacing: "-2.56px", - fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", - }, - ".h-heading-2-bold": { - fontSize: "40px", - lineHeight: "56px", - letterSpacing: "-1.6px", - fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", - }, - ".h-heading-2-medium": { - fontSize: "40px", - lineHeight: "56px", - letterSpacing: "-1.6px", - fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", - }, - ".h-heading-2-regular": { - fontSize: "40px", - lineHeight: "56px", - letterSpacing: "-1.6px", - fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", - }, - ".h-heading-3-bold": { - fontSize: "27px", - lineHeight: "42px", - letterSpacing: "-1.08px", - fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", - }, - ".h-heading-3-medium": { - fontSize: "27px", - lineHeight: "42px", - letterSpacing: "-1.08px", - fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", - }, - ".h-heading-3-regular": { - fontSize: "27px", - lineHeight: "42px", - letterSpacing: "-1.08px", - fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", - }, - ".h-heading-4-bold": { - fontSize: "16px", - lineHeight: "24px", - letterSpacing: "-0.64px", - fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", - }, - ".h-heading-4-medium": { - fontSize: "16px", - lineHeight: "24px", - letterSpacing: "-0.64px", - fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", - }, - ".h-heading-4-regular": { - fontSize: "16px", - lineHeight: "24px", - letterSpacing: "-0.64px", - fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", - }, - ".h-body-1-bold": { - fontSize: "17px", - lineHeight: "26px", - letterSpacing: "-0.68px", - fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", - }, - ".h-body-1-medium": { - fontSize: "17px", - lineHeight: "26px", - letterSpacing: "-0.68px", - fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", - }, - ".h-body-1-regular": { - fontSize: "17px", - lineHeight: "26px", - letterSpacing: "-0.68px", - fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", - }, - ".h-body-2-bold": { - fontSize: "14px", - lineHeight: "22px", - letterSpacing: "-0.56px", - fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", - }, - ".h-body-2-medium": { - fontSize: "14px", - lineHeight: "22px", - letterSpacing: "-0.56px", - fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", - }, - ".h-body-2-regular": { - fontSize: "14px", - lineHeight: "22px", - letterSpacing: "-0.56px", - fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", - }, - ".h-detail-1-bold": { - fontSize: "12px", - lineHeight: "16px", - letterSpacing: "-0.4px", - fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", - }, - ".h-detail-1-medium": { - fontSize: "12px", - lineHeight: "16px", - letterSpacing: "-0.4px", - fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", - }, - ".h-detail-1-regular": { - fontSize: "12px", - lineHeight: "16px", - letterSpacing: "-0.4px", - fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", - }, - }; - addUtilities(fontUtilities); + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: {}, }, - ], + plugins: [ + function ({ addUtilities }) { + const fontUtilities = { + ".h-heading-1-bold": { + fontSize: "64px", + lineHeight: "80px", + letterSpacing: "-2.56px", + fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", + }, + ".h-heading-1-medium": { + fontSize: "64px", + lineHeight: "80px", + letterSpacing: "-2.56px", + fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", + }, + ".h-heading-1-regular": { + fontSize: "64px", + lineHeight: "80px", + letterSpacing: "-2.56px", + fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", + }, + ".h-heading-2-bold": { + fontSize: "40px", + lineHeight: "56px", + letterSpacing: "-1.6px", + fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", + }, + ".h-heading-2-medium": { + fontSize: "40px", + lineHeight: "56px", + letterSpacing: "-1.6px", + fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", + }, + ".h-heading-2-regular": { + fontSize: "40px", + lineHeight: "56px", + letterSpacing: "-1.6px", + fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", + }, + ".h-heading-3-bold": { + fontSize: "27px", + lineHeight: "42px", + letterSpacing: "-1.08px", + fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", + }, + ".h-heading-3-medium": { + fontSize: "27px", + lineHeight: "42px", + letterSpacing: "-1.08px", + fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", + }, + ".h-heading-3-regular": { + fontSize: "27px", + lineHeight: "42px", + letterSpacing: "-1.08px", + fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", + }, + ".h-heading-4-bold": { + fontSize: "16px", + lineHeight: "24px", + letterSpacing: "-0.64px", + fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", + }, + ".h-heading-4-medium": { + fontSize: "16px", + lineHeight: "24px", + letterSpacing: "-0.64px", + fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", + }, + ".h-heading-4-regular": { + fontSize: "16px", + lineHeight: "24px", + letterSpacing: "-0.64px", + fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", + }, + ".h-body-1-bold": { + fontSize: "17px", + lineHeight: "26px", + letterSpacing: "-0.68px", + fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", + }, + ".h-body-1-medium": { + fontSize: "17px", + lineHeight: "26px", + letterSpacing: "-0.68px", + fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", + }, + ".h-body-1-regular": { + fontSize: "17px", + lineHeight: "26px", + letterSpacing: "-0.68px", + fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", + }, + ".h-body-2-bold": { + fontSize: "14px", + lineHeight: "22px", + letterSpacing: "-0.56px", + fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", + }, + ".h-body-2-medium": { + fontSize: "14px", + lineHeight: "22px", + letterSpacing: "-0.56px", + fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", + }, + ".h-body-2-regular": { + fontSize: "14px", + lineHeight: "22px", + letterSpacing: "-0.56px", + fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", + }, + ".h-detail-1-bold": { + fontSize: "12px", + lineHeight: "16px", + letterSpacing: "-0.4px", + fontFamily: "HyundaiSansTextOffice-Bold, sans-serif", + }, + ".h-detail-1-medium": { + fontSize: "12px", + lineHeight: "16px", + letterSpacing: "-0.4px", + fontFamily: "HyundaiSansTextOffice-Medium, sans-serif", + }, + ".h-detail-1-regular": { + fontSize: "12px", + lineHeight: "16px", + letterSpacing: "-0.4px", + fontFamily: "HyundaiSansTextOffice-Regular, sans-serif", + }, + }; + addUtilities(fontUtilities); + }, + ], }; From 9f9aed7bbcf929e3b0c65cfe890b915771c56799 Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Tue, 6 Aug 2024 16:10:44 +0900 Subject: [PATCH 03/13] =?UTF-8?q?fix:=20=EC=9E=84=EC=8B=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=B2=84=ED=8A=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/features/Rush/EventList.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/admin/src/features/Rush/EventList.tsx b/admin/src/features/Rush/EventList.tsx index f113b0b5..ee618b5f 100644 --- a/admin/src/features/Rush/EventList.tsx +++ b/admin/src/features/Rush/EventList.tsx @@ -97,10 +97,7 @@ export default function EventList() {
-
- - -
+ ); } From bb64bb19fede579421a8d815ee9ee56280300e48 Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Tue, 6 Aug 2024 17:22:12 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20=EC=B0=B8=EC=97=AC=EC=9E=90=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EB=B3=B4=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/public/assets/icons/left-arrow.svg | 4 + admin/src/components/Dropdown/index.tsx | 57 +++++------ admin/src/constants/rush.ts | 7 ++ admin/src/constants/tabOptions.ts | 14 --- admin/src/features/Rush/ApplicantList.tsx | 109 ++++++++++++++++++++++ admin/src/features/Rush/EventList.tsx | 32 ++++++- admin/src/pages/Rush/index.tsx | 32 ++++++- admin/src/types/rush.ts | 15 +++ 8 files changed, 225 insertions(+), 45 deletions(-) create mode 100644 admin/public/assets/icons/left-arrow.svg create mode 100644 admin/src/constants/rush.ts create mode 100644 admin/src/features/Rush/ApplicantList.tsx diff --git a/admin/public/assets/icons/left-arrow.svg b/admin/public/assets/icons/left-arrow.svg new file mode 100644 index 00000000..7a643015 --- /dev/null +++ b/admin/public/assets/icons/left-arrow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/admin/src/components/Dropdown/index.tsx b/admin/src/components/Dropdown/index.tsx index 497a4d09..ddbd6eff 100644 --- a/admin/src/components/Dropdown/index.tsx +++ b/admin/src/components/Dropdown/index.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; interface DropdownProps { options: string[]; @@ -8,6 +8,19 @@ interface DropdownProps { export default function Dropdown({ options, selectedIdx, handleClickOption }: DropdownProps) { const [isVisibleOptions, setIsVisibleOptions] = useState(false); + const dropdownRef = useRef(null); + + const handleClose = (e: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + setIsVisibleOptions(false); + } + }; + + useEffect(() => { + document.addEventListener("click", handleClose); + + return () => document.removeEventListener("click", handleClose); + }, []); const handleClick = (idx: number) => { handleClickOption(idx); @@ -15,31 +28,23 @@ export default function Dropdown({ options, selectedIdx, handleClickOption }: Dr }; return ( - <> -
setIsVisibleOptions(false)} - /> -
-
setIsVisibleOptions(!isVisibleOptions)}> - {options[selectedIdx]} -
- {isVisibleOptions && ( -
-
- {options.map((option, idx) => ( -

handleClick(idx)} - className="break-keep text-nowrap" - > - {option} -

- ))} -
+
+
setIsVisibleOptions(!isVisibleOptions)}>{options[selectedIdx]}
+ {isVisibleOptions && ( +
+
+ {options.map((option, idx) => ( +

handleClick(idx)} + className="break-keep text-nowrap" + > + {option} +

+ ))}
- )} -
- +
+ )} +
); } diff --git a/admin/src/constants/rush.ts b/admin/src/constants/rush.ts new file mode 100644 index 00000000..ba66e28d --- /dev/null +++ b/admin/src/constants/rush.ts @@ -0,0 +1,7 @@ +export const RUSH_SECTION = { + EVENT_LIST: "event_list", + APPLICANT_LIST: "applicant_list", + SELECTION_MANAGE: "selection_manage", + GIFT_MANAGE: "gift_manage", +}; +export type RushSectionType = (typeof RUSH_SECTION)[keyof typeof RUSH_SECTION]; diff --git a/admin/src/constants/tabOptions.ts b/admin/src/constants/tabOptions.ts index 7d9ed742..c0bcfdcf 100644 --- a/admin/src/constants/tabOptions.ts +++ b/admin/src/constants/tabOptions.ts @@ -2,17 +2,3 @@ export const TAB_OPTIONS = [ { title: "캐스퍼 일렉트릭 봇 만들기 추첨 이벤트", route: "/lottery" }, { title: "선착순 밸런스 게임 이벤트", route: "/rush" }, ]; - -export const header = [ - "ID", - "이벤트 진행 날짜", - "오픈 시간", - "종료 시간", - "활성화 시간", - "선택지 관리", - "경품 관리", - "선착순 당첨 인원 수", - "진행 상태", - "참여자 리스트 보기", - "관리", -]; diff --git a/admin/src/features/Rush/ApplicantList.tsx b/admin/src/features/Rush/ApplicantList.tsx new file mode 100644 index 00000000..2a1d974c --- /dev/null +++ b/admin/src/features/Rush/ApplicantList.tsx @@ -0,0 +1,109 @@ +import { useEffect, useState } from "react"; +import Button from "@/components/Button"; +import Dropdown from "@/components/Dropdown"; +import Table from "@/components/Table"; +import useRushEventStateContext from "@/hooks/useRushEventStateContext"; +import { RushApplicantType, RushSelectionType } from "@/types/rush"; + +interface ApplicantListProps { + navigatePrevSection: () => void; + selectedIdx: number | null; +} + +export default function ApplicantList({ navigatePrevSection, selectedIdx }: ApplicantListProps) { + const { rushList } = useRushEventStateContext(); + + const [selectionList, setSelectionList] = useState([]); + const [applicantList, setApplicantList] = useState([]); + const [selectedOption, setSelectedOption] = useState(0); + + const selectionTitleList = selectionList.map( + (selection, idx) => `옵션 ${idx + 1} : ${selection.main_text}` + ); + + const APPLICANT_LIST_HEADER = [ + "ID", + "전화번호", + "등수", + "클릭 시간", + setSelectedOption(idx)} + />, + ]; + + useEffect(() => { + // TODO: 데이터 패칭 로직 구현 필요 + setApplicantList([ + { + phone_number: "010-1111-2222", + balance_game_choice: "1", + created_at: "2024-07-25 20:00 123", + }, + { + phone_number: "010-1111-2222", + balance_game_choice: "1", + created_at: "2024-07-25 20:00 125", + }, + { + phone_number: "010-1111-2222", + balance_game_choice: "1", + created_at: "2024-07-25 20:00 127", + }, + ]); + setSelectionList([ + { + rush_option_id: "1", + main_text: "첫 차로 저렴한 차 사기", + sub_text: " 첫 차는 가성비가 짱이지!", + result_main_text: "누구보다 가성비 갑인 캐스퍼 일렉트릭", + result_sub_text: "전기차 평균보다 훨씬 저렴한 캐스퍼 일렉트릭!", + image_url: "left_image.png", + }, + { + rush_option_id: "2", + main_text: "첫 차로 성능 좋은 차 사기", + sub_text: " 차는 당연히 성능이지!", + result_main_text: "필요한 건 다 갖춘 캐스퍼 일렉트릭", + result_sub_text: "전기차 평균보다 훨씨니 저렴한 캐스퍼 일렉트릭!", + image_url: "left_image.png", + }, + ]); + }, []); + + if (selectedIdx === null) { + return <>; + } + + const selectedRushItem = rushList[selectedIdx]; + const data = applicantList.map((applicant, idx) => { + const selectedOptionIdx = parseInt(applicant.balance_game_choice) - 1; + return [ + idx + 1, + applicant.phone_number, + idx + 1, + applicant.created_at, + `옵션 ${selectedOptionIdx + 1} : ${selectionList[selectedOptionIdx].main_text}`, + ]; + }); + + return ( +
+
+ 뒤로 가기 버튼 +

+ {selectedRushItem.event_date} 선착순 참여자 리스트 {applicantList.length} 명 +

+ +
+ +
+ + ); +} diff --git a/admin/src/features/Rush/EventList.tsx b/admin/src/features/Rush/EventList.tsx index ee618b5f..31d5e2ff 100644 --- a/admin/src/features/Rush/EventList.tsx +++ b/admin/src/features/Rush/EventList.tsx @@ -3,17 +3,36 @@ import Button from "@/components/Button"; import DatePicker from "@/components/DatePicker"; import Table from "@/components/Table"; import TimePicker from "@/components/TimePicker"; -import { header } from "@/constants/tabOptions"; +import { RUSH_SECTION, RushSectionType } from "@/constants/rush"; import useRushEventDispatchContext from "@/hooks/useRushEventDispatchContext"; import useRushEventStateContext from "@/hooks/useRushEventStateContext"; import { RUSH_ACTION } from "@/types/rush"; import { getTimeDifference } from "@/utils/getTimeDifference"; -export default function EventList() { +interface EventListProps { + handleSelectSection: (idx: number, section: RushSectionType) => void; +} + +const EVENT_LIST_HEADER = [ + "ID", + "이벤트 진행 날짜", + "오픈 시간", + "종료 시간", + "활성화 시간", + "선택지 관리", + "경품 관리", + "선착순 당첨 인원 수", + "진행 상태", + "참여자 리스트 보기", + "관리", +]; + +export default function EventList({ handleSelectSection }: EventListProps) { const { rushList } = useRushEventStateContext(); const dispatch = useRushEventDispatchContext(); useEffect(() => { + // TODO: 데이터 패칭 로직 구현 dispatch({ type: RUSH_ACTION.SET_EVENT_LIST, payload: [ @@ -83,7 +102,12 @@ export default function EventList() {

편집

, "오픈 전", - , + , , ]; }); @@ -95,7 +119,7 @@ export default function EventList() { -
+
diff --git a/admin/src/pages/Rush/index.tsx b/admin/src/pages/Rush/index.tsx index bd971be6..cf1e8a9b 100644 --- a/admin/src/pages/Rush/index.tsx +++ b/admin/src/pages/Rush/index.tsx @@ -1,14 +1,44 @@ +import { useState } from "react"; import TabHeader from "@/components/TabHeader"; +import { RUSH_SECTION, RushSectionType } from "@/constants/rush"; import { RushEventContext } from "@/contexts/rushEventContext"; +import ApplicantList from "@/features/Rush/ApplicantList"; import EventList from "@/features/Rush/EventList"; export default function Rush() { + const [selectedSection, setSelectedSection] = useState( + RUSH_SECTION.EVENT_LIST + ); + const [selectedIdx, setSelectedIdx] = useState(null); + + const handleSelectSection = (idx: number, section: RushSectionType) => { + setSelectedIdx(idx); + setSelectedSection(section); + }; + + console.log(selectedSection); + + const renderChildren = () => { + if (selectedSection === RUSH_SECTION.EVENT_LIST) { + return ; + } else if (selectedSection === RUSH_SECTION.APPLICANT_LIST) { + return ( + setSelectedSection(RUSH_SECTION.EVENT_LIST)} + selectedIdx={selectedIdx} + /> + ); + } + + return <>; + }; + return (
- + {renderChildren()}
); diff --git a/admin/src/types/rush.ts b/admin/src/types/rush.ts index 9d6e23bf..dde663d8 100644 --- a/admin/src/types/rush.ts +++ b/admin/src/types/rush.ts @@ -24,3 +24,18 @@ export type RushEventAction = { }; export type RushEventDispatchType = Dispatch; + +export interface RushApplicantType { + phone_number: string; + balance_game_choice: string; + created_at: string; +} + +export interface RushSelectionType { + rush_option_id: string; + main_text: string; + sub_text: string; + result_main_text: string; + result_sub_text: string; + image_url: string; +} From a099a33a0d2d4e9e1b7c00c2f772643a92abc5a6 Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Tue, 6 Aug 2024 17:51:11 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20=EC=84=A0=EC=B0=A9=EC=88=9C=20?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=EC=9E=90=20=EB=AA=A9=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/public/assets/icons/down-arrow.svg | 3 + admin/src/components/Dropdown/index.tsx | 7 ++- admin/src/components/Layout/index.tsx | 5 +- admin/src/components/Table/index.tsx | 2 +- admin/src/features/Rush/ApplicantList.tsx | 29 +++------ admin/src/pages/Rush/index.tsx | 74 +++++++++++++---------- 6 files changed, 64 insertions(+), 56 deletions(-) create mode 100644 admin/public/assets/icons/down-arrow.svg diff --git a/admin/public/assets/icons/down-arrow.svg b/admin/public/assets/icons/down-arrow.svg new file mode 100644 index 00000000..c7c03ae9 --- /dev/null +++ b/admin/public/assets/icons/down-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/admin/src/components/Dropdown/index.tsx b/admin/src/components/Dropdown/index.tsx index ddbd6eff..8d9ce9d5 100644 --- a/admin/src/components/Dropdown/index.tsx +++ b/admin/src/components/Dropdown/index.tsx @@ -28,8 +28,11 @@ export default function Dropdown({ options, selectedIdx, handleClickOption }: Dr }; return ( -
-
setIsVisibleOptions(!isVisibleOptions)}>{options[selectedIdx]}
+
+
setIsVisibleOptions(!isVisibleOptions)} className="flex gap-1"> +

{options[selectedIdx]}

+ 드롭다운 토글 아이콘 +
{isVisibleOptions && (
diff --git a/admin/src/components/Layout/index.tsx b/admin/src/components/Layout/index.tsx index 2d599896..c37c7bb6 100644 --- a/admin/src/components/Layout/index.tsx +++ b/admin/src/components/Layout/index.tsx @@ -1,4 +1,5 @@ import { Outlet } from "react-router-dom"; +import { RushEventContext } from "@/contexts/rushEventContext"; import Header from "../Header"; export default function Layout() { @@ -6,7 +7,9 @@ export default function Layout() {
- + + +
); diff --git a/admin/src/components/Table/index.tsx b/admin/src/components/Table/index.tsx index 574e886f..1cfd6952 100644 --- a/admin/src/components/Table/index.tsx +++ b/admin/src/components/Table/index.tsx @@ -10,7 +10,7 @@ export default function Table({ headers, data }: TableProps) {
- + {headers.map((header, idx) => (
diff --git a/admin/src/features/Rush/ApplicantList.tsx b/admin/src/features/Rush/ApplicantList.tsx index 2a1d974c..62a22d93 100644 --- a/admin/src/features/Rush/ApplicantList.tsx +++ b/admin/src/features/Rush/ApplicantList.tsx @@ -5,14 +5,11 @@ import Table from "@/components/Table"; import useRushEventStateContext from "@/hooks/useRushEventStateContext"; import { RushApplicantType, RushSelectionType } from "@/types/rush"; -interface ApplicantListProps { - navigatePrevSection: () => void; - selectedIdx: number | null; -} - -export default function ApplicantList({ navigatePrevSection, selectedIdx }: ApplicantListProps) { +export default function ApplicantList() { const { rushList } = useRushEventStateContext(); + const [selectedRush, setSelectedRush] = useState(0); + const [selectionList, setSelectionList] = useState([]); const [applicantList, setApplicantList] = useState([]); const [selectedOption, setSelectedOption] = useState(0); @@ -70,13 +67,8 @@ export default function ApplicantList({ navigatePrevSection, selectedIdx }: Appl image_url: "left_image.png", }, ]); - }, []); - - if (selectedIdx === null) { - return <>; - } + }, [selectedRush]); - const selectedRushItem = rushList[selectedIdx]; const data = applicantList.map((applicant, idx) => { const selectedOptionIdx = parseInt(applicant.balance_game_choice) - 1; return [ @@ -91,15 +83,12 @@ export default function ApplicantList({ navigatePrevSection, selectedIdx }: Appl return (
- 뒤로 가기 버튼 rush.event_date)} + selectedIdx={selectedRush} + handleClickOption={(idx) => setSelectedRush(idx)} /> -

- {selectedRushItem.event_date} 선착순 참여자 리스트 {applicantList.length} 명 -

+

선착순 참여자 리스트 {applicantList.length} 명

diff --git a/admin/src/pages/Rush/index.tsx b/admin/src/pages/Rush/index.tsx index cf1e8a9b..65c522f5 100644 --- a/admin/src/pages/Rush/index.tsx +++ b/admin/src/pages/Rush/index.tsx @@ -1,45 +1,55 @@ -import { useState } from "react"; +import { useEffect } from "react"; import TabHeader from "@/components/TabHeader"; -import { RUSH_SECTION, RushSectionType } from "@/constants/rush"; -import { RushEventContext } from "@/contexts/rushEventContext"; import ApplicantList from "@/features/Rush/ApplicantList"; -import EventList from "@/features/Rush/EventList"; +import useRushEventDispatchContext from "@/hooks/useRushEventDispatchContext"; +import { RUSH_ACTION } from "@/types/rush"; export default function Rush() { - const [selectedSection, setSelectedSection] = useState( - RUSH_SECTION.EVENT_LIST - ); - const [selectedIdx, setSelectedIdx] = useState(null); - - const handleSelectSection = (idx: number, section: RushSectionType) => { - setSelectedIdx(idx); - setSelectedSection(section); - }; - - console.log(selectedSection); - - const renderChildren = () => { - if (selectedSection === RUSH_SECTION.EVENT_LIST) { - return ; - } else if (selectedSection === RUSH_SECTION.APPLICANT_LIST) { - return ( - setSelectedSection(RUSH_SECTION.EVENT_LIST)} - selectedIdx={selectedIdx} - /> - ); - } + const dispatch = useRushEventDispatchContext(); - return <>; - }; + useEffect(() => { + // TODO: 데이터 패칭 로직 구현 + dispatch({ + type: RUSH_ACTION.SET_EVENT_LIST, + payload: [ + { + rush_event_id: 1, + event_date: "2024-07-25", + open_time: "20:00:00", + close_time: "20:10:00", + winner_count: 315, + prize_image_url: "prize1.png", + prize_description: "스타벅스 1만원 기프트카드", + }, + { + rush_event_id: 2, + event_date: "2024-07-26", + open_time: "20:00:00", + close_time: "20:10:00", + winner_count: 315, + prize_image_url: "prize2.png", + prize_description: "올리브영 1만원 기프트카드", + }, + { + rush_event_id: 2, + event_date: "2024-07-27", + open_time: "20:00:00", + close_time: "20:10:00", + winner_count: 315, + prize_image_url: "prize3.png", + prize_description: "배달의 민족 1만원 기프트카드", + }, + ], + }); + }, []); return ( - + <>
- {renderChildren()} +
-
+ ); } From daafc393eeec040e06057d60bf008a65d21b5e7c Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Tue, 6 Aug 2024 18:07:41 +0900 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20=EB=8B=B9=EC=B2=A8=EC=9E=90=20?= =?UTF-8?q?=EC=B6=94=EC=B2=A8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/pages/Lottery/index.tsx | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/admin/src/pages/Lottery/index.tsx b/admin/src/pages/Lottery/index.tsx index f84cae68..bdac658b 100644 --- a/admin/src/pages/Lottery/index.tsx +++ b/admin/src/pages/Lottery/index.tsx @@ -1,9 +1,35 @@ +import { ChangeEvent, useEffect, useState } from "react"; +import Button from "@/components/Button"; import TabHeader from "@/components/TabHeader"; export default function Lottery() { + const [giftCount, setGiftCount] = useState(0); + + useEffect(() => { + setGiftCount(363); + }, []); + + const handleChangeInput = (e: ChangeEvent) => { + const count = parseInt(e.target.value); + setGiftCount(count); + }; + return ( -
+
+ +
+
+

전체 참여자 수

+

1000

+

당첨자 수

+
+ +
+
+ + +
); } From 012d261d867b679a51073a562fdbcd0342be493b Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Tue, 6 Aug 2024 18:26:16 +0900 Subject: [PATCH 07/13] =?UTF-8?q?feat:=20=EB=8B=B9=EC=B2=A8=EC=9E=90=20?= =?UTF-8?q?=EC=B6=94=EC=B2=A8=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/pages/Lottery/index.tsx | 13 +++- admin/src/pages/LotteryWinner/index.tsx | 82 +++++++++++++++++++++++++ admin/src/router.tsx | 12 +++- 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 admin/src/pages/LotteryWinner/index.tsx diff --git a/admin/src/pages/Lottery/index.tsx b/admin/src/pages/Lottery/index.tsx index bdac658b..a429adb7 100644 --- a/admin/src/pages/Lottery/index.tsx +++ b/admin/src/pages/Lottery/index.tsx @@ -1,11 +1,15 @@ import { ChangeEvent, useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; import Button from "@/components/Button"; import TabHeader from "@/components/TabHeader"; export default function Lottery() { + const navigate = useNavigate(); + const [giftCount, setGiftCount] = useState(0); useEffect(() => { + // TODO: 추첨 이벤트 정보 불러오기 setGiftCount(363); }, []); @@ -14,6 +18,11 @@ export default function Lottery() { setGiftCount(count); }; + const handleLottery = () => { + // TODO: 당첨자 추첨 + navigate("/lottery/winner"); + }; + return (
@@ -28,7 +37,9 @@ export default function Lottery() {
- +
); diff --git a/admin/src/pages/LotteryWinner/index.tsx b/admin/src/pages/LotteryWinner/index.tsx new file mode 100644 index 00000000..ecdb90e6 --- /dev/null +++ b/admin/src/pages/LotteryWinner/index.tsx @@ -0,0 +1,82 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import Button from "@/components/Button"; +import TabHeader from "@/components/TabHeader"; +import Table from "@/components/Table"; + +const LOTTERY_WINNER_HEADER = [ + "등수", + "ID", + "생성 시간", + "전화번호", + "공유 링크 클릭 횟수", + "기대평 작성 여부", + "총 응모 횟수", +]; +const data = [ + { + phone_number: "010-1111-2222", + link_clicked_counts: "1", + expectation: "1", + }, + { + phone_number: "010-1111-2223", + link_clicked_counts: "3", + expectation: "1", + }, + { + phone_number: "010-1111-2224", + link_clicked_counts: "4", + expectation: "0", + }, +]; + +export default function LotteryWinner() { + const navigate = useNavigate(); + + const [winnerList, setWinnerList] = useState([] as any); + + useEffect(() => { + setWinnerList( + data.map((d, idx) => { + return [ + idx + 1, + d.phone_number, + d.phone_number, + d.phone_number, + d.link_clicked_counts, + d.link_clicked_counts, + d.link_clicked_counts, + ]; + }) + ); + }, []); + + const handleLottery = () => { + // TODO: 다시 추첨하는 로직 구현 + }; + + return ( +
+ + +
+
+ 뒤로 가기 버튼 navigate(-1)} + /> +

당첨자 추첨

+
+ + + + + + + ); +} diff --git a/admin/src/router.tsx b/admin/src/router.tsx index 38e3e0b1..9a5ed0c4 100644 --- a/admin/src/router.tsx +++ b/admin/src/router.tsx @@ -2,6 +2,7 @@ import { createBrowserRouter } from "react-router-dom"; import Layout from "./components/Layout"; import Login from "./pages/Login"; import Lottery from "./pages/Lottery"; +import LotteryWinner from "./pages/LotteryWinner"; import Rush from "./pages/Rush"; export const router = createBrowserRouter([ @@ -15,7 +16,16 @@ export const router = createBrowserRouter([ }, { path: "lottery/", - element: , + children: [ + { + index: true, + element: , + }, + { + path: "winner", + element: , + }, + ], }, { path: "rush/", From 40c5c1302f98c792cb6b0f45bdf0168f40d0999d Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Tue, 6 Aug 2024 18:27:26 +0900 Subject: [PATCH 08/13] =?UTF-8?q?fix:=20nan=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/pages/Lottery/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/src/pages/Lottery/index.tsx b/admin/src/pages/Lottery/index.tsx index a429adb7..3943c6b9 100644 --- a/admin/src/pages/Lottery/index.tsx +++ b/admin/src/pages/Lottery/index.tsx @@ -15,7 +15,7 @@ export default function Lottery() { const handleChangeInput = (e: ChangeEvent) => { const count = parseInt(e.target.value); - setGiftCount(count); + setGiftCount(count || 0); }; const handleLottery = () => { From ef2759d40eb7fc1268d9490dc392fa7305d97514 Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Wed, 7 Aug 2024 17:40:57 +0900 Subject: [PATCH 09/13] =?UTF-8?q?refactor:=20=EB=B9=88=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/pages/Rush/index.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/admin/src/pages/Rush/index.tsx b/admin/src/pages/Rush/index.tsx index 65c522f5..e3f0e947 100644 --- a/admin/src/pages/Rush/index.tsx +++ b/admin/src/pages/Rush/index.tsx @@ -44,12 +44,10 @@ export default function Rush() { }, []); return ( - <> -
- +
+ - -
- + +
); } From 47a725b4f2c83a163245a8e81bb86058986c2f7e Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Wed, 7 Aug 2024 17:41:57 +0900 Subject: [PATCH 10/13] =?UTF-8?q?refactor:=20svg=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/public/assets/icons/calendar.svg | 9 +++++++++ admin/src/components/DatePicker/index.tsx | 10 +--------- 2 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 admin/public/assets/icons/calendar.svg diff --git a/admin/public/assets/icons/calendar.svg b/admin/public/assets/icons/calendar.svg new file mode 100644 index 00000000..6d839977 --- /dev/null +++ b/admin/public/assets/icons/calendar.svg @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/admin/src/components/DatePicker/index.tsx b/admin/src/components/DatePicker/index.tsx index b92dd2e7..77f005ee 100644 --- a/admin/src/components/DatePicker/index.tsx +++ b/admin/src/components/DatePicker/index.tsx @@ -13,15 +13,7 @@ export default function DatePicker({ date, onChangeDate }: DatePickerProps) { return (
- + 달력 아이콘
Date: Wed, 7 Aug 2024 17:44:34 +0900 Subject: [PATCH 11/13] =?UTF-8?q?refactor:=20dark=EB=AA=A8=EB=93=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/components/DatePicker/index.tsx | 2 +- admin/src/components/Table/index.tsx | 9 +++------ admin/src/components/TimePicker/index.tsx | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/admin/src/components/DatePicker/index.tsx b/admin/src/components/DatePicker/index.tsx index 77f005ee..b35fe66f 100644 --- a/admin/src/components/DatePicker/index.tsx +++ b/admin/src/components/DatePicker/index.tsx @@ -19,7 +19,7 @@ export default function DatePicker({ date, onChangeDate }: DatePickerProps) { type="date" value={date} onChange={handleChange} - className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5" placeholder="Select date" />
diff --git a/admin/src/components/Table/index.tsx b/admin/src/components/Table/index.tsx index 1cfd6952..6f2cb37e 100644 --- a/admin/src/components/Table/index.tsx +++ b/admin/src/components/Table/index.tsx @@ -9,8 +9,8 @@ export default function Table({ headers, data }: TableProps) { return (
-
- +
+ {headers.map((header, idx) => ( {data.map((tableData, idx) => ( - + {tableData.map((dataNode, idx) => (
@@ -21,10 +21,7 @@ export default function Table({ headers, data }: TableProps) {