diff --git a/public/uploads/1762349790327-56eucbsdkiy.webp b/public/uploads/1762349790327-56eucbsdkiy.webp new file mode 100644 index 0000000..8373104 Binary files /dev/null and b/public/uploads/1762349790327-56eucbsdkiy.webp differ diff --git a/public/uploads/1762349798482-lg2199h4w0h.webp b/public/uploads/1762349798482-lg2199h4w0h.webp new file mode 100644 index 0000000..9bbf507 Binary files /dev/null and b/public/uploads/1762349798482-lg2199h4w0h.webp differ diff --git a/src/app/components/AppHeader.tsx b/src/app/components/AppHeader.tsx index 9fd1f5f..fea93c4 100644 --- a/src/app/components/AppHeader.tsx +++ b/src/app/components/AppHeader.tsx @@ -377,7 +377,7 @@ export function AppHeader() { if (!e.currentTarget.contains(next)) setMegaOpen(false); }} > -
+
{categories.map((cat, idx) => (
{/*
{cat.name}
*/} -
+
{cat.boards.map((b) => ( {b.name} diff --git a/src/app/components/BoardPanelClient.tsx b/src/app/components/BoardPanelClient.tsx index d9cb16d..74f949f 100644 --- a/src/app/components/BoardPanelClient.tsx +++ b/src/app/components/BoardPanelClient.tsx @@ -43,19 +43,26 @@ type BoardPanelData = { textPosts?: PostData[]; }; -export function BoardPanelClient({ - initialBoardId, - boardsData -}: { - initialBoardId: string; +export function BoardPanelClient({ + initialBoardId, + boardsData +}: { + initialBoardId: string; boardsData: BoardPanelData[]; }) { const [selectedBoardId, setSelectedBoardId] = useState(initialBoardId); - + // 선택된 게시판 데이터 찾기 const selectedBoardData = boardsData.find(bd => bd.board.id === selectedBoardId) || boardsData[0]; const { board, categoryName, siblingBoards } = selectedBoardData; + const isNewWithin1Hour = (createdAt: Date | string | number | null | undefined): boolean => { + if (!createdAt) return false; + const t = new Date(createdAt).getTime(); + if (Number.isNaN(t)) return false; + return (Date.now() - t) <= 60 * 60 * 1000; + }; + function formatDateYmd(d: Date) { const date = new Date(d); const yyyy = date.getFullYear(); @@ -96,15 +103,22 @@ export function BoardPanelClient({
{categoryName || board.name}
+ + + + + + + +
{siblingBoards.map((sb) => ( @@ -113,45 +127,49 @@ export function BoardPanelClient({
-
-
+
+
{selectedBoardData.specialRankUsers.map((user, idx) => { const rank = idx + 1; return ( - -
- +
+ -
- +
+
-
-
-
-
- {rank === 1 && } - {rank === 2 && } - {rank === 3 && } -
-
+
+
+
+ { + (rank === 1 || rank === 2 || rank === 3) && ( +
+ {rank === 1 && } + {rank === 2 && } + {rank === 3 && } +
+ ) + } +
{rank}위
-
+
{user.nickname || "익명"}
-
- - +
+ + - {user.points.toLocaleString()} + {user.points.toLocaleString()}
@@ -172,15 +190,22 @@ export function BoardPanelClient({
{categoryName || board.name}
+ + + + + + + +
{siblingBoards.map((sb) => ( @@ -189,14 +214,14 @@ export function BoardPanelClient({
-
-
+
+
{selectedBoardData.previewPosts.map((post) => { // attachments에서 이미지를 먼저 찾고, 없으면 content에서 추출 const firstImage = post.attachments?.[0]?.url || extractImageFromContent(post.content); return ( - -
+ +
{firstImage ? ( )}
-
-
-
+
+
+
{board.name}
-
-
-
n
-
- {stripHtml(post.title)} + {isNewWithin1Hour(post.createdAt) && ( +
+ + + +
n
+
+ )} + {stripHtml(post.title)} {(post.stat?.commentsCount ?? 0) > 0 && ( - [{post.stat?.commentsCount}] + [{post.stat?.commentsCount}] )}
- + {formatDateYmd(post.createdAt)}
@@ -248,15 +277,24 @@ export function BoardPanelClient({
{categoryName || board.name}
+ + {/* 기본 아이콘 */} + + + + {/* 호버 아이콘 */} + + + +
{siblingBoards.map((sb) => ( @@ -264,23 +302,29 @@ export function BoardPanelClient({
-
- {board.name} - 더보기 -
+ {!isTextMain && ( +
+ {board.name} + 더보기 +
+ )}
{isTextMain && selectedBoardData.textPosts ? (
    {selectedBoardData.textPosts.map((p) => ( -
  • +
  • - -
    -
    -
    n
    -
    - {stripHtml(p.title)} + + {isNewWithin1Hour(p.createdAt) && ( +
    + + + +
    n
    +
    + )} + {stripHtml(p.title)} {(p.stat?.commentsCount ?? 0) > 0 && ( [{p.stat?.commentsCount}] )} @@ -292,7 +336,7 @@ export function BoardPanelClient({
) : ( - + )}
diff --git a/src/app/components/HeroBanner.tsx b/src/app/components/HeroBanner.tsx index 6b58cab..5481bd3 100644 --- a/src/app/components/HeroBanner.tsx +++ b/src/app/components/HeroBanner.tsx @@ -88,7 +88,7 @@ export function HeroBanner({ subItems, activeSubId, hideSubOnMobile }: { subItem className={ s.id === activeSubId ? "px-3 h-[28px] rounded-full bg-[#F94B37] text-white text-[12px] leading-[28px] whitespace-nowrap" - : "px-3 h-[28px] rounded-full bg-transparent text-white/85 hover:text-white border border-white/30 text-[12px] leading-[28px] whitespace-nowrap" + : "px-3 h-[28px] rounded-full bg-transparent text-white/85 hover:bg-[#F94B37] hover:text-white text-[12px] leading-[28px] whitespace-nowrap" } > {s.name} @@ -130,6 +130,32 @@ export function HeroBanner({ subItems, activeSubId, hideSubOnMobile }: { subItem ))}
+ {/* 좌우 내비게이션 버튼 */} + {numSlides > 1 && ( +
+ + +
+ )} + {/* Pagination - Figma 스타일: 활성은 주황 바, 비활성은 회색 점 (배너 위에 오버랩) */} {numSlides > 1 && (
@@ -161,7 +187,7 @@ export function HeroBanner({ subItems, activeSubId, hideSubOnMobile }: { subItem className={ s.id === activeSubId ? "px-3 h-[28px] rounded-full bg-[#F94B37] text-white text-[12px] leading-[28px] whitespace-nowrap" - : "px-3 h-[28px] rounded-full bg-transparent text-white/85 hover:text-white border border-white/30 text-[12px] leading-[28px] whitespace-nowrap" + : "px-3 h-[28px] rounded-full bg-transparent text-white/85 hover:bg-[#F94B37] hover:text-white text-[12px] leading-[28px] whitespace-nowrap" } > {s.name} diff --git a/src/app/components/PostList.tsx b/src/app/components/PostList.tsx index e67c19d..819df63 100644 --- a/src/app/components/PostList.tsx +++ b/src/app/components/PostList.tsx @@ -35,7 +35,7 @@ function stripHtml(html: string | null | undefined): string { return html.replace(/<[^>]*>/g, "").trim(); } -export function PostList({ boardId, sort = "recent", q, tag, author, authorId, start, end, variant = "default", newPostHref }: { boardId?: string; sort?: "recent" | "popular"; q?: string; tag?: string; author?: string; authorId?: string; start?: string; end?: string; variant?: "default" | "board"; newPostHref?: string }) { +export function PostList({ boardId, sort = "recent", q, tag, author, authorId, start, end, variant = "default", newPostHref, titleHoverOrange, pageSizeOverride, compact }: { boardId?: string; sort?: "recent" | "popular"; q?: string; tag?: string; author?: string; authorId?: string; start?: string; end?: string; variant?: "default" | "board"; newPostHref?: string; titleHoverOrange?: boolean; pageSizeOverride?: number; compact?: boolean }) { const sp = useSearchParams(); const listContainerRef = useRef(null); const [lockedMinHeight, setLockedMinHeight] = useState(null); @@ -43,7 +43,9 @@ export function PostList({ boardId, sort = "recent", q, tag, author, authorId, s // board 변형에서는 URL에서 pageSize를 읽고, 기본값은 20 const defaultPageSize = variant === "board" ? 20 : 10; const pageSizeParam = sp.get("pageSize"); - const pageSize = pageSizeParam ? Math.min(50, Math.max(10, parseInt(pageSizeParam, 10))) : defaultPageSize; + const pageSize = (variant === "board" && pageSizeOverride) + ? pageSizeOverride + : (pageSizeParam ? Math.min(50, Math.max(10, parseInt(pageSizeParam, 10))) : defaultPageSize); // board 변형: 번호 페이지네이션 const initialPage = useMemo(() => Math.max(1, parseInt(sp.get("page") || "1", 10)), [sp]); @@ -168,15 +170,17 @@ export function PostList({ boardId, sort = "recent", q, tag, author, authorId, s
    {items.map((p) => ( -
  • +
  • {/* bullet/공지 아이콘 자리 */}
    {p.isPinned ? "★" : "•"}
    - + {p.isPinned && 공지} - {stripHtml(p.title)} + + {stripHtml(p.title)} + {(p.stat?.commentsCount ?? 0) > 0 && ( [{p.stat?.commentsCount}] )} diff --git a/src/app/page.tsx b/src/app/page.tsx index 936fc34..2b045cc 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -57,6 +57,14 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s if (admin) currentUser = admin; } + // 내가 쓴 게시글/댓글 수 + let myPostsCount = 0; + let myCommentsCount = 0; + if (currentUser) { + myPostsCount = await prisma.post.count({ where: { authorId: currentUser.userId, status: "published" } }); + myCommentsCount = await prisma.comment.count({ where: { authorId: currentUser.userId } }); + } + // 메인페이지 설정 불러오기 const SETTINGS_KEY = "mainpage_settings" as const; const settingRow = await prisma.setting.findUnique({ where: { key: SETTINGS_KEY } }); @@ -138,7 +146,7 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s select: { userId: true, nickname: true, points: true, profileImage: true, grade: true }, where: { status: "active" }, orderBy: { points: "desc" }, - take: 3, + take: 6, }); } else if (isPreview) { previewPosts = await prisma.post.findMany({ @@ -157,14 +165,14 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s stat: { select: { commentsCount: true } }, }, orderBy: { createdAt: "desc" }, - take: 3, + take: 6, }); } else if (isTextMain) { textPosts = await prisma.post.findMany({ where: { boardId: sb.id, status: "published" }, select: { id: true, title: true, createdAt: true, stat: { select: { recommendCount: true, commentsCount: true } } }, orderBy: { createdAt: "desc" }, - take: 7, + take: 16, }); } // 기본 타입은 PostList가 자체적으로 API를 호출하므로 데이터 미리 가져오지 않음 @@ -270,28 +278,38 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s
    - - - - 내 정보 페이지 + + + + + 내 정보 페이지 + - - - - 포인트 히스토리 + + + + + 포인트 히스토리 + - - - - 내가 쓴 게시글 + + + + + 내가 쓴 게시글 + + {myPostsCount.toLocaleString()}개 - - - - 내가 쓴 댓글 + + + + + 내가 쓴 댓글 + + {myCommentsCount.toLocaleString()}개
    diff --git a/src/app/posts/new/page.tsx b/src/app/posts/new/page.tsx index d5e2a85..a783395 100644 --- a/src/app/posts/new/page.tsx +++ b/src/app/posts/new/page.tsx @@ -82,7 +82,7 @@ export default function NewPostPage() { className="h-16 rounded-2xl border border-neutral-300 px-6 text-base text-neutral-900 bg-white hover:bg-neutral-50" onClick={() => {/* 태그 선택 자리표시 */}} > - 테그선택 + 태그선택