page.tsx 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import '@/app/styles/community.scss';
  2. import '@/app/styles/data-table.scss';
  3. import { Metadata } from 'next';
  4. import Link from 'next/link';
  5. import { fetchTrackRecordLeaderboard } from '@/lib/api/community';
  6. import TierBadge from '@/components/community/TierBadge';
  7. export const metadata: Metadata = {
  8. title: '예측 적중률 리더보드 — 개미투자',
  9. description: '종목 예측글 채점 기반 적중률 순위 (표본 10건 이상 · 전체 기간)'
  10. };
  11. const PAGE_SIZE = 50;
  12. type Props = {
  13. searchParams: Promise<{
  14. page?: string;
  15. }>;
  16. };
  17. export default async function TrackRecordLeaderboardPage({ searchParams }: Props)
  18. {
  19. const query = await searchParams;
  20. const page = Math.max(Number(query.page) || 1, 1);
  21. const res = await fetchTrackRecordLeaderboard({ page });
  22. const data = res.success ? res.data : null;
  23. const rows = data?.list ?? [];
  24. const totalPages = data ? Math.max(Math.ceil(data.total / PAGE_SIZE), 1) : 1;
  25. return (
  26. <div className='track-leaderboard'>
  27. <div className='track-leaderboard__head'>
  28. <h1 className='track-leaderboard__title'>예측 적중률 리더보드</h1>
  29. <span className='track-leaderboard__subtitle'>
  30. 종목 예측글이 채점된 회원 순위입니다 (표본 10건 이상 · 전체 기간). 순위는 명예·프로필 배지만 부여됩니다.
  31. </span>
  32. </div>
  33. {!res.success ? (
  34. <div className='track-leaderboard__error' role='alert'>리더보드를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.</div>
  35. ) : rows.length === 0 ? (
  36. <div className='track-leaderboard__empty'>아직 채점된 예측이 있는 회원이 없습니다.</div>
  37. ) : (
  38. <div className='track-leaderboard__table-wrap'>
  39. <table className='data-table'>
  40. <caption className='data-table__caption data-table__caption--sr'>
  41. 예측 적중률 리더보드 — 순위·닉네임·적중률·최고 스트릭·표본수·티어
  42. </caption>
  43. <thead>
  44. <tr>
  45. <th scope='col' data-align='center' data-priority='high'>순위</th>
  46. <th scope='col' data-align='left' data-priority='high'>닉네임</th>
  47. <th scope='col' data-align='right' data-priority='high'>적중률</th>
  48. <th scope='col' data-align='right' data-priority='medium'>최고 스트릭</th>
  49. <th scope='col' data-align='right' data-priority='medium'>표본수</th>
  50. <th scope='col' data-align='center' data-priority='low'>티어</th>
  51. </tr>
  52. </thead>
  53. <tbody>
  54. {rows.map(row => (
  55. <tr key={row.sid}>
  56. <td data-align='center'><span className='track-leaderboard__rank'>{row.rank}</span></td>
  57. <td data-align='left'>
  58. <Link href={`/user/${row.sid}`} className='track-leaderboard__name'>{row.name}</Link>
  59. </td>
  60. <td data-align='right'><span className='track-leaderboard__hitrate'>{row.hitRate.toFixed(1)}%</span></td>
  61. <td data-align='right'>{row.bestStreak.toLocaleString()}</td>
  62. <td data-align='right'>{row.predictions.toLocaleString()}</td>
  63. <td data-align='center'><TierBadge tierCode={row.tierCode} /></td>
  64. </tr>
  65. ))}
  66. </tbody>
  67. </table>
  68. </div>
  69. )}
  70. {res.success && rows.length > 0 && data && data.total > PAGE_SIZE && (
  71. <nav className='track-leaderboard__actions' aria-label='페이지'>
  72. {page > 1 && (
  73. <Link href={`/ranking/track-record?page=${page - 1}`} className='track-leaderboard__btn'>이전</Link>
  74. )}
  75. <span className='track-leaderboard__rank'>{page} / {totalPages}</span>
  76. {page < totalPages && (
  77. <Link href={`/ranking/track-record?page=${page + 1}`} className='track-leaderboard__btn'>다음</Link>
  78. )}
  79. </nav>
  80. )}
  81. </div>
  82. );
  83. }