| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- 'use client';
- import { useCallback, useState, type ChangeEvent, type FormEvent } from 'react';
- import { Search } from 'lucide-react';
- import { fetchApi } from '@/lib/utils/client';
- import type { LiveChannelItem, LiveChannelListResponse } from '@/types/response/channel/live-list';
- import LiveChannelCard from '../_components/LiveChannelCard';
- import LiveChannelEmpty from '../_components/LiveChannelEmpty';
- type SortKey = 'viewers'|'newest'|'name';
- type Props = {
- initialChannels: LiveChannelItem[];
- initialTotal: number;
- pageSize: number;
- };
- const SORT_OPTIONS: { value: SortKey; label: string }[] = [
- { value: 'viewers', label: '시청자순' },
- { value: 'newest', label: '최신순' },
- { value: 'name', label: '채널명순' }
- ];
- export default function LiveExplorer({ initialChannels, initialTotal, pageSize }: Props)
- {
- const [channels, setChannels] = useState<LiveChannelItem[]>(initialChannels);
- const [total, setTotal] = useState(initialTotal);
- const [keyword, setKeyword] = useState('');
- const [appliedKeyword, setAppliedKeyword] = useState('');
- const [sort, setSort] = useState<SortKey>('viewers');
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string|null>(null);
- const hasMore = channels.length < total;
- const fetchList = useCallback(async (kw: string, sortKey: SortKey, offset: number) => {
- setLoading(true);
- setError(null);
- try {
- const params = new URLSearchParams({ limit: String(pageSize), offset: String(offset), sort: sortKey });
- if (kw) {
- params.set('keyword', kw);
- }
- const res = await fetchApi<LiveChannelListResponse>(`/api/channel/live-list?${params.toString()}`, { silent: true });
- if (res.success && res.data) {
- const data = res.data;
- setChannels(prev => offset === 0 ? data.channels : [...prev, ...data.channels]);
- setTotal(data.total);
- } else {
- setError('목록을 불러오지 못했습니다.');
- }
- } catch {
- setError('목록을 불러오지 못했습니다.');
- } finally {
- setLoading(false);
- }
- }, [pageSize]);
- const handleSearch = useCallback((e: FormEvent) => {
- e.preventDefault();
- setAppliedKeyword(keyword);
- fetchList(keyword, sort, 0);
- }, [keyword, sort, fetchList]);
- const handleSortChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
- const next = e.target.value as SortKey;
- setSort(next);
- fetchList(appliedKeyword, next, 0);
- }, [appliedKeyword, fetchList]);
- const handleLoadMore = useCallback(() => {
- if (loading || !hasMore) {
- return;
- }
- fetchList(appliedKeyword, sort, channels.length);
- }, [loading, hasMore, appliedKeyword, sort, channels.length, fetchList]);
- return (
- <section className="live-page" aria-labelledby="live-page-heading">
- <header className="live-page__head">
- <h1 id="live-page-heading" className="live-page__title">
- 생방송
- <span className="live-page__count">{total.toLocaleString()}</span>
- </h1>
- <form className="live-page__controls" onSubmit={handleSearch}>
- <select className="live-page__sort" value={sort} onChange={handleSortChange} aria-label="정렬">
- {SORT_OPTIONS.map(o => (
- <option key={o.value} value={o.value}>{o.label}</option>
- ))}
- </select>
- <input
- type="search"
- className="live-page__search"
- value={keyword}
- onChange={e => setKeyword(e.target.value)}
- placeholder="채널명 또는 핸들 검색"
- aria-label="검색"
- />
- <button type="submit" className="live-page__search-btn" aria-label="검색"><Search size={18} /></button>
- </form>
- </header>
- {channels.length === 0 ? (
- <LiveChannelEmpty />
- ) : (
- <div className="live-grid" role="list">
- {channels.map((ch, idx) => (
- <div key={ch.channelSID} role="listitem" className="live-grid__item">
- <LiveChannelCard channel={ch} priority={idx < 4} />
- </div>
- ))}
- </div>
- )}
- {hasMore && (
- <div className="home__load-more">
- <button type="button" onClick={handleLoadMore} disabled={loading} className="home__load-more-btn">
- {loading ? '불러오는 중...' : '더 보기'}
- </button>
- </div>
- )}
- {error && <div role="alert" className="home__error">{error}</div>}
- </section>
- );
- }
|