import { useEffect, useState } from "react"; import { dict, type Lang } from "@/lib/i18n"; import { relativeTime } from "@/lib/format"; import { listComments, postComment, getSessionNickname, rerollSessionNickname, getDeviceId, ApiError, } from "@/lib/api"; import type { Comment } from "@/lib/types"; const FIRST = 3; // 처음 보여줄 최신 댓글 수 const PAGE = 10; // "더보기" 한 번에 더 불러올 수 // 한 댓글을 식별하는 합성 키 (id 비노출이라 시각+내용으로 dedup/렌더) const keyOf = (c: Comment) => `${c.createdAt}|${c.nickname}|${c.body}`; // 경기별 한마디(댓글) — 완전 익명. 최신 3개 + 더보기/접기. export default function Comments({ matchId, lang, }: { matchId: string; lang: Lang; }) { const t = dict(lang); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [cursor, setCursor] = useState(0); // 서버에서 페이징으로 받은 개수 const [loadingMore, setLoadingMore] = useState(false); const [expanded, setExpanded] = useState(false); const [body, setBody] = useState(""); const [nickname, setNickname] = useState(""); // 세션 닉(캐싱) const [posting, setPosting] = useState(false); const [error, setError] = useState(""); useEffect(() => { let alive = true; listComments(matchId, FIRST, 0) .then((res) => { if (!alive) return; setItems(res.items); setTotal(res.total); setCursor(res.items.length); }) .catch(() => {}); getSessionNickname(matchId) .then((n) => alive && setNickname(n)) .catch(() => {}); return () => { alive = false; }; }, [matchId]); const visible = expanded ? items : items.slice(0, FIRST); const unloaded = Math.max(0, total - items.length); const hiddenWhenCollapsed = Math.max(0, total - FIRST); async function showMore() { setExpanded(true); if (items.length <= FIRST && unloaded > 0) await loadMore(); } async function loadMore() { if (loadingMore) return; setLoadingMore(true); try { const res = await listComments(matchId, PAGE, cursor); setItems((prev) => { const seen = new Set(prev.map(keyOf)); return [...prev, ...res.items.filter((c) => !seen.has(keyOf(c)))]; }); setTotal(res.total); setCursor((c) => c + res.items.length); } catch { /* 조용히 무시 — 다시 누르면 재시도 */ } finally { setLoadingMore(false); } } async function reroll() { try { const n = await rerollSessionNickname(matchId); setNickname(n); } catch { /* 무시 */ } } async function submit() { const text = body.trim(); if (posting || !text) return; setError(""); setPosting(true); try { const created = await postComment(matchId, { deviceId: getDeviceId(), nickname, body: text, }); setItems((prev) => [created, ...prev]); // 낙관적: 맨 위에 즉시 표시 setTotal((n) => n + 1); setBody(""); } catch (e) { if (e instanceof ApiError && e.status === 429) setError(t.commentCooldown); else setError(t.commentError); } finally { setPosting(false); } } return (
{/* 헤더 */}

💬 {t.commentsTitle}

{t.commentsCount(total)}
{/* 입력 */}
{nickname && (

{/^(KBO|MLB)_/.test(matchId) ? "⚾" : "⚽"} {t.commentAs}{" "} {nickname}

)}