| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- import '../style.scss';
- import { Metadata } from 'next';
- import Link from 'next/link';
- import PaperNav from '../_components/PaperNav';
- import { fetchPaperLeaderboard } from '@/lib/api/paper';
- import { changeDirection, changeSymbol } from '@/lib/utils/stock';
- import { formatBpPercent, formatBpAbs, bpDirection } from '@/lib/utils/paper';
- import type { PaperLeaderboardSort, PaperLeaderboardPeriod } from '@/types/paper';
- export const metadata: Metadata = {
- title: '모의투자 리더보드 — 개미투자',
- description: '수익률·승률·MDD 기준 모의투자 순위 (주간/월간/전체)'
- };
- const SORT_TABS: { value: PaperLeaderboardSort; label: string }[] = [
- { value: 'return', label: '수익률' },
- { value: 'winrate', label: '승률' },
- { value: 'mdd', label: 'MDD' }
- ];
- const PERIOD_TABS: { value: PaperLeaderboardPeriod; label: string }[] = [
- { value: 'week', label: '주간' },
- { value: 'month', label: '월간' },
- { value: 'all', label: '전체' }
- ];
- const PAGE_SIZE = 50;
- type Props = {
- searchParams: Promise<{
- sort?: string;
- period?: string;
- page?: string;
- }>;
- };
- export default async function PaperLeaderboardPage({ searchParams }: Props)
- {
- const query = await searchParams;
- const sort: PaperLeaderboardSort = SORT_TABS.some(c => c.value === query.sort) ? (query.sort as PaperLeaderboardSort) : 'return';
- const period: PaperLeaderboardPeriod = PERIOD_TABS.some(c => c.value === query.period) ? (query.period as PaperLeaderboardPeriod) : 'all';
- const page = Math.max(Number(query.page) || 1, 1);
- const res = await fetchPaperLeaderboard({ sort, period, page });
- const rows = res.success ? (res.data?.rows ?? []) : [];
- const buildHref = (next: { sort?: PaperLeaderboardSort; period?: PaperLeaderboardPeriod }) => {
- const params = new URLSearchParams();
- params.set('sort', next.sort ?? sort);
- params.set('period', next.period ?? period);
- return `/paper/leaderboard?${params.toString()}`;
- };
- return (
- <div className='paper'>
- <div className='paper__head'>
- <h1 className='paper__title'>리더보드</h1>
- <span className='paper__subtitle'>순위는 명예·프로필 배지만 부여됩니다 (별도 상금 없음)</span>
- </div>
- <PaperNav />
- <div className='paper__tab-group'>
- <span className='paper__tab-group-label'>정렬</span>
- {SORT_TABS.map(tab => (
- <Link
- key={tab.value}
- href={buildHref({ sort: tab.value })}
- className={`paper__tab${sort === tab.value ? ' paper__tab--active' : ''}`}
- {...(sort === tab.value ? { 'aria-current': 'page' as const } : {})}
- >
- {tab.label}
- </Link>
- ))}
- </div>
- <div className='paper__tab-group'>
- <span className='paper__tab-group-label'>기간</span>
- {PERIOD_TABS.map(tab => (
- <Link
- key={tab.value}
- href={buildHref({ period: tab.value })}
- className={`paper__tab${period === tab.value ? ' paper__tab--active' : ''}`}
- {...(period === tab.value ? { 'aria-current': 'page' as const } : {})}
- >
- {tab.label}
- </Link>
- ))}
- </div>
- {!res.success ? (
- <div className='paper__error' role='alert'>리더보드를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.</div>
- ) : rows.length === 0 ? (
- <div className='paper__empty'>아직 등재된 참여자가 없습니다.</div>
- ) : (
- <div className='paper__table-wrap'>
- <table className='paper__table'>
- <caption className='sr-only'>모의투자 리더보드 — 순위·닉네임·수익률·승률·MDD·체결수</caption>
- <thead>
- <tr>
- <th className='paper__th--center'>순위</th>
- <th className='paper__th--left'>닉네임</th>
- <th>수익률</th>
- <th>승률</th>
- <th>MDD</th>
- <th>체결수</th>
- </tr>
- </thead>
- <tbody>
- {rows.map(row => {
- const dir = bpDirection(row.returnBp);
- const symbol = changeSymbol(changeDirection(row.returnBp));
- return (
- <tr key={row.memberSID}>
- <td className='paper__td--center paper__rank'>{row.rank}</td>
- <td className='paper__td--left'>
- <span className='paper__nick'>
- {row.thumb ? (
- // eslint-disable-next-line @next/next/no-img-element
- <img src={row.thumb} alt='' className='paper__nick-thumb' />
- ) : null}
- <Link href={`/user/${row.memberSID}/paper`} className='paper__nick-name'>
- {row.nickname || '익명'}
- </Link>
- </span>
- </td>
- <td className={`paper__value--${dir}`}>{symbol} {formatBpPercent(row.returnBp)}</td>
- <td>{formatBpAbs(row.winRateBp)}</td>
- <td className='paper__value--down'>{formatBpAbs(row.mddBp)}</td>
- <td>{row.fillCount.toLocaleString()}</td>
- </tr>
- );
- })}
- </tbody>
- </table>
- </div>
- )}
- {res.success && rows.length > 0 && (
- <nav className='paper__actions' aria-label='페이지'>
- {page > 1 && (
- <Link href={`/paper/leaderboard?sort=${sort}&period=${period}&page=${page - 1}`} className='paper__btn paper__btn--sm'>이전</Link>
- )}
- {rows.length >= PAGE_SIZE && (
- <Link href={`/paper/leaderboard?sort=${sort}&period=${period}&page=${page + 1}`} className='paper__btn paper__btn--sm'>다음</Link>
- )}
- </nav>
- )}
- </div>
- );
- }
|