| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- 'use client';
- import { useState, useEffect, useCallback } from 'react';
- import { fetchApi } from '@/lib/utils/client';
- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
- import { faXmark, faCrown, faCircleUser } from '@fortawesome/free-solid-svg-icons';
- import type { LiveLeaderboardResponse, LeaderboardItem } from '@/types/response/channel/leaderboard';
- import './leaderboard-panel.scss';
- type Props = {
- channelSID: string;
- open: boolean;
- onClose: () => void;
- };
- export default function LeaderboardPanel({ channelSID, open, onClose }: Props) {
- const [data, setData] = useState<LiveLeaderboardResponse|null>(null);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string|null>(null);
- const load = useCallback(async () => {
- setLoading(true);
- setError(null);
- try {
- const res = await fetchApi<LiveLeaderboardResponse>(
- `/api/channel/${channelSID}/live-leaderboard`,
- { silent: true }
- );
- if (res.data) {
- setData(res.data);
- } else {
- setError('리더보드를 불러오지 못했습니다.');
- }
- } catch {
- setError('리더보드를 불러오지 못했습니다.');
- } finally {
- setLoading(false);
- }
- }, [channelSID]);
- useEffect(() => {
- if (!open) {
- return;
- }
- load();
- const interval = setInterval(load, 15000); // 15초마다 새로고침
- return () => clearInterval(interval);
- }, [open, load]);
- if (!open) {
- return null;
- }
- const renderItem = (item: LeaderboardItem) => {
- const isCrown = item.rank >= 1 && item.rank <= 3;
- const badgeUrl = item.titleIconUrl || item.gradeImageUrl || item.memberIcon;
- const rankClass = isCrown ? `leaderboard-panel__rank--top${item.rank}` : '';
- return (
- <li key={item.memberID} className='leaderboard-panel__item'>
- <div className={`leaderboard-panel__rank ${rankClass}`}>
- {isCrown ? <FontAwesomeIcon icon={faCrown} /> : item.rank}
- </div>
- <div className='leaderboard-panel__avatar'>
- {badgeUrl ? (
- <img src={badgeUrl} alt='' />
- ) : (
- <FontAwesomeIcon icon={faCircleUser} className='leaderboard-panel__avatar-fallback' />
- )}
- </div>
- <div className='leaderboard-panel__name'>
- <span className='leaderboard-panel__name-text'>{item.memberName}</span>
- <span className='leaderboard-panel__sid'>@{item.memberSID}</span>
- </div>
- <div className='leaderboard-panel__xp'>{item.xp.toLocaleString()}XP</div>
- </li>
- );
- };
- return (
- <div className='leaderboard-panel' role='dialog' aria-modal='true' aria-labelledby='leaderboard-title'>
- <div className='leaderboard-panel__overlay' onClick={onClose} />
- <div className='leaderboard-panel__box'>
- <div className='leaderboard-panel__header'>
- <button type='button' className='leaderboard-panel__back' onClick={onClose} aria-label='닫기'>
- <FontAwesomeIcon icon={faXmark} />
- </button>
- <h3 id='leaderboard-title' className='leaderboard-panel__title'>열성 팬</h3>
- <span className='leaderboard-panel__count'>{data?.total ?? 0}명</span>
- </div>
- <div className='leaderboard-panel__body'>
- {loading && data === null && <div className='leaderboard-panel__loading'>불러오는 중...</div>}
- {error && <div className='leaderboard-panel__error'>{error}</div>}
- {data && data.broadcastSessionID === null && (
- <div className='leaderboard-panel__empty'>
- 현재 진행 중인 방송 세션이 없습니다.
- </div>
- )}
- {data && data.broadcastSessionID !== null && data.list.length === 0 && (
- <div className='leaderboard-panel__empty'>
- 아직 리더보드 참여자가 없습니다.
- </div>
- )}
- {data && data.list.length > 0 && (
- <ul className='leaderboard-panel__list'>
- {data.list.map(renderItem)}
- </ul>
- )}
- </div>
- {data?.myRank && (
- <div className='leaderboard-panel__footer'>
- {data.myRank.rank === 0 ? (
- <p className='leaderboard-panel__my-empty'>
- 채팅이나 후원으로 참여해 보세요.
- </p>
- ) : (
- <p className='leaderboard-panel__my'>
- 내 순위: <strong>{data.myRank.rank}위</strong> / {data.myRank.xp.toLocaleString()}XP
- </p>
- )}
- </div>
- )}
- </div>
- </div>
- );
- }
|