2025-10-09 16:31:46 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
import useSWRInfinite from "swr/infinite";
|
|
|
|
|
|
|
|
|
|
|
|
type Item = {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
createdAt: string;
|
|
|
|
|
|
isPinned: boolean;
|
|
|
|
|
|
status: string;
|
|
|
|
|
|
stat?: { recommendCount: number; views: number; commentsCount: number } | null;
|
2025-10-09 16:57:29 +09:00
|
|
|
|
postTags?: { tag: { name: string; slug: string } }[];
|
2025-10-24 21:24:51 +09:00
|
|
|
|
author?: { nickname: string } | null;
|
2025-10-09 16:31:46 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type Resp = {
|
|
|
|
|
|
total: number;
|
|
|
|
|
|
page: number;
|
|
|
|
|
|
pageSize: number;
|
|
|
|
|
|
items: Item[];
|
|
|
|
|
|
sort: "recent" | "popular";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
|
|
|
|
|
|
2025-10-09 16:54:24 +09:00
|
|
|
|
export function PostList({ boardId, sort = "recent", q, tag, author, start, end }: { boardId?: string; sort?: "recent" | "popular"; q?: string; tag?: string; author?: string; start?: string; end?: string }) {
|
2025-10-09 16:31:46 +09:00
|
|
|
|
const pageSize = 10;
|
|
|
|
|
|
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);
|
2025-10-09 16:35:19 +09:00
|
|
|
|
if (q) sp.set("q", q);
|
2025-10-09 16:54:24 +09:00
|
|
|
|
if (tag) sp.set("tag", tag);
|
|
|
|
|
|
if (author) sp.set("author", author);
|
|
|
|
|
|
if (start) sp.set("start", start);
|
|
|
|
|
|
if (end) sp.set("end", end);
|
2025-10-09 16:31:46 +09:00
|
|
|
|
return `/api/posts?${sp.toString()}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
const { data, size, setSize, isLoading } = useSWRInfinite<Resp>(getKey, fetcher);
|
|
|
|
|
|
const items = data?.flatMap((d) => d.items) ?? [];
|
|
|
|
|
|
const canLoadMore = (data?.at(-1)?.items.length ?? 0) === pageSize;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-10-24 21:24:51 +09:00
|
|
|
|
<div className="w-full">
|
|
|
|
|
|
{/* 정렬 스위치 */}
|
2025-10-24 21:40:16 +09:00
|
|
|
|
<div className="px-4 py-2 border-b border-neutral-200 mb-2 flex items-center gap-2">
|
|
|
|
|
|
<span className="text-xs text-neutral-500 mr-1">정렬</span>
|
2025-10-09 16:35:19 +09:00
|
|
|
|
<a
|
2025-10-24 21:40:16 +09:00
|
|
|
|
className={`text-xs px-3 py-1 rounded-full border transition-colors ${
|
|
|
|
|
|
sort === "recent" ? "bg-neutral-900 text-white border-neutral-900" : "bg-white text-neutral-700 border-neutral-300 hover:bg-neutral-100"
|
|
|
|
|
|
}`}
|
2025-10-09 16:35:19 +09:00
|
|
|
|
href={`${q ? "/search" : "/"}?${(() => { const p = new URLSearchParams(); if (q) p.set("q", q); if (boardId) p.set("boardId", boardId); p.set("sort", "recent"); return p.toString(); })()}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
최신
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<a
|
2025-10-24 21:40:16 +09:00
|
|
|
|
className={`text-xs px-3 py-1 rounded-full border transition-colors ${
|
|
|
|
|
|
sort === "popular" ? "bg-neutral-900 text-white border-neutral-900" : "bg-white text-neutral-700 border-neutral-300 hover:bg-neutral-100"
|
|
|
|
|
|
}`}
|
2025-10-09 16:35:19 +09:00
|
|
|
|
href={`${q ? "/search" : "/"}?${(() => { const p = new URLSearchParams(); if (q) p.set("q", q); if (boardId) p.set("boardId", boardId); p.set("sort", "popular"); return p.toString(); })()}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
인기
|
|
|
|
|
|
</a>
|
2025-10-09 16:31:46 +09:00
|
|
|
|
</div>
|
2025-10-24 21:24:51 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 리스트 테이블 헤더 */}
|
|
|
|
|
|
<div className="hidden md:grid grid-cols-[1fr_auto_auto_auto] items-center px-4 py-2 text-xs text-neutral-500 border-b border-neutral-200">
|
|
|
|
|
|
<div>제목</div>
|
|
|
|
|
|
<div className="w-28 text-center">작성자</div>
|
|
|
|
|
|
<div className="w-24 text-center">지표</div>
|
|
|
|
|
|
<div className="w-24 text-right">작성일</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 아이템들 */}
|
|
|
|
|
|
<ul className="divide-y divide-neutral-100">
|
2025-10-09 16:31:46 +09:00
|
|
|
|
{items.map((p) => (
|
2025-10-24 21:24:51 +09:00
|
|
|
|
<li key={p.id} className="px-4 py-3 hover:bg-neutral-50 transition-colors">
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-[1fr_auto_auto_auto] items-center gap-2">
|
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
|
<a href={`/posts/${p.id}`} className="block truncate text-[15px] md:text-base text-neutral-900">
|
|
|
|
|
|
{p.isPinned && <span className="mr-2 inline-flex items-center rounded bg-orange-100 text-orange-700 px-1.5 py-0.5 text-[11px]">공지</span>}
|
|
|
|
|
|
{p.title}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
{!!p.postTags?.length && (
|
|
|
|
|
|
<div className="mt-1 hidden md:flex flex-wrap gap-1 text-[11px] text-neutral-500">
|
|
|
|
|
|
{p.postTags?.map((pt) => (
|
|
|
|
|
|
<a key={pt.tag.slug} href={`/search?tag=${pt.tag.slug}`} className="hover:underline">#{pt.tag.name}</a>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-10-09 16:57:29 +09:00
|
|
|
|
</div>
|
2025-10-24 21:24:51 +09:00
|
|
|
|
<div className="md:w-28 text-xs text-neutral-600 text-center">{p.author?.nickname ?? "익명"}</div>
|
|
|
|
|
|
<div className="md:w-24 text-[11px] text-neutral-600 text-center flex md:block gap-3 md:gap-0">
|
|
|
|
|
|
<span>👍 {p.stat?.recommendCount ?? 0}</span>
|
|
|
|
|
|
<span>👁️ {p.stat?.views ?? 0}</span>
|
|
|
|
|
|
<span>💬 {p.stat?.commentsCount ?? 0}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="md:w-24 text-xs text-neutral-500 text-right">{new Date(p.createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</div>
|
|
|
|
|
|
</div>
|
2025-10-09 16:31:46 +09:00
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
2025-10-24 21:24:51 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 페이지 더보기 */}
|
|
|
|
|
|
<div className="mt-3 flex justify-center">
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="h-9 px-4 rounded-md border border-neutral-300 bg-white text-sm hover:bg-neutral-100 disabled:opacity-50"
|
|
|
|
|
|
disabled={!canLoadMore || isLoading}
|
|
|
|
|
|
onClick={() => setSize(size + 1)}
|
|
|
|
|
|
>
|
2025-10-09 16:31:46 +09:00
|
|
|
|
{isLoading ? "로딩 중..." : canLoadMore ? "더 보기" : "끝"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|