Skip to content

Commit 1cfea0a

Browse files
authored
[25.05.14 / TASK-82,TASK-180] Feature - 리더보드 페이지 제작 (#35)
1 parent ee42ed6 commit 1cfea0a

File tree

18 files changed

+224
-170
lines changed

18 files changed

+224
-170
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@tailwindcss/typography": "^0.5.16",
2828
"@tanstack/react-query": "^5.69.0",
2929
"chart.js": "^4.4.7",
30+
"holy-loader": "^2.3.13",
3031
"next": "14.2.18",
3132
"qrcode.react": "^4.2.0",
3233
"react": "^18",

pnpm-lock.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/apis/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './leaderboard.request';
12
export * from './dashboard.request';
23
export * from './instance.request';
34
export * from './notice.request';

src/apis/leaderboard.request.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { LeaderboardListDto } from '@/types/leaderboard.type';
2+
import { instance } from './instance.request';
3+
import { PATHS } from '@/constants';
4+
5+
export const leaderboardList = async ({
6+
based,
7+
sort,
8+
dateRange,
9+
limit,
10+
}: {
11+
based: string;
12+
sort: string;
13+
dateRange: string;
14+
limit: string;
15+
}) =>
16+
await instance<null, LeaderboardListDto>(
17+
`${PATHS.LEADERBOARD}/${based}?sort=${sort}&dateRange=${dateRange}&limit=${limit}`,
18+
);

src/app/(auth-required)/components/header/Section.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { usePathname } from 'next/navigation';
22
import { Icon, NameType } from '@/components';
33
import { COLORS } from '@/constants';
4-
import { useCustomNavigation } from '@/hooks';
4+
import { useRouter } from 'next/navigation';
5+
import { startHolyLoader } from 'holy-loader';
56

67
export const defaultStyle =
78
'w-[180px] h-[65px] px-9 transition-all duration-300 shrink-0 max-MBI:w-[65px] max-MBI:px-0';
@@ -38,11 +39,17 @@ export const Section = <T extends clickType>({
3839
icon,
3940
}: PropType<T>) => {
4041
const currentPath = usePathname();
41-
const { push } = useCustomNavigation();
42+
const { push } = useRouter();
4243

4344
if (clickType === 'link') {
4445
return (
45-
<div onClick={() => push(action)} className={`${defaultStyle} ${navigateStyle}`}>
46+
<div
47+
onClick={() => {
48+
startHolyLoader();
49+
push(action);
50+
}}
51+
className={`${defaultStyle} ${navigateStyle}`}
52+
>
4653
<Icon
4754
size={25}
4855
color={

src/app/(auth-required)/components/header/index.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
44
import { usePathname, useRouter } from 'next/navigation';
55
import { useEffect, useRef, useState } from 'react';
66
import Image from 'next/image';
7+
import { startHolyLoader } from 'holy-loader';
78
import { revalidate } from '@/utils/revalidateUtil';
89
import { PATHS, SCREENS } from '@/constants';
910
import { Icon, NameType } from '@/components';
10-
import { useCustomNavigation, useResponsive } from '@/hooks';
11+
import { useResponsive } from '@/hooks';
1112
import { logout, me } from '@/apis';
1213
import { useModal } from '@/hooks/useModal';
1314
import { defaultStyle, Section, textStyle } from './Section';
@@ -16,7 +17,7 @@ import { QRCode } from '../QRCode';
1617

1718
const PARAMS = {
1819
MAIN: '?asc=false&sort=',
19-
LEADERBOARDS: '?type=views',
20+
LEADERBOARDS: '?based=user&sort=viewCount&limit=10&dateRange=30',
2021
};
2122

2223
const layouts: Array<{ icon: NameType; title: string; path: string }> = [
@@ -33,7 +34,7 @@ export const Header = () => {
3334
const { open: ModalOpen } = useModal();
3435
const menu = useRef<HTMLDivElement | null>(null);
3536
const path = usePathname();
36-
const { replace, start } = useCustomNavigation();
37+
const { replace } = useRouter();
3738
const { replace: replaceWithoutLoading } = useRouter();
3839
const width = useResponsive();
3940
const barWidth = width < SCREENS.MBI ? 65 : 180;
@@ -44,6 +45,7 @@ export const Header = () => {
4445
onSuccess: async () => {
4546
await revalidate();
4647
client.clear();
48+
startHolyLoader();
4749
replace('/');
4850
},
4951
});
@@ -108,7 +110,7 @@ export const Header = () => {
108110
<button
109111
className="text-DESTRUCTIVE-SUB text-I3 p-5 max-MBI:p-4 flex whitespace-nowrap w-full justify-center hover:bg-BG-ALT"
110112
onClick={() => {
111-
start();
113+
startHolyLoader();
112114
out();
113115
}}
114116
>

src/app/(auth-required)/leaderboards/Content.tsx

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,107 @@
11
'use client';
22

3+
import { useQuery } from '@tanstack/react-query';
4+
import { useMemo } from 'react';
5+
import { startHolyLoader } from 'holy-loader';
36
import { Dropdown } from '@/components';
4-
5-
import { SCREENS } from '@/constants';
7+
import { PATHS, SCREENS } from '@/constants';
68
import { useResponsive, useSearchParam } from '@/hooks';
9+
import { leaderboardList } from '@/apis/leaderboard.request';
10+
import { LeaderboardItemType } from '@/types/leaderboard.type';
711
import { Ranker, Rank } from './components';
812

9-
const data = [
10-
{ rank: 1, name: '정현우', count: 1235 },
11-
{ rank: 2, name: '최하온', count: 1234 },
12-
{ rank: 3, name: '이하준', count: 1233 },
13-
{ rank: 4, name: '육기준', count: 1232 },
14-
{ rank: 5, name: '칠기준', count: 1231 },
15-
{ rank: 6, name: '팔기준', count: 1230 },
16-
];
13+
export type searchParamsType = {
14+
based: 'user' | 'post';
15+
sort: 'viewCount' | 'likeCount';
16+
limit: string;
17+
dateRange: string;
18+
};
1719

1820
export const Content = () => {
1921
const width = useResponsive();
20-
const [, setSearchParams] = useSearchParam();
22+
const [searchParams, setSearchParams] = useSearchParam<searchParamsType>();
23+
24+
const { data: boards } = useQuery({
25+
queryKey: [PATHS.LEADERBOARD, searchParams],
26+
queryFn: async () => await leaderboardList(searchParams),
27+
});
28+
29+
const data = useMemo(() => {
30+
const value = (
31+
searchParams.based === 'user' ? boards?.users : boards?.posts
32+
) as LeaderboardItemType[];
33+
return (
34+
value.map((item) => ({
35+
name: searchParams.based === 'user' ? item.email.split('@')[0] : item.title,
36+
value: searchParams.sort === 'viewCount' ? item.viewDiff : item.likeDiff,
37+
})) || []
38+
);
39+
}, [boards, searchParams.based, searchParams.sort]);
40+
41+
const handleChange = (param: Partial<searchParamsType>) => {
42+
startHolyLoader();
43+
setSearchParams(param);
44+
};
2145

2246
return (
23-
<div className="flex w-full h-full flex-col gap-[30px] overflow-auto items-center max-TBL:gap-5">
24-
<Dropdown
25-
options={[
26-
['조회수', 'views'],
27-
['좋아요', 'likes'],
28-
]}
29-
onChange={(data) => setSearchParams({ type: data as string })}
30-
defaultValue={'조회수'}
31-
/>
32-
<div className="w-full flex gap-10 max-MBI:flex-col max-MBI:gap-5">
33-
<Ranker name="최하온" rank={2} count={1234} />
34-
<div className={`${width < SCREENS.MBI && 'order-first'} w-full`}>
35-
<Ranker name="정현우" rank={1} count={1235} />
47+
<div className="flex w-full h-full flex-col gap-[30px] overflow-hidden items-center max-TBL:gap-5">
48+
<div className="flex items-center gap-4 flex-wrap justify-center">
49+
<div className="flex items-center gap-4">
50+
<Dropdown
51+
options={[
52+
['사용자 기준', 'user'],
53+
['게시글 기준', 'post'],
54+
]}
55+
onChange={(data) => handleChange({ based: data as 'user' | 'post' })}
56+
defaultValue="사용자 기준"
57+
/>
58+
<Dropdown
59+
options={
60+
[
61+
['조회수 증가순', 'viewCount'],
62+
['좋아요 증가순', 'likeCount'],
63+
] as const
64+
}
65+
onChange={(data) => handleChange({ sort: data as 'viewCount' | 'likeCount' })}
66+
defaultValue="조회수 증가순"
67+
/>
68+
</div>
69+
<div className="flex items-center gap-4">
70+
<Dropdown
71+
options={[
72+
['10위까지', '10'],
73+
['30위까지', '30'],
74+
]}
75+
onChange={(data) => handleChange({ limit: data as string })}
76+
defaultValue={`${searchParams.limit}위까지`}
77+
/>
78+
<Dropdown
79+
options={[
80+
['지난 30일', '30'],
81+
['지난 7일', '7'],
82+
]}
83+
onChange={(data) => handleChange({ dateRange: data as string })}
84+
defaultValue="지난 30일"
85+
/>
3686
</div>
37-
<Ranker name="이호준" rank={3} count={1233} />
3887
</div>
39-
<div className="w-full flex flex-wrap gap-10 max-TBL:gap-5">
40-
{data?.map((i) => (i.rank > 3 ? <Rank key={i.rank} {...i} /> : null))}
88+
89+
<div className="h-full overflow-auto flex flex-col gap-[30px] max-TBL:gap-5">
90+
<div className="w-full flex gap-10 max-MBI:flex-col max-MBI:gap-5">
91+
<Ranker name={data?.[1].name || '유저 없음'} rank={2} count={data?.[1].value} />
92+
<Ranker
93+
name={data?.[0].name || '유저 없음'}
94+
rank={1}
95+
count={data?.[0].value}
96+
className={width < SCREENS.MBI ? 'order-first' : ''}
97+
/>
98+
<Ranker name={data?.[2].name || '유저 없음'} rank={3} count={data?.[2].value} />
99+
</div>
100+
<div className="w-full flex flex-wrap gap-10 max-TBL:gap-5">
101+
{data?.map(({ name, value }, index) =>
102+
index >= 3 ? <Rank name={name} key={index} count={value} rank={index + 1} /> : null,
103+
)}
104+
</div>
41105
</div>
42106
</div>
43107
);
Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,36 @@
1-
export interface IProp {
1+
import { HTMLAttributes } from 'react';
2+
import { twMerge } from 'tailwind-merge';
3+
4+
export interface IProp extends HTMLAttributes<HTMLDivElement> {
25
name: string;
3-
rank: string | number;
4-
count: number;
6+
rank?: number;
7+
count?: string | number;
58
suffix?: string;
69
}
710

8-
export const Ranker = ({ name, rank, count, suffix = '회' }: IProp) => {
11+
export const Ranker = ({ name, rank, count, suffix = '회', ...rest }: IProp) => {
912
return (
1013
<div
11-
className={`${rank === 1 ? 'w-full border-BORDER-SUB' : 'w-[70%] border-BORDER-MAIN'} max-MBI:border-[3px] flex justify-center items-center gap-[10px] h-[250px] p-[25px] bg-BG-SUB rounded-[4px] max-MBI:w-full max-MBI:h-fit max-MBI:flex-row max-MBI:justify-between`}
14+
{...rest}
15+
className={twMerge(
16+
`${rank === 1 ? 'w-full border-BORDER-SUB' : 'w-[70%] border-BORDER-MAIN'} max-MBI:border-[3px] flex justify-center items-center gap-[10px] h-[250px] p-[25px] bg-BG-SUB rounded-[4px] max-MBI:w-full max-MBI:h-fit max-MBI:flex-row max-MBI:justify-between`,
17+
rest.className,
18+
)}
1219
>
1320
<div className="flex gap-3 items-center MBI:flex-col">
1421
<span className="text-T4 text-TEXT-ALT max-TBL:text-T5">{rank}</span>
1522
<span
16-
className="flex items-center gap-3 text-T1 text-TEXT-MAIN after:text-PRIMARY-SUB after:content-['('_attr(data-count)_')'] after:text-ST3 max-TBL:text-T2 max-MBI:text-ST4 max-TBL:after:text-ST4 max-MBI:after:hidden"
23+
className={`flex items-center gap-3 text-T1 text-TEXT-MAIN after:text-PRIMARY-SUB ${count && "after:content-['('_attr(data-count)_')']"} after:text-ST3 max-TBL:text-T2 max-MBI:text-ST4 max-TBL:after:text-ST4 max-MBI:after:hidden`}
1724
data-count={count + suffix}
1825
>
19-
{name}
26+
{name.length >= 10 ? name.slice(0, 10) + '...' : name}
2027
</span>
2128
</div>
22-
<span className="text-TEXT-SUB text-ST4 MBI:hidden">{count}</span>
29+
{count !== undefined ? (
30+
<span className="text-TEXT-SUB text-ST4 MBI:hidden">{count}</span>
31+
) : (
32+
<></>
33+
)}
2334
</div>
2435
);
2536
};
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
import { Metadata } from 'next';
2-
import { ArriveSoon } from '@/components';
3-
// import { Content } from './Content';
2+
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
3+
import { getQueryClient } from '@/utils/queryUtil';
4+
import { PATHS } from '@/constants';
5+
import { leaderboardList } from '@/apis/leaderboard.request';
6+
import { Content, searchParamsType } from './Content';
47

58
export const metadata: Metadata = {
69
title: '리더보드',
710
};
811

9-
export default function Page() {
10-
return <ArriveSoon />;
12+
export default async function Page({ searchParams }: { searchParams: searchParamsType }) {
13+
const client = getQueryClient();
14+
15+
await client.prefetchQuery({
16+
queryKey: [PATHS.LEADERBOARD, searchParams],
17+
queryFn: async () => await leaderboardList(searchParams),
18+
});
19+
20+
return (
21+
<HydrationBoundary state={dehydrate(client)}>
22+
<Content />
23+
</HydrationBoundary>
24+
);
1125
}

0 commit comments

Comments
 (0)