Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/blog/src/app/(main)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
39 changes: 39 additions & 0 deletions apps/blog/src/libs/api/find-posts.test.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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' }),
Expand Down
8 changes: 7 additions & 1 deletion apps/blog/src/libs/api/find-posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
3 changes: 3 additions & 0 deletions apps/blog/src/libs/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export const Constants = {
series: {
latestId: 'latest',
// '최신글' 통합 피드(랜딩 페이지 기본 보기 + /posts/latest)에서 제외할 시리즈 ID.
// 개별 시리즈 페이지(/posts/{id})와 태그·사이트맵 등 전체 글 목록에는 영향을 주지 않는다.
excludedFromLatestIds: [] as string[],
},
a11y: {
// skip 링크(본문 바로가기)와 <main> 타깃을 잇는 앵커 id. 두 파일이 같은 값을 공유하도록 단일화한다.
Expand Down
Loading