Merge branch 'subwork' into mainwork
This commit is contained in:
@@ -188,7 +188,6 @@ async function upsertBoards(admin, categoryMap) {
|
||||
{ name: "이벤트", slug: "event", description: "이벤트", sortOrder: 4, requiredTags: { required: ["이벤트"] } },
|
||||
{ name: "자유게시판", slug: "free", description: "자유", sortOrder: 5 },
|
||||
{ name: "무엇이든", slug: "qna", description: "무엇이든 물어보세요", sortOrder: 6 },
|
||||
{ name: "마사지꿀팁", slug: "tips", description: "팁", sortOrder: 7 },
|
||||
{ name: "익명게시판", slug: "anonymous", description: "익명", sortOrder: 8, allowAnonymousPost: true, allowSecretComment: true },
|
||||
{ name: "청와대", slug: "blue-house", description: "레벨 제한", sortOrder: 10, readLevel: "member" },
|
||||
// 특수
|
||||
|
||||
@@ -13,7 +13,7 @@ export function BoardToolbar({ boardId }: { boardId: string }) {
|
||||
const onChangeSort = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const next = new URLSearchParams(sp.toString());
|
||||
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>) => {
|
||||
@@ -26,7 +26,7 @@ export function BoardToolbar({ boardId }: { boardId: string }) {
|
||||
if (v === "1w") now.setDate(now.getDate() - 7);
|
||||
if (v === "1m") now.setMonth(now.getMonth() - 1);
|
||||
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) => {
|
||||
@@ -41,7 +41,7 @@ export function BoardToolbar({ boardId }: { boardId: string }) {
|
||||
next.delete("author");
|
||||
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 (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
import useSWRInfinite from "swr/infinite";
|
||||
import useSWR from "swr";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import ViewsIcon from "@/app/svgs/ViewsIcon";
|
||||
import LikeIcon from "@/app/svgs/LikeIcon";
|
||||
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 }) {
|
||||
const pageSize = 10;
|
||||
const router = useRouter();
|
||||
const sp = useSearchParams();
|
||||
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [lockedMinHeight, setLockedMinHeight] = useState<number | null>(null);
|
||||
|
||||
const getKey = (index: number, prev: Resp | 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 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 (
|
||||
@@ -122,10 +138,11 @@ export function PostList({ boardId, sort = "recent", q, tag, author, start, end,
|
||||
)}
|
||||
|
||||
{/* 아이템들 */}
|
||||
<ul className="divide-y divide-[#ececec]">
|
||||
{items.map((p) => (
|
||||
<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">
|
||||
<div ref={listContainerRef} style={{ minHeight: lockedMinHeight ? `${lockedMinHeight}px` : undefined }}>
|
||||
<ul className="divide-y divide-[#ececec]">
|
||||
{items.map((p) => (
|
||||
<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/공지 아이콘 자리 */}
|
||||
<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 className="md:w-[80px] text-xs text-neutral-500 text-right">{new Date(p.createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
{!isEmpty && (
|
||||
@@ -165,11 +183,14 @@ export function PostList({ boardId, sort = "recent", q, tag, author, start, end,
|
||||
{/* Previous */}
|
||||
<button
|
||||
onClick={() => {
|
||||
lockHeight();
|
||||
const next = Math.max(1, page - 1);
|
||||
setPage(next);
|
||||
const nextSp = new URLSearchParams(Array.from(sp.entries()));
|
||||
nextSp.set("page", String(next));
|
||||
router.push(`?${nextSp.toString()}`);
|
||||
if (typeof window !== "undefined") {
|
||||
window.history.replaceState(null, "", `?${nextSp.toString()}`);
|
||||
}
|
||||
}}
|
||||
disabled={page <= 1}
|
||||
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
|
||||
key={`p-${n}-${idx}`}
|
||||
onClick={() => {
|
||||
lockHeight();
|
||||
setPage(n);
|
||||
const nextSp = new URLSearchParams(Array.from(sp.entries()));
|
||||
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}
|
||||
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 */}
|
||||
<button
|
||||
onClick={() => {
|
||||
lockHeight();
|
||||
const next = Math.min(totalPages, page + 1);
|
||||
setPage(next);
|
||||
const nextSp = new URLSearchParams(Array.from(sp.entries()));
|
||||
nextSp.set("page", String(next));
|
||||
router.push(`?${nextSp.toString()}`);
|
||||
if (typeof window !== "undefined") {
|
||||
window.history.replaceState(null, "", `?${nextSp.toString()}`);
|
||||
}
|
||||
}}
|
||||
disabled={page >= totalPages}
|
||||
className="h-9 px-3 rounded-md border border-neutral-300 bg-white text-sm disabled:opacity-50"
|
||||
|
||||
Reference in New Issue
Block a user