Files
msgapp/src/app/components/PostList.tsx
koreacomp5 53b6376966 222
2025-10-24 21:40:16 +09:00

119 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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;
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 }: { boardId?: string; sort?: "recent" | "popular"; q?: string; tag?: string; author?: string; start?: string; end?: string }) {
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);
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()}`;
};
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 (
<div className="w-full">
{/* 정렬 스위치 */}
<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>
<a
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"
}`}
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
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"
}`}
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>
</div>
{/* 리스트 테이블 헤더 */}
<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">
{items.map((p) => (
<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>
)}
</div>
<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>
</li>
))}
</ul>
{/* 페이지 더보기 */}
<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)}
>
{isLoading ? "로딩 중..." : canLoadMore ? "더 보기" : "끝"}
</button>
</div>
</div>
);
}