This commit is contained in:
mota
2025-11-02 15:13:03 +09:00
parent b10d41532b
commit fadd402e63
31 changed files with 1105 additions and 192 deletions

View File

@@ -1,13 +1,15 @@
import Link from "next/link";
export function AppFooter() {
return (
<footer className="py-[72px]">
<div className="text-[#626262] text-[16px] leading-[14px] flex flex-row mb-[30px]">
<div className="flex-1"></div>
<div className="border-r border-dotted border-[#626262] px-[8px] cursor-pointer hover:text-[#2BAF7E]"></div>
<div className="hidden lg:block border-r border-dotted border-[#626262] px-[8px] cursor-pointer hover:text-[#2BAF7E]"> </div>
<div className=" border-r border-dotted border-[#626262] px-[8px] cursor-pointer hover:text-[#2BAF7E]"> </div>
<div className="hidden lg:block border-r border-dotted border-[#626262] px-[8px] cursor-pointer hover:text-[#2BAF7E]"></div>
<div className="px-[8px] cursor-pointer hover:text-[#2BAF7E]"></div>
<Link href="/privacy" className="border-r border-dotted border-[#626262] px-[8px] cursor-pointer hover:text-[#2BAF7E]"></Link>
<Link href="/email-deny" className="hidden lg:block border-r border-dotted border-[#626262] px-[8px] cursor-pointer hover:text-[#2BAF7E]"> </Link>
<Link href="/legal" className=" border-r border-dotted border-[#626262] px-[8px] cursor-pointer hover:text-[#2BAF7E]"> </Link>
<Link href="/guide" className="hidden lg:block border-r border-dotted border-[#626262] px-[8px] cursor-pointer hover:text-[#2BAF7E]"></Link>
<Link href="/contact" className="px-[8px] cursor-pointer hover:text-[#2BAF7E]"></Link>
<div className="flex-1"></div>
</div>
<div className="text-[#888] text-center font-[Pretendard] text-[16px] font-normal leading-[100%] mt-[24px]">

View File

@@ -3,6 +3,9 @@
import Image from "next/image";
import Link from "next/link";
import { SearchBar } from "@/app/components/SearchBar";
import useSWR from "swr";
import { UserAvatar } from "@/app/components/UserAvatar";
import { GradeIcon } from "@/app/components/GradeIcon";
import React from "react";
import { usePathname, useSearchParams } from "next/navigation";
import { SinglePageLogo } from "@/app/components/SinglePageLogo";
@@ -23,6 +26,11 @@ export function AppHeader() {
const [blockWidths, setBlockWidths] = React.useState<Record<string, number>>({});
const closeTimer = React.useRef<number | null>(null);
const [navMinWidths, setNavMinWidths] = React.useState<Record<string, number>>({});
// 모바일 사이드바 열릴 때만 현재 사용자 정보를 가져옵니다(훅은 항상 동일한 순서로 호출)
const { data: meData } = useSWR<{ user: { userId: string; nickname: string; profileImage: string | null; points: number; level: number; grade: number } | null }>(
mobileOpen ? "/api/me" : null,
(u: string) => fetch(u).then((r) => r.json())
);
// 현재 경로 기반 활성 보드/카테고리 계산
const pathname = usePathname();
@@ -416,6 +424,28 @@ export function AppHeader() {
<div className="mb-3 h-10 flex items-center justify-between">
</div>
<div className="flex flex-col gap-4">
{/* 미니 프로필 패널 */}
<div className="rounded-xl border border-neutral-200 p-3">
{meData?.user ? (
<div className="flex items-center gap-3">
<UserAvatar src={meData.user.profileImage} alt={meData.user.nickname || "프로필"} width={48} height={48} className="rounded-full" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-neutral-900 truncate">{meData.user.nickname}</span>
<GradeIcon grade={meData.user.grade} width={20} height={20} />
</div>
<div className="text-xs text-neutral-600">Lv.{meData.user.level} · {meData.user.points.toLocaleString()}</div>
</div>
</div>
) : (
<div className="text-sm text-neutral-600"> ...</div>
)}
<div className="grid grid-cols-3 gap-2 mt-3">
<Link href="/my-page?tab=points" onClick={() => setMobileOpen(false)} className="h-9 rounded-md border border-neutral-300 text-xs flex items-center justify-center hover:bg-neutral-100"> </Link>
<Link href="/my-page?tab=posts" onClick={() => setMobileOpen(false)} className="h-9 rounded-md border border-neutral-300 text-xs flex items-center justify-center hover:bg-neutral-100"> </Link>
<Link href="/my-page?tab=comments" onClick={() => setMobileOpen(false)} className="h-9 rounded-md border border-neutral-300 text-xs flex items-center justify-center hover:bg-neutral-100"> </Link>
</div>
</div>
<SearchBar />
<Link
href="/admin"

View File

@@ -31,7 +31,7 @@ type PostData = {
createdAt: Date;
content?: string | null;
attachments?: { url: string }[];
stat?: { recommendCount: number | null };
stat?: { recommendCount: number | null; commentsCount: number | null };
};
type BoardPanelData = {
@@ -78,7 +78,7 @@ export function BoardPanelClient({
return imgMatch[1];
}
// figure 안의 img 태그도 확인
const figureMatch = content.match(/<figure[^>]*>.*?<img[^>]+src=["']([^"']+)["'][^>]*>/is);
const figureMatch = content.match(/<figure[^>]*>[\s\S]*?<img[^>]+src=["']([^"']+)["'][^>]*>/i);
if (figureMatch && figureMatch[1]) {
return figureMatch[1];
}
@@ -97,7 +97,7 @@ export function BoardPanelClient({
<div className="flex items-center gap-[8px] shrink-0">
<div className="text-[30px] text-[#5c5c5c] leading-[30px]">{categoryName || board.name}</div>
</div>
<div className="flex items-center gap-[8px] overflow-x-auto flex-nowrap min-w-0 flex-1 scrollbar-thin-x">
<div className="flex items-center gap-[8px] overflow-x-auto flex-nowrap min-w-0 flex-1 no-scrollbar">
{siblingBoards.map((sb) => (
<button
key={sb.id}
@@ -113,12 +113,12 @@ export function BoardPanelClient({
</div>
<div className="rounded-xl overflow-hidden h-full min-h-0 flex flex-col">
<div className="flex-1 min-h-0 overflow-hidden p-0">
<div className="px-[24px] pt-[8px] pb-[16px]">
<div className="px-[0px] pt-[8px] pb-[16px]">
<div className="flex flex-col gap-[16px]">
{selectedBoardData.specialRankUsers.map((user, idx) => {
const rank = idx + 1;
return (
<div key={user.userId} className="flex h-[150px] items-center rounded-[16px] overflow-hidden bg-white">
<Link href="/boards/ranking" key={user.userId} className="flex h-[150px] items-center rounded-[16px] overflow-hidden bg-white hover:bg-neutral-50">
<div className="h-[150px] w-[160px] relative shrink-0 bg-[#d5d5d5] overflow-hidden">
<UserAvatar
src={user.profileImage}
@@ -131,7 +131,7 @@ export function BoardPanelClient({
<GradeIcon grade={user.grade} width={40} height={40} />
</div>
</div>
<div className="flex-1 flex items-center gap-[10px] px-[30px] py-[24px] min-w-0">
<div className="flex-1 flex items-center gap-[10px] px-[24px] md:px-[30px] py-[24px] min-w-0">
<div className="flex flex-col gap-[12px] min-w-0 flex-1">
<div className="flex items-center gap-[12px]">
<div className="relative w-[20px] h-[20px] shrink-0">
@@ -154,7 +154,7 @@ export function BoardPanelClient({
<span className="text-[20px] font-semibold text-[#5c5c5c] leading-[22px]">{user.points.toLocaleString()}</span>
</div>
</div>
</div>
</Link>
);
})}
</div>
@@ -173,7 +173,7 @@ export function BoardPanelClient({
<div className="flex items-center gap-[8px] shrink-0">
<div className="text-[30px] text-[#5c5c5c] leading-[30px]">{categoryName || board.name}</div>
</div>
<div className="flex items-center gap-[8px] overflow-x-auto flex-nowrap min-w-0 flex-1 scrollbar-thin-x">
<div className="flex items-center gap-[8px] overflow-x-auto flex-nowrap min-w-0 flex-1 no-scrollbar">
{siblingBoards.map((sb) => (
<button
key={sb.id}
@@ -189,7 +189,7 @@ export function BoardPanelClient({
</div>
<div className="rounded-xl overflow-hidden h-full min-h-0 flex flex-col">
<div className="flex-1 min-h-0 overflow-hidden p-0">
<div className="px-[24px] pt-[8px] pb-[16px]">
<div className="px-[0px] pt-[8px] pb-[16px]">
<div className="flex flex-col gap-[16px]">
{selectedBoardData.previewPosts.map((post) => {
// attachments에서 이미지를 먼저 찾고, 없으면 content에서 추출
@@ -209,7 +209,7 @@ export function BoardPanelClient({
</div>
)}
</div>
<div className="flex-1 flex items-center gap-[10px] px-[30px] py-[24px] min-w-0">
<div className="flex-1 flex items-center gap-[10px] px-[24px] md:px-[30px] py-[24px] min-w-0">
<div className="flex flex-col gap-[12px] min-w-0 flex-1">
<div className="bg-white border border-[#d5d5d5] px-[16px] py-[5px] rounded-[12px] text-[14px] text-[#5c5c5c] shrink-0 w-fit">
{board.name}
@@ -220,6 +220,9 @@ export function BoardPanelClient({
<div className="absolute inset-0 flex items-center justify-center text-[10px] text-white font-extrabold select-none">n</div>
</div>
<span className="text-[16px] leading-[22px] text-[#5c5c5c] truncate">{stripHtml(post.title)}</span>
{(post.stat?.commentsCount ?? 0) > 0 && (
<span className="ml-1 text-[14px] text-[#f45f00] font-bold shrink-0">[{post.stat?.commentsCount}]</span>
)}
</div>
<div className="h-[16px] relative">
<span className="absolute top-1/2 translate-y-[-50%] text-[12px] leading-[12px] tracking-[-0.24px] text-[#8c8c8c]">
@@ -246,7 +249,7 @@ export function BoardPanelClient({
<div className="flex items-center gap-[8px] shrink-0">
<div className="text-[30px] text-[#5c5c5c] leading-[30px]">{categoryName || board.name}</div>
</div>
<div className="flex items-center gap-[8px] overflow-x-auto flex-nowrap min-w-0 flex-1 scrollbar-thin-x">
<div className="flex items-center gap-[8px] overflow-x-auto flex-nowrap min-w-0 flex-1 no-scrollbar">
{siblingBoards.map((sb) => (
<button
key={sb.id}
@@ -278,7 +281,9 @@ export function BoardPanelClient({
<div className="absolute inset-0 flex items-center justify-center text-[10px] text-white font-extrabold select-none">n</div>
</div>
<span className="text-[16px] leading-[22px] text-[#5c5c5c] truncate max-w-[calc(100vw-280px)]">{stripHtml(p.title)}</span>
<span className="text-[16px] text-[#f45f00] font-bold">+{p.stat?.recommendCount ?? 0}</span>
{(p.stat?.commentsCount ?? 0) > 0 && (
<span className="text-[14px] text-[#f45f00] font-bold">[{p.stat?.commentsCount}]</span>
)}
</Link>
<span className="text-[12px] leading-[12px] tracking-[-0.24px] text-[#8c8c8c]">{formatDateYmd(p.createdAt)}</span>
</div>

View File

@@ -45,8 +45,19 @@ export function BoardToolbar({ boardId }: { boardId: string }) {
};
return (
<div className="flex items-center justify-between px-0 py-2">
<div className="flex items-center gap-2">
<div className="px-0 py-2 flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
{/* 검색바: 모바일에서는 상단 전체폭 */}
<form action={onSubmit} className="order-1 md:order-2 flex items-center gap-2 w-full md:w-auto">
<select name="scope" aria-label="검색대상" className="h-8 px-2 rounded-md border border-neutral-300 bg-white text-sm shrink-0" defaultValue={scope}>
<option value="q">+</option>
<option value="author"></option>
</select>
<input name="text" defaultValue={defaultText} placeholder="검색어를 입력해 주세요." className="h-8 w-full md:w-72 px-3 rounded-md border border-neutral-300 text-sm placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-neutral-300" />
<button type="submit" className="h-8 px-3 rounded-md bg-neutral-900 text-white text-sm hover:bg-neutral-800 shrink-0"></button>
</form>
{/* 필터: 모바일에서는 검색 아래쪽 */}
<div className="order-2 md:order-1 flex items-center gap-2">
<select aria-label="정렬" className="h-8 px-2 rounded-md border border-neutral-300 bg-white text-sm" defaultValue={sort} onChange={onChangeSort}>
<option value="recent"></option>
<option value="popular"></option>
@@ -58,14 +69,6 @@ export function BoardToolbar({ boardId }: { boardId: string }) {
<option value="1m">1</option>
</select>
</div>
<form action={onSubmit} className="flex items-center gap-2">
<select name="scope" aria-label="검색대상" className="h-8 px-2 rounded-md border border-neutral-300 bg-white text-sm" defaultValue={scope}>
<option value="q">+</option>
<option value="author"></option>
</select>
<input name="text" defaultValue={defaultText} placeholder="검색어를 입력해 주세요." className="h-8 w-56 md:w-72 px-3 rounded-md border border-neutral-300 text-sm placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-neutral-300" />
<button type="submit" className="h-8 px-3 rounded-md bg-neutral-900 text-white text-sm hover:bg-neutral-800"></button>
</form>
</div>
);
}

View File

@@ -0,0 +1,265 @@
"use client";
import { useEffect, useState } from "react";
import { UserAvatar } from "./UserAvatar";
import Link from "next/link";
type CommentAuthor = {
userId: string;
nickname: string | null;
profileImage: string | null;
};
type Comment = {
id: string;
parentId: string | null;
depth: number;
content: string;
isAnonymous: boolean;
isSecret: boolean;
author: CommentAuthor | null;
anonId?: string;
createdAt: string;
updatedAt: string;
replies: Comment[];
};
type Props = {
postId: string;
};
export function CommentSection({ postId }: Props) {
const [comments, setComments] = useState<Comment[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [replyingTo, setReplyingTo] = useState<string | null>(null);
const [newContent, setNewContent] = useState("");
const [replyContents, setReplyContents] = useState<Record<string, string>>({});
const [expandedReplies, setExpandedReplies] = useState<Set<string>>(new Set());
useEffect(() => {
loadComments();
}, [postId]);
async function loadComments() {
try {
const res = await fetch(`/api/posts/${postId}/comments`);
if (!res.ok) return;
const data = await res.json();
setComments(data.comments || []);
} catch (e) {
console.error(e);
} finally {
setIsLoading(false);
}
}
async function handleSubmitReply(parentId: string | null) {
const content = parentId ? (replyContents[parentId] ?? "") : newContent;
if (!content.trim()) return;
try {
const res = await fetch("/api/comments", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
postId,
parentId,
content,
}),
});
if (!res.ok) {
const error = await res.json();
alert(error.error || "댓글 작성 실패");
return;
}
if (parentId) {
setReplyContents((prev) => {
const next = { ...prev };
delete next[parentId!];
return next;
});
} else {
setNewContent("");
}
setReplyingTo(null);
loadComments();
} catch (e) {
console.error(e);
alert("댓글 작성 실패");
}
}
function toggleReplies(commentId: string) {
const newExpanded = new Set(expandedReplies);
if (newExpanded.has(commentId)) {
newExpanded.delete(commentId);
} else {
newExpanded.add(commentId);
}
setExpandedReplies(newExpanded);
}
function formatDate(dateString: string) {
const date = new Date(dateString);
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return "방금 전";
if (minutes < 60) return `${minutes}분 전`;
if (hours < 24) return `${hours}시간 전`;
if (days < 7) return `${days}일 전`;
return date.toLocaleDateString();
}
function CommentItem({ comment, depth = 0 }: { comment: Comment; depth?: number }) {
const canReply = depth < 2; // 최대 3단계까지만
const hasReplies = comment.replies && comment.replies.length > 0;
const isExpanded = expandedReplies.has(comment.id);
return (
<div className={`${depth > 0 ? "ml-8 border-l-2 border-neutral-200 pl-4" : ""}`}>
<div className="flex gap-3 py-3">
<UserAvatar
src={comment.author?.profileImage}
alt={comment.author?.nickname || "익명"}
width={40}
height={40}
className="rounded-full shrink-0"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-semibold text-neutral-900">
{comment.isAnonymous ? `익명${comment.anonId}` : comment.author?.nickname || "익명"}
</span>
<span className="text-xs text-neutral-500">{formatDate(comment.createdAt)}</span>
{comment.updatedAt !== comment.createdAt && (
<span className="text-xs text-neutral-400">()</span>
)}
</div>
<div className="text-sm text-neutral-700 mb-2 whitespace-pre-wrap break-words">
{comment.content}
</div>
<div className="flex items-center gap-3">
{canReply && (
<button
onClick={() => {
setReplyingTo(comment.id);
setReplyContents((prev) => ({ ...prev, [comment.id]: prev[comment.id] ?? "" }));
}}
className="text-xs text-neutral-500 hover:text-neutral-900"
>
</button>
)}
{hasReplies && (
<button
onClick={() => toggleReplies(comment.id)}
className="text-xs text-neutral-500 hover:text-neutral-900"
>
{isExpanded ? "답글 숨기기" : `답글 ${comment.replies.length}`}
</button>
)}
</div>
{/* 답글 입력 폼 */}
{replyingTo === comment.id && (
<div className="mt-3 p-3 bg-neutral-50 rounded-lg">
<textarea
value={replyContents[comment.id] ?? ""}
onChange={(e) => setReplyContents((prev) => ({ ...prev, [comment.id]: e.target.value }))}
placeholder="답글을 입력하세요..."
className="w-full p-2 border border-neutral-300 rounded-md text-sm resize-none focus:outline-none focus:ring-2 focus:ring-neutral-400"
rows={3}
/>
<div className="flex gap-2 mt-2 justify-end">
<button
onClick={() => {
setReplyingTo(null);
setReplyContents((prev) => {
const next = { ...prev };
delete next[comment.id];
return next;
});
}}
className="px-3 py-1 text-xs border border-neutral-300 rounded-md hover:bg-neutral-100"
>
</button>
<button
onClick={() => handleSubmitReply(comment.id)}
className="px-3 py-1 text-xs bg-neutral-900 text-white rounded-md hover:bg-neutral-800"
>
</button>
</div>
</div>
)}
</div>
</div>
{/* 대댓글 목록 */}
{hasReplies && isExpanded && (
<div className="mt-2">
{comment.replies.map((reply) => (
<CommentItem key={reply.id} comment={reply} depth={depth + 1} />
))}
</div>
)}
</div>
);
}
if (isLoading) {
return (
<section className="rounded-xl overflow-hidden bg-white p-4">
<div className="text-center text-neutral-500 py-8"> ...</div>
</section>
);
}
return (
<section className="rounded-xl overflow-hidden bg-white">
<header className="px-4 py-3 border-b border-neutral-200">
<h2 className="text-lg font-bold text-neutral-900"> {comments.length}</h2>
</header>
<div className="p-4">
{/* 댓글 입력 폼 */}
<div className="mb-6 pb-6 border-b border-neutral-200">
<textarea
value={newContent}
onChange={(e) => setNewContent(e.target.value)}
placeholder="댓글을 입력하세요..."
className="w-full p-3 border border-neutral-300 rounded-lg text-sm resize-none focus:outline-none focus:ring-2 focus:ring-neutral-400"
rows={4}
/>
<div className="flex justify-end mt-2">
<button
onClick={() => handleSubmitReply(null)}
disabled={!newContent.trim()}
className="px-4 py-2 text-sm bg-neutral-900 text-white rounded-md hover:bg-neutral-800 disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
</div>
</div>
{/* 댓글 목록 */}
{comments.length === 0 ? (
<div className="text-center text-neutral-500 py-8"> .</div>
) : (
<div className="space-y-0">
{comments.map((comment) => (
<CommentItem key={comment.id} comment={comment} />
))}
</div>
)}
</div>
</section>
);
}

View File

@@ -10,7 +10,7 @@ type SubItem = { id: string; name: string; href: string };
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export function HeroBanner({ subItems, activeSubId }: { subItems?: SubItem[]; activeSubId?: string }) {
export function HeroBanner({ subItems, activeSubId, hideSubOnMobile }: { subItems?: SubItem[]; activeSubId?: string; hideSubOnMobile?: boolean }) {
// SWR 캐시 사용: 페이지 전환 시 재요청/깜빡임 최소화
const { data } = useSWR<{ banners: Banner[] }>("/api/banners", fetcher, {
revalidateOnFocus: false,
@@ -78,7 +78,7 @@ export function HeroBanner({ subItems, activeSubId }: { subItems?: SubItem[]; ac
<section className="relative w-full overflow-hidden rounded-xl bg-neutral-900 text-white" aria-roledescription="carousel">
<SelectedBanner className="h-56 sm:h-72 md:h-[264px]" />
{/* 분리된 하단 블랙 바: 높이 58px, 중앙 서브카테고리 (스켈레톤 상태에서도 동일 레이아웃 유지) */}
<div className="h-[58px] bg-black rounded-xl flex items-center justify-center px-2">
<div className={`${hideSubOnMobile ? "hidden md:flex" : "flex"} h-[58px] bg-black rounded-xl items-center justify-center px-2`}>
{Array.isArray(subItems) && subItems.length > 0 && (
<div className="flex flex-wrap items-center gap-[8px]">
{subItems.map((s) => (
@@ -151,7 +151,7 @@ export function HeroBanner({ subItems, activeSubId }: { subItems?: SubItem[]; ac
</div>
{/* 분리된 하단 블랙 바: 높이 58px, 중앙 서브카테고리 */}
<div className="h-[58px] bg-black rounded-xl flex items-center justify-center px-2">
<div className={`${hideSubOnMobile ? "hidden md:flex" : "flex"} h-[58px] bg-black rounded-xl items-center justify-center px-2`}>
{Array.isArray(subItems) && subItems.length > 0 && (
<div className="flex flex-wrap items-center gap-[8px]">
{subItems.map((s) => (

View File

@@ -177,6 +177,9 @@ export function PostList({ boardId, sort = "recent", q, tag, author, authorId, s
<Link 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>}
{stripHtml(p.title)}
{(p.stat?.commentsCount ?? 0) > 0 && (
<span className="ml-1 text-[12px] md:text-[12px] text-[#f94b37] align-middle">[{p.stat?.commentsCount}]</span>
)}
</Link>
{!!p.postTags?.length && (
<div className="mt-1 hidden md:flex flex-wrap gap-1 text-[11px] text-neutral-500">
@@ -205,7 +208,8 @@ export function PostList({ boardId, sort = "recent", q, tag, author, authorId, s
{/* 페이지네이션 */}
{!isEmpty && (
variant === "board" ? (
<div className="mt-4 flex items-center justify-between px-4">
<div className="mt-4 px-4 space-y-3">
{/* 상단: 페이지 이동 컨트롤 */}
<div className="flex items-center gap-2">
{/* Previous */}
<button
@@ -281,35 +285,38 @@ export function PostList({ boardId, sort = "recent", q, tag, author, authorId, s
Next
</button>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-neutral-600"> </span>
<select
value={currentPageSize}
onChange={(e) => {
const newSize = parseInt(e.target.value, 10);
setCurrentPageSize(newSize);
setPage(1);
const nextSp = new URLSearchParams(Array.from(sp.entries()));
nextSp.set("pageSize", String(newSize));
nextSp.set("page", "1");
if (typeof window !== "undefined") {
window.history.replaceState(null, "", `?${nextSp.toString()}`);
}
}}
className="h-9 px-2 rounded-md border border-neutral-300 bg-white text-sm"
>
<option value="10">10</option>
<option value="20">20</option>
<option value="30">30</option>
<option value="40">40</option>
<option value="50">50</option>
</select>
{/* 하단: 표시 개수 + 글쓰기 (모바일에서 아래로 분리) */}
<div className="flex items-center justify-between md:justify-end gap-2">
<div className="flex items-center gap-2">
<span className="text-xs text-neutral-600"> </span>
<select
value={currentPageSize}
onChange={(e) => {
const newSize = parseInt(e.target.value, 10);
setCurrentPageSize(newSize);
setPage(1);
const nextSp = new URLSearchParams(Array.from(sp.entries()));
nextSp.set("pageSize", String(newSize));
nextSp.set("page", "1");
if (typeof window !== "undefined") {
window.history.replaceState(null, "", `?${nextSp.toString()}`);
}
}}
className="h-9 px-2 rounded-md border border-neutral-300 bg-white text-sm"
>
<option value="10">10</option>
<option value="20">20</option>
<option value="30">30</option>
<option value="40">40</option>
<option value="50">50</option>
</select>
</div>
{newPostHref && (
<Link href={newPostHref} className="shrink-0">
<button className="h-9 px-4 rounded-[10px] bg-[#f94b37] text-white text-sm border border-[#d73b29] hover:opacity-95"></button>
</Link>
)}
</div>
{newPostHref && (
<Link href={newPostHref} className="shrink-0">
<button className="h-9 px-4 rounded-[10px] bg-[#f94b37] text-white text-sm border border-[#d73b29] hover:opacity-95"></button>
</Link>
)}
</div>
) : (
<div className="mt-3 flex justify-center">