적용적용
Some checks failed
deploy-on-main / deploy (push) Failing after 23s

This commit is contained in:
koreacomp5
2025-11-08 01:21:44 +09:00
parent bb71b892ca
commit 1c2222da67
9 changed files with 276 additions and 60 deletions

View File

@@ -15,7 +15,7 @@ const navItems = [
export default function AdminSidebar() { export default function AdminSidebar() {
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<aside className="w-56 shrink-0 border-r border-neutral-200 bg-white/80 backdrop-blur h-full"> <aside className="w-56 shrink-0 border-r border-neutral-200 bg-white/90 backdrop-blur h-full">
<div className="px-4 py-4 border-b border-neutral-200"> <div className="px-4 py-4 border-b border-neutral-200">
<Link href="/admin" className="block text-lg font-bold text-neutral-900"></Link> <Link href="/admin" className="block text-lg font-bold text-neutral-900"></Link>
</div> </div>

View File

@@ -83,7 +83,7 @@ export default async function BoardDetail({ params, searchParams }: { params: an
)} )}
{/* 검색/필터 툴바 + 리스트 */} {/* 검색/필터 툴바 + 리스트 */}
<section> <section className="px-[0px] md:px-[30px] ">
{!isSpecialRanking && <BoardToolbar boardId={board?.slug} />} {!isSpecialRanking && <BoardToolbar boardId={board?.slug} />}
<div className="p-0"> <div className="p-0">
{isSpecialRanking ? ( {isSpecialRanking ? (
@@ -133,6 +133,7 @@ export default async function BoardDetail({ params, searchParams }: { params: an
boardId={id} boardId={id}
sort={sort} sort={sort}
variant="board" variant="board"
titleHoverOrange
newPostHref={`/posts/new?boardId=${id}${board?.slug ? `&boardSlug=${board.slug}` : ""}`} newPostHref={`/posts/new?boardId=${id}${board?.slug ? `&boardSlug=${board.slug}` : ""}`}
/> />
)} )}

View File

@@ -343,7 +343,7 @@ export function AppHeader() {
return ( return (
<header <header
ref={headerRef} ref={headerRef}
className={`relative flex items-center justify-between px-4 py-3 bg-white/80 backdrop-blur supports-[backdrop-filter]:bg-white/60 ${ className={`relative flex items-center justify-between px-4 py-3 bg-white/90 backdrop-blur supports-[backdrop-filter]:bg-white/60 ${
megaOpen ? "shadow-[0_6px_24px_rgba(0,0,0,0.10)]" : "" megaOpen ? "shadow-[0_6px_24px_rgba(0,0,0,0.10)]" : ""
}`} }`}
> >
@@ -423,7 +423,7 @@ export function AppHeader() {
/> />
</div> </div>
<div <div
className={`fixed left-0 right-0 z-50 bg-white/80 backdrop-blur supports-[backdrop-filter]:bg-white/60 shadow-[0_12px_32px_rgba(0,0,0,0.12)] transition-all duration-200 ${ className={`fixed left-0 right-0 z-50 bg-white/90 backdrop-blur supports-[backdrop-filter]:bg-white/60 shadow-[0_12px_32px_rgba(0,0,0,0.12)] transition-all duration-200 ${
megaOpen ? "opacity-100" : "pointer-events-none opacity-0" megaOpen ? "opacity-100" : "pointer-events-none opacity-0"
}`} }`}
style={{ top: headerBottom }} style={{ top: headerBottom }}

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import React from "react"; import React, { useState, useRef, useEffect } from "react";
export function BoardToolbar({ boardId }: { boardId: string }) { export function BoardToolbar({ boardId }: { boardId: string }) {
const router = useRouter(); const router = useRouter();
const sp = useSearchParams(); const sp = useSearchParams();
const sort = (sp.get("sort") as "recent" | "popular" | null) ?? "recent"; const sort = (sp.get("sort") as "recent" | "popular" | "views" | "likes" | "comments" | null) ?? "recent";
const scope = (sp.get("scope") as "q" | "author" | null) ?? "q"; // q: 제목+내용, author: 작성자 const scope = (sp.get("scope") as "q" | "author" | null) ?? "q"; // q: 제목+내용, author: 작성자
const defaultText = scope === "author" ? sp.get("author") ?? "" : sp.get("q") ?? ""; const defaultText = scope === "author" ? sp.get("author") ?? "" : sp.get("q") ?? "";
const period = sp.get("period") ?? "all"; // all | 1d | 1w | 1m const period = sp.get("period") ?? "all"; // all | 1d | 1w | 1m
@@ -16,6 +16,109 @@ export function BoardToolbar({ boardId }: { boardId: string }) {
router.push(`/boards/${boardId}?${next.toString()}` , { scroll: false }); router.push(`/boards/${boardId}?${next.toString()}` , { scroll: false });
}; };
const pushSort = (value: string) => {
const next = new URLSearchParams(sp.toString());
next.set("sort", value);
router.push(`/boards/${boardId}?${next.toString()}`, { scroll: false });
};
const SortDropdown = () => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const handler = (ev: MouseEvent) => { if (ref.current && !ref.current.contains(ev.target as Node)) setOpen(false); };
window.addEventListener("click", handler);
return () => window.removeEventListener("click", handler);
}, []);
const label =
sort === "recent" ? "최신순" : sort === "views" ? "조회순" : sort === "likes" ? "좋아요순" : sort === "comments" ? "댓글순" : "인기순";
const items: { value: string; text: string }[] = [
{ value: "recent", text: "최신순" },
{ value: "views", text: "조회순" },
{ value: "likes", text: "좋아요순" },
{ value: "comments", text: "댓글순" },
];
return (
<div ref={ref} className="relative">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="inline-flex h-12 px-4 items-center justify-center gap-1 rounded-[16px] border border-[#d5d5d5] bg-white text-[14px] text-[#161616] min-w-[93px] cursor-pointer hover:bg-neutral-100"
>
<span>{label}</span>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden>
<path d="M5 8l5 5 5-5" stroke="#707070" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
{open && (
<div className="absolute mt-2 left-0 z-20 bg-white rounded-[16px] p-2 shadow-[0_0_2px_rgba(0,0,0,0.05),0_4px_8px_rgba(0,0,0,0.08)] w-[93px]">
<div className="flex flex-col gap-1">
{items.map((it) => (
<button
key={it.value}
type="button"
onClick={() => { setOpen(false); pushSort(it.value); }}
className={`w-full px-3 py-2 rounded-[8px] text-[14px] inline-flex items-center justify-center cursor-pointer ${
sort === it.value ? "bg-[#707070] text-white" : "text-[#707070] hover:bg-neutral-100"
}`}
>
{it.text}
</button>
))}
</div>
</div>
)}
</div>
);
};
const ScopeDropdown = ({ value, onSelect }: { value: "q" | "author"; onSelect: (v: "q" | "author") => void }) => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const handler = (ev: MouseEvent) => { if (ref.current && !ref.current.contains(ev.target as Node)) setOpen(false); };
window.addEventListener("click", handler);
return () => window.removeEventListener("click", handler);
}, []);
const label = value === "author" ? "작성자" : "제목+내용";
const items: { value: "q" | "author"; text: string }[] = [
{ value: "q", text: "제목+내용" },
{ value: "author", text: "작성자" },
];
return (
<div ref={ref} className="relative">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="inline-flex h-12 px-4 items-center justify-center gap-1 rounded-[16px] border border-[#d5d5d5] bg-white text-[14px] text-[#161616] min-w-[93px] cursor-pointer hover:bg-neutral-100"
>
<span>{label}</span>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden>
<path d="M5 8l5 5 5-5" stroke="#707070" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
{open && (
<div className="absolute mt-2 left-0 z-20 bg-white rounded-[16px] p-2 shadow-[0_0_2px_rgba(0,0,0,0.05),0_4px_8px_rgba(0,0,0,0.08)] w-[120px]">
<div className="flex flex-col gap-1">
{items.map((it) => (
<button
key={it.value}
type="button"
onClick={() => { onSelect(it.value); setOpen(false); }}
className={`w-full px-3 py-2 rounded-[8px] text-[14px] inline-flex items-center justify-center cursor-pointer ${
value === it.value ? "bg-[#707070] text-white" : "text-[#707070] hover:bg-neutral-100"
}`}
>
{it.text}
</button>
))}
</div>
</div>
)}
</div>
);
};
const onChangePeriod = (e: React.ChangeEvent<HTMLSelectElement>) => { const onChangePeriod = (e: React.ChangeEvent<HTMLSelectElement>) => {
const next = new URLSearchParams(sp.toString()); const next = new URLSearchParams(sp.toString());
const v = e.target.value; const v = e.target.value;
@@ -48,22 +151,67 @@ export function BoardToolbar({ boardId }: { boardId: string }) {
<div className="px-0 py-2 flex flex-col gap-2 md:flex-row md:items-center md:justify-between"> <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"> <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}> <input type="hidden" name="scope" value={scope} />
<option value="q">+</option> <ScopeDropdown
<option value="author"></option> value={scope}
</select> onSelect={(v) => {
<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" /> // hidden input을 업데이트하기 위해 URL 파라미터는 변경하지 않고 폼 값만 유지
<button type="submit" className="h-8 px-3 rounded-md bg-neutral-900 text-white text-sm hover:bg-neutral-800 shrink-0"></button> // 검색 제출 시 onSubmit에서 반영됨
const form = (document.activeElement as HTMLElement)?.closest("form") as HTMLFormElement | null;
if (form) {
const input = form.querySelector('input[name="scope"]') as HTMLInputElement | null;
if (input) input.value = v;
}
}}
/>
<div className="relative w-full md:w-96 group">
<button
type="submit"
aria-label="검색 실행"
className="absolute right-2 top-2 w-8 h-8 text-neutral-500 cursor-pointer"
>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
className="w-8 h-8 block group-hover:hidden group-focus-within:hidden"
>
<path
d="M21 21L17.682 17.682M17.682 17.682C18.4963 16.8676 19 15.7426 19 14.5C19 12.0147 16.9853 10 14.5 10C12.0147 10 10 12.0147 10 14.5C10 16.9853 12.0147 19 14.5 19C15.7426 19 16.8676 18.4963 17.682 17.682ZM28 16C28 22.6274 22.6274 28 16 28C9.37258 28 4 22.6274 4 16C4 9.37258 9.37258 4 16 4C22.6274 4 28 9.37258 28 16Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
className="w-8 h-8 hidden group-hover:block group-focus-within:block"
>
<path d="M11 14.5C11 12.567 12.567 11 14.5 11C16.433 11 18 12.567 18 14.5C18 15.4668 17.6093 16.3404 16.9749 16.9749C16.3404 17.6093 15.4668 18 14.5 18C12.567 18 11 16.433 11 14.5Z" fill="#707070" />
<path fillRule="evenodd" clipRule="evenodd" d="M16 3C8.8203 3 3 8.8203 3 16C3 23.1797 8.8203 29 16 29C23.1797 29 29 23.1797 29 16C29 8.8203 23.1797 3 16 3ZM14.5 9C11.4624 9 9 11.4624 9 14.5C9 17.5376 11.4624 20 14.5 20C15.6571 20 16.7316 19.6419 17.6174 19.0316L20.2929 21.7071C20.6834 22.0976 21.3166 22.0976 21.7071 21.7071C22.0976 21.3166 22.0976 20.6834 21.7071 20.2929L19.0316 17.6174C19.6419 16.7316 20 15.6571 20 14.5C20 11.4624 17.5376 9 14.5 9Z" fill="#707070" />
</svg>
</button>
<input
name="text"
defaultValue={defaultText}
placeholder="검색어를 입력해 주세요."
className="w-full h-12 pr-12 pl-2 rounded-2xl border bg-white border-neutral-300 hover:border-[2px] hover:border-neutral-500 focus:border-2 focus:border-neutral-800 focus:outline-none transition-colors"
/>
</div>
</form> </form>
{/* 필터: 모바일에서는 검색 아래쪽 */} {/* 필터: 모바일에서는 검색 아래쪽 */}
<div className="order-2 md:order-1 flex items-center gap-2"> <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}> <SortDropdown />
<option value="recent"></option>
<option value="views"></option>
<option value="likes"></option>
<option value="comments"></option>
</select>
</div> </div>
</div> </div>
); );

View File

@@ -7,6 +7,7 @@ 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";
import PlusIcon from "@/app/svgs/PlusIcon";
type Item = { type Item = {
id: string; id: string;
@@ -123,6 +124,60 @@ export function PostList({ boardId, sort = "recent", q, tag, author, authorId, s
const initials = (name?: string | null) => (name ? name.trim().slice(0, 1) : "익"); const initials = (name?: string | null) => (name ? name.trim().slice(0, 1) : "익");
// 표시 개수 드롭다운 (BoardToolbar 드롭다운과 동일한 디자인/호버)
const PageSizeDropdown = () => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const handler = (ev: MouseEvent) => { if (ref.current && !ref.current.contains(ev.target as Node)) setOpen(false); };
window.addEventListener("click", handler);
return () => window.removeEventListener("click", handler);
}, []);
const sizes = [10, 20, 30, 40, 50];
const onSelectSize = (newSize: number) => {
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()}`);
}
};
return (
<div ref={ref} className="relative">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="inline-flex h-12 px-4 items-center justify-center gap-1 rounded-[16px] border border-[#d5d5d5] bg-white text-[14px] text-[#161616] min-w-[93px] cursor-pointer hover:bg-neutral-100"
>
<span>{currentPageSize}</span>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden>
<path d="M5 8l5 5 5-5" stroke="#707070" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
{open && (
<div className="absolute mt-2 left-0 z-20 bg-white rounded-[16px] p-2 shadow-[0_0_2px_rgba(0,0,0,0.05),0_4px_8px_rgba(0,0,0,0.08)] w-[120px]">
<div className="flex flex-col gap-1">
{sizes.map((sz) => (
<button
key={sz}
type="button"
onClick={() => { setOpen(false); onSelectSize(sz); }}
className={`w-full px-3 py-2 rounded-[8px] text-[14px] inline-flex items-center justify-center cursor-pointer ${
currentPageSize === sz ? "bg-[#707070] text-white" : "text-[#707070] hover:bg-neutral-100"
}`}
>
{sz}
</button>
))}
</div>
</div>
)}
</div>
);
};
return ( return (
<div className="w-full"> <div className="w-full">
{/* 정렬 스위치 (board 변형에서는 상단 툴바를 숨김) */} {/* 정렬 스위치 (board 변형에서는 상단 툴바를 숨김) */}
@@ -170,15 +225,30 @@ export function PostList({ boardId, sort = "recent", q, tag, author, authorId, s
<div ref={listContainerRef} style={{ minHeight: lockedMinHeight ? `${lockedMinHeight}px` : undefined }}> <div ref={listContainerRef} style={{ minHeight: lockedMinHeight ? `${lockedMinHeight}px` : undefined }}>
<ul className="divide-y divide-[#ececec]"> <ul className="divide-y divide-[#ececec]">
{items.map((p) => ( {items.map((p) => (
<li key={p.id} className={`px-4 ${variant === "board" ? (compact ? "py-1.5" : "py-2.5") : "py-3 md:py-3"} hover:bg-neutral-50 transition-colors`}> <li key={p.id} className={`px-4 ${variant === "board" ? "" : "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 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>
<div className="min-w-0"> <div className="min-w-0">
<Link href={`/posts/${p.id}`} className={`group block truncate text-neutral-900`}> <Link href={`/posts/${p.id}`} className={`group block truncate text-neutral-900 ${variant === "board" ? "py-[36px]" : ""}`}>
{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.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>}
<span className={`${titleHoverOrange ? "group-hover:text-[var(--red-50,#F94B37)] group-hover:font-[700] group-hover:text-[16px] group-hover:leading-[22px]" : ""} ${compact ? "text-[14px] leading-[20px]" : "text-[15px] md:text-base"}`} style={{ fontFamily: "var(--font-family-Font-1, Pretendard)" }}> <span
className={`${
variant === "board" && titleHoverOrange
? "text-[24px] leading-[24px] font-[400] text-[#161616]"
: compact
? "text-[14px] leading-[20px]"
: "text-[15px] md:text-base"
} ${
titleHoverOrange
? variant === "board"
? "group-hover:text-[var(--red-50,#F94B37)] group-hover:font-[700] group-hover:text-[24px] group-hover:leading-[24px]"
: "group-hover:text-[var(--red-50,#F94B37)] group-hover:font-[700]"
: ""
}`}
style={{ fontFamily: "var(--font-family-Font-1, Pretendard)" }}
>
{stripHtml(p.title)} {stripHtml(p.title)}
</span> </span>
{(p.stat?.commentsCount ?? 0) > 0 && ( {(p.stat?.commentsCount ?? 0) > 0 && (
@@ -214,7 +284,7 @@ export function PostList({ boardId, sort = "recent", q, tag, author, authorId, s
variant === "board" ? ( variant === "board" ? (
<div className="mt-4 px-4 space-y-3"> <div className="mt-4 px-4 space-y-3">
{/* 상단: 페이지 이동 컨트롤 */} {/* 상단: 페이지 이동 컨트롤 */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-1">
{/* Previous */} {/* Previous */}
<button <button
onClick={() => { onClick={() => {
@@ -228,7 +298,7 @@ export function PostList({ boardId, sort = "recent", q, tag, author, authorId, s
} }
}} }}
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="inline-flex items-center justify-center gap-1 h-[36px] px-[12px] py-[8px] rounded-[6px] border border-transparent bg-transparent text-[14px] text-[#161616] disabled:opacity-50 cursor-pointer hover:bg-neutral-100"
> >
Previous Previous
</button> </button>
@@ -261,12 +331,12 @@ export function PostList({ boardId, sort = "recent", q, tag, author, authorId, s
} }
}} }}
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={`inline-flex items-center justify-center h-[36px] w-[36px] rounded-[6px] text-[14px] cursor-pointer ${n === page ? "border border-[#d5d5d5] bg-white text-[#f94b37] font-semibold hover:bg-neutral-100" : "border border-transparent bg-transparent text-[#161616] hover:bg-neutral-100"}`}
> >
{n} {n}
</button> </button>
) : ( ) : (
<span key={`e-${idx}`} className="h-9 w-9 inline-flex items-center justify-center text-neutral-900"></span> <span key={`e-${idx}`} className="inline-flex items-center justify-center h-[36px] w-[36px] text-[#161616]"></span>
) )
); );
})()} })()}
@@ -284,7 +354,7 @@ export function PostList({ boardId, sort = "recent", q, tag, author, authorId, s
} }
}} }}
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="inline-flex items-center justify-center gap-1 h-[36px] px-[12px] py-[8px] rounded-[6px] border border-transparent bg-transparent text-[14px] text-[#161616] disabled:opacity-50 cursor-pointer hover:bg-neutral-100"
> >
Next Next
</button> </button>
@@ -293,31 +363,14 @@ export function PostList({ boardId, sort = "recent", q, tag, author, authorId, s
<div className="flex items-center justify-between md:justify-end gap-2"> <div className="flex items-center justify-between md:justify-end gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs text-neutral-600"> </span> <span className="text-xs text-neutral-600"> </span>
<select <PageSizeDropdown />
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> </div>
{newPostHref && ( {newPostHref && (
<Link href={newPostHref} className="shrink-0"> <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> <button className="inline-flex justify-center items-center gap-1 shrink-0 h-[36px] px-[12px] py-[6px] rounded-[10px] border border-[#d73b29] bg-[#f94b37] hover:bg-[#d73b29] text-white text-sm cursor-pointer">
<PlusIcon width={16} height={16} fill="white" />
<span></span>
</button>
</Link> </Link>
)} )}
</div> </div>

View File

@@ -25,7 +25,7 @@ export default function RootLayout({
<ToastProvider> <ToastProvider>
<AutoLoginAdmin /> <AutoLoginAdmin />
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
<div className="sticky top-0 z-50 bg-white/80 backdrop-blur"> <div className="sticky top-0 z-50 bg-white/90 backdrop-blur">
<div className="mx-auto w-full"> <div className="mx-auto w-full">
<Suspense fallback={null}> <Suspense fallback={null}>
<AppHeader /> <AppHeader />
@@ -33,7 +33,7 @@ export default function RootLayout({
</div> </div>
</div> </div>
<main className="flex-1 bg-[#F2F2F2]"> <main className="flex-1 bg-[#F2F2F2]">
<div className="max-w-[1920px] mx-auto px-4 py-6"> <div className="max-w-[1500px] mx-auto px-4 py-6">
{children} {children}
</div> </div>
</main> </main>

View File

@@ -195,7 +195,7 @@ export function RelatedPosts({ boardId, boardName, currentPostId, pageSize = 10
<button <button
onClick={() => handlePageChange(Math.max(1, page - 1))} onClick={() => handlePageChange(Math.max(1, page - 1))}
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 cursor-pointer hover:bg-neutral-100"
> >
Previous Previous
</button> </button>
@@ -220,7 +220,7 @@ export function RelatedPosts({ boardId, boardName, currentPostId, pageSize = 10
key={`p-${n}-${idx}`} key={`p-${n}-${idx}`}
onClick={() => handlePageChange(n)} onClick={() => handlePageChange(n)}
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 cursor-pointer ${n === page ? "border-neutral-300 text-[#f94b37] font-semibold hover:bg-neutral-100" : "border-neutral-300 text-neutral-900 hover:bg-neutral-100"}`}
> >
{n} {n}
</button> </button>
@@ -234,7 +234,7 @@ export function RelatedPosts({ boardId, boardName, currentPostId, pageSize = 10
<button <button
onClick={() => handlePageChange(Math.min(totalPages, page + 1))} onClick={() => handlePageChange(Math.min(totalPages, page + 1))}
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 cursor-pointer hover:bg-neutral-100"
> >
Next Next
</button> </button>
@@ -244,7 +244,7 @@ export function RelatedPosts({ boardId, boardName, currentPostId, pageSize = 10
<select <select
value={currentPageSize} value={currentPageSize}
onChange={(e) => handlePageSizeChange(parseInt(e.target.value, 10))} onChange={(e) => handlePageSizeChange(parseInt(e.target.value, 10))}
className="h-9 px-2 rounded-md border border-neutral-300 bg-white text-sm" className="h-[36px] px-[12px] rounded-[6px] border border-[#d5d5d5] bg-white text-[14px] text-[#161616] cursor-pointer hover:bg-neutral-100"
> >
<option value="10">10</option> <option value="10">10</option>
<option value="20">20</option> <option value="20">20</option>

View File

@@ -64,12 +64,14 @@ export default async function PostDetail({ params }: { params: any }) {
{/* 같은 게시판 게시글 목록 */} {/* 같은 게시판 게시글 목록 */}
{post?.boardId && post?.board && ( {post?.boardId && post?.board && (
<RelatedPosts <section className="px-[0px] md:px-[30px]">
boardId={post.boardId} <RelatedPosts
boardName={post.board.name} boardId={post.boardId}
currentPostId={id} boardName={post.board.name}
pageSize={10} currentPostId={id}
/> pageSize={10}
/>
</section>
)} )}
</div> </div>
); );

12
src/app/svgs/PlusIcon.tsx Normal file
View File

@@ -0,0 +1,12 @@
"use client";
import React from "react";
export default function PlusIcon({ width = 16, height = 16, fill = "white", className }: { width?: number; height?: number; fill?: string; className?: string }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 16 16" fill="none" className={className} aria-hidden>
<path d="M7 14V9H2C1.44772 9 1 8.55228 1 8C1 7.44772 1.44772 7 2 7H7V2C7 1.44772 7.44772 1 8 1C8.55228 1 9 1.44772 9 2V7H14C14.5523 7 15 7.44772 15 8C15 8.55228 14.5523 9 14 9H9V14C9 14.5523 8.55228 15 8 15C7.44772 15 7 14.5523 7 14Z" fill={fill}/>
</svg>
);
}