| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- 'use client';
- import { useEffect, useMemo, useState, type ChangeEvent } from 'react';
- import { Search } from 'lucide-react';
- import type { CreatorItem } from '@/types/response/channel/creators';
- import CreatorCard from './CreatorCard';
- type SortKey = 'random'|'subscribers'|'followers'|'name';
- const SORT_OPTIONS: { value: SortKey; label: string }[] = [
- { value: 'random', label: '무작위' },
- { value: 'subscribers', label: '구독자순' },
- { value: 'followers', label: '팔로워순' },
- { value: 'name', label: '이름순' }
- ];
- const PAGE_SIZE = 50;
- function shuffle<T>(arr: T[]): T[]
- {
- const a = [...arr];
- for (let i = a.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [a[i], a[j]] = [a[j], a[i]];
- }
- return a;
- }
- export default function CreatorExplorer({ creators }: { creators: CreatorItem[] })
- {
- const [keyword, setKeyword] = useState('');
- const [sort, setSort] = useState<SortKey>('random');
- const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
- const [randomized, setRandomized] = useState<CreatorItem[]|null>(null);
- // 첫 진입 무작위 정렬 — 마운트 후 1회 (SSR/hydration 불일치 방지)
- useEffect(() => {
- setRandomized(shuffle(creators));
- }, [creators]);
- const filtered = useMemo(() => {
- let list = sort === 'random' ? (randomized ?? creators) : [...creators];
- const kw = keyword.trim().toLowerCase();
- if (kw) {
- list = list.filter(c => c.name.toLowerCase().includes(kw) || (c.handle?.toLowerCase().includes(kw) ?? false));
- }
- if (sort === 'subscribers') {
- list = [...list].sort((a, b) => b.subscriberCount - a.subscriberCount);
- } else if (sort === 'followers') {
- list = [...list].sort((a, b) => b.followerCount - a.followerCount);
- } else if (sort === 'name') {
- list = [...list].sort((a, b) => a.name.localeCompare(b.name));
- }
- return list;
- }, [creators, randomized, keyword, sort]);
- const visible = filtered.slice(0, visibleCount);
- const hasMore = visible.length < filtered.length;
- const handleSortChange = (e: ChangeEvent<HTMLSelectElement>) => {
- setSort(e.target.value as SortKey);
- setVisibleCount(PAGE_SIZE);
- };
- const handleKeywordChange = (e: ChangeEvent<HTMLInputElement>) => {
- setKeyword(e.target.value);
- setVisibleCount(PAGE_SIZE);
- };
- return (
- <section>
- <header className="mb-4 flex flex-wrap items-center justify-between gap-3">
- <h1 className="flex items-center gap-2 text-xl font-bold">
- 크리에이터
- <span className="rounded-full bg-[var(--bg-elevated)] px-2 py-0.5 text-sm font-semibold text-[var(--text-secondary)]">
- {filtered.length.toLocaleString()}
- </span>
- </h1>
- <div className="flex flex-wrap gap-2">
- <select
- value={sort}
- onChange={handleSortChange}
- aria-label="정렬"
- className="h-9 rounded-lg border border-[var(--border-default)] bg-[var(--bg-elevated)] px-3 text-sm"
- >
- {SORT_OPTIONS.map(o => (
- <option key={o.value} value={o.value}>{o.label}</option>
- ))}
- </select>
- <form role="search" onSubmit={(e) => e.preventDefault()} className="flex gap-2">
- <input
- type="search"
- value={keyword}
- onChange={handleKeywordChange}
- placeholder="채널명 또는 핸들 검색"
- aria-label="검색"
- className="h-9 min-w-[200px] rounded-lg border border-[var(--border-default)] bg-[var(--bg-elevated)] px-3 text-sm"
- />
- <button
- type="submit"
- aria-label="검색"
- className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[#FFD700] text-[#333] transition hover:bg-[#FFC400]"
- >
- <Search className="size-4" />
- </button>
- </form>
- </div>
- </header>
- {filtered.length === 0 ? (
- <p className="py-16 text-center text-[var(--text-muted)]">크리에이터가 없습니다.</p>
- ) : (
- <>
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
- {visible.map(c => (
- <CreatorCard key={c.channelSID} creator={c} />
- ))}
- </div>
- {hasMore && (
- <div className="mt-6 flex justify-center">
- <button
- type="button"
- onClick={() => setVisibleCount(v => v + PAGE_SIZE)}
- className="rounded-lg border border-[var(--border-default)] px-5 py-2 text-sm font-semibold"
- >
- 더 보기
- </button>
- </div>
- )}
- </>
- )}
- </section>
- );
- }
|