LiveChannelGrid.tsx 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. 'use client';
  2. import { useCallback, useState } from 'react';
  3. import { fetchApi } from '@/lib/utils/client';
  4. import type { LiveChannelItem, LiveChannelListResponse } from '@/types/response/channel/live-list';
  5. import LiveChannelCard from './LiveChannelCard';
  6. import LiveChannelEmpty from './LiveChannelEmpty';
  7. type Props = {
  8. initialChannels: LiveChannelItem[];
  9. initialTotal: number;
  10. pageSize: number;
  11. };
  12. export default function LiveChannelGrid({ initialChannels, initialTotal, pageSize }: Props)
  13. {
  14. const [channels, setChannels] = useState<LiveChannelItem[]>(initialChannels);
  15. const [total, setTotal] = useState(initialTotal);
  16. const [loading, setLoading] = useState(false);
  17. const [error, setError] = useState<string|null>(null);
  18. const hasMore = channels.length < total;
  19. const handleLoadMore = useCallback(async () => {
  20. if (loading || !hasMore) {
  21. return;
  22. }
  23. setLoading(true);
  24. setError(null);
  25. try {
  26. const res = await fetchApi<LiveChannelListResponse>(`/api/channel/live-list?limit=${pageSize}&offset=${channels.length}`, { silent: true });
  27. if (res.success && res.data) {
  28. setChannels(prev => [...prev, ...res.data!.channels]);
  29. setTotal(res.data.total);
  30. } else {
  31. setError('목록을 불러오지 못했습니다.');
  32. }
  33. } catch {
  34. setError('목록을 불러오지 못했습니다.');
  35. } finally {
  36. setLoading(false);
  37. }
  38. }, [channels.length, hasMore, loading, pageSize]);
  39. if (channels.length === 0) {
  40. return <LiveChannelEmpty />;
  41. }
  42. return (
  43. <section className="home__live" aria-labelledby="home-live-heading">
  44. <header className="home__section-head">
  45. <h2 id="home-live-heading" className="home__section-title">
  46. 라이브 방송 중
  47. <span className="home__section-count" aria-label={`총 ${total}개 채널`}>
  48. {total.toLocaleString()}
  49. </span>
  50. </h2>
  51. </header>
  52. <div className="live-grid" role="list">
  53. {channels.map((ch, idx) => (
  54. <div key={ch.channelSID} role="listitem" className="live-grid__item">
  55. <LiveChannelCard channel={ch} priority={idx < 4} />
  56. </div>
  57. ))}
  58. </div>
  59. {hasMore && (
  60. <div className="home__load-more">
  61. <button
  62. type="button"
  63. onClick={handleLoadMore}
  64. disabled={loading}
  65. className="home__load-more-btn"
  66. aria-label={loading ? '불러오는 중' : '더 보기'}
  67. >
  68. {loading ? '불러오는 중...' : '더 보기'}
  69. </button>
  70. </div>
  71. )}
  72. {error && (
  73. <div role="alert" className="home__error">{error}</div>
  74. )}
  75. </section>
  76. );
  77. }