| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143 |
- 'use client';
- import './style.scss';
- import { useEffect, useState, useCallback } from 'react';
- import { useRouter } from 'next/navigation';
- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
- import { faSun, faMoon, faGlobe, faBookmark } from '@fortawesome/free-solid-svg-icons';
- import { fetchApi } from '@/lib/utils/client';
- import { useSignalRContext } from '@/contexts/signalrProvider';
- import { ChannelListItem, ChannelStatusUpdate } from '@/types/channel';
- import { ChannelListResponse } from '@/types/response/channel/list';
- import useTheme from '@/hooks/useTheme';
- export default function ChannelSidebar() {
- const [channels, setChannels] = useState<ChannelListItem[]>([]);
- const [loading, setLoading] = useState(true);
- const [langOpen, setLangOpen] = useState(false);
- const router = useRouter();
- const { chatConnection } = useSignalRContext();
- const { toggleTheme, isDark } = useTheme();
- const sortChannels = useCallback((list: ChannelListItem[]) => {
- return [...list].sort((a, b) => {
- if (a.isLive && !b.isLive) return -1;
- if (!a.isLive && b.isLive) return 1;
- if (a.isLive && b.isLive) return b.viewerCount - a.viewerCount;
- return a.name.localeCompare(b.name);
- });
- }, []);
- const loadChannels = useCallback(async () => {
- try {
- const res = await fetchApi<ChannelListResponse>('/api/channel/list');
- if (res.data?.channels) {
- setChannels(sortChannels(res.data.channels));
- }
- } catch {}
- setLoading(false);
- }, [sortChannels]);
- useEffect(() => {
- loadChannels();
- }, [loadChannels]);
- // SignalR: 채널 상태 실시간 업데이트
- useEffect(() => {
- if (!chatConnection) return;
- const handler = (status: ChannelStatusUpdate) => {
- setChannels(prev => {
- const updated = prev.map(ch =>
- ch.channelSID === status.channelSID
- ? { ...ch, isLive: status.isLive, viewerCount: status.viewerCount, videoId: status.videoId }
- : ch
- );
- return sortChannels(updated);
- });
- };
- chatConnection.on('ReceiveChannelStatus', handler);
- return () => {
- chatConnection.off('ReceiveChannelStatus', handler);
- };
- }, [chatConnection, sortChannels]);
- const handleClick = (ch: ChannelListItem) => {
- if (ch.isLive && ch.videoId) {
- router.push(`/watch/${ch.channelSID}`);
- } else {
- router.push(`/channel/${ch.channelSID}`);
- }
- };
- const formatCount = (n: number) => {
- if (n >= 10000) return `${(n / 10000).toFixed(1)}만`;
- if (n >= 1000) return `${(n / 1000).toFixed(1)}K`;
- return n.toString();
- };
- const getThumbnailUrl = (ch: ChannelListItem) => {
- if (ch.thumbnailUrl) return ch.thumbnailUrl;
- return `https://yt3.googleusercontent.com/a/${ch.channelSID}=s48-c-k-c0x00ffffff-no-rj`;
- };
- if (loading) {
- return (
- <div className="channel-sidebar">
- <div className="channel-sidebar__header">채널</div>
- <div className="channel-sidebar__loading">로딩 중...</div>
- </div>
- );
- }
- return (
- <div className="channel-sidebar">
- <div className="channel-sidebar__header">채널</div>
- <div className="channel-sidebar__list">
- {channels.length === 0 && (
- <div className="channel-sidebar__empty">등록된 채널이 없습니다</div>
- )}
- {channels.map(ch => (
- <div key={ch.channelSID} className={`channel-sidebar__item${ch.isLive ? ' channel-sidebar__item--live' : ''}`} onClick={() => handleClick(ch)}>
- <div className="channel-sidebar__thumb">
- <img src={getThumbnailUrl(ch)} alt={ch.name} width={36} height={36} />
- </div>
- <div className="channel-sidebar__info">
- <div className="channel-sidebar__name">{ch.name}</div>
- <div className="channel-sidebar__sub">
- {ch.subscriberCount > 0 ? `구독자 ${formatCount(ch.subscriberCount)}` : ch.handle || ''}
- </div>
- </div>
- <div className="channel-sidebar__status">
- {ch.isLive && (
- <span className="channel-sidebar__viewers">👁 {formatCount(ch.viewerCount)}</span>
- )}
- <span className={`channel-sidebar__led${ch.isLive ? ' channel-sidebar__led--on' : ''}`} />
- </div>
- </div>
- ))}
- </div>
- {/* 하단: 다크모드 / 다국어 / 즐겨찾기 */}
- <div className="channel-sidebar__footer">
- {langOpen && (
- <div className="channel-sidebar__lang-dropdown">
- <button type="button" className="channel-sidebar__lang-option channel-sidebar__lang-option--active" onClick={() => setLangOpen(false)}>한국어</button>
- <button type="button" className="channel-sidebar__lang-option" onClick={() => setLangOpen(false)}>English</button>
- </div>
- )}
- <div className="channel-sidebar__footer-actions">
- <button type="button" className="channel-sidebar__icon-btn" onClick={toggleTheme} aria-label={isDark ? '라이트 모드' : '다크 모드'} title={isDark ? '라이트 모드' : '다크 모드'}>
- <FontAwesomeIcon icon={isDark ? faSun : faMoon} />
- </button>
- <button type="button" className="channel-sidebar__icon-btn" onClick={() => setLangOpen(prev => !prev)} aria-label="다국어" title="다국어">
- <FontAwesomeIcon icon={faGlobe} />
- </button>
- <button type="button" className="channel-sidebar__icon-btn" onClick={() => alert('Ctrl+D (Mac: ⌘+D)를 누르면 사이트를 즐겨찾기에 추가할 수 있습니다.')} aria-label="즐겨찾기" title="즐겨찾기">
- <FontAwesomeIcon icon={faBookmark} />
- </button>
- </div>
- </div>
- </div>
- );
- }
|