This commit is contained in:
koreacomp5
2025-11-02 04:59:04 +09:00
parent c6e60cd34d
commit 0bf270d884
3 changed files with 43 additions and 17 deletions

View File

@@ -188,7 +188,6 @@ async function upsertBoards(admin, categoryMap) {
{ name: "이벤트", slug: "event", description: "이벤트", sortOrder: 4, requiredTags: { required: ["이벤트"] } }, { name: "이벤트", slug: "event", description: "이벤트", sortOrder: 4, requiredTags: { required: ["이벤트"] } },
{ name: "자유게시판", slug: "free", description: "자유", sortOrder: 5 }, { name: "자유게시판", slug: "free", description: "자유", sortOrder: 5 },
{ name: "무엇이든", slug: "qna", description: "무엇이든 물어보세요", sortOrder: 6 }, { name: "무엇이든", slug: "qna", description: "무엇이든 물어보세요", sortOrder: 6 },
{ name: "마사지꿀팁", slug: "tips", description: "팁", sortOrder: 7 },
{ name: "익명게시판", slug: "anonymous", description: "익명", sortOrder: 8, allowAnonymousPost: true, allowSecretComment: true }, { name: "익명게시판", slug: "anonymous", description: "익명", sortOrder: 8, allowAnonymousPost: true, allowSecretComment: true },
{ name: "청와대", slug: "blue-house", description: "레벨 제한", sortOrder: 10, readLevel: "member" }, { name: "청와대", slug: "blue-house", description: "레벨 제한", sortOrder: 10, readLevel: "member" },
// 특수 // 특수

View File

@@ -13,7 +13,7 @@ export function BoardToolbar({ boardId }: { boardId: string }) {
const onChangeSort = (e: React.ChangeEvent<HTMLSelectElement>) => { const onChangeSort = (e: React.ChangeEvent<HTMLSelectElement>) => {
const next = new URLSearchParams(sp.toString()); const next = new URLSearchParams(sp.toString());
next.set("sort", e.target.value); next.set("sort", e.target.value);
router.push(`/boards/${boardId}?${next.toString()}`); router.push(`/boards/${boardId}?${next.toString()}` , { scroll: false });
}; };
const onChangePeriod = (e: React.ChangeEvent<HTMLSelectElement>) => { const onChangePeriod = (e: React.ChangeEvent<HTMLSelectElement>) => {
@@ -26,7 +26,7 @@ export function BoardToolbar({ boardId }: { boardId: string }) {
if (v === "1w") now.setDate(now.getDate() - 7); if (v === "1w") now.setDate(now.getDate() - 7);
if (v === "1m") now.setMonth(now.getMonth() - 1); if (v === "1m") now.setMonth(now.getMonth() - 1);
if (v === "all") next.delete("start"); else next.set("start", now.toISOString()); if (v === "all") next.delete("start"); else next.set("start", now.toISOString());
router.push(`/boards/${boardId}?${next.toString()}`); router.push(`/boards/${boardId}?${next.toString()}` , { scroll: false });
}; };
const onSubmit = (formData: FormData) => { const onSubmit = (formData: FormData) => {
@@ -41,7 +41,7 @@ export function BoardToolbar({ boardId }: { boardId: string }) {
next.delete("author"); next.delete("author");
if (text) next.set("q", text); else next.delete("q"); if (text) next.set("q", text); else next.delete("q");
} }
router.push(`/boards/${boardId}?${next.toString()}`); router.push(`/boards/${boardId}?${next.toString()}` , { scroll: false });
}; };
return ( return (

View File

@@ -1,9 +1,9 @@
"use client"; "use client";
import useSWRInfinite from "swr/infinite"; import useSWRInfinite from "swr/infinite";
import useSWR from "swr"; import useSWR from "swr";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import ViewsIcon from "@/app/svgs/ViewsIcon"; import ViewsIcon from "@/app/svgs/ViewsIcon";
import LikeIcon from "@/app/svgs/LikeIcon"; import LikeIcon from "@/app/svgs/LikeIcon";
import CommentIcon from "@/app/svgs/CommentIcon"; import CommentIcon from "@/app/svgs/CommentIcon";
@@ -31,8 +31,9 @@ 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 }) { 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 pageSize = 10;
const router = useRouter();
const sp = useSearchParams(); const sp = useSearchParams();
const listContainerRef = useRef<HTMLDivElement | null>(null);
const [lockedMinHeight, setLockedMinHeight] = useState<number | null>(null);
const getKey = (index: number, prev: Resp | null) => { const getKey = (index: number, prev: Resp | null) => {
if (prev && prev.items.length === 0) return null; if (prev && prev.items.length === 0) return null;
@@ -76,6 +77,21 @@ export function PostList({ boardId, sort = "recent", q, tag, author, start, end,
const items = variant === "board" ? itemsSingle : itemsInfinite; const items = variant === "board" ? itemsSingle : itemsInfinite;
const isEmpty = variant === "board" ? isEmptySingle : isEmptyInfinite; 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) : "익"); const initials = (name?: string | null) => (name ? name.trim().slice(0, 1) : "익");
return ( return (
@@ -122,10 +138,11 @@ export function PostList({ boardId, sort = "recent", q, tag, author, start, end,
)} )}
{/* 아이템들 */} {/* 아이템들 */}
<ul className="divide-y divide-[#ececec]"> <div ref={listContainerRef} style={{ minHeight: lockedMinHeight ? `${lockedMinHeight}px` : undefined }}>
{items.map((p) => ( <ul className="divide-y divide-[#ececec]">
<li key={p.id} className={`px-4 ${variant === "board" ? "py-2.5" : "py-3 md:py-3"} hover:bg-neutral-50 transition-colors`}> {items.map((p) => (
<div className="grid grid-cols-1 md:grid-cols-[20px_1fr_120px_120px_80px] items-center gap-2"> <li key={p.id} className={`px-4 ${variant === "board" ? "py-2.5" : "py-3 md:py-3"} hover:bg-neutral-50 transition-colors`}>
<div className="grid grid-cols-1 md:grid-cols-[20px_1fr_120px_120px_80px] items-center gap-2">
{/* bullet/공지 아이콘 자리 */} {/* bullet/공지 아이콘 자리 */}
<div className="hidden md:flex items-center justify-center text-[#f94b37]">{p.isPinned ? "★" : "•"}</div> <div className="hidden md:flex items-center justify-center text-[#f94b37]">{p.isPinned ? "★" : "•"}</div>
@@ -153,9 +170,10 @@ export function PostList({ boardId, sort = "recent", q, tag, author, start, end,
</div> </div>
<div className="md:w-[80px] text-xs text-neutral-500 text-right">{new Date(p.createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</div> <div className="md:w-[80px] text-xs text-neutral-500 text-right">{new Date(p.createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</div>
</div> </div>
</li> </li>
))} ))}
</ul> </ul>
</div>
{/* 페이지네이션 */} {/* 페이지네이션 */}
{!isEmpty && ( {!isEmpty && (
@@ -165,11 +183,14 @@ export function PostList({ boardId, sort = "recent", q, tag, author, start, end,
{/* Previous */} {/* Previous */}
<button <button
onClick={() => { onClick={() => {
lockHeight();
const next = Math.max(1, page - 1); const next = Math.max(1, page - 1);
setPage(next); setPage(next);
const nextSp = new URLSearchParams(Array.from(sp.entries())); const nextSp = new URLSearchParams(Array.from(sp.entries()));
nextSp.set("page", String(next)); nextSp.set("page", String(next));
router.push(`?${nextSp.toString()}`); if (typeof window !== "undefined") {
window.history.replaceState(null, "", `?${nextSp.toString()}`);
}
}} }}
disabled={page <= 1} disabled={page <= 1}
className="h-9 px-3 rounded-md border border-neutral-300 bg-white text-sm disabled:opacity-50" className="h-9 px-3 rounded-md border border-neutral-300 bg-white text-sm disabled:opacity-50"
@@ -196,10 +217,13 @@ export function PostList({ boardId, sort = "recent", q, tag, author, start, end,
<button <button
key={`p-${n}-${idx}`} key={`p-${n}-${idx}`}
onClick={() => { onClick={() => {
lockHeight();
setPage(n); setPage(n);
const nextSp = new URLSearchParams(Array.from(sp.entries())); const nextSp = new URLSearchParams(Array.from(sp.entries()));
nextSp.set("page", String(n)); nextSp.set("page", String(n));
router.push(`?${nextSp.toString()}`); if (typeof window !== "undefined") {
window.history.replaceState(null, "", `?${nextSp.toString()}`);
}
}} }}
aria-current={n === page ? "page" : undefined} aria-current={n === page ? "page" : undefined}
className={`h-9 w-9 rounded-md border ${n === page ? "border-neutral-300 text-[#f94b37] font-semibold" : "border-neutral-300 text-neutral-900"}`} className={`h-9 w-9 rounded-md border ${n === page ? "border-neutral-300 text-[#f94b37] font-semibold" : "border-neutral-300 text-neutral-900"}`}
@@ -215,11 +239,14 @@ export function PostList({ boardId, sort = "recent", q, tag, author, start, end,
{/* Next */} {/* Next */}
<button <button
onClick={() => { onClick={() => {
lockHeight();
const next = Math.min(totalPages, page + 1); const next = Math.min(totalPages, page + 1);
setPage(next); setPage(next);
const nextSp = new URLSearchParams(Array.from(sp.entries())); const nextSp = new URLSearchParams(Array.from(sp.entries()));
nextSp.set("page", String(next)); nextSp.set("page", String(next));
router.push(`?${nextSp.toString()}`); if (typeof window !== "undefined") {
window.history.replaceState(null, "", `?${nextSp.toString()}`);
}
}} }}
disabled={page >= totalPages} disabled={page >= totalPages}
className="h-9 px-3 rounded-md border border-neutral-300 bg-white text-sm disabled:opacity-50" className="h-9 px-3 rounded-md border border-neutral-300 bg-white text-sm disabled:opacity-50"