From 36f5340d72f7caa540f7418ac9b0a03ca2a79db3 Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Fri, 9 Aug 2024 11:43:03 +0900 Subject: [PATCH 01/37] =?UTF-8?q?feat:=20admin=20api=20=ED=8B=80=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/apis/lotteryAPI.ts | 33 +++++++++++++++++ admin/src/hooks/useFetch.ts | 22 +++++++++++ admin/src/pages/Lottery/index.tsx | 17 ++++++--- admin/src/router.tsx | 2 + admin/src/types/lottery.ts | 7 ++++ admin/src/utils/fetchWithTimeout.ts | 14 +++++++ .../CasperCustom/CasperCustomFinish.tsx | 8 +--- .../CasperCustom/CasperCustomFinishing.tsx | 9 +---- client/src/hooks/useBlockNavigation.ts | 37 ------------------- client/src/pages/CasperCustom/index.tsx | 14 +------ client/src/pages/Lottery/index.tsx | 4 +- client/src/types/lotteryApi.ts | 7 ++-- 12 files changed, 101 insertions(+), 73 deletions(-) create mode 100644 admin/src/apis/lotteryAPI.ts create mode 100644 admin/src/hooks/useFetch.ts create mode 100644 admin/src/types/lottery.ts create mode 100644 admin/src/utils/fetchWithTimeout.ts delete mode 100644 client/src/hooks/useBlockNavigation.ts diff --git a/admin/src/apis/lotteryAPI.ts b/admin/src/apis/lotteryAPI.ts new file mode 100644 index 00000000..5f6516b1 --- /dev/null +++ b/admin/src/apis/lotteryAPI.ts @@ -0,0 +1,33 @@ +import { GetLotteryResponse } from "@/types/lottery"; +import { fetchWithTimeout } from "@/utils/fetchWithTimeout"; + +const baseURL = `${import.meta.env.VITE_API_URL}/admin/event/lottery`; +const headers = { + "Content-Type": "application/json", +}; + +export const LotteryAPI = { + async getLottery(): Promise { + try { + return new Promise((resolve) => + resolve([ + { + lotteryEventId: 1, + startDate: "2024-07-26 00:00", + endDate: "2024-08-25 23:59", + appliedCount: 1000000, + winnerCount: 363, + }, + ]) + ); + const response = await fetchWithTimeout(`${baseURL}`, { + method: "POST", + headers: headers, + }); + return response.json(); + } catch (error) { + console.error("Error:", error); + throw error; + } + }, +}; diff --git a/admin/src/hooks/useFetch.ts b/admin/src/hooks/useFetch.ts new file mode 100644 index 00000000..36a96a1a --- /dev/null +++ b/admin/src/hooks/useFetch.ts @@ -0,0 +1,22 @@ +import { useState } from "react"; + +export default function useFetch(fetch: (params: P) => Promise) { + const [data, setData] = useState(null); + const [isSuccess, setIsSuccess] = useState(false); + const [isError, setIsError] = useState(false); + + const fetchData = async (params?: P) => { + try { + console.log("hohi"); + const data = await fetch(params as P); + console.log(data); + setData(data); + setIsSuccess(!!data); + } catch (error) { + setIsError(true); + console.error(error); + } + }; + + return { data, isSuccess, isError, fetchData }; +} diff --git a/admin/src/pages/Lottery/index.tsx b/admin/src/pages/Lottery/index.tsx index 3943c6b9..41d858ee 100644 --- a/admin/src/pages/Lottery/index.tsx +++ b/admin/src/pages/Lottery/index.tsx @@ -1,17 +1,24 @@ import { ChangeEvent, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useLoaderData, useNavigate } from "react-router-dom"; import Button from "@/components/Button"; import TabHeader from "@/components/TabHeader"; +import { GetLotteryResponse } from "@/types/lottery"; export default function Lottery() { + const data = useLoaderData() as GetLotteryResponse; + const navigate = useNavigate(); + const [totalCount, setTotalCount] = useState(0); const [giftCount, setGiftCount] = useState(0); useEffect(() => { - // TODO: 추첨 이벤트 정보 불러오기 - setGiftCount(363); - }, []); + if (data.length !== 0) { + const currentLotttery = data[0]; + setGiftCount(currentLotttery.winnerCount); + setTotalCount(currentLotttery.appliedCount); + } + }, [data]); const handleChangeInput = (e: ChangeEvent) => { const count = parseInt(e.target.value); @@ -30,7 +37,7 @@ export default function Lottery() {

전체 참여자 수

-

1000

+

{totalCount}

당첨자 수

diff --git a/admin/src/router.tsx b/admin/src/router.tsx index 9a5ed0c4..8b2d9ca9 100644 --- a/admin/src/router.tsx +++ b/admin/src/router.tsx @@ -1,4 +1,5 @@ import { createBrowserRouter } from "react-router-dom"; +import { LotteryAPI } from "./apis/lotteryAPI"; import Layout from "./components/Layout"; import Login from "./pages/Login"; import Lottery from "./pages/Lottery"; @@ -20,6 +21,7 @@ export const router = createBrowserRouter([ { index: true, element: , + loader: LotteryAPI.getLottery, }, { path: "winner", diff --git a/admin/src/types/lottery.ts b/admin/src/types/lottery.ts new file mode 100644 index 00000000..f0ab443c --- /dev/null +++ b/admin/src/types/lottery.ts @@ -0,0 +1,7 @@ +export type GetLotteryResponse = { + lotteryEventId: number; + startDate: string; + endDate: string; + appliedCount: number; + winnerCount: number; +}[]; diff --git a/admin/src/utils/fetchWithTimeout.ts b/admin/src/utils/fetchWithTimeout.ts new file mode 100644 index 00000000..371107f3 --- /dev/null +++ b/admin/src/utils/fetchWithTimeout.ts @@ -0,0 +1,14 @@ +export async function fetchWithTimeout(url: string, options: RequestInit = {}, timeout = 5000) { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + options.signal = controller.signal; + + try { + const response = await fetch(url, options); + clearTimeout(id); + return response; + } catch (error) { + clearTimeout(id); + throw error; + } +} diff --git a/client/src/features/CasperCustom/CasperCustomFinish.tsx b/client/src/features/CasperCustom/CasperCustomFinish.tsx index fad14a85..2e19f0f6 100644 --- a/client/src/features/CasperCustom/CasperCustomFinish.tsx +++ b/client/src/features/CasperCustom/CasperCustomFinish.tsx @@ -18,13 +18,9 @@ import ArrowIcon from "/public/assets/icons/arrow.svg?react"; interface CasperCustomFinishProps { handleResetStep: () => void; - unblockNavigation: () => void; } -export function CasperCustomFinish({ - handleResetStep, - unblockNavigation, -}: CasperCustomFinishProps) { +export function CasperCustomFinish({ handleResetStep }: CasperCustomFinishProps) { const [cookies] = useCookies([COOKIE_TOKEN_KEY]); const dispatch = useCasperCustomDispatchContext(); @@ -38,8 +34,6 @@ export function CasperCustomFinish({ if (!cookies[COOKIE_TOKEN_KEY]) { return; } - - unblockNavigation(); getApplyCount(); }, []); diff --git a/client/src/features/CasperCustom/CasperCustomFinishing.tsx b/client/src/features/CasperCustom/CasperCustomFinishing.tsx index a5befcba..e99b4681 100644 --- a/client/src/features/CasperCustom/CasperCustomFinishing.tsx +++ b/client/src/features/CasperCustom/CasperCustomFinishing.tsx @@ -27,18 +27,13 @@ export function CasperCustomFinishing({ navigateNextStep }: CasperCustomFinishin useEffect(() => { showToast(); - const flipTimer = setTimeout(() => { + setTimeout(() => { setIsFlipped(true); }, 3000); - const navigateTimer = setTimeout(() => { + setTimeout(() => { navigateNextStep(); }, 6000); - - return () => { - clearTimeout(flipTimer); - clearTimeout(navigateTimer); - }; }, []); return ( diff --git a/client/src/hooks/useBlockNavigation.ts b/client/src/hooks/useBlockNavigation.ts deleted file mode 100644 index e43d4c0c..00000000 --- a/client/src/hooks/useBlockNavigation.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect, useState } from "react"; -import { unstable_usePrompt, useLocation } from "react-router-dom"; - -export function useBlockNavigation(message: string) { - const location = useLocation(); - const [isBlocking, setIsBlocking] = useState(false); - - unstable_usePrompt({ when: isBlocking, message }); - - const unblockNavigation = () => { - setIsBlocking(false); - }; - - const handleBeforeUnload = (e: BeforeUnloadEvent) => { - if (isBlocking) { - e.preventDefault(); - e.returnValue = ""; - } - }; - - useEffect(() => { - setIsBlocking(true); - - return () => { - setIsBlocking(false); - }; - }, [location]); - useEffect(() => { - window.addEventListener("beforeunload", handleBeforeUnload); - - return () => { - window.removeEventListener("beforeunload", handleBeforeUnload); - }; - }, [isBlocking]); - - return { unblockNavigation }; -} diff --git a/client/src/pages/CasperCustom/index.tsx b/client/src/pages/CasperCustom/index.tsx index 6155e1d9..91655af9 100644 --- a/client/src/pages/CasperCustom/index.tsx +++ b/client/src/pages/CasperCustom/index.tsx @@ -14,17 +14,12 @@ import { CasperCustomForm, CasperCustomProcess, } from "@/features/CasperCustom"; -import { useBlockNavigation } from "@/hooks/useBlockNavigation"; import useHeaderStyleObserver from "@/hooks/useHeaderStyleObserver"; import { SCROLL_MOTION } from "../../constants/animation"; const INITIAL_STEP = 0; export default function CasperCustom() { - const { unblockNavigation } = useBlockNavigation( - "이 페이지를 떠나면 모든 변경 사항이 저장되지 않습니다. 페이지를 떠나시겠습니까?" - ); - const containerRef = useHeaderStyleObserver({ darkSections: [CASPER_CUSTOM_SECTIONS.CUSTOM], }); @@ -33,7 +28,7 @@ export default function CasperCustom() { const selectedStep = CUSTOM_STEP_OPTION_ARRAY[selectedStepIdx]; const handleClickNextStep = () => { - setSelectedStepIdx((prevSelectedIdx) => prevSelectedIdx + 1); + setSelectedStepIdx(selectedStepIdx + 1); }; const handleResetStep = () => { @@ -48,12 +43,7 @@ export default function CasperCustom() { } else if (selectedStep === CUSTOM_STEP_OPTION.FINISHING) { return ; } else if (selectedStep === CUSTOM_STEP_OPTION.FINISH) { - return ( - - ); + return ; } return <>; }; diff --git a/client/src/pages/Lottery/index.tsx b/client/src/pages/Lottery/index.tsx index 071a1d05..1ad7d63d 100644 --- a/client/src/pages/Lottery/index.tsx +++ b/client/src/pages/Lottery/index.tsx @@ -62,8 +62,8 @@ export default function Lottery() { const { showToast, ToastComponent } = useToast("이벤트 기간이 아닙니다"); const handleClickShortCut = useCallback(() => { - const startDate = getMsTime(data.eventStartDate); - const endDate = getMsTime(data.eventEndDate); + const startDate = getMsTime(data.startDate); + const endDate = getMsTime(data.endDate); const currentDate = new Date().getTime(); const isEventPeriod = currentDate >= startDate && currentDate <= endDate; diff --git a/client/src/types/lotteryApi.ts b/client/src/types/lotteryApi.ts index 55b5253d..1e4f2782 100644 --- a/client/src/types/lotteryApi.ts +++ b/client/src/types/lotteryApi.ts @@ -24,7 +24,8 @@ export interface GetApplyCountResponse { } export interface GetLotteryResponse { - eventStartDate: string; - eventEndDate: string; - activePeriod: number; + lotteryEventId: number; + startDate: string; + endDate: string; + winnerCount: number; } From eae0d3f57e3836986684f436b2af275b0a448fd4 Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Fri, 9 Aug 2024 11:55:40 +0900 Subject: [PATCH 02/37] =?UTF-8?q?feat:=20=EC=B6=94=EC=B2=A8=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/apis/lotteryAPI.ts | 15 ++++++++++++++- admin/src/hooks/useFetch.ts | 2 -- admin/src/pages/Lottery/index.tsx | 24 +++++++++++++++++------- admin/src/types/lottery.ts | 8 ++++++++ 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/admin/src/apis/lotteryAPI.ts b/admin/src/apis/lotteryAPI.ts index 5f6516b1..a1847911 100644 --- a/admin/src/apis/lotteryAPI.ts +++ b/admin/src/apis/lotteryAPI.ts @@ -1,4 +1,4 @@ -import { GetLotteryResponse } from "@/types/lottery"; +import { GetLotteryResponse, PostLotteryParams, PostLotteryResponse } from "@/types/lottery"; import { fetchWithTimeout } from "@/utils/fetchWithTimeout"; const baseURL = `${import.meta.env.VITE_API_URL}/admin/event/lottery`; @@ -21,6 +21,19 @@ export const LotteryAPI = { ]) ); const response = await fetchWithTimeout(`${baseURL}`, { + method: "GET", + headers: headers, + }); + return response.json(); + } catch (error) { + console.error("Error:", error); + throw error; + } + }, + async postLotteryWinner({ id }: PostLotteryParams): Promise { + try { + return new Promise((resolve) => resolve({ message: "요청에 성공하였습니다." })); + const response = await fetchWithTimeout(`${baseURL}/${id}/winner`, { method: "POST", headers: headers, }); diff --git a/admin/src/hooks/useFetch.ts b/admin/src/hooks/useFetch.ts index 36a96a1a..db697c6a 100644 --- a/admin/src/hooks/useFetch.ts +++ b/admin/src/hooks/useFetch.ts @@ -7,9 +7,7 @@ export default function useFetch(fetch: (params: P) => Promise) const fetchData = async (params?: P) => { try { - console.log("hohi"); const data = await fetch(params as P); - console.log(data); setData(data); setIsSuccess(!!data); } catch (error) { diff --git a/admin/src/pages/Lottery/index.tsx b/admin/src/pages/Lottery/index.tsx index 41d858ee..d063c2f6 100644 --- a/admin/src/pages/Lottery/index.tsx +++ b/admin/src/pages/Lottery/index.tsx @@ -1,24 +1,35 @@ import { ChangeEvent, useEffect, useState } from "react"; import { useLoaderData, useNavigate } from "react-router-dom"; +import { LotteryAPI } from "@/apis/lotteryAPI"; import Button from "@/components/Button"; import TabHeader from "@/components/TabHeader"; -import { GetLotteryResponse } from "@/types/lottery"; +import useFetch from "@/hooks/useFetch"; +import { GetLotteryResponse, PostLotteryResponse } from "@/types/lottery"; export default function Lottery() { - const data = useLoaderData() as GetLotteryResponse; + const lottery = useLoaderData() as GetLotteryResponse; + const lotteryId = lottery.length !== 0 ? lottery[0].lotteryEventId : -1; const navigate = useNavigate(); const [totalCount, setTotalCount] = useState(0); const [giftCount, setGiftCount] = useState(0); + const { isSuccess: isSuccessPostLottery, fetchData: postLottery } = + useFetch(() => LotteryAPI.postLotteryWinner({ id: lotteryId })); + useEffect(() => { - if (data.length !== 0) { - const currentLotttery = data[0]; + if (lottery.length !== 0) { + const currentLotttery = lottery[0]; setGiftCount(currentLotttery.winnerCount); setTotalCount(currentLotttery.appliedCount); } - }, [data]); + }, [lottery]); + useEffect(() => { + if (isSuccessPostLottery) { + navigate("/lottery/winner"); + } + }, [isSuccessPostLottery]); const handleChangeInput = (e: ChangeEvent) => { const count = parseInt(e.target.value); @@ -26,8 +37,7 @@ export default function Lottery() { }; const handleLottery = () => { - // TODO: 당첨자 추첨 - navigate("/lottery/winner"); + postLottery(); }; return ( diff --git a/admin/src/types/lottery.ts b/admin/src/types/lottery.ts index f0ab443c..a48dfbd2 100644 --- a/admin/src/types/lottery.ts +++ b/admin/src/types/lottery.ts @@ -5,3 +5,11 @@ export type GetLotteryResponse = { appliedCount: number; winnerCount: number; }[]; + +export interface PostLotteryParams { + id: number; +} + +export interface PostLotteryResponse { + message: string; +} From 85a8eb126c9c98af67a8174ee1652aafdb3ecc72 Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Fri, 9 Aug 2024 14:36:34 +0900 Subject: [PATCH 03/37] fix --- admin/src/apis/lotteryAPI.ts | 57 +++++++++++++++++++- admin/src/hooks/useInfiniteFetch.ts | 58 ++++++++++++++++++++ admin/src/pages/Lottery/index.tsx | 6 +-- admin/src/pages/LotteryWinner/index.tsx | 70 ++++++++++++------------- admin/src/types/common.ts | 4 ++ admin/src/types/lottery.ts | 21 +++++++- 6 files changed, 172 insertions(+), 44 deletions(-) create mode 100644 admin/src/hooks/useInfiniteFetch.ts create mode 100644 admin/src/types/common.ts diff --git a/admin/src/apis/lotteryAPI.ts b/admin/src/apis/lotteryAPI.ts index a1847911..655530a3 100644 --- a/admin/src/apis/lotteryAPI.ts +++ b/admin/src/apis/lotteryAPI.ts @@ -1,4 +1,10 @@ -import { GetLotteryResponse, PostLotteryParams, PostLotteryResponse } from "@/types/lottery"; +import { + GetLotteryResponse, + GetLotteryWinnerResponse, + PostLotteryWinnerParams, + PostLotteryWinnerResponse, + getLotteryWinnerParams, +} from "@/types/lottery"; import { fetchWithTimeout } from "@/utils/fetchWithTimeout"; const baseURL = `${import.meta.env.VITE_API_URL}/admin/event/lottery`; @@ -30,7 +36,7 @@ export const LotteryAPI = { throw error; } }, - async postLotteryWinner({ id }: PostLotteryParams): Promise { + async postLotteryWinner({ id }: PostLotteryWinnerParams): Promise { try { return new Promise((resolve) => resolve({ message: "요청에 성공하였습니다." })); const response = await fetchWithTimeout(`${baseURL}/${id}/winner`, { @@ -43,4 +49,51 @@ export const LotteryAPI = { throw error; } }, + async getLotteryWinner({ + id, + size, + page, + }: getLotteryWinnerParams): Promise { + try { + return new Promise((resolve) => + resolve({ + data: [ + { + id: 1, + phoneNumber: "010-1111-2222", + linkClickedCounts: 1, + expectation: 1, + appliedCount: 3, + }, + { + id: 2, + phoneNumber: "010-1111-2223", + linkClickedCounts: 1, + expectation: 1, + appliedCount: 3, + }, + { + id: 3, + phoneNumber: "010-1111-2224", + linkClickedCounts: 1, + expectation: 1, + appliedCount: 3, + }, + ], + isLastPage: false, + }) + ); + const response = await fetchWithTimeout( + `${baseURL}/${id}/winner?size=${size}&page=${page}`, + { + method: "GET", + headers: headers, + } + ); + return response.json(); + } catch (error) { + console.error("Error:", error); + throw error; + } + }, }; diff --git a/admin/src/hooks/useInfiniteFetch.ts b/admin/src/hooks/useInfiniteFetch.ts new file mode 100644 index 00000000..f6c0e1dd --- /dev/null +++ b/admin/src/hooks/useInfiniteFetch.ts @@ -0,0 +1,58 @@ +import { useCallback, useEffect, useState } from "react"; +import { InfiniteListData } from "@/types/common"; + +interface UseInfiniteFetchProps { + fetch: (pageParam: number) => Promise; + initialPageParam?: number; + getNextPageParam: (currentPageParam: number, lastPage: R) => number | undefined; +} + +interface InfiniteScrollData { + data: T[]; + fetchNextPage: () => void; + hasNextPage: boolean; + isLoading: boolean; + isError: boolean; +} + +export default function useInfiniteFetch({ + fetch, + initialPageParam, + getNextPageParam, +}: UseInfiniteFetchProps>): InfiniteScrollData { + const [data, setData] = useState([]); + const [currentPageParam, setCurrentPageParam] = useState(initialPageParam); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + const [hasNextPage, setHasNextPage] = useState(true); + + const fetchNextPage = useCallback(async () => { + if (!hasNextPage || isLoading || !currentPageParam) return; + + setIsLoading(true); + try { + const lastPage = await fetch(currentPageParam); + const nextPageParam = getNextPageParam(currentPageParam, lastPage); + + setData((prevData) => [...prevData, ...lastPage.data]); + setCurrentPageParam(nextPageParam); + setHasNextPage(nextPageParam !== undefined); + } catch (error) { + setIsError(true); + } finally { + setIsLoading(false); + } + }, [fetch, getNextPageParam, currentPageParam, data, hasNextPage, isLoading]); + + useEffect(() => { + fetchNextPage(); + }, [fetchNextPage]); + + return { + data, + fetchNextPage, + hasNextPage, + isLoading, + isError, + }; +} diff --git a/admin/src/pages/Lottery/index.tsx b/admin/src/pages/Lottery/index.tsx index d063c2f6..8b29d359 100644 --- a/admin/src/pages/Lottery/index.tsx +++ b/admin/src/pages/Lottery/index.tsx @@ -4,7 +4,7 @@ import { LotteryAPI } from "@/apis/lotteryAPI"; import Button from "@/components/Button"; import TabHeader from "@/components/TabHeader"; import useFetch from "@/hooks/useFetch"; -import { GetLotteryResponse, PostLotteryResponse } from "@/types/lottery"; +import { GetLotteryResponse, PostLotteryWinnerResponse } from "@/types/lottery"; export default function Lottery() { const lottery = useLoaderData() as GetLotteryResponse; @@ -16,7 +16,7 @@ export default function Lottery() { const [giftCount, setGiftCount] = useState(0); const { isSuccess: isSuccessPostLottery, fetchData: postLottery } = - useFetch(() => LotteryAPI.postLotteryWinner({ id: lotteryId })); + useFetch(() => LotteryAPI.postLotteryWinner({ id: lotteryId })); useEffect(() => { if (lottery.length !== 0) { @@ -27,7 +27,7 @@ export default function Lottery() { }, [lottery]); useEffect(() => { if (isSuccessPostLottery) { - navigate("/lottery/winner"); + navigate("/lottery/winner", { state: { id: lotteryId } }); } }, [isSuccessPostLottery]); diff --git a/admin/src/pages/LotteryWinner/index.tsx b/admin/src/pages/LotteryWinner/index.tsx index ecdb90e6..4832419a 100644 --- a/admin/src/pages/LotteryWinner/index.tsx +++ b/admin/src/pages/LotteryWinner/index.tsx @@ -1,59 +1,55 @@ -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useMemo } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { LotteryAPI } from "@/apis/lotteryAPI"; import Button from "@/components/Button"; import TabHeader from "@/components/TabHeader"; import Table from "@/components/Table"; +import useInfiniteFetch from "@/hooks/useInfiniteFetch"; +import { GetLotteryWinnerResponse } from "@/types/lottery"; 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 location = useLocation(); const navigate = useNavigate(); - const [winnerList, setWinnerList] = useState([] as any); + const lotteryId = location.state.id; + + if (!lotteryId) { + navigate("/"); + return null; + } - 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 { data: winnerInfo } = useInfiniteFetch({ + fetch: (pageParam: number) => + LotteryAPI.getLotteryWinner({ id: lotteryId, size: 10, page: pageParam }), + initialPageParam: 1, + getNextPageParam: (currentPageParam: number, lastPage: GetLotteryWinnerResponse) => { + return lastPage.isLastPage ? currentPageParam + 1 : undefined; + }, + }); + const winnerList = useMemo( + () => + winnerInfo.map((winner, idx) => [ + idx + 1, + winner.id, + winner.phoneNumber, + winner.linkClickedCounts, + winner.expectation, + winner.appliedCount, + ]), + [winnerInfo] + ); const handleLottery = () => { - // TODO: 다시 추첨하는 로직 구현 + navigate("/lottery"); }; return ( diff --git a/admin/src/types/common.ts b/admin/src/types/common.ts new file mode 100644 index 00000000..e97ed4ad --- /dev/null +++ b/admin/src/types/common.ts @@ -0,0 +1,4 @@ +export interface InfiniteListData { + data: T[]; + isLastPage: boolean; +} diff --git a/admin/src/types/lottery.ts b/admin/src/types/lottery.ts index a48dfbd2..e119f03c 100644 --- a/admin/src/types/lottery.ts +++ b/admin/src/types/lottery.ts @@ -1,3 +1,5 @@ +import { InfiniteListData } from "./common"; + export type GetLotteryResponse = { lotteryEventId: number; startDate: string; @@ -6,10 +8,25 @@ export type GetLotteryResponse = { winnerCount: number; }[]; -export interface PostLotteryParams { +export interface PostLotteryWinnerParams { id: number; } -export interface PostLotteryResponse { +export interface PostLotteryWinnerResponse { message: string; } + +export interface getLotteryWinnerParams { + id: number; + size: number; + page: number; +} + +export interface LotteryWinnerType { + id: number; + phoneNumber: string; + linkClickedCounts: number; + expectation: number; + appliedCount: number; +} +export interface GetLotteryWinnerResponse extends InfiniteListData {} From 8c8e3360c5d0468d0c8d200c01f3bdceb9af783e Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Fri, 9 Aug 2024 17:58:14 +0900 Subject: [PATCH 04/37] =?UTF-8?q?fix:=20next=20page=20param=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/apis/lotteryAPI.ts | 2 +- admin/src/hooks/useInfiniteFetch.ts | 6 +++--- admin/src/pages/LotteryWinner/index.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/admin/src/apis/lotteryAPI.ts b/admin/src/apis/lotteryAPI.ts index 655530a3..5442617c 100644 --- a/admin/src/apis/lotteryAPI.ts +++ b/admin/src/apis/lotteryAPI.ts @@ -80,7 +80,7 @@ export const LotteryAPI = { appliedCount: 3, }, ], - isLastPage: false, + isLastPage: true, }) ); const response = await fetchWithTimeout( diff --git a/admin/src/hooks/useInfiniteFetch.ts b/admin/src/hooks/useInfiniteFetch.ts index f6c0e1dd..9e9a471c 100644 --- a/admin/src/hooks/useInfiniteFetch.ts +++ b/admin/src/hooks/useInfiniteFetch.ts @@ -27,7 +27,7 @@ export default function useInfiniteFetch({ const [hasNextPage, setHasNextPage] = useState(true); const fetchNextPage = useCallback(async () => { - if (!hasNextPage || isLoading || !currentPageParam) return; + if (!hasNextPage || isLoading || currentPageParam === undefined) return; setIsLoading(true); try { @@ -42,11 +42,11 @@ export default function useInfiniteFetch({ } finally { setIsLoading(false); } - }, [fetch, getNextPageParam, currentPageParam, data, hasNextPage, isLoading]); + }, [fetch, getNextPageParam, currentPageParam, data, hasNextPage]); useEffect(() => { fetchNextPage(); - }, [fetchNextPage]); + }, []); return { data, diff --git a/admin/src/pages/LotteryWinner/index.tsx b/admin/src/pages/LotteryWinner/index.tsx index 4832419a..d70eb89b 100644 --- a/admin/src/pages/LotteryWinner/index.tsx +++ b/admin/src/pages/LotteryWinner/index.tsx @@ -32,7 +32,7 @@ export default function LotteryWinner() { LotteryAPI.getLotteryWinner({ id: lotteryId, size: 10, page: pageParam }), initialPageParam: 1, getNextPageParam: (currentPageParam: number, lastPage: GetLotteryWinnerResponse) => { - return lastPage.isLastPage ? currentPageParam + 1 : undefined; + return lastPage.isLastPage ? undefined : currentPageParam + 1; }, }); const winnerList = useMemo( From b5799896991dfe6d97e1f278a2e8f76217240730 Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sat, 10 Aug 2024 15:26:30 +0900 Subject: [PATCH 05/37] =?UTF-8?q?fix:=20useInfiniteFetch=20setData=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/hooks/useInfiniteFetch.ts | 5 +++-- admin/src/pages/LotteryWinner/index.tsx | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/admin/src/hooks/useInfiniteFetch.ts b/admin/src/hooks/useInfiniteFetch.ts index 9e9a471c..8acf3148 100644 --- a/admin/src/hooks/useInfiniteFetch.ts +++ b/admin/src/hooks/useInfiniteFetch.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { InfiniteListData } from "@/types/common"; interface UseInfiniteFetchProps { @@ -33,8 +33,9 @@ export default function useInfiniteFetch({ try { const lastPage = await fetch(currentPageParam); const nextPageParam = getNextPageParam(currentPageParam, lastPage); + console.log(lastPage); - setData((prevData) => [...prevData, ...lastPage.data]); + setData([...data, ...lastPage.data]); setCurrentPageParam(nextPageParam); setHasNextPage(nextPageParam !== undefined); } catch (error) { diff --git a/admin/src/pages/LotteryWinner/index.tsx b/admin/src/pages/LotteryWinner/index.tsx index d70eb89b..66c5954c 100644 --- a/admin/src/pages/LotteryWinner/index.tsx +++ b/admin/src/pages/LotteryWinner/index.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { LotteryAPI } from "@/apis/lotteryAPI"; import Button from "@/components/Button"; @@ -27,7 +27,7 @@ export default function LotteryWinner() { return null; } - const { data: winnerInfo } = useInfiniteFetch({ + const { data: winnerInfo, fetchNextPage: getWinnerInfo } = useInfiniteFetch({ fetch: (pageParam: number) => LotteryAPI.getLotteryWinner({ id: lotteryId, size: 10, page: pageParam }), initialPageParam: 1, From e60f03668894e39b5083fc57a5493505dff892da Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sat, 10 Aug 2024 15:47:46 +0900 Subject: [PATCH 06/37] =?UTF-8?q?feat:=20table=20=EB=AC=B4=ED=95=9C=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EB=A1=9C=EC=A7=81=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/components/Table/index.tsx | 15 +++++-- admin/src/hooks/useInfiniteFetch.ts | 7 +++- admin/src/hooks/useIntersectionObserver.ts | 46 ++++++++++++++++++++++ admin/src/pages/LotteryWinner/index.tsx | 22 +++++++++-- 4 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 admin/src/hooks/useIntersectionObserver.ts diff --git a/admin/src/components/Table/index.tsx b/admin/src/components/Table/index.tsx index 6f2cb37e..9f61f5f3 100644 --- a/admin/src/components/Table/index.tsx +++ b/admin/src/components/Table/index.tsx @@ -1,13 +1,17 @@ -import { ReactNode } from "react"; +import { ReactNode, RefObject, forwardRef } from "react"; interface TableProps { headers: ReactNode[]; data: ReactNode[][]; + dataLastItem?: RefObject; } -export default function Table({ headers, data }: TableProps) { +const Table = forwardRef(function Table( + { headers, data, dataLastItem }, + ref +) { return ( -
+
@@ -32,9 +36,12 @@ export default function Table({ headers, data }: TableProps) { ))} ))} +
); -} +}); + +export default Table; diff --git a/admin/src/hooks/useInfiniteFetch.ts b/admin/src/hooks/useInfiniteFetch.ts index 8acf3148..38f59883 100644 --- a/admin/src/hooks/useInfiniteFetch.ts +++ b/admin/src/hooks/useInfiniteFetch.ts @@ -11,7 +11,7 @@ interface InfiniteScrollData { data: T[]; fetchNextPage: () => void; hasNextPage: boolean; - isLoading: boolean; + isSuccess: boolean; isError: boolean; } @@ -23,6 +23,7 @@ export default function useInfiniteFetch({ const [data, setData] = useState([]); const [currentPageParam, setCurrentPageParam] = useState(initialPageParam); const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); const [isError, setIsError] = useState(false); const [hasNextPage, setHasNextPage] = useState(true); @@ -38,8 +39,10 @@ export default function useInfiniteFetch({ setData([...data, ...lastPage.data]); setCurrentPageParam(nextPageParam); setHasNextPage(nextPageParam !== undefined); + setIsSuccess(true); } catch (error) { setIsError(true); + setIsSuccess(false); } finally { setIsLoading(false); } @@ -53,7 +56,7 @@ export default function useInfiniteFetch({ data, fetchNextPage, hasNextPage, - isLoading, + isSuccess, isError, }; } diff --git a/admin/src/hooks/useIntersectionObserver.ts b/admin/src/hooks/useIntersectionObserver.ts new file mode 100644 index 00000000..f80abb28 --- /dev/null +++ b/admin/src/hooks/useIntersectionObserver.ts @@ -0,0 +1,46 @@ +import { useEffect, useRef } from "react"; + +interface UseIntersectionObserverOptions { + onIntersect: () => void; + enabled: boolean; + root?: Element | null; + rootMargin?: string; +} + +function useIntersectionObserver({ + onIntersect, + enabled, + root = document.body, + rootMargin = "0px", +}: UseIntersectionObserverOptions) { + const targetRef = useRef(null); + + useEffect(() => { + if (!enabled || !targetRef.current) { + return; + } + + const observerCallback = (entries: IntersectionObserverEntry[]) => { + const isIntersect = entries.some((entry) => entry.isIntersecting); + if (isIntersect) { + onIntersect(); + } + }; + + const observer = new IntersectionObserver(observerCallback, { + root, + rootMargin, + }); + observer.observe(targetRef.current); + + return () => { + if (targetRef.current) { + observer.unobserve(targetRef.current); + } + }; + }, [targetRef, root, onIntersect, enabled]); + + return { targetRef }; +} + +export default useIntersectionObserver; diff --git a/admin/src/pages/LotteryWinner/index.tsx b/admin/src/pages/LotteryWinner/index.tsx index 66c5954c..7d6e001b 100644 --- a/admin/src/pages/LotteryWinner/index.tsx +++ b/admin/src/pages/LotteryWinner/index.tsx @@ -1,10 +1,11 @@ -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { LotteryAPI } from "@/apis/lotteryAPI"; import Button from "@/components/Button"; import TabHeader from "@/components/TabHeader"; import Table from "@/components/Table"; import useInfiniteFetch from "@/hooks/useInfiniteFetch"; +import useIntersectionObserver from "@/hooks/useIntersectionObserver"; import { GetLotteryWinnerResponse } from "@/types/lottery"; const LOTTERY_WINNER_HEADER = [ @@ -27,7 +28,11 @@ export default function LotteryWinner() { return null; } - const { data: winnerInfo, fetchNextPage: getWinnerInfo } = useInfiniteFetch({ + const { + data: winnerInfo, + isSuccess: isSuccessGetLotteryWinner, + fetchNextPage: getWinnerInfo, + } = useInfiniteFetch({ fetch: (pageParam: number) => LotteryAPI.getLotteryWinner({ id: lotteryId, size: 10, page: pageParam }), initialPageParam: 1, @@ -48,6 +53,12 @@ export default function LotteryWinner() { [winnerInfo] ); + const tableContainerRef = useRef(null); + const { targetRef } = useIntersectionObserver({ + onIntersect: getWinnerInfo, + enabled: isSuccessGetLotteryWinner, + }); + const handleLottery = () => { navigate("/lottery"); }; @@ -67,7 +78,12 @@ export default function LotteryWinner() {

당첨자 추첨

- +
+ + + + + + + {data.map((tableData, idx) => ( + + {tableData.map((dataNode, idx) => ( + + ))} + + ))} + +
+ {header} +
+ {dataNode} +
+
+
+ ); +} diff --git a/admin/src/pages/Login/index.tsx b/admin/src/pages/Login/index.tsx index 99c95f9b..122090e6 100644 --- a/admin/src/pages/Login/index.tsx +++ b/admin/src/pages/Login/index.tsx @@ -2,6 +2,7 @@ import { ChangeEvent, FormEvent, useState } from "react"; import { useNavigate } from "react-router-dom"; import Button from "@/components/Button"; import Input from "@/components/Input"; +import SelectForm from "@/components/SelectForm"; export default function Login() { const navigate = useNavigate(); @@ -29,6 +30,25 @@ export default function Login() { navigate("/lottery"); }; + const data = [ + [ + "메인 문구", +
+ 첫 차로 +
+ 저렴한 차 사기 +
, + ], + [ + "서브 문구", +
+ 첫 차로 +
+ 저렴한 차 사기 +
, + ], + ]; + return (
로그인 + + ); } From f0dd1f0a8dc7797b5c5ca083962b930921b2deb0 Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sat, 10 Aug 2024 16:32:32 +0900 Subject: [PATCH 09/37] =?UTF-8?q?fix:=20=EB=9D=BC=EC=9A=B0=ED=8C=85=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/Login/index.tsx | 21 ---- admin/src/pages/Lottery/index.tsx | 62 +---------- admin/src/pages/LotteryWinner/index.tsx | 111 +++++++------------- admin/src/pages/LotteryWinnerList/index.tsx | 94 +++++++++++++++++ admin/src/router.tsx | 5 + 5 files changed, 140 insertions(+), 153 deletions(-) create mode 100644 admin/src/pages/LotteryWinnerList/index.tsx diff --git a/admin/src/pages/Login/index.tsx b/admin/src/pages/Login/index.tsx index 122090e6..4803235d 100644 --- a/admin/src/pages/Login/index.tsx +++ b/admin/src/pages/Login/index.tsx @@ -30,25 +30,6 @@ export default function Login() { navigate("/lottery"); }; - const data = [ - [ - "메인 문구", -
- 첫 차로 -
- 저렴한 차 사기 -
, - ], - [ - "서브 문구", -
- 첫 차로 -
- 저렴한 차 사기 -
, - ], - ]; - return (
로그인 - - ); } diff --git a/admin/src/pages/Lottery/index.tsx b/admin/src/pages/Lottery/index.tsx index 8b29d359..8185bb64 100644 --- a/admin/src/pages/Lottery/index.tsx +++ b/admin/src/pages/Lottery/index.tsx @@ -1,63 +1,3 @@ -import { ChangeEvent, useEffect, useState } from "react"; -import { useLoaderData, useNavigate } from "react-router-dom"; -import { LotteryAPI } from "@/apis/lotteryAPI"; -import Button from "@/components/Button"; -import TabHeader from "@/components/TabHeader"; -import useFetch from "@/hooks/useFetch"; -import { GetLotteryResponse, PostLotteryWinnerResponse } from "@/types/lottery"; - export default function Lottery() { - const lottery = useLoaderData() as GetLotteryResponse; - const lotteryId = lottery.length !== 0 ? lottery[0].lotteryEventId : -1; - - const navigate = useNavigate(); - - const [totalCount, setTotalCount] = useState(0); - const [giftCount, setGiftCount] = useState(0); - - const { isSuccess: isSuccessPostLottery, fetchData: postLottery } = - useFetch(() => LotteryAPI.postLotteryWinner({ id: lotteryId })); - - useEffect(() => { - if (lottery.length !== 0) { - const currentLotttery = lottery[0]; - setGiftCount(currentLotttery.winnerCount); - setTotalCount(currentLotttery.appliedCount); - } - }, [lottery]); - useEffect(() => { - if (isSuccessPostLottery) { - navigate("/lottery/winner", { state: { id: lotteryId } }); - } - }, [isSuccessPostLottery]); - - const handleChangeInput = (e: ChangeEvent) => { - const count = parseInt(e.target.value); - setGiftCount(count || 0); - }; - - const handleLottery = () => { - postLottery(); - }; - - return ( -
- - -
-
-

전체 참여자 수

-

{totalCount}

-

당첨자 수

-
- -
-
- - -
-
- ); + return
추첨 이벤트 메인 페이지
; } diff --git a/admin/src/pages/LotteryWinner/index.tsx b/admin/src/pages/LotteryWinner/index.tsx index 7af249d5..04c01931 100644 --- a/admin/src/pages/LotteryWinner/index.tsx +++ b/admin/src/pages/LotteryWinner/index.tsx @@ -1,92 +1,61 @@ -import { useMemo, useRef } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; +import { ChangeEvent, useEffect, useState } from "react"; +import { useLoaderData, useNavigate } from "react-router-dom"; import { LotteryAPI } from "@/apis/lotteryAPI"; import Button from "@/components/Button"; import TabHeader from "@/components/TabHeader"; -import Table from "@/components/Table"; -import useInfiniteFetch from "@/hooks/useInfiniteFetch"; -import useIntersectionObserver from "@/hooks/useIntersectionObserver"; -import { GetLotteryWinnerResponse } from "@/types/lottery"; - -const LOTTERY_WINNER_HEADER = [ - "등수", - "ID", - "전화번호", - "공유 링크 클릭 횟수", - "기대평 작성 여부", - "총 응모 횟수", -]; +import useFetch from "@/hooks/useFetch"; +import { GetLotteryResponse, PostLotteryWinnerResponse } from "@/types/lottery"; export default function LotteryWinner() { - const location = useLocation(); - const navigate = useNavigate(); - - const lotteryId = location.state.id; - - if (!lotteryId) { - navigate("/"); - return null; - } + const lottery = useLoaderData() as GetLotteryResponse; + const lotteryId = lottery.length !== 0 ? lottery[0].lotteryEventId : -1; - const { - data: winnerInfo, - isSuccess: isSuccessGetLotteryWinner, - fetchNextPage: getWinnerInfo, - } = useInfiniteFetch({ - fetch: (pageParam: number) => - LotteryAPI.getLotteryWinner({ id: lotteryId, size: 10, page: pageParam }), - initialPageParam: 1, - getNextPageParam: (currentPageParam: number, lastPage: GetLotteryWinnerResponse) => { - return lastPage.isLastPage ? undefined : currentPageParam + 1; - }, - }); - const winnerList = useMemo( - () => - winnerInfo.map((winner, idx) => [ - idx + 1, - winner.id, - winner.phoneNumber, - winner.linkClickedCounts, - winner.expectation, - winner.appliedCount, - ]), - [winnerInfo] - ); + const navigate = useNavigate(); - const tableContainerRef = useRef(null); - const { targetRef } = useIntersectionObserver({ - onIntersect: getWinnerInfo, - enabled: isSuccessGetLotteryWinner, - }); + const [totalCount, setTotalCount] = useState(0); + const [giftCount, setGiftCount] = useState(0); + + const { isSuccess: isSuccessPostLottery, fetchData: postLottery } = + useFetch(() => LotteryAPI.postLotteryWinner({ id: lotteryId })); + + useEffect(() => { + if (lottery.length !== 0) { + const currentLotttery = lottery[0]; + setGiftCount(currentLotttery.winnerCount); + setTotalCount(currentLotttery.appliedCount); + } + }, [lottery]); + useEffect(() => { + if (isSuccessPostLottery) { + navigate("/lottery/winner-list", { state: { id: lotteryId } }); + } + }, [isSuccessPostLottery]); + + const handleChangeInput = (e: ChangeEvent) => { + const count = parseInt(e.target.value); + setGiftCount(count || 0); + }; const handleLottery = () => { - navigate("/lottery"); + postLottery(); }; return (
-
-
- 뒤로 가기 버튼 navigate(-1)} - /> -

당첨자 추첨

+
+
+

전체 참여자 수

+

{totalCount}

+

당첨자 수

+
+ +
- - diff --git a/admin/src/pages/LotteryWinnerList/index.tsx b/admin/src/pages/LotteryWinnerList/index.tsx new file mode 100644 index 00000000..157e9f34 --- /dev/null +++ b/admin/src/pages/LotteryWinnerList/index.tsx @@ -0,0 +1,94 @@ +import { useMemo, useRef } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { LotteryAPI } from "@/apis/lotteryAPI"; +import Button from "@/components/Button"; +import TabHeader from "@/components/TabHeader"; +import Table from "@/components/Table"; +import useInfiniteFetch from "@/hooks/useInfiniteFetch"; +import useIntersectionObserver from "@/hooks/useIntersectionObserver"; +import { GetLotteryWinnerResponse } from "@/types/lottery"; + +const LOTTERY_WINNER_HEADER = [ + "등수", + "ID", + "전화번호", + "공유 링크 클릭 횟수", + "기대평 작성 여부", + "총 응모 횟수", +]; + +export default function LotteryWinnerList() { + const location = useLocation(); + const navigate = useNavigate(); + + const lotteryId = location.state.id; + + if (!lotteryId) { + navigate("/"); + return null; + } + + const { + data: winnerInfo, + isSuccess: isSuccessGetLotteryWinner, + fetchNextPage: getWinnerInfo, + } = useInfiniteFetch({ + fetch: (pageParam: number) => + LotteryAPI.getLotteryWinner({ id: lotteryId, size: 10, page: pageParam }), + initialPageParam: 1, + getNextPageParam: (currentPageParam: number, lastPage: GetLotteryWinnerResponse) => { + return lastPage.isLastPage ? undefined : currentPageParam + 1; + }, + }); + const winnerList = useMemo( + () => + winnerInfo.map((winner, idx) => [ + idx + 1, + winner.id, + winner.phoneNumber, + winner.linkClickedCounts, + winner.expectation, + winner.appliedCount, + ]), + [winnerInfo] + ); + + const tableContainerRef = useRef(null); + const { targetRef } = useIntersectionObserver({ + onIntersect: getWinnerInfo, + enabled: isSuccessGetLotteryWinner, + }); + + const handleLottery = () => { + navigate("/lottery"); + }; + + return ( +
+ + +
+
+ 뒤로 가기 버튼 navigate(-1)} + /> +

당첨자 추첨

+
+ +
+ + + + + ); +} diff --git a/admin/src/router.tsx b/admin/src/router.tsx index 8b2d9ca9..d1d6ef24 100644 --- a/admin/src/router.tsx +++ b/admin/src/router.tsx @@ -4,6 +4,7 @@ import Layout from "./components/Layout"; import Login from "./pages/Login"; import Lottery from "./pages/Lottery"; import LotteryWinner from "./pages/LotteryWinner"; +import LotteryWinnerList from "./pages/LotteryWinnerList"; import Rush from "./pages/Rush"; export const router = createBrowserRouter([ @@ -27,6 +28,10 @@ export const router = createBrowserRouter([ path: "winner", element: , }, + { + path: "winner-list", + element: , + }, ], }, { From 51a046b865916c66be94f76159a47421e84b4873 Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sat, 10 Aug 2024 20:40:38 +0900 Subject: [PATCH 10/37] =?UTF-8?q?feat:=20rush=20API=20=ED=8B=80=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/apis/rushAPI.ts | 59 ++++++++++++++++++++++++ admin/src/pages/RushWinnerList/index.tsx | 3 ++ admin/src/router.tsx | 14 +++++- admin/src/types/rush.ts | 10 ++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 admin/src/apis/rushAPI.ts create mode 100644 admin/src/pages/RushWinnerList/index.tsx diff --git a/admin/src/apis/rushAPI.ts b/admin/src/apis/rushAPI.ts new file mode 100644 index 00000000..46c743f0 --- /dev/null +++ b/admin/src/apis/rushAPI.ts @@ -0,0 +1,59 @@ +import { GetRushParticipantListParams } from "@/types/rush"; +import { fetchWithTimeout } from "@/utils/fetchWithTimeout"; + +const baseURL = `${import.meta.env.VITE_API_URL}/admin/event/rush`; +const headers = { + "Content-Type": "application/json", +}; + +export const RushAPI = { + async getRushParticipantList({ + id, + phoneNumber, + size, + page, + option, + }: GetRushParticipantListParams): Promise { + try { + return new Promise((resolve) => + resolve({ + data: [ + { + id: 1, + phoneNumber: "010-1111-2222", + linkClickedCounts: 1, + expectation: 1, + appliedCount: 3, + }, + { + id: 2, + phoneNumber: "010-1111-2223", + linkClickedCounts: 1, + expectation: 1, + appliedCount: 3, + }, + { + id: 3, + phoneNumber: "010-1111-2224", + linkClickedCounts: 1, + expectation: 1, + appliedCount: 3, + }, + ], + isLastPage: true, + }) + ); + const response = await fetchWithTimeout( + `${baseURL}/${id}/participants?number=${phoneNumber}&size=${size}&page=${page}&option=${option}`, + { + method: "GET", + headers: headers, + } + ); + return response.json(); + } catch (error) { + console.error("Error:", error); + throw error; + } + }, +}; diff --git a/admin/src/pages/RushWinnerList/index.tsx b/admin/src/pages/RushWinnerList/index.tsx new file mode 100644 index 00000000..0aaa1d05 --- /dev/null +++ b/admin/src/pages/RushWinnerList/index.tsx @@ -0,0 +1,3 @@ +export default function RushWinnerList() { + return
; +} diff --git a/admin/src/router.tsx b/admin/src/router.tsx index d1d6ef24..72094bc7 100644 --- a/admin/src/router.tsx +++ b/admin/src/router.tsx @@ -6,6 +6,7 @@ import Lottery from "./pages/Lottery"; import LotteryWinner from "./pages/LotteryWinner"; import LotteryWinnerList from "./pages/LotteryWinnerList"; import Rush from "./pages/Rush"; +import RushWinnerList from "./pages/RushWinnerList"; export const router = createBrowserRouter([ { @@ -22,11 +23,11 @@ export const router = createBrowserRouter([ { index: true, element: , - loader: LotteryAPI.getLottery, }, { path: "winner", element: , + loader: LotteryAPI.getLottery, }, { path: "winner-list", @@ -36,7 +37,16 @@ export const router = createBrowserRouter([ }, { path: "rush/", - element: , + children: [ + { + index: true, + element: , + }, + { + path: "winner-list", + element: , + }, + ], }, ], }, diff --git a/admin/src/types/rush.ts b/admin/src/types/rush.ts index dde663d8..7c484231 100644 --- a/admin/src/types/rush.ts +++ b/admin/src/types/rush.ts @@ -39,3 +39,13 @@ export interface RushSelectionType { result_sub_text: string; image_url: string; } + +export interface GetRushParticipantListParams { + id: number; + size: number; + page: number; + option: number; + number: string; +} + +export type GetRushParticipantListResponse = {}; From 1c4874f24a3eac862b23bfedef5a6b1e97831f38 Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sat, 10 Aug 2024 23:11:14 +0900 Subject: [PATCH 11/37] =?UTF-8?q?feat:=20=EB=B0=B8=EB=9F=B0=EC=8A=A4=20?= =?UTF-8?q?=EA=B2=8C=EC=9E=84=20=EC=B0=B8=EC=97=AC=EC=9E=90=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/apis/rushAPI.ts | 69 ++++++++++--- admin/src/features/Rush/ApplicantList.tsx | 98 ------------------ admin/src/features/Rush/EventList.tsx | 54 +++++----- admin/src/hooks/useInfiniteFetch.ts | 9 +- admin/src/pages/LotteryWinnerList/index.tsx | 5 - admin/src/pages/Rush/index.tsx | 48 +++++---- admin/src/pages/RushWinnerList/index.tsx | 105 +++++++++++++++++++- admin/src/types/rush.ts | 50 ++++++---- 8 files changed, 248 insertions(+), 190 deletions(-) delete mode 100644 admin/src/features/Rush/ApplicantList.tsx diff --git a/admin/src/apis/rushAPI.ts b/admin/src/apis/rushAPI.ts index 46c743f0..64083576 100644 --- a/admin/src/apis/rushAPI.ts +++ b/admin/src/apis/rushAPI.ts @@ -1,4 +1,9 @@ -import { GetRushParticipantListParams } from "@/types/rush"; +import { + GetRushOptionsParams, + GetRushOptionsResponse, + GetRushParticipantListParams, + GetRushParticipantListResponse, +} from "@/types/rush"; import { fetchWithTimeout } from "@/utils/fetchWithTimeout"; const baseURL = `${import.meta.env.VITE_API_URL}/admin/event/rush`; @@ -13,7 +18,7 @@ export const RushAPI = { size, page, option, - }: GetRushParticipantListParams): Promise { + }: GetRushParticipantListParams): Promise { try { return new Promise((resolve) => resolve({ @@ -21,26 +26,26 @@ export const RushAPI = { { id: 1, phoneNumber: "010-1111-2222", - linkClickedCounts: 1, - expectation: 1, - appliedCount: 3, + balanceGameChoice: 1, + createdAt: "2024-07-25 20:00:123", + rank: 1, }, { - id: 2, - phoneNumber: "010-1111-2223", - linkClickedCounts: 1, - expectation: 1, - appliedCount: 3, + id: 3, + phoneNumber: "010-1111-2222", + balanceGameChoice: 1, + createdAt: "2024-07-25 20:00:125", + rank: 2, }, { - id: 3, - phoneNumber: "010-1111-2224", - linkClickedCounts: 1, - expectation: 1, - appliedCount: 3, + id: 4, + phoneNumber: "010-1111-2222", + balanceGameChoice: 1, + createdAt: "2024-07-25 20:00:127", + rank: 3, }, ], - isLastPage: true, + isLastPage: false, }) ); const response = await fetchWithTimeout( @@ -56,4 +61,36 @@ export const RushAPI = { throw error; } }, + async getRushOptions({ id }: GetRushOptionsParams): Promise { + try { + return new Promise((resolve) => + resolve([ + { + rushOptionId: 1, + mainText: "첫 차로 저렴한 차 사기", + subText: " 첫 차는 가성비가 짱이지!", + resultMainText: "누구보다 가성비 갑인 캐스퍼 일렉트릭", + resultSubText: "전기차 평균보다 훨씬 저렴한 캐스퍼 일렉트릭!", + imageUrl: "left_image.png", + }, + { + rushOptionId: 2, + mainText: "첫 차로 성능 좋은 차 사기", + subText: " 차는 당연히 성능이지!", + resultMainText: "필요한 건 다 갖춘 캐스퍼 일렉트릭", + resultSubText: "전기차 평균보다 훨씨니 저렴한 캐스퍼 일렉트릭!", + imageUrl: "left_image.png", + }, + ]) + ); + const response = await fetchWithTimeout(`${baseURL}/${id}/options`, { + method: "GET", + headers: headers, + }); + return response.json(); + } catch (error) { + console.error("Error:", error); + throw error; + } + }, }; diff --git a/admin/src/features/Rush/ApplicantList.tsx b/admin/src/features/Rush/ApplicantList.tsx deleted file mode 100644 index 62a22d93..00000000 --- a/admin/src/features/Rush/ApplicantList.tsx +++ /dev/null @@ -1,98 +0,0 @@ -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"; - -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); - - 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", - }, - ]); - }, [selectedRush]); - - 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 ( -
-
- rush.event_date)} - selectedIdx={selectedRush} - handleClickOption={(idx) => setSelectedRush(idx)} - /> -

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

- -
- -
- - ); -} diff --git a/admin/src/features/Rush/EventList.tsx b/admin/src/features/Rush/EventList.tsx index 31d5e2ff..d4d41ac3 100644 --- a/admin/src/features/Rush/EventList.tsx +++ b/admin/src/features/Rush/EventList.tsx @@ -37,31 +37,31 @@ export default function EventList({ handleSelectSection }: EventListProps) { 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만원 기프트카드", + rushEventId: 1, + eventDate: "2024-07-25", + openTime: "20:00:00", + closeTime: "20:10:00", + winnerCount: 315, + prizeImageUrl: "prize1.png", + prizeDescription: "스타벅스 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만원 기프트카드", + rushEventId: 2, + eventDate: "2024-07-26", + openTime: "20:00:00", + closeTime: "20:10:00", + winnerCount: 315, + prizeImageUrl: "prize2.png", + prizeDescription: "올리브영 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만원 기프트카드", + rushEventId: 2, + eventDate: "2024-07-27", + openTime: "20:00:00", + closeTime: "20:10:00", + winnerCount: 315, + prizeImageUrl: "prize3.png", + prizeDescription: "배달의 민족 1만원 기프트카드", }, ], }); @@ -81,24 +81,24 @@ export default function EventList({ handleSelectSection }: EventListProps) { const getTableData = () => { return rushList.map((item, idx) => { return [ - item.rush_event_id, + item.rushEventId, handleChangeItem("event_date", idx, date)} />, handleChangeItem("open_time", idx, time)} />, handleChangeItem("close_time", idx, time)} />, - getTimeDifference(item.open_time, item.close_time), + getTimeDifference(item.openTime, item.closeTime), , ,
-

{item.winner_count}

+

{item.winnerCount}

편집

, "오픈 전", diff --git a/admin/src/hooks/useInfiniteFetch.ts b/admin/src/hooks/useInfiniteFetch.ts index b01daa16..76d66b20 100644 --- a/admin/src/hooks/useInfiniteFetch.ts +++ b/admin/src/hooks/useInfiniteFetch.ts @@ -5,6 +5,7 @@ interface UseInfiniteFetchProps { fetch: (pageParam: number) => Promise; initialPageParam?: number; getNextPageParam: (currentPageParam: number, lastPage: R) => number | undefined; + startFetching?: boolean; } interface InfiniteScrollData { @@ -19,6 +20,7 @@ export default function useInfiniteFetch({ fetch, initialPageParam, getNextPageParam, + startFetching = true, }: UseInfiniteFetchProps>): InfiniteScrollData { const [data, setData] = useState([]); const [currentPageParam, setCurrentPageParam] = useState(initialPageParam); @@ -34,7 +36,6 @@ export default function useInfiniteFetch({ try { const lastPage = await fetch(currentPageParam); const nextPageParam = getNextPageParam(currentPageParam, lastPage); - console.log(lastPage); setData([...data, ...lastPage.data]); setCurrentPageParam(nextPageParam); @@ -49,8 +50,10 @@ export default function useInfiniteFetch({ }, [fetch, getNextPageParam, currentPageParam, data, hasNextPage]); useEffect(() => { - fetchNextPage(); - }, []); + if (startFetching) { + fetchNextPage(); + } + }, [startFetching]); return { data, diff --git a/admin/src/pages/LotteryWinnerList/index.tsx b/admin/src/pages/LotteryWinnerList/index.tsx index 157e9f34..f9de4bcb 100644 --- a/admin/src/pages/LotteryWinnerList/index.tsx +++ b/admin/src/pages/LotteryWinnerList/index.tsx @@ -23,11 +23,6 @@ export default function LotteryWinnerList() { const lotteryId = location.state.id; - if (!lotteryId) { - navigate("/"); - return null; - } - const { data: winnerInfo, isSuccess: isSuccessGetLotteryWinner, diff --git a/admin/src/pages/Rush/index.tsx b/admin/src/pages/Rush/index.tsx index e3f0e947..58584ca9 100644 --- a/admin/src/pages/Rush/index.tsx +++ b/admin/src/pages/Rush/index.tsx @@ -1,10 +1,12 @@ import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; import TabHeader from "@/components/TabHeader"; import ApplicantList from "@/features/Rush/ApplicantList"; import useRushEventDispatchContext from "@/hooks/useRushEventDispatchContext"; import { RUSH_ACTION } from "@/types/rush"; export default function Rush() { + const navigate = useNavigate(); const dispatch = useRushEventDispatchContext(); useEffect(() => { @@ -13,31 +15,31 @@ export default function Rush() { 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만원 기프트카드", + rushEventId: 1, + eventDate: "2024-07-25", + openTime: "20:00:00", + closeTime: "20:10:00", + winnerCount: 315, + prizeImageUrl: "prize1.png", + prizeDescription: "스타벅스 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만원 기프트카드", + rushEventId: 2, + eventDate: "2024-07-26", + openTime: "20:00:00", + closeTime: "20:10:00", + winnerCount: 315, + prizeImageUrl: "prize2.png", + prizeDescription: "올리브영 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만원 기프트카드", + rushEventId: 2, + eventDate: "2024-07-27", + openTime: "20:00:00", + closeTime: "20:10:00", + winnerCount: 315, + prizeImageUrl: "prize3.png", + prizeDescription: "배달의 민족 1만원 기프트카드", }, ], }); @@ -47,7 +49,9 @@ export default function Rush() {
- +
); } diff --git a/admin/src/pages/RushWinnerList/index.tsx b/admin/src/pages/RushWinnerList/index.tsx index 0aaa1d05..bb65dadf 100644 --- a/admin/src/pages/RushWinnerList/index.tsx +++ b/admin/src/pages/RushWinnerList/index.tsx @@ -1,3 +1,106 @@ +import { useEffect, useRef, useState } from "react"; +import { useLocation } from "react-router-dom"; +import { RushAPI } from "@/apis/rushAPI"; +import Button from "@/components/Button"; +import Dropdown from "@/components/Dropdown"; +import TabHeader from "@/components/TabHeader"; +import Table from "@/components/Table"; +import useInfiniteFetch from "@/hooks/useInfiniteFetch"; +import useIntersectionObserver from "@/hooks/useIntersectionObserver"; +import { GetRushParticipantListResponse, RushOptionType } from "@/types/rush"; + export default function RushWinnerList() { - return
; + const location = useLocation(); + + const rushId = location.state.id; + + const [isWinnerToggle, setIsWinnerToggle] = useState(false); + const [options, setOptions] = useState([]); + const [selectedOptionIdx, setSelectedOptionIdx] = useState(0); + + const optionTitleList = options.map((option, idx) => `옵션 ${idx + 1} : ${option.mainText}`); + + const { + data: participants, + isSuccess: isSuccessGetRushParticipantList, + fetchNextPage: getRushParticipantList, + } = useInfiniteFetch({ + fetch: (pageParam: number) => + RushAPI.getRushParticipantList({ + id: rushId, + size: 10, + page: pageParam, + option: options[selectedOptionIdx].rushOptionId, + }), + initialPageParam: 1, + getNextPageParam: (currentPageParam: number, lastPage: GetRushParticipantListResponse) => { + return lastPage.isLastPage ? undefined : currentPageParam + 1; + }, + startFetching: options.length !== 0, + }); + + const tableContainerRef = useRef(null); + const { targetRef } = useIntersectionObserver({ + onIntersect: getRushParticipantList, + enabled: isSuccessGetRushParticipantList, + }); + + const APPLICANT_LIST_HEADER = [ + "ID", + "전화번호", + "등수", + "클릭 시간", + setSelectedOptionIdx(idx)} + />, + ]; + + const data = participants.map((participant, idx) => { + const selectedOptionIdx = participant.balanceGameChoice - 1; + return [ + idx + 1, + participant.phoneNumber, + idx + 1, + participant.createdAt, + `옵션 ${selectedOptionIdx + 1} : ${options[selectedOptionIdx].mainText}`, + ]; + }); + + useEffect(() => { + handleGetOptions(); + getRushParticipantList(); + }, []); + + const handleGetOptions = async () => { + const data = await RushAPI.getRushOptions({ id: rushId }); + setOptions(data); + setSelectedOptionIdx(0); + }; + + return ( +
+ + +
+
+

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

+ +
+ +
+ + + ); } diff --git a/admin/src/types/rush.ts b/admin/src/types/rush.ts index 7c484231..28d659d2 100644 --- a/admin/src/types/rush.ts +++ b/admin/src/types/rush.ts @@ -1,13 +1,14 @@ import { Dispatch } from "react"; +import { InfiniteListData } from "./common"; 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; + rushEventId: number; + eventDate: string; + openTime: string; + closeTime: string; + winnerCount: number; + prizeImageUrl: string; + prizeDescription: string; } export interface RushEventStateType { @@ -31,21 +32,34 @@ export interface RushApplicantType { 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; -} - export interface GetRushParticipantListParams { id: number; size: number; page: number; option: number; - number: string; + phoneNumber?: string; +} + +export interface RushParticipantType { + id: number; + phoneNumber: string; + balanceGameChoice: number; + createdAt: string; + rank: number; +} +export interface GetRushParticipantListResponse extends InfiniteListData {} + +export interface GetRushOptionsParams { + id: number; +} + +export interface RushOptionType { + rushOptionId: number; + mainText: string; + subText: string; + resultMainText: string; + resultSubText: string; + imageUrl: string; } -export type GetRushParticipantListResponse = {}; +export type GetRushOptionsResponse = RushOptionType[]; From 66c3f7711aebf362a74946fceabe43c3518ec045 Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sun, 11 Aug 2024 11:25:45 +0900 Subject: [PATCH 12/37] =?UTF-8?q?feat:=20=EB=B0=B8=EB=9F=B0=EC=8A=A4=20?= =?UTF-8?q?=EA=B2=8C=EC=9E=84=20=EB=8B=B9=EC=B2=A8=EC=9E=90=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=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/apis/rushAPI.ts | 49 +++++++++++ admin/src/hooks/useInfiniteFetch.ts | 12 +++ admin/src/pages/RushWinnerList/index.tsx | 107 ++++++++++++++++------- admin/src/types/rush.ts | 8 ++ 4 files changed, 145 insertions(+), 31 deletions(-) diff --git a/admin/src/apis/rushAPI.ts b/admin/src/apis/rushAPI.ts index 64083576..36751066 100644 --- a/admin/src/apis/rushAPI.ts +++ b/admin/src/apis/rushAPI.ts @@ -3,6 +3,7 @@ import { GetRushOptionsResponse, GetRushParticipantListParams, GetRushParticipantListResponse, + GetRushWinnerListParams, } from "@/types/rush"; import { fetchWithTimeout } from "@/utils/fetchWithTimeout"; @@ -61,6 +62,54 @@ export const RushAPI = { throw error; } }, + async getRushWinnerList({ + id, + phoneNumber, + size, + page, + }: GetRushWinnerListParams): Promise { + try { + return new Promise((resolve) => + resolve({ + data: [ + { + id: 1, + phoneNumber: "010-3843-6999", + balanceGameChoice: 1, + createdAt: "2024-07-25 20:00:123", + rank: 1, + }, + { + id: 3, + phoneNumber: "010-1111-2222", + balanceGameChoice: 1, + createdAt: "2024-07-25 20:00:125", + rank: 2, + }, + { + id: 4, + phoneNumber: "010-1111-2222", + balanceGameChoice: 1, + createdAt: "2024-07-25 20:00:127", + rank: 3, + }, + ], + isLastPage: false, + }) + ); + const response = await fetchWithTimeout( + `${baseURL}/${id}/participants?number=${phoneNumber}&size=${size}&page=${page}`, + { + method: "GET", + headers: headers, + } + ); + return response.json(); + } catch (error) { + console.error("Error:", error); + throw error; + } + }, async getRushOptions({ id }: GetRushOptionsParams): Promise { try { return new Promise((resolve) => diff --git a/admin/src/hooks/useInfiniteFetch.ts b/admin/src/hooks/useInfiniteFetch.ts index 76d66b20..aa1b3781 100644 --- a/admin/src/hooks/useInfiniteFetch.ts +++ b/admin/src/hooks/useInfiniteFetch.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useState } from "react"; +import { flushSync } from "react-dom"; import { InfiniteListData } from "@/types/common"; interface UseInfiniteFetchProps { @@ -11,6 +12,7 @@ interface UseInfiniteFetchProps { interface InfiniteScrollData { data: T[]; fetchNextPage: () => void; + refetch: () => void; hasNextPage: boolean; isSuccess: boolean; isError: boolean; @@ -32,6 +34,7 @@ export default function useInfiniteFetch({ const fetchNextPage = useCallback(async () => { if (!hasNextPage || isLoading || currentPageParam === undefined) return; + console.log(currentPageParam); setIsLoading(true); try { const lastPage = await fetch(currentPageParam); @@ -49,6 +52,14 @@ export default function useInfiniteFetch({ } }, [fetch, getNextPageParam, currentPageParam, data, hasNextPage]); + const refetch = useCallback(async () => { + flushSync(() => { + setCurrentPageParam(initialPageParam); + setData([]); + }); + fetchNextPage(); + }, [fetchNextPage]); + useEffect(() => { if (startFetching) { fetchNextPage(); @@ -58,6 +69,7 @@ export default function useInfiniteFetch({ return { data, fetchNextPage, + refetch, hasNextPage, isSuccess, isError, diff --git a/admin/src/pages/RushWinnerList/index.tsx b/admin/src/pages/RushWinnerList/index.tsx index bb65dadf..0faeddf6 100644 --- a/admin/src/pages/RushWinnerList/index.tsx +++ b/admin/src/pages/RushWinnerList/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useLocation } from "react-router-dom"; import { RushAPI } from "@/apis/rushAPI"; import Button from "@/components/Button"; @@ -18,12 +18,11 @@ export default function RushWinnerList() { const [options, setOptions] = useState([]); const [selectedOptionIdx, setSelectedOptionIdx] = useState(0); - const optionTitleList = options.map((option, idx) => `옵션 ${idx + 1} : ${option.mainText}`); - const { data: participants, isSuccess: isSuccessGetRushParticipantList, fetchNextPage: getRushParticipantList, + refetch: refetchRushParticipantList, } = useInfiniteFetch({ fetch: (pageParam: number) => RushAPI.getRushParticipantList({ @@ -38,54 +37,100 @@ export default function RushWinnerList() { }, startFetching: options.length !== 0, }); - - const tableContainerRef = useRef(null); - const { targetRef } = useIntersectionObserver({ - onIntersect: getRushParticipantList, - enabled: isSuccessGetRushParticipantList, + const { + data: winners, + isSuccess: isSuccessGetRushWinnerList, + fetchNextPage: getRushWinnerList, + refetch: refetchRushWinnerList, + } = useInfiniteFetch({ + fetch: (pageParam: number) => + RushAPI.getRushWinnerList({ + id: rushId, + size: 10, + page: pageParam, + }), + initialPageParam: 1, + getNextPageParam: (currentPageParam: number, lastPage: GetRushParticipantListResponse) => { + return lastPage.isLastPage ? undefined : currentPageParam + 1; + }, }); - const APPLICANT_LIST_HEADER = [ - "ID", - "전화번호", - "등수", - "클릭 시간", - setSelectedOptionIdx(idx)} - />, - ]; + const currentData = isWinnerToggle ? winners : participants; - const data = participants.map((participant, idx) => { - const selectedOptionIdx = participant.balanceGameChoice - 1; - return [ - idx + 1, - participant.phoneNumber, - idx + 1, - participant.createdAt, - `옵션 ${selectedOptionIdx + 1} : ${options[selectedOptionIdx].mainText}`, - ]; + const tableContainerRef = useRef(null); + const { targetRef } = useIntersectionObserver({ + onIntersect: isWinnerToggle ? getRushWinnerList : getRushParticipantList, + enabled: isSuccessGetRushParticipantList && isSuccessGetRushWinnerList, }); useEffect(() => { handleGetOptions(); getRushParticipantList(); + getRushWinnerList(); }, []); + useEffect(() => { + return () => { + if (tableContainerRef.current) { + console.log("scroll"); + tableContainerRef.current.scroll({ top: 0 }); + } + }; + }, [isWinnerToggle]); + const handleGetOptions = async () => { const data = await RushAPI.getRushOptions({ id: rushId }); setOptions(data); setSelectedOptionIdx(0); }; + const handleClickOption = (idx: number) => { + setSelectedOptionIdx(idx); + + refetchRushParticipantList(); + refetchRushWinnerList(); + }; + + const optionTitleList = useMemo( + () => options.map((option, idx) => `옵션 ${idx + 1} : ${option.mainText}`), + [options] + ); + const participantHeader = useMemo( + () => [ + "ID", + "전화번호", + "등수", + "클릭 시간", + , + ], + [optionTitleList, selectedOptionIdx] + ); + const dataList = useMemo( + () => + currentData.map((participant) => { + const selectedOptionIdx = participant.balanceGameChoice - 1; + return [ + participant.id, + participant.phoneNumber, + participant.rank, + participant.createdAt, + `옵션 ${selectedOptionIdx + 1} : ${options[selectedOptionIdx].mainText}`, + ]; + }), + [currentData, selectedOptionIdx] + ); + return (
-

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

+

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

diff --git a/admin/src/types/rush.ts b/admin/src/types/rush.ts index 28d659d2..83e974cd 100644 --- a/admin/src/types/rush.ts +++ b/admin/src/types/rush.ts @@ -49,6 +49,14 @@ export interface RushParticipantType { } export interface GetRushParticipantListResponse extends InfiniteListData {} +export interface GetRushWinnerListParams { + id: number; + size: number; + page: number; + phoneNumber?: string; +} +export interface GetRushWinnerListResponse extends InfiniteListData {} + export interface GetRushOptionsParams { id: number; } From dd070955d557f4104e8037a03a0c9f10a94505fe Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sun, 11 Aug 2024 11:48:34 +0900 Subject: [PATCH 13/37] =?UTF-8?q?fix:=20=EC=B0=B8=EC=97=AC=EC=9E=90/?= =?UTF-8?q?=EB=8B=B9=EC=B2=A8=EC=9E=90=20=EB=AA=A9=EB=A1=9D=20toggle=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/components/Table/index.tsx | 2 +- admin/src/hooks/useInfiniteFetch.ts | 1 - admin/src/pages/RushWinnerList/index.tsx | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/admin/src/components/Table/index.tsx b/admin/src/components/Table/index.tsx index 9f61f5f3..c10b6769 100644 --- a/admin/src/components/Table/index.tsx +++ b/admin/src/components/Table/index.tsx @@ -12,7 +12,7 @@ const Table = forwardRef(function Table( ) { return (
-
+
diff --git a/admin/src/hooks/useInfiniteFetch.ts b/admin/src/hooks/useInfiniteFetch.ts index aa1b3781..c3f50140 100644 --- a/admin/src/hooks/useInfiniteFetch.ts +++ b/admin/src/hooks/useInfiniteFetch.ts @@ -34,7 +34,6 @@ export default function useInfiniteFetch({ const fetchNextPage = useCallback(async () => { if (!hasNextPage || isLoading || currentPageParam === undefined) return; - console.log(currentPageParam); setIsLoading(true); try { const lastPage = await fetch(currentPageParam); diff --git a/admin/src/pages/RushWinnerList/index.tsx b/admin/src/pages/RushWinnerList/index.tsx index 0faeddf6..bf576302 100644 --- a/admin/src/pages/RushWinnerList/index.tsx +++ b/admin/src/pages/RushWinnerList/index.tsx @@ -72,8 +72,8 @@ export default function RushWinnerList() { useEffect(() => { return () => { if (tableContainerRef.current) { - console.log("scroll"); - tableContainerRef.current.scroll({ top: 0 }); + const table = tableContainerRef.current.querySelector(".table-contents"); + table?.scrollTo({ top: 0 }); } }; }, [isWinnerToggle]); From 3921651070f93dce7ec1ec64840c34c9249b8acd Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sun, 11 Aug 2024 11:56:34 +0900 Subject: [PATCH 14/37] =?UTF-8?q?fix:=20=EB=8B=B9=EC=B2=A8=EC=9E=90=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/pages/RushWinnerList/index.tsx | 35 ++++++++++++++---------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/admin/src/pages/RushWinnerList/index.tsx b/admin/src/pages/RushWinnerList/index.tsx index bf576302..be8af53f 100644 --- a/admin/src/pages/RushWinnerList/index.tsx +++ b/admin/src/pages/RushWinnerList/index.tsx @@ -65,19 +65,19 @@ export default function RushWinnerList() { useEffect(() => { handleGetOptions(); - getRushParticipantList(); - getRushWinnerList(); }, []); useEffect(() => { - return () => { - if (tableContainerRef.current) { - const table = tableContainerRef.current.querySelector(".table-contents"); - table?.scrollTo({ top: 0 }); - } - }; + return () => handleTableScrollTop(); }, [isWinnerToggle]); + const handleTableScrollTop = () => { + if (tableContainerRef.current) { + const table = tableContainerRef.current.querySelector(".table-contents"); + table?.scrollTo({ top: 0 }); + } + }; + const handleGetOptions = async () => { const data = await RushAPI.getRushOptions({ id: rushId }); setOptions(data); @@ -85,8 +85,9 @@ export default function RushWinnerList() { }; const handleClickOption = (idx: number) => { - setSelectedOptionIdx(idx); + handleTableScrollTop(); + setSelectedOptionIdx(idx); refetchRushParticipantList(); refetchRushWinnerList(); }; @@ -101,13 +102,17 @@ export default function RushWinnerList() { "전화번호", "등수", "클릭 시간", - , + isWinnerToggle ? ( + "선택한 옵션" + ) : ( + + ), ], - [optionTitleList, selectedOptionIdx] + [optionTitleList, isWinnerToggle, selectedOptionIdx] ); const dataList = useMemo( () => From cc6f2aefe8617245c6c5ae22958efb376efb956d Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sun, 11 Aug 2024 12:00:02 +0900 Subject: [PATCH 15/37] =?UTF-8?q?chore:=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/pages/Rush/index.tsx | 1 - admin/src/pages/RushWinnerList/index.tsx | 11 +++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/admin/src/pages/Rush/index.tsx b/admin/src/pages/Rush/index.tsx index 58584ca9..4f66be17 100644 --- a/admin/src/pages/Rush/index.tsx +++ b/admin/src/pages/Rush/index.tsx @@ -1,7 +1,6 @@ import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; import TabHeader from "@/components/TabHeader"; -import ApplicantList from "@/features/Rush/ApplicantList"; import useRushEventDispatchContext from "@/hooks/useRushEventDispatchContext"; import { RUSH_ACTION } from "@/types/rush"; diff --git a/admin/src/pages/RushWinnerList/index.tsx b/admin/src/pages/RushWinnerList/index.tsx index be8af53f..ba01b56a 100644 --- a/admin/src/pages/RushWinnerList/index.tsx +++ b/admin/src/pages/RushWinnerList/index.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { useLocation } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { RushAPI } from "@/apis/rushAPI"; import Button from "@/components/Button"; import Dropdown from "@/components/Dropdown"; @@ -11,6 +11,7 @@ import { GetRushParticipantListResponse, RushOptionType } from "@/types/rush"; export default function RushWinnerList() { const location = useLocation(); + const navigate = useNavigate(); const rushId = location.state.id; @@ -130,11 +131,17 @@ export default function RushWinnerList() { ); return ( -
+
+ 뒤로 가기 버튼 navigate(-1)} + />

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

, ,
-

{item.winner_count}

+

{item.winnerCount}

편집

, "오픈 전", , diff --git a/admin/src/features/Rush/Layout.tsx b/admin/src/features/Rush/Layout.tsx new file mode 100644 index 00000000..21b8749f --- /dev/null +++ b/admin/src/features/Rush/Layout.tsx @@ -0,0 +1,10 @@ +import { Outlet } from "react-router-dom"; +import { RushEventContext } from "@/contexts/rushEventContext"; + +export default function RushLayout() { + return ( + + + + ); +} diff --git a/admin/src/pages/Rush/index.tsx b/admin/src/pages/Rush/index.tsx index e3f0e947..5f200e1d 100644 --- a/admin/src/pages/Rush/index.tsx +++ b/admin/src/pages/Rush/index.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import TabHeader from "@/components/TabHeader"; -import ApplicantList from "@/features/Rush/ApplicantList"; +import EventList from "@/features/Rush/EventList"; import useRushEventDispatchContext from "@/hooks/useRushEventDispatchContext"; import { RUSH_ACTION } from "@/types/rush"; @@ -13,31 +13,31 @@ export default function Rush() { 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만원 기프트카드", + rushEventId: 1, + eventDate: "2024-07-25", + openTime: "20:00:00", + closeTime: "20:10:00", + winnerCount: 315, + prizeImageUrl: "prize1.png", + prizeDescription: "스타벅스 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만원 기프트카드", + rushEventId: 2, + eventDate: "2024-07-26", + openTime: "20:00:00", + closeTime: "20:10:00", + winnerCount: 315, + prizeImageUrl: "prize2.png", + prizeDescription: "올리브영 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만원 기프트카드", + rushEventId: 2, + eventDate: "2024-07-27", + openTime: "20:00:00", + closeTime: "20:10:00", + winnerCount: 315, + prizeImageUrl: "prize3.png", + prizeDescription: "배달의 민족 1만원 기프트카드", }, ], }); @@ -47,7 +47,7 @@ export default function Rush() {
- +
); } diff --git a/admin/src/router.tsx b/admin/src/router.tsx index 9a5ed0c4..a05087b4 100644 --- a/admin/src/router.tsx +++ b/admin/src/router.tsx @@ -1,5 +1,6 @@ import { createBrowserRouter } from "react-router-dom"; import Layout from "./components/Layout"; +import RushLayout from "./features/Rush/Layout"; import Login from "./pages/Login"; import Lottery from "./pages/Lottery"; import LotteryWinner from "./pages/LotteryWinner"; @@ -29,7 +30,13 @@ export const router = createBrowserRouter([ }, { path: "rush/", - element: , + element: , + children: [ + { + index: true, + element: , + }, + ], }, ], }, diff --git a/admin/src/types/rush.ts b/admin/src/types/rush.ts index dde663d8..0701e921 100644 --- a/admin/src/types/rush.ts +++ b/admin/src/types/rush.ts @@ -1,13 +1,13 @@ 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; + rushEventId: number; + eventDate: string; + openTime: string; + closeTime: string; + winnerCount: number; + prizeImageUrl: string; + prizeDescription: string; } export interface RushEventStateType { From 672275c7333bc7d7a102e9295e154d1a2dce45eb Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sun, 11 Aug 2024 13:22:01 +0900 Subject: [PATCH 18/37] =?UTF-8?q?feat:=20=EC=84=A0=ED=83=9D=EC=A7=80=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=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/components/SelectForm/index.tsx | 38 +++++++ admin/src/components/TextField/index.tsx | 13 +++ admin/src/contexts/rushEventContext.tsx | 4 +- admin/src/features/Rush/EventList.tsx | 9 +- admin/src/pages/RushSelectForm/index.tsx | 129 ++++++++++++++++++++++ admin/src/router.tsx | 5 + admin/src/types/rush.ts | 39 +++---- 7 files changed, 216 insertions(+), 21 deletions(-) create mode 100644 admin/src/components/SelectForm/index.tsx create mode 100644 admin/src/components/TextField/index.tsx create mode 100644 admin/src/pages/RushSelectForm/index.tsx diff --git a/admin/src/components/SelectForm/index.tsx b/admin/src/components/SelectForm/index.tsx new file mode 100644 index 00000000..693d7b32 --- /dev/null +++ b/admin/src/components/SelectForm/index.tsx @@ -0,0 +1,38 @@ +import { ReactNode } from "react"; + +interface SelectFormProps { + header: ReactNode; + data: ReactNode[][]; +} + +export default function SelectForm({ header, data }: SelectFormProps) { + return ( +
+
+
+ + + + + + + {data.map((tableData, idx) => ( + + {tableData.map((dataNode, idx) => ( + + ))} + + ))} + +
+ {header} +
+ {dataNode} +
+
+
+ ); +} diff --git a/admin/src/components/TextField/index.tsx b/admin/src/components/TextField/index.tsx new file mode 100644 index 00000000..3cfff79a --- /dev/null +++ b/admin/src/components/TextField/index.tsx @@ -0,0 +1,13 @@ +import { ComponentProps } from "react"; + +interface TextFieldProps extends ComponentProps<"textarea"> {} + +export default function TextField({ ...restProps }: TextFieldProps) { + return ( +