From 36f5340d72f7caa540f7418ac9b0a03ca2a79db3 Mon Sep 17 00:00:00 2001 From: jhj2713 Date: Fri, 9 Aug 2024 11:43:03 +0900 Subject: [PATCH 01/26] =?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/26] =?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/26] 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/26] =?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/26] =?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/26] =?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/26] =?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/26] =?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/26] =?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/26] =?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/26] =?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/26] =?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/26] =?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} 명

+ + +
+ ); } diff --git a/admin/src/types/lottery.ts b/admin/src/types/lottery.ts index e119f03c..fa53e94b 100644 --- a/admin/src/types/lottery.ts +++ b/admin/src/types/lottery.ts @@ -16,12 +16,24 @@ export interface PostLotteryWinnerResponse { message: string; } -export interface getLotteryWinnerParams { +export interface GetLotteryWinnerParams { id: number; size: number; page: number; } +export interface GetLotteryExpectationsParams { + lotteryId: number; + participantId: number; +} + +export interface LotteryExpectationsType { + casperId: number; + expectation: string; +} + +export type GetLotteryExpectationsResponse = LotteryExpectationsType[]; + export interface LotteryWinnerType { id: number; phoneNumber: string; From fa28f5ad7c8e445764a171c77db899c1dbeb7a80 Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sun, 11 Aug 2024 14:50:25 +0900 Subject: [PATCH 18/26] =?UTF-8?q?feat:=20input=20border-bottom=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/pages/LotteryWinner/index.tsx | 2 +- admin/src/pages/LotteryWinnerList/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/src/pages/LotteryWinner/index.tsx b/admin/src/pages/LotteryWinner/index.tsx index 04c01931..edfcde0f 100644 --- a/admin/src/pages/LotteryWinner/index.tsx +++ b/admin/src/pages/LotteryWinner/index.tsx @@ -49,7 +49,7 @@ export default function LotteryWinner() {

전체 참여자 수

{totalCount}

당첨자 수

-
+
diff --git a/admin/src/pages/LotteryWinnerList/index.tsx b/admin/src/pages/LotteryWinnerList/index.tsx index 80ab1b31..b89da42c 100644 --- a/admin/src/pages/LotteryWinnerList/index.tsx +++ b/admin/src/pages/LotteryWinnerList/index.tsx @@ -48,7 +48,7 @@ export default function LotteryWinnerList() { }); const handleLottery = () => { - navigate("/lottery"); + navigate("/lottery/winner"); }; const handleClickExpectation = async (winnerId: number) => { From 0db61492d9eadb3d0ff87b4afde496c854ae1332 Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sun, 11 Aug 2024 15:21:27 +0900 Subject: [PATCH 19/26] =?UTF-8?q?refactor:=20lottery=20=EC=9D=BC=EB=B0=98?= =?UTF-8?q?=20type=20-=20api=20type=20=EB=B6=84=EB=A6=AC?= 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 | 1 - admin/src/pages/Login/index.tsx | 1 - admin/src/pages/LotteryWinner/index.tsx | 2 +- admin/src/pages/LotteryWinnerList/index.tsx | 3 ++- admin/src/pages/RushWinnerList/index.tsx | 1 - admin/src/types/lottery.ts | 26 +------------------- admin/src/types/lotteryApi.ts | 27 +++++++++++++++++++++ 8 files changed, 32 insertions(+), 31 deletions(-) create mode 100644 admin/src/types/lotteryApi.ts diff --git a/admin/src/apis/lotteryAPI.ts b/admin/src/apis/lotteryAPI.ts index c0fb1c13..87147788 100644 --- a/admin/src/apis/lotteryAPI.ts +++ b/admin/src/apis/lotteryAPI.ts @@ -6,7 +6,7 @@ import { GetLotteryWinnerResponse, PostLotteryWinnerParams, PostLotteryWinnerResponse, -} from "@/types/lottery"; +} from "@/types/lotteryApi"; import { fetchWithTimeout } from "@/utils/fetchWithTimeout"; const baseURL = `${import.meta.env.VITE_API_URL}/admin/event/lottery`; diff --git a/admin/src/hooks/useInfiniteFetch.ts b/admin/src/hooks/useInfiniteFetch.ts index 0e401b03..1e2b023a 100644 --- a/admin/src/hooks/useInfiniteFetch.ts +++ b/admin/src/hooks/useInfiniteFetch.ts @@ -1,5 +1,4 @@ import { useCallback, useEffect, useState } from "react"; -import { flushSync } from "react-dom"; import { InfiniteListData } from "@/types/common"; interface UseInfiniteFetchProps { diff --git a/admin/src/pages/Login/index.tsx b/admin/src/pages/Login/index.tsx index 4803235d..99c95f9b 100644 --- a/admin/src/pages/Login/index.tsx +++ b/admin/src/pages/Login/index.tsx @@ -2,7 +2,6 @@ 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(); diff --git a/admin/src/pages/LotteryWinner/index.tsx b/admin/src/pages/LotteryWinner/index.tsx index edfcde0f..540fe7b4 100644 --- a/admin/src/pages/LotteryWinner/index.tsx +++ b/admin/src/pages/LotteryWinner/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, PostLotteryWinnerResponse } from "@/types/lottery"; +import { GetLotteryResponse, PostLotteryWinnerResponse } from "@/types/lotteryApi"; export default function LotteryWinner() { const lottery = useLoaderData() as GetLotteryResponse; diff --git a/admin/src/pages/LotteryWinnerList/index.tsx b/admin/src/pages/LotteryWinnerList/index.tsx index b89da42c..a87f6142 100644 --- a/admin/src/pages/LotteryWinnerList/index.tsx +++ b/admin/src/pages/LotteryWinnerList/index.tsx @@ -7,7 +7,8 @@ import Table from "@/components/Table"; import useInfiniteFetch from "@/hooks/useInfiniteFetch"; import useIntersectionObserver from "@/hooks/useIntersectionObserver"; import useModal from "@/hooks/useModal"; -import { GetLotteryWinnerResponse, LotteryExpectationsType } from "@/types/lottery"; +import { LotteryExpectationsType } from "@/types/lottery"; +import { GetLotteryWinnerResponse } from "@/types/lotteryApi"; const LOTTERY_WINNER_HEADER = [ "등수", diff --git a/admin/src/pages/RushWinnerList/index.tsx b/admin/src/pages/RushWinnerList/index.tsx index 491568fb..487128cd 100644 --- a/admin/src/pages/RushWinnerList/index.tsx +++ b/admin/src/pages/RushWinnerList/index.tsx @@ -1,5 +1,4 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { flushSync } from "react-dom"; import { useLocation, useNavigate } from "react-router-dom"; import { RushAPI } from "@/apis/rushAPI"; import Button from "@/components/Button"; diff --git a/admin/src/types/lottery.ts b/admin/src/types/lottery.ts index fa53e94b..bf28ce10 100644 --- a/admin/src/types/lottery.ts +++ b/admin/src/types/lottery.ts @@ -1,30 +1,9 @@ -import { InfiniteListData } from "./common"; - -export type GetLotteryResponse = { +export interface LotteryType { lotteryEventId: number; startDate: string; endDate: string; appliedCount: number; winnerCount: number; -}[]; - -export interface PostLotteryWinnerParams { - id: number; -} - -export interface PostLotteryWinnerResponse { - message: string; -} - -export interface GetLotteryWinnerParams { - id: number; - size: number; - page: number; -} - -export interface GetLotteryExpectationsParams { - lotteryId: number; - participantId: number; } export interface LotteryExpectationsType { @@ -32,8 +11,6 @@ export interface LotteryExpectationsType { expectation: string; } -export type GetLotteryExpectationsResponse = LotteryExpectationsType[]; - export interface LotteryWinnerType { id: number; phoneNumber: string; @@ -41,4 +18,3 @@ export interface LotteryWinnerType { expectation: number; appliedCount: number; } -export interface GetLotteryWinnerResponse extends InfiniteListData {} diff --git a/admin/src/types/lotteryApi.ts b/admin/src/types/lotteryApi.ts new file mode 100644 index 00000000..fd4b894a --- /dev/null +++ b/admin/src/types/lotteryApi.ts @@ -0,0 +1,27 @@ +import { InfiniteListData } from "./common"; +import { LotteryExpectationsType, LotteryType, LotteryWinnerType } from "./lottery"; + +export type GetLotteryResponse = LotteryType[]; + +export interface PostLotteryWinnerParams { + id: number; +} + +export interface PostLotteryWinnerResponse { + message: string; +} + +export interface GetLotteryWinnerParams { + id: number; + size: number; + page: number; +} + +export interface GetLotteryExpectationsParams { + lotteryId: number; + participantId: number; +} + +export type GetLotteryExpectationsResponse = LotteryExpectationsType[]; + +export type GetLotteryWinnerResponse = InfiniteListData; From 56530d151eece51a489366e5f930eb8f5615764b Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sun, 11 Aug 2024 15:23:21 +0900 Subject: [PATCH 20/26] =?UTF-8?q?refactor:=20rush=20=EC=9D=BC=EB=B0=98=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20-=20api=20=ED=83=80=EC=9E=85=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/apis/rushAPI.ts | 2 +- admin/src/pages/RushWinnerList/index.tsx | 3 ++- admin/src/types/lottery.ts | 2 +- admin/src/types/lotteryApi.ts | 4 ++-- admin/src/types/rush.ts | 24 ---------------------- admin/src/types/rushApi.ts | 26 ++++++++++++++++++++++++ 6 files changed, 32 insertions(+), 29 deletions(-) create mode 100644 admin/src/types/rushApi.ts diff --git a/admin/src/apis/rushAPI.ts b/admin/src/apis/rushAPI.ts index f752ab37..b66f5ba2 100644 --- a/admin/src/apis/rushAPI.ts +++ b/admin/src/apis/rushAPI.ts @@ -4,7 +4,7 @@ import { GetRushParticipantListParams, GetRushParticipantListResponse, GetRushWinnerListParams, -} from "@/types/rush"; +} from "@/types/rushApi"; import { fetchWithTimeout } from "@/utils/fetchWithTimeout"; const baseURL = `${import.meta.env.VITE_API_URL}/admin/event/rush`; diff --git a/admin/src/pages/RushWinnerList/index.tsx b/admin/src/pages/RushWinnerList/index.tsx index 487128cd..e74f5b58 100644 --- a/admin/src/pages/RushWinnerList/index.tsx +++ b/admin/src/pages/RushWinnerList/index.tsx @@ -7,7 +7,8 @@ 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"; +import { RushOptionType } from "@/types/rush"; +import { GetRushParticipantListResponse } from "@/types/rushApi"; export default function RushWinnerList() { const location = useLocation(); diff --git a/admin/src/types/lottery.ts b/admin/src/types/lottery.ts index bf28ce10..078e0197 100644 --- a/admin/src/types/lottery.ts +++ b/admin/src/types/lottery.ts @@ -1,4 +1,4 @@ -export interface LotteryType { +export interface LotteryEventType { lotteryEventId: number; startDate: string; endDate: string; diff --git a/admin/src/types/lotteryApi.ts b/admin/src/types/lotteryApi.ts index fd4b894a..d4b16ea2 100644 --- a/admin/src/types/lotteryApi.ts +++ b/admin/src/types/lotteryApi.ts @@ -1,7 +1,7 @@ import { InfiniteListData } from "./common"; -import { LotteryExpectationsType, LotteryType, LotteryWinnerType } from "./lottery"; +import { LotteryEventType, LotteryExpectationsType, LotteryWinnerType } from "./lottery"; -export type GetLotteryResponse = LotteryType[]; +export type GetLotteryResponse = LotteryEventType[]; export interface PostLotteryWinnerParams { id: number; diff --git a/admin/src/types/rush.ts b/admin/src/types/rush.ts index 83e974cd..e7f33810 100644 --- a/admin/src/types/rush.ts +++ b/admin/src/types/rush.ts @@ -1,5 +1,4 @@ import { Dispatch } from "react"; -import { InfiniteListData } from "./common"; export interface RushEventType { rushEventId: number; @@ -32,14 +31,6 @@ export interface RushApplicantType { created_at: string; } -export interface GetRushParticipantListParams { - id: number; - size: number; - page: number; - option: number; - phoneNumber?: string; -} - export interface RushParticipantType { id: number; phoneNumber: string; @@ -47,19 +38,6 @@ export interface RushParticipantType { createdAt: string; rank: number; } -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; -} export interface RushOptionType { rushOptionId: number; @@ -69,5 +47,3 @@ export interface RushOptionType { resultSubText: string; imageUrl: string; } - -export type GetRushOptionsResponse = RushOptionType[]; diff --git a/admin/src/types/rushApi.ts b/admin/src/types/rushApi.ts new file mode 100644 index 00000000..a9a32e25 --- /dev/null +++ b/admin/src/types/rushApi.ts @@ -0,0 +1,26 @@ +import { InfiniteListData } from "./common"; +import { RushOptionType, RushParticipantType } from "./rush"; + +export interface GetRushParticipantListParams { + id: number; + size: number; + page: number; + option: number; + phoneNumber?: string; +} + +export type GetRushOptionsResponse = RushOptionType[]; + +export type GetRushParticipantListResponse = InfiniteListData; + +export interface GetRushWinnerListParams { + id: number; + size: number; + page: number; + phoneNumber?: string; +} +export type GetRushWinnerListResponse = InfiniteListData; + +export interface GetRushOptionsParams { + id: number; +} From ac9b88fa14c2924a1af4d9b50b222a5539279de7 Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sun, 11 Aug 2024 19:08:59 +0900 Subject: [PATCH 21/26] =?UTF-8?q?fix:=20=EB=A8=B8=EC=A7=80=20=ED=9B=84=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 --- .../CasperCustom/CasperCustomFinish.tsx | 8 +++- .../CasperCustom/CasperCustomFinishing.tsx | 9 ++++- client/src/hooks/useBlockNavigation.ts | 37 +++++++++++++++++++ client/src/pages/CasperCustom/index.tsx | 14 ++++++- client/src/types/lotteryApi.ts | 3 +- 5 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 client/src/hooks/useBlockNavigation.ts diff --git a/client/src/features/CasperCustom/CasperCustomFinish.tsx b/client/src/features/CasperCustom/CasperCustomFinish.tsx index 54c4e1ed..4e0b8914 100644 --- a/client/src/features/CasperCustom/CasperCustomFinish.tsx +++ b/client/src/features/CasperCustom/CasperCustomFinish.tsx @@ -21,9 +21,13 @@ import ArrowIcon from "/public/assets/icons/arrow.svg?react"; interface CasperCustomFinishProps { handleResetStep: () => void; + unblockNavigation: () => void; } -export function CasperCustomFinish({ handleResetStep }: CasperCustomFinishProps) { +export function CasperCustomFinish({ + handleResetStep, + unblockNavigation, +}: CasperCustomFinishProps) { const [cookies] = useCookies([COOKIE_TOKEN_KEY]); const { @@ -41,6 +45,8 @@ export function CasperCustomFinish({ handleResetStep }: CasperCustomFinishProps) if (!cookies[COOKIE_TOKEN_KEY]) { return; } + + unblockNavigation(); getApplyCount(); }, [cookies]); diff --git a/client/src/features/CasperCustom/CasperCustomFinishing.tsx b/client/src/features/CasperCustom/CasperCustomFinishing.tsx index e99b4681..a5befcba 100644 --- a/client/src/features/CasperCustom/CasperCustomFinishing.tsx +++ b/client/src/features/CasperCustom/CasperCustomFinishing.tsx @@ -27,13 +27,18 @@ export function CasperCustomFinishing({ navigateNextStep }: CasperCustomFinishin useEffect(() => { showToast(); - setTimeout(() => { + const flipTimer = setTimeout(() => { setIsFlipped(true); }, 3000); - setTimeout(() => { + const navigateTimer = setTimeout(() => { navigateNextStep(); }, 6000); + + return () => { + clearTimeout(flipTimer); + clearTimeout(navigateTimer); + }; }, []); return ( diff --git a/client/src/hooks/useBlockNavigation.ts b/client/src/hooks/useBlockNavigation.ts new file mode 100644 index 00000000..e43d4c0c --- /dev/null +++ b/client/src/hooks/useBlockNavigation.ts @@ -0,0 +1,37 @@ +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 91655af9..6155e1d9 100644 --- a/client/src/pages/CasperCustom/index.tsx +++ b/client/src/pages/CasperCustom/index.tsx @@ -14,12 +14,17 @@ 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], }); @@ -28,7 +33,7 @@ export default function CasperCustom() { const selectedStep = CUSTOM_STEP_OPTION_ARRAY[selectedStepIdx]; const handleClickNextStep = () => { - setSelectedStepIdx(selectedStepIdx + 1); + setSelectedStepIdx((prevSelectedIdx) => prevSelectedIdx + 1); }; const handleResetStep = () => { @@ -43,7 +48,12 @@ 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/types/lotteryApi.ts b/client/src/types/lotteryApi.ts index 3b7d9c40..55b5253d 100644 --- a/client/src/types/lotteryApi.ts +++ b/client/src/types/lotteryApi.ts @@ -24,8 +24,7 @@ export interface GetApplyCountResponse { } export interface GetLotteryResponse { - lotteryEventId: number; eventStartDate: string; eventEndDate: string; - winnerCount: number; + activePeriod: number; } From 851049531e24a3955d1d8c9e4b95295a953c60a8 Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sun, 11 Aug 2024 19:22:11 +0900 Subject: [PATCH 22/26] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=BF=A0=ED=82=A4=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=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/package.json | 1 + admin/src/apis/authAPI.ts | 29 +++++++++++++++ admin/src/constants/cookie.ts | 3 ++ admin/src/pages/Login/index.tsx | 29 ++++++++++++--- admin/src/types/authApi.ts | 8 +++++ admin/yarn.lock | 64 +++++++++++++++++++++++---------- 6 files changed, 110 insertions(+), 24 deletions(-) create mode 100644 admin/src/apis/authAPI.ts create mode 100644 admin/src/constants/cookie.ts create mode 100644 admin/src/types/authApi.ts diff --git a/admin/package.json b/admin/package.json index 14483444..12509be1 100644 --- a/admin/package.json +++ b/admin/package.json @@ -12,6 +12,7 @@ "dependencies": { "class-variance-authority": "^0.7.0", "react": "^18.3.1", + "react-cookie": "^7.2.0", "react-dom": "^18.3.1", "react-router-dom": "^6.25.1" }, diff --git a/admin/src/apis/authAPI.ts b/admin/src/apis/authAPI.ts new file mode 100644 index 00000000..d50d8850 --- /dev/null +++ b/admin/src/apis/authAPI.ts @@ -0,0 +1,29 @@ +import { PostAuthParams, PostAuthResponse } from "@/types/authApi"; +import "@/types/lotteryApi"; +import { fetchWithTimeout } from "@/utils/fetchWithTimeout"; + +const baseURL = `${import.meta.env.VITE_API_URL}/admin/auth`; +const headers = { + "Content-Type": "application/json", +}; + +export const AuthAPI = { + async postAuth(body: PostAuthParams): Promise { + try { + return new Promise((resolve) => + resolve({ + accessToken: "access token", + }) + ); + const response = await fetchWithTimeout(`${baseURL}`, { + method: "POST", + headers: headers, + body: JSON.stringify(body), + }); + return response.json(); + } catch (error) { + console.error("Error:", error); + throw error; + } + }, +}; diff --git a/admin/src/constants/cookie.ts b/admin/src/constants/cookie.ts new file mode 100644 index 00000000..e7273426 --- /dev/null +++ b/admin/src/constants/cookie.ts @@ -0,0 +1,3 @@ +export const COOKIE_KEY = { + ACCESS_TOKEN: "token", +} as const; diff --git a/admin/src/pages/Login/index.tsx b/admin/src/pages/Login/index.tsx index 99c95f9b..f177ab9d 100644 --- a/admin/src/pages/Login/index.tsx +++ b/admin/src/pages/Login/index.tsx @@ -1,17 +1,39 @@ -import { ChangeEvent, FormEvent, useState } from "react"; +import { ChangeEvent, FormEvent, useEffect, useState } from "react"; +import { useCookies } from "react-cookie"; import { useNavigate } from "react-router-dom"; +import { AuthAPI } from "@/apis/authAPI"; import Button from "@/components/Button"; import Input from "@/components/Input"; +import { COOKIE_KEY } from "@/constants/cookie"; +import useFetch from "@/hooks/useFetch"; +import { PostAuthResponse } from "@/types/authApi"; export default function Login() { const navigate = useNavigate(); + const [_cookies, setCookie] = useCookies([COOKIE_KEY.ACCESS_TOKEN]); + const [id, setId] = useState(""); const [password, setPassword] = useState(""); const [errorMessage, setErrorMessage] = useState(""); + const { + data: token, + isSuccess: isSuccessPostAuth, + fetchData: postAuth, + } = useFetch(() => AuthAPI.postAuth({ adminId: id, password })); + const isValidButton = id.length !== 0 && password.length !== 0; + useEffect(() => { + if (isSuccessPostAuth && token) { + setCookie(COOKIE_KEY.ACCESS_TOKEN, token.accessToken); + + setErrorMessage(""); + navigate("/lottery"); + } + }, [isSuccessPostAuth, token]); + const handleChangeId = (e: ChangeEvent) => { setId(e.target.value); }; @@ -23,10 +45,7 @@ export default function Login() { const handleSubmit = (e: FormEvent) => { e.preventDefault(); - // TODO: 로그인 로직 - - setErrorMessage(""); - navigate("/lottery"); + postAuth(); }; return ( diff --git a/admin/src/types/authApi.ts b/admin/src/types/authApi.ts new file mode 100644 index 00000000..924756b0 --- /dev/null +++ b/admin/src/types/authApi.ts @@ -0,0 +1,8 @@ +export interface PostAuthParams { + adminId: string; + password: string; +} + +export interface PostAuthResponse { + accessToken: string; +} diff --git a/admin/yarn.lock b/admin/yarn.lock index 950b0081..3ab98a80 100644 --- a/admin/yarn.lock +++ b/admin/yarn.lock @@ -621,11 +621,24 @@ dependencies: "@babel/types" "^7.20.7" +"@types/cookie@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" + integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== + "@types/estree@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/hoist-non-react-statics@^3.3.5": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" + integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/prop-types@*": version "15.7.12" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" @@ -1067,6 +1080,11 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -1768,6 +1786,13 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: dependencies: function-bind "^1.1.2" +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + ignore@^5.2.0, ignore@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" @@ -2464,6 +2489,15 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +react-cookie@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/react-cookie/-/react-cookie-7.2.0.tgz#5770cd8d6b3c60c5d34ec4b7926f90281aee22ae" + integrity sha512-mqhPERUyfOljq5yJ4woDFI33bjEtigsl8JDJdPPeNhr0eSVZmBc/2Vdf8mFxOUktQxhxTR1T+uF0/FRTZyBEgw== + dependencies: + "@types/hoist-non-react-statics" "^3.3.5" + hoist-non-react-statics "^3.3.2" + universal-cookie "^7.0.0" + react-dom@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" @@ -2472,7 +2506,7 @@ react-dom@^18.3.1: loose-envify "^1.1.0" scheduler "^0.23.2" -react-is@^16.13.1: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -2708,16 +2742,7 @@ source-map@^0.5.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -2789,14 +2814,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -3000,6 +3018,14 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +universal-cookie@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-7.2.0.tgz#1f3fa9c575d863ac41b4e42272d240ae2d32c047" + integrity sha512-PvcyflJAYACJKr28HABxkGemML5vafHmiL4ICe3e+BEKXRMt0GaFLZhAwgv637kFFnnfiSJ8e6jknrKkMrU+PQ== + dependencies: + "@types/cookie" "^0.6.0" + cookie "^0.6.0" + update-browserslist-db@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" From ce169175b83fc2bd44af26059dc58d5a36ad72ad Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sun, 11 Aug 2024 19:41:15 +0900 Subject: [PATCH 23/26] =?UTF-8?q?refactor:=20useFetch=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/pages/RushWinnerList/index.tsx | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/admin/src/pages/RushWinnerList/index.tsx b/admin/src/pages/RushWinnerList/index.tsx index e74f5b58..e2a78bfe 100644 --- a/admin/src/pages/RushWinnerList/index.tsx +++ b/admin/src/pages/RushWinnerList/index.tsx @@ -5,10 +5,11 @@ import Button from "@/components/Button"; import Dropdown from "@/components/Dropdown"; import TabHeader from "@/components/TabHeader"; import Table from "@/components/Table"; +import useFetch from "@/hooks/useFetch"; import useInfiniteFetch from "@/hooks/useInfiniteFetch"; import useIntersectionObserver from "@/hooks/useIntersectionObserver"; import { RushOptionType } from "@/types/rush"; -import { GetRushParticipantListResponse } from "@/types/rushApi"; +import { GetRushOptionsResponse, GetRushParticipantListResponse } from "@/types/rushApi"; export default function RushWinnerList() { const location = useLocation(); @@ -56,6 +57,12 @@ export default function RushWinnerList() { }, }); + const { + data: rushOptions, + isSuccess: isSuccessGetRushOptions, + fetchData: getRushOptions, + } = useFetch(() => RushAPI.getRushOptions({ id: rushId })); + const currentData = isWinnerToggle ? winners : participants; const tableContainerRef = useRef(null); @@ -65,9 +72,15 @@ export default function RushWinnerList() { }); useEffect(() => { - handleGetOptions(); + getRushOptions(); }, []); + useEffect(() => { + if (isSuccessGetRushOptions && rushOptions) { + setOptions(rushOptions); + setSelectedOptionIdx(0); + } + }, [isSuccessGetRushOptions, rushOptions]); useEffect(() => { return () => handleTableScrollTop(); }, [isWinnerToggle]); @@ -82,12 +95,6 @@ export default function RushWinnerList() { } }; - const handleGetOptions = async () => { - const data = await RushAPI.getRushOptions({ id: rushId }); - setOptions(data); - setSelectedOptionIdx(0); - }; - const handleClickOption = (idx: number) => { handleTableScrollTop(); setSelectedOptionIdx(() => idx); From d2c9af45cfa43997a6e77a81618be60e5c0bcf3c Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sun, 11 Aug 2024 20:28:09 +0900 Subject: [PATCH 24/26] =?UTF-8?q?feat:=20protectedRoute=20/=20unProtectedR?= =?UTF-8?q?oute=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/components/Route/index.tsx | 27 ++++++++++++++ admin/src/router.tsx | 53 +++++++++++++++++----------- 2 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 admin/src/components/Route/index.tsx diff --git a/admin/src/components/Route/index.tsx b/admin/src/components/Route/index.tsx new file mode 100644 index 00000000..3fc05ffb --- /dev/null +++ b/admin/src/components/Route/index.tsx @@ -0,0 +1,27 @@ +import { useCookies } from "react-cookie"; +import { Navigate, Outlet } from "react-router-dom"; +import { COOKIE_KEY } from "@/constants/cookie"; + +interface RouteProps { + redirectPath?: string; +} + +export function ProtectedRoute({ redirectPath = "/login" }: RouteProps) { + const [cookies] = useCookies([COOKIE_KEY.ACCESS_TOKEN]); + + if (!cookies[COOKIE_KEY.ACCESS_TOKEN]) { + return ; + } + + return ; +} + +export function UnProtectedRoute({ redirectPath = "/lottery" }: RouteProps) { + const [cookies] = useCookies([COOKIE_KEY.ACCESS_TOKEN]); + + if (cookies[COOKIE_KEY.ACCESS_TOKEN]) { + return ; + } + + return ; +} diff --git a/admin/src/router.tsx b/admin/src/router.tsx index 72094bc7..8fdc2eb8 100644 --- a/admin/src/router.tsx +++ b/admin/src/router.tsx @@ -1,6 +1,7 @@ import { createBrowserRouter } from "react-router-dom"; import { LotteryAPI } from "./apis/lotteryAPI"; import Layout from "./components/Layout"; +import { ProtectedRoute, UnProtectedRoute } from "./components/Route"; import Login from "./pages/Login"; import Lottery from "./pages/Lottery"; import LotteryWinner from "./pages/LotteryWinner"; @@ -14,37 +15,47 @@ export const router = createBrowserRouter([ element: , children: [ { - path: "login/", - element: , - }, - { - path: "lottery/", + element: , children: [ { - index: true, - element: , - }, - { - path: "winner", - element: , - loader: LotteryAPI.getLottery, - }, - { - path: "winner-list", - element: , + path: "login/", + element: , }, ], }, { - path: "rush/", + element: , children: [ { - index: true, - element: , + path: "lottery/", + children: [ + { + index: true, + element: , + }, + { + path: "winner", + element: , + loader: LotteryAPI.getLottery, + }, + { + path: "winner-list", + element: , + }, + ], }, { - path: "winner-list", - element: , + path: "rush/", + children: [ + { + index: true, + element: , + }, + { + path: "winner-list", + element: , + }, + ], }, ], }, From f8be524bd5e980ae8296716499cddbd0fe9fb578 Mon Sep 17 00:00:00 2001 From: juhyojeong Date: Sun, 11 Aug 2024 22:11:02 +0900 Subject: [PATCH 25/26] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B2=80=EC=83=89=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/components/Input/index.tsx | 25 +++++++++-- admin/src/pages/RushWinnerList/index.tsx | 57 ++++++++++++++++++------ 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/admin/src/components/Input/index.tsx b/admin/src/components/Input/index.tsx index 91e18d73..20fdc455 100644 --- a/admin/src/components/Input/index.tsx +++ b/admin/src/components/Input/index.tsx @@ -1,15 +1,32 @@ -import { HTMLProps } from "react"; +import { ComponentProps } from "react"; +import { cva } from "class-variance-authority"; -interface InputProps extends HTMLProps { +interface InputProps extends ComponentProps<"input"> { label?: string; + inputSize?: "lg" | "sm"; } -export default function Input({ label, value, onChange, ...restProps }: InputProps) { +const InputVariants = cva(`border border-neutral-950 rounded-lg text-neutral-950 h-body-1-medium`, { + variants: { + inputSize: { + lg: "p-[16px] w-[360px]", + sm: "p-[8px] w-[240px]", + }, + }, +}); + +export default function Input({ + label, + value, + onChange, + inputSize = "lg", + ...restProps +}: InputProps) { return (
{label &&

{label}

} ([]); const [selectedOptionIdx, setSelectedOptionIdx] = useState(0); + const [phoneNumber, setPhoneNumber] = useState(""); + const { data: participants, isSuccess: isSuccessGetRushParticipantList, @@ -33,6 +36,7 @@ export default function RushWinnerList() { size: 10, page: pageParam, option: options[selectedOptionIdx].rushOptionId, + phoneNumber, }), initialPageParam: 1, getNextPageParam: (currentPageParam: number, lastPage: GetRushParticipantListResponse) => { @@ -44,12 +48,14 @@ export default function RushWinnerList() { data: winners, isSuccess: isSuccessGetRushWinnerList, fetchNextPage: getRushWinnerList, + refetch: refetchRushWinnerList, } = useInfiniteFetch({ fetch: (pageParam: number) => RushAPI.getRushWinnerList({ id: rushId, size: 10, page: pageParam, + phoneNumber, }), initialPageParam: 1, getNextPageParam: (currentPageParam: number, lastPage: GetRushParticipantListResponse) => { @@ -88,6 +94,14 @@ export default function RushWinnerList() { refetchRushParticipantList(); }, [selectedOptionIdx]); + const handleSearchPhoneNumber = () => { + if (isWinnerToggle) { + refetchRushWinnerList(); + } else { + refetchRushParticipantList(); + } + }; + const handleTableScrollTop = () => { if (tableContainerRef.current) { const table = tableContainerRef.current.querySelector(".table-contents"); @@ -142,20 +156,35 @@ export default function RushWinnerList() {
-
- 뒤로 가기 버튼 navigate(-1)} - /> -

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

- +
+
+ 뒤로 가기 버튼 navigate(-1)} + /> +

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

+ +
+ +
+ setPhoneNumber(e.target.value)} + /> + +
Date: Sun, 11 Aug 2024 22:24:06 +0900 Subject: [PATCH 26/26] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B2=80=EC=83=89=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/apis/lotteryAPI.ts | 5 ++- admin/src/components/Input/index.tsx | 21 +--------- admin/src/pages/LotteryWinnerList/index.tsx | 43 ++++++++++++++++----- admin/src/pages/RushWinnerList/index.tsx | 17 ++++---- admin/src/types/lotteryApi.ts | 1 + 5 files changed, 49 insertions(+), 38 deletions(-) diff --git a/admin/src/apis/lotteryAPI.ts b/admin/src/apis/lotteryAPI.ts index 87147788..3cef3109 100644 --- a/admin/src/apis/lotteryAPI.ts +++ b/admin/src/apis/lotteryAPI.ts @@ -55,6 +55,7 @@ export const LotteryAPI = { id, size, page, + phoneNumber, }: GetLotteryWinnerParams): Promise { try { return new Promise((resolve) => @@ -82,11 +83,11 @@ export const LotteryAPI = { appliedCount: 3, }, ], - isLastPage: true, + isLastPage: false, }) ); const response = await fetchWithTimeout( - `${baseURL}/${id}/winner?size=${size}&page=${page}`, + `${baseURL}/${id}/winner?size=${size}&page=${page}&number=${phoneNumber}`, { method: "GET", headers: headers, diff --git a/admin/src/components/Input/index.tsx b/admin/src/components/Input/index.tsx index 20fdc455..ffd5ec8d 100644 --- a/admin/src/components/Input/index.tsx +++ b/admin/src/components/Input/index.tsx @@ -1,32 +1,15 @@ import { ComponentProps } from "react"; -import { cva } from "class-variance-authority"; interface InputProps extends ComponentProps<"input"> { label?: string; - inputSize?: "lg" | "sm"; } -const InputVariants = cva(`border border-neutral-950 rounded-lg text-neutral-950 h-body-1-medium`, { - variants: { - inputSize: { - lg: "p-[16px] w-[360px]", - sm: "p-[8px] w-[240px]", - }, - }, -}); - -export default function Input({ - label, - value, - onChange, - inputSize = "lg", - ...restProps -}: InputProps) { +export default function Input({ label, value, onChange, ...restProps }: InputProps) { return (
{label &&

{label}

} ([]); + const phoneNumberRef = useRef(""); + const phoneNumberInputRef = useRef(null); const { data: winnerInfo, isSuccess: isSuccessGetLotteryWinner, fetchNextPage: getWinnerInfo, + refetch: refetchWinnerInfo, } = useInfiniteFetch({ fetch: (pageParam: number) => - LotteryAPI.getLotteryWinner({ id: lotteryId, size: 10, page: pageParam }), + LotteryAPI.getLotteryWinner({ + id: lotteryId, + size: 10, + page: pageParam, + phoneNumber: phoneNumberRef.current, + }), initialPageParam: 1, getNextPageParam: (currentPageParam: number, lastPage: GetLotteryWinnerResponse) => { return lastPage.isLastPage ? undefined : currentPageParam + 1; @@ -48,6 +56,11 @@ export default function LotteryWinnerList() { enabled: isSuccessGetLotteryWinner, }); + const handleRefetch = () => { + phoneNumberRef.current = phoneNumberInputRef.current?.value || ""; + refetchWinnerInfo(); + }; + const handleLottery = () => { navigate("/lottery/winner"); }; @@ -90,14 +103,26 @@ export default function LotteryWinnerList() {
-
- 뒤로 가기 버튼 navigate(-1)} - /> -

당첨자 추첨

+
+
+ 뒤로 가기 버튼 navigate(-1)} + /> +

당첨자 추첨

+
+ +
+ + +
([]); const [selectedOptionIdx, setSelectedOptionIdx] = useState(0); - const [phoneNumber, setPhoneNumber] = useState(""); + const phoneNumberRef = useRef(""); + const phoneNumberInputRef = useRef(null); const { data: participants, @@ -36,7 +36,7 @@ export default function RushWinnerList() { size: 10, page: pageParam, option: options[selectedOptionIdx].rushOptionId, - phoneNumber, + phoneNumber: phoneNumberRef.current, }), initialPageParam: 1, getNextPageParam: (currentPageParam: number, lastPage: GetRushParticipantListResponse) => { @@ -55,7 +55,7 @@ export default function RushWinnerList() { id: rushId, size: 10, page: pageParam, - phoneNumber, + phoneNumber: phoneNumberRef.current, }), initialPageParam: 1, getNextPageParam: (currentPageParam: number, lastPage: GetRushParticipantListResponse) => { @@ -95,6 +95,8 @@ export default function RushWinnerList() { }, [selectedOptionIdx]); const handleSearchPhoneNumber = () => { + phoneNumberRef.current = phoneNumberInputRef.current?.value || ""; + if (isWinnerToggle) { refetchRushWinnerList(); } else { @@ -176,10 +178,9 @@ export default function RushWinnerList() {
- setPhoneNumber(e.target.value)} +