diff --git a/apps/blog/src/app/(main)/page.tsx b/apps/blog/src/app/(main)/page.tsx index d6466aa..2902133 100644 --- a/apps/blog/src/app/(main)/page.tsx +++ b/apps/blog/src/app/(main)/page.tsx @@ -20,6 +20,11 @@ export default async function LandingPage() { continue; } + // 랜딩 페이지(최신글 통합 피드)에서 제외할 시리즈. 시리즈 자체 페이지(/posts/{id})는 영향받지 않는다. + if (Constants.series.excludedFromLatestIds.includes(series.id)) { + continue; + } + const seriesPost = await findPosts({ seriesId: series.id, }); diff --git a/apps/blog/src/libs/api/find-posts.test.ts b/apps/blog/src/libs/api/find-posts.test.ts index 8dbd823..62088b2 100644 --- a/apps/blog/src/libs/api/find-posts.test.ts +++ b/apps/blog/src/libs/api/find-posts.test.ts @@ -1,5 +1,6 @@ import { findPosts } from '@libs/api/find-posts'; import { getAllMdxFiles, MdxFileInfo } from '@libs/api/mdx-utils'; +import { Constants } from '@libs/constants'; // fs를 읽는 getAllMdxFiles를 mock해 실제 글과 무관하게 "필터/정렬/limit" 로직만 검증한다. jest.mock('@libs/api/mdx-utils'); @@ -59,6 +60,44 @@ describe('findPosts', () => { expect(posts).toHaveLength(2); }); + describe('excludedFromLatestIds (#25)', () => { + const original = Constants.series.excludedFromLatestIds; + afterEach(() => { + Constants.series.excludedFromLatestIds = original; + }); + + it("seriesId가 'latest'면 제외 목록의 시리즈 글을 뺀다", async () => { + Constants.series.excludedFromLatestIds = ['fullstack']; + mockGetAll.mockResolvedValue([ + f('frontend/a', { series: 'frontend', date: '2026-01-01 00:00' }), + f('fullstack/x', { series: 'fullstack', date: '2026-02-01 00:00' }), + ]); + + const posts = await findPosts({ seriesId: 'latest' }); + expect(posts.map((p) => p.slug)).toEqual(['frontend/a']); + }); + + it('제외 목록에 있어도 해당 시리즈를 직접 조회하면 그대로 반환한다', async () => { + Constants.series.excludedFromLatestIds = ['fullstack']; + mockGetAll.mockResolvedValue([ + f('fullstack/x', { series: 'fullstack', date: '2026-02-01 00:00' }), + ]); + + const posts = await findPosts({ seriesId: 'fullstack' }); + expect(posts.map((p) => p.slug)).toEqual(['fullstack/x']); + }); + + it('seriesId 없이 호출하면(전체 글 목록) 제외 목록의 영향을 받지 않는다', async () => { + Constants.series.excludedFromLatestIds = ['fullstack']; + mockGetAll.mockResolvedValue([ + f('fullstack/x', { series: 'fullstack', date: '2026-02-01 00:00' }), + ]); + + const posts = await findPosts(); + expect(posts.map((p) => p.slug)).toEqual(['fullstack/x']); + }); + }); + it("orderBy 'createAtASC'는 오래된 순으로 정렬한다", async () => { mockGetAll.mockResolvedValue([ f('a', { series: 's', date: '2026-03-01 00:00' }), diff --git a/apps/blog/src/libs/api/find-posts.ts b/apps/blog/src/libs/api/find-posts.ts index 2c667fd..f38be1a 100644 --- a/apps/blog/src/libs/api/find-posts.ts +++ b/apps/blog/src/libs/api/find-posts.ts @@ -21,7 +21,13 @@ export async function findPosts( posts = posts.filter((post) => !post.frontMatter.isSeriesLanding); // 시리즈 필터링 - if (seriesId && seriesId !== Constants.series.latestId) { + if (seriesId === Constants.series.latestId) { + // '최신글' 통합 피드(/posts/latest)에서는 설정된 시리즈를 제외한다. + // seriesId가 없는 호출(전체 글 목록·태그·사이트맵 등)은 영향받지 않는다. + posts = posts.filter( + (post) => !Constants.series.excludedFromLatestIds.includes(post.frontMatter.series ?? ''), + ); + } else if (seriesId) { posts = posts.filter((post) => post.frontMatter.series === seriesId); } diff --git a/apps/blog/src/libs/constants.ts b/apps/blog/src/libs/constants.ts index af2c38f..57e7fcb 100644 --- a/apps/blog/src/libs/constants.ts +++ b/apps/blog/src/libs/constants.ts @@ -1,6 +1,9 @@ export const Constants = { series: { latestId: 'latest', + // '최신글' 통합 피드(랜딩 페이지 기본 보기 + /posts/latest)에서 제외할 시리즈 ID. + // 개별 시리즈 페이지(/posts/{id})와 태그·사이트맵 등 전체 글 목록에는 영향을 주지 않는다. + excludedFromLatestIds: [] as string[], }, a11y: { // skip 링크(본문 바로가기)와
타깃을 잇는 앵커 id. 두 파일이 같은 값을 공유하도록 단일화한다.