diff --git a/LOG.md b/LOG.md index 1fa2b73..c111ca0 100644 --- a/LOG.md +++ b/LOG.md @@ -2,6 +2,13 @@ ## 2026-05-28 +- 홈 대표 글 영역을 제거하고 조회수 내림차순 `viewCount,desc` 기준 인기 글 5개 패널로 대체했다. +- 인기 글과 최신 글을 같은 `lg:grid-cols-2` 반반 레이아웃으로 맞추고, 인기 글에는 순위와 조회수를 작은 보조 메타로 표시했다. +- 검증: `npm run lint` 통과, `npm run build` 통과. build 중 sitemap fetch는 로컬 네트워크 제한으로 기존처럼 경고를 출력했지만 실패하지 않았다. Browser에서 홈의 인기 글/최신 글 반반 레이아웃을 확인했다. 로컬 `localhost:3000`에서 배포 API 호출은 CORS 정책상 실데이터가 비어 보여 production Origin 배포 후 실데이터 렌더링 확인이 필요하다. +- 다음 추천 작업: 배포 후 `https://blog.wypark.me` 홈에서 인기 글 5개가 조회수 순으로 보이는지 확인한다. + +## 2026-05-28 + - 상단 우측의 테마 토글, 로그인/회원가입, 관리자/새 글/로그아웃 액션을 작은 메뉴 버튼 뒤로 숨기고, 클릭 시 왼쪽으로 펼쳐지는 패널 애니메이션으로 노출되도록 수정했다. - 메뉴 바깥 클릭과 Escape 키로 닫히게 하고, 링크 이동/로그아웃 시 메뉴가 닫히도록 정리했다. - 검증: `npm run lint` 통과, `npm run build` 통과. build 중 sitemap fetch는 로컬 네트워크 제한으로 기존처럼 경고를 출력했지만 실패하지 않았다. Browser로 데스크톱/모바일 홈에서 닫힘 상태와 펼침 상태를 확인했다. diff --git a/src/app/page.tsx b/src/app/page.tsx index a3b5f2d..7c615fe 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Suspense } from 'react'; +import { Suspense, type ReactNode } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useSearchParams } from 'next/navigation'; import Link from 'next/link'; @@ -10,6 +10,7 @@ import { Clock, Loader2, Search, + TrendingUp, } from 'lucide-react'; import { getPosts } from '@/api/posts'; import EmptyState from '@/components/ui/EmptyState'; @@ -122,43 +123,15 @@ function NoticeStrip({ notices }: { notices: Post[] }) { ); } -function FeaturedPost({ post }: { post?: Post }) { - if (!post) { - return ( - - - - ); - } - - const summary = getSummary(post.content, 170); - - return ( - - -
- {post.categoryName || '미분류'} - -
-
-

대표 글

-

- {post.title} -

- {summary && ( -

- {summary} -

- )} -
-
- - ); -} - -function CompactPostRow({ post }: { post: Post }) { +function CompactPostRow({ + post, + rank, + showViews = false, +}: { + post: Post; + rank?: number; + showViews?: boolean; +}) { const summary = getSummary(post.content, 92); return ( @@ -166,7 +139,12 @@ function CompactPostRow({ post }: { post: Post }) { href={`/posts/${post.slug}`} className="group flex items-start justify-between gap-4 rounded-lg px-1 py-4 transition duration-150 hover:bg-black/[0.025] hover:px-3 dark:hover:bg-white/[0.07]" > -
+ {rank !== undefined && ( + + {rank} + + )} +
{post.categoryName || '미분류'} @@ -174,6 +152,11 @@ function CompactPostRow({ post }: { post: Post }) { + {showViews && ( + + 조회 {post.viewCount.toLocaleString()} + + )}

{post.title} @@ -189,6 +172,48 @@ function CompactPostRow({ post }: { post: Post }) { ); } +function PostListPanel({ + title, + icon, + posts, + isPopular = false, +}: { + title: string; + icon: ReactNode; + posts: Post[]; + isPopular?: boolean; +}) { + return ( + +
+
+ {icon} +

{title}

+
+ + 전체 보기 + + +
+ + {posts.length > 0 ? ( +
+ {posts.map((post, index) => ( + + ))} +
+ ) : ( + + )} +
+ ); +} + function HomeContent() { const searchParams = useSearchParams(); const keyword = searchParams.get('keyword') || ''; @@ -201,7 +226,13 @@ function HomeContent() { const { data: latestData, isLoading: isLatestLoading } = useQuery({ queryKey: ['posts', 'latest'], - queryFn: () => getPosts({ size: 7, sort: 'createdAt,desc' }), + queryFn: () => getPosts({ size: 8, sort: 'createdAt,desc' }), + retry: 0, + }); + + const { data: popularData, isLoading: isPopularLoading } = useQuery({ + queryKey: ['posts', 'popular'], + queryFn: () => getPosts({ size: 8, sort: 'viewCount,desc' }), retry: 0, }); @@ -213,10 +244,9 @@ function HomeContent() { }); const notices = noticesData?.content || []; - const latestPosts = (latestData?.content || []).filter((post) => !isNoticePost(post)); - const featuredPost = latestPosts[0]; - const latestList = latestPosts.slice(1, 7); - const isHomeLoading = !keyword && (isNoticesLoading || isLatestLoading); + const latestList = (latestData?.content || []).filter((post) => !isNoticePost(post)).slice(0, 5); + const popularList = (popularData?.content || []).filter((post) => !isNoticePost(post)).slice(0, 5); + const isHomeLoading = !keyword && (isNoticesLoading || isLatestLoading || isPopularLoading); if (keyword) { return ( @@ -254,29 +284,18 @@ function HomeContent() { -
0 ? 'grid gap-6 lg:grid-cols-[1.1fr_0.9fr]' : 'grid gap-6'}> - - - {latestList.length > 0 && ( - -
-
- -

최신 글

-
- - 전체 보기 - - -
- -
- {latestList.map((post) => ( - - ))} -
-
- )} +
+ } + posts={popularList} + isPopular + /> + } + posts={latestList} + />
);