"use client"; import useSWRInfinite from "swr/infinite"; import useSWR from "swr"; import { useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import ViewsIcon from "@/app/svgs/ViewsIcon"; import LikeIcon from "@/app/svgs/LikeIcon"; import CommentIcon from "@/app/svgs/CommentIcon"; type Item = { id: string; title: string; createdAt: string; isPinned: boolean; status: string; stat?: { recommendCount: number; views: number; commentsCount: number } | null; postTags?: { tag: { name: string; slug: string } }[]; author?: { nickname: string } | null; }; type Resp = { total: number; page: number; pageSize: number; items: Item[]; sort: "recent" | "popular"; }; const fetcher = (url: string) => fetch(url).then((r) => r.json()); export function PostList({ boardId, sort = "recent", q, tag, author, start, end, variant = "default", newPostHref }: { boardId?: string; sort?: "recent" | "popular"; q?: string; tag?: string; author?: string; start?: string; end?: string; variant?: "default" | "board"; newPostHref?: string }) { const pageSize = 10; const sp = useSearchParams(); const listContainerRef = useRef(null); const [lockedMinHeight, setLockedMinHeight] = useState(null); const getKey = (index: number, prev: Resp | null) => { if (prev && prev.items.length === 0) return null; const page = index + 1; const sp = new URLSearchParams({ page: String(page), pageSize: String(pageSize), sort }); if (boardId) sp.set("boardId", boardId); if (q) sp.set("q", q); if (tag) sp.set("tag", tag); if (author) sp.set("author", author); if (start) sp.set("start", start); if (end) sp.set("end", end); return `/api/posts?${sp.toString()}`; }; // default(무한 스크롤 형태) const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher, { revalidateFirstPage: false }); const itemsInfinite = data?.flatMap((d) => d.items) ?? []; const canLoadMore = (data?.at(-1)?.items.length ?? 0) === pageSize; const isEmptyInfinite = !isLoading && itemsInfinite.length === 0; // board 변형: 번호 페이지네이션 const initialPage = useMemo(() => Math.max(1, parseInt(sp.get("page") || "1", 10)), [sp]); const [page, setPage] = useState(initialPage); useEffect(() => { setPage(initialPage); }, [initialPage]); const singleKey = useMemo(() => { if (variant !== "board") return null; const usp = new URLSearchParams({ page: String(page), pageSize: String(pageSize), sort }); if (boardId) usp.set("boardId", boardId); if (q) usp.set("q", q); if (tag) usp.set("tag", tag); if (author) usp.set("author", author); if (start) usp.set("start", start); if (end) usp.set("end", end); return `/api/posts?${usp.toString()}`; }, [variant, page, pageSize, sort, boardId, q, tag, author, start, end]); const { data: singlePageResp, isLoading: isLoadingSingle } = useSWR(singleKey, fetcher); const itemsSingle = singlePageResp?.items ?? []; const totalSingle = singlePageResp?.total ?? 0; const totalPages = Math.max(1, Math.ceil(totalSingle / pageSize)); const isEmptySingle = !isLoadingSingle && itemsSingle.length === 0; const items = variant === "board" ? itemsSingle : itemsInfinite; const isEmpty = variant === "board" ? isEmptySingle : isEmptyInfinite; // 잠깐 높이 고정: 페이지 변경으로 재로딩될 때 현재 높이를 min-height로 유지 const lockHeight = () => { const el = listContainerRef.current; if (!el) return; const h = el.offsetHeight; if (h > 0) setLockedMinHeight(h); }; // 로딩이 끝나면 해제 useEffect(() => { if (variant === "board" && !isLoadingSingle) { setLockedMinHeight(null); } }, [variant, isLoadingSingle]); const initials = (name?: string | null) => (name ? name.trim().slice(0, 1) : "익"); return (
{/* 정렬 스위치 (board 변형에서는 상단 툴바를 숨김) */} {variant !== "board" && ( )} {/* 리스트 테이블 헤더 (board 변형에서는 숨김) */} {variant !== "board" && (
제목
작성자
지표
작성일
)} {/* 빈 상태 */} {isEmpty && (

글이 없습니다.

)} {/* 아이템들 */}
    {items.map((p) => (
  • {/* bullet/공지 아이콘 자리 */}
    {p.isPinned ? "★" : "•"}
    {p.isPinned && 공지} {p.title} {!!p.postTags?.length && (
    {p.postTags?.map((pt) => ( #{pt.tag.name} ))}
    )}
    {initials(p.author?.nickname)} {p.author?.nickname ?? "익명"}
    {p.stat?.views ?? 0} {p.stat?.recommendCount ?? 0} {p.stat?.commentsCount ?? 0}
    {new Date(p.createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
  • ))}
{/* 페이지네이션 */} {!isEmpty && ( variant === "board" ? (
{/* Previous */} {/* Numbers with ellipsis */}
{(() => { const nodes: (number | string)[] = []; if (totalPages <= 7) { for (let i = 1; i <= totalPages; i++) nodes.push(i); } else { nodes.push(1); const start = Math.max(2, page - 1); const end = Math.min(totalPages - 1, page + 1); if (start > 2) nodes.push("..."); for (let i = start; i <= end; i++) nodes.push(i); if (end < totalPages - 1) nodes.push("..."); nodes.push(totalPages); } return nodes.map((n, idx) => typeof n === "number" ? ( ) : ( ) ); })()}
{/* Next */}
{newPostHref && ( )}
) : (
) )}
); }