Skip to content

Commit

Permalink
Merge pull request #135 from softeerbootcamp4th/feat/#134-admin-ui
Browse files Browse the repository at this point in the history
[Feat] 어드민 UI 구현
  • Loading branch information
sooyeoniya authored Aug 12, 2024
2 parents 61f8acb + 01c2feb commit f9ae720
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 13 deletions.
56 changes: 56 additions & 0 deletions admin/src/apis/lotteryAPI.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
GetLotteryExpectationsParams,
GetLotteryExpectationsResponse,
GetLotteryParticipantResponse,
GetLotteryResponse,
GetLotteryWinnerParams,
GetLotteryWinnerResponse,
Expand Down Expand Up @@ -52,6 +53,61 @@ export const LotteryAPI = {
throw error;
}
},
async getLotteryParticipant({
id,
size,
page,
phoneNumber,
}: GetLotteryWinnerParams): Promise<GetLotteryParticipantResponse> {
try {
return new Promise((resolve) =>
resolve({
data: [
{
id: 1,
phoneNumber: "010-1111-2222",
linkClickedCounts: 1,
expectation: 1,
appliedCount: 3,
createdAt: "2024-08-12T02:10:37.279369",
updatedAt: "2024-08-12T02:13:48.390954",
},
{
id: 2,
phoneNumber: "010-1111-2223",
linkClickedCounts: 1,
expectation: 1,
appliedCount: 3,
createdAt: "2024-08-12T02:10:37.279369",
updatedAt: "2024-08-12T02:13:48.390954",
},
{
id: 3,
phoneNumber: "010-1111-2224",
linkClickedCounts: 1,
expectation: 1,
appliedCount: 3,
createdAt: "2024-08-12T02:10:37.279369",
updatedAt: "2024-08-12T02:13:48.390954",
},
],
isLastPage: true,
totalParticipants: 10000,
})
);
const response = await fetchWithTimeout(
`${baseURL}/${id}/participants?size=${size}&page=${page}&number=${phoneNumber}`,
{
method: "GET",
headers: headers,
}
);
return response.json();
} catch (error) {
console.error("Error:", error);
throw error;
}
},
async getLotteryWinner({
id,
size,
Expand Down
19 changes: 19 additions & 0 deletions admin/src/constants/lottery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,22 @@ export const LOTTERY_HEADER = [
"추첨 당첨 인원 수",
"진행 상태",
];

export const LOTTERY_WINNER_HEADER = [
"등수",
"ID",
"전화번호",
"공유 링크 클릭 횟수",
"기대평 작성 여부",
"총 응모 횟수",
];
export const LOTTERY_EXPECTATIONS_HEADER = ["캐스퍼 ID", "기대평"];
export const LOTTERY_PARTICIPANT_HEADER = [
"ID",
"생성 날짜",
"생성 시간",
"전화 번호",
"공유 링크 클릭 횟수",
"기대평 작성 여부",
"총 응모 횟수",
];
9 changes: 8 additions & 1 deletion admin/src/pages/Lottery/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,14 @@ export default function Lottery() {
<Table headers={LOTTERY_HEADER} data={getLotteryData()} height="auto" />

<div className="self-end flex gap-4">
<Button buttonSize="sm" onClick={() => navigate("/lottery/participant-list")}>
<Button
buttonSize="sm"
onClick={() =>
navigate("/lottery/participant-list", {
state: { id: lottery.lotteryEventId },
})
}
>
참여자 리스트 보러가기
</Button>
<Button
Expand Down
143 changes: 143 additions & 0 deletions admin/src/pages/LotteryParticipantList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { useMemo, useRef, useState } 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 { LOTTERY_EXPECTATIONS_HEADER, LOTTERY_PARTICIPANT_HEADER } from "@/constants/lottery";
import useInfiniteFetch from "@/hooks/useInfiniteFetch";
import useIntersectionObserver from "@/hooks/useIntersectionObserver";
import useModal from "@/hooks/useModal";
import { LotteryExpectationsType } from "@/types/lottery";
import { GetLotteryParticipantResponse } from "@/types/lotteryApi";

export default function LotteryParticipantList() {
const location = useLocation();
const navigate = useNavigate();

const lotteryId = location.state.id;

const { handleOpenModal, ModalComponent } = useModal();
const [selectedParticipant, setSelectedParticipant] = useState<LotteryExpectationsType[]>([]);
const phoneNumberRef = useRef<string>("");
const phoneNumberInputRef = useRef<HTMLInputElement>(null);

const {
data: participantInfo,
totalLength: participantLength,
isSuccess: isSuccessGetParticipant,
fetchNextPage: getParticipantInfo,
refetch: refetchParticipantInfo,
} = useInfiniteFetch({
fetch: (pageParam: number) =>
LotteryAPI.getLotteryParticipant({
id: lotteryId,
size: 10,
page: pageParam,
phoneNumber: phoneNumberRef.current,
}),
initialPageParam: 1,
getNextPageParam: (currentPageParam: number, lastPage: GetLotteryParticipantResponse) => {
return lastPage.isLastPage ? undefined : currentPageParam + 1;
},
});

const tableContainerRef = useRef<HTMLDivElement>(null);
const { targetRef } = useIntersectionObserver<HTMLTableRowElement>({
onIntersect: getParticipantInfo,
enabled: isSuccessGetParticipant,
});

const handleRefetch = () => {
phoneNumberRef.current = phoneNumberInputRef.current?.value || "";
refetchParticipantInfo();
};

const handleLotteryWinner = () => {
navigate("/lottery/winner-list", { state: { id: lotteryId } });
};

const handleClickExpectation = async (participantId: number) => {
handleOpenModal();

const data = await LotteryAPI.getLotteryExpectations({
lotteryId,
participantId: participantId,
});
setSelectedParticipant(data);
};

const expectations = selectedParticipant.map((participant) => [
participant.casperId,
participant.expectation,
]);

const participantList = useMemo(
() =>
participantInfo.map((participant) => [
participant.id,
participant.createdAt,
participant.createdAt,
participant.phoneNumber,
participant.linkClickedCounts,
<div className="flex justify-between">
<span>{participant.expectation}</span>
<span
className="cursor-pointer"
onClick={() => handleClickExpectation(participant.id)}
>
기대평 보기
</span>
</div>,
participant.appliedCount,
]),
[participantInfo]
);

return (
<div className="flex flex-col items-center h-screen">
<TabHeader />

<div className="w-[1560px] flex flex-col items-center justify-center gap-8 mt-10">
<div className="flex w-full justify-between">
<div className="flex items-center gap-2">
<img
alt="뒤로 가기 버튼"
src="/assets/icons/left-arrow.svg"
className="cursor-pointer"
onClick={() => navigate(-1)}
/>
<p className="h-body-1-medium">
전체 참여자 리스트 {participantLength.toLocaleString("en-US")}
</p>
</div>

<div className="flex gap-2">
<input
ref={phoneNumberInputRef}
className="border border-neutral-950 rounded-lg text-neutral-950 h-body-1-medium"
/>
<Button buttonSize="sm" onClick={handleRefetch}>
검색
</Button>
</div>
</div>

<Table
ref={tableContainerRef}
headers={LOTTERY_PARTICIPANT_HEADER}
data={participantList}
dataLastItem={targetRef}
/>

<Button buttonSize="lg" onClick={handleLotteryWinner}>
당첨자 보러가기
</Button>
</div>

<ModalComponent>
<Table headers={LOTTERY_EXPECTATIONS_HEADER} data={expectations} height="auto" />
</ModalComponent>
</div>
);
}
11 changes: 1 addition & 10 deletions admin/src/pages/LotteryWinnerList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,13 @@ import { LotteryAPI } from "@/apis/lotteryAPI";
import Button from "@/components/Button";
import TabHeader from "@/components/TabHeader";
import Table from "@/components/Table";
import { LOTTERY_EXPECTATIONS_HEADER, LOTTERY_WINNER_HEADER } from "@/constants/lottery";
import useInfiniteFetch from "@/hooks/useInfiniteFetch";
import useIntersectionObserver from "@/hooks/useIntersectionObserver";
import useModal from "@/hooks/useModal";
import { LotteryExpectationsType } from "@/types/lottery";
import { GetLotteryWinnerResponse } from "@/types/lotteryApi";

const LOTTERY_WINNER_HEADER = [
"등수",
"ID",
"전화번호",
"공유 링크 클릭 횟수",
"기대평 작성 여부",
"총 응모 횟수",
];
const LOTTERY_EXPECTATIONS_HEADER = ["캐스퍼 ID", "기대평"];

export default function LotteryWinnerList() {
const location = useLocation();
const navigate = useNavigate();
Expand Down
5 changes: 4 additions & 1 deletion admin/src/pages/RushWinnerList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,10 @@ export default function RushWinnerList() {
/>
<p className="h-body-1-medium">
선착순 참여자 리스트{" "}
{isWinnerToggle ? winnersLength : participantsLength}
{(isWinnerToggle ? winnersLength : participantsLength).toLocaleString(
"en-US"
)}{" "}
</p>
<Button
buttonSize="sm"
Expand Down
5 changes: 5 additions & 0 deletions admin/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ProtectedRoute, UnProtectedRoute } from "./components/Route";
import RushLayout from "./features/Rush/Layout";
import Login from "./pages/Login";
import Lottery from "./pages/Lottery";
import LotteryParticipantList from "./pages/LotteryParticipantList";
import LotteryWinner from "./pages/LotteryWinner";
import LotteryWinnerList from "./pages/LotteryWinnerList";
import Rush from "./pages/Rush";
Expand Down Expand Up @@ -63,6 +64,10 @@ export const router = createBrowserRouter([
element: <LotteryWinner />,
loader: LotteryAPI.getLottery,
},
{
path: "participant-list",
element: <LotteryParticipantList />,
},
{
path: "winner-list",
element: <LotteryWinnerList />,
Expand Down
5 changes: 5 additions & 0 deletions admin/src/types/lottery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ export interface LotteryWinnerType {
expectation: number;
appliedCount: number;
}

export interface LotteryParticipantType extends LotteryWinnerType {
createdAt: string;
updatedAt: string;
}
9 changes: 8 additions & 1 deletion admin/src/types/lotteryApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { InfiniteListData } from "./common";
import { LotteryEventType, LotteryExpectationsType, LotteryWinnerType } from "./lottery";
import {
LotteryEventType,
LotteryExpectationsType,
LotteryParticipantType,
LotteryWinnerType,
} from "./lottery";

export type GetLotteryResponse = LotteryEventType[];

Expand All @@ -22,3 +27,5 @@ export interface GetLotteryExpectationsParams {
export type GetLotteryExpectationsResponse = LotteryExpectationsType[];

export type GetLotteryWinnerResponse = InfiniteListData<LotteryWinnerType>;

export type GetLotteryParticipantResponse = InfiniteListData<LotteryParticipantType>;

0 comments on commit f9ae720

Please sign in to comment.