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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,4 @@ next-env.d.ts
.husky/
.sisyphus/
.claude/
/.agentation/
.idea
25 changes: 13 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
- 현재 MDX 파이프라인을 보존한다. MDX는 `next.config.mjs`의 `@mdx-js/loader`
기반 커스텀 webpack rule에 남겨둔다. 전체 콘텐츠 파이프라인을 의도적으로
마이그레이션하지 않는 한 Next.js 내장 MDX로 바꾸지 않는다.
- 시각화 중심 UI는 `shared/visualization/`에 둔다. 블로그 MDX가 재사용하는
시각화 컴포넌트는 도메인 코드가 아니라 shared 시각화 모듈로 관리한다.
- 시각화 중심 UI는 `blog/ui/visualization/`에 둔다. 블로그 MDX가 재사용하는
시각화 컴포넌트는 블로그 콘텐츠 렌더링 책임으로 관리한다.
- `any`를 사용하지 않는다. 구체 타입을 쓰거나 `unknown`과 narrowing을
사용한다.
- `p-[13px]` 같은 arbitrary Tailwind value를 사용하지 않는다. 표준 utility,
shared token, 기존 스타일 패턴을 사용한다.
ui token, 기존 스타일 패턴을 사용한다.
- DDD의 전략적 설계 개념을 차용한 domain-first modular monolith 구조를
유지한다. 주요 도메인은 `src`와 같은 최상위 모듈로 분리하고, `src/app`은
Next.js route adapter로 제한한다.
유지한다. `app/`은 Next.js route adapter로 제한하고, 주요 도메인은
저장소 최상위 모듈로 분리한다.
- `src/` 디렉터리를 재도입하지 않는다. 새 코드 경계는 `app/`, 도메인 모듈,
`site/`, `infra/`, `ui/`, `styles/` 중 하나에 둔다.
- 블로그 콘텐츠 구조는 `posts/**/index.mdx`와 주변 `meta.json`으로 유지한다.
중첩된 series 디렉터리도 보존한다.
- 변경이 아래 ADR 작성 조건에 걸리면 반드시 ADR을 작성하거나 갱신한다.
Expand All @@ -39,19 +41,18 @@ ADR 작성 기준:

## 프로젝트 구조

- `src/app/`: Next.js App Router route adapter, route handler, metadata entry
- `app/`: Next.js App Router route adapter, route handler, metadata entry
- `blog/`: 글 도메인. post schema, repository, publication policy, series,
blog UI, view-count use case
blog UI, RSS feed serialization, view-count use case
- `resume/`: 이력서 도메인. resume data, ordering, resume UI
- `search/`: 검색 도메인. command palette, search action, recommendation
- `site/`: 도메인 조합 layer. home, AppShell, navigation, provider, site config
- `platform/`: 외부/런타임 인프라. Supabase, Umami analytics, SEO helper,
devtool integration
- `shared/`: 도메인 지식 없는 UI primitive, motion helper, testing helper,
visualization widget
- `infra/`: 외부/런타임 인프라. Supabase, Umami analytics, SEO helper,
integration adapter
- `ui/`: 도메인 지식 없는 UI primitive, layout primitive, motion helper
- `styles/`: design token과 global style
- `posts/`: 중첩 가능한 `index.mdx`와 `meta.json` 기반 블로그 콘텐츠
- `tests/`: Playwright E2E 테스트
- `tests/`: Playwright E2E 테스트와 Vitest support helper
- `tooling/`: script와 tool configuration

## 개발 명령
Expand Down
139 changes: 0 additions & 139 deletions ARCHITECTURE.md

This file was deleted.

10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,17 @@

```text
eunu.log/
├── 📁 src/
│ └── 📁 app/ # Next.js route adapter
├── 📁 app/ # Next.js route adapter
├── 📁 blog/ # 글 도메인
├── 📁 resume/ # 이력서 도메인
├── 📁 search/ # 검색 도메인
├── 📁 site/ # 홈, AppShell, provider, site config
├── 📁 platform/ # Supabase, Umami, SEO, devtool integration
├── 📁 shared/ # 도메인 지식 없는 UI/모션/테스트/시각화
├── 📁 infra/ # Supabase, Umami, SEO integration
├── 📁 ui/ # 도메인 지식 없는 UI/레이아웃/모션
├── 📁 styles/ # 전역 스타일과 토큰
├── 📁 tests/
│ └── 📁 e2e/ # Playwright E2E 테스트
│ ├── 📁 e2e/ # Playwright E2E 테스트
│ └── 📁 support/ # Vitest support helper
├── 📁 tooling/
│ ├── 📁 config/ # lint/spell 설정
│ └── 📁 scripts/ # 자동화/유틸 스크립트
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions src/app/blog/[slug]/error.tsx → app/blog/[slug]/error.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';

import { useEffect } from 'react';
import { RouteError } from '@/shared/ui';
import { AnalyticsEvents, trackEvent } from '@/platform/analytics/lib/analytics';
import { RouteError } from '@/ui';
import { AnalyticsEvents, trackEvent } from '@/infra/analytics/lib/analytics';

interface BlogPostErrorProps {
error: Error & { digest?: string };
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions src/app/blog/error.tsx → app/blog/error.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';

import { useEffect } from 'react';
import { RouteError } from '@/shared/ui';
import { AnalyticsEvents, trackEvent } from '@/platform/analytics/lib/analytics';
import { RouteError } from '@/ui';
import { AnalyticsEvents, trackEvent } from '@/infra/analytics/lib/analytics';

interface BlogErrorProps {
error: Error & { digest?: string };
Expand Down
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions src/app/error.tsx → app/error.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';

import { useEffect } from 'react';
import { RouteError } from '@/shared/ui';
import { AnalyticsEvents, trackEvent } from '@/platform/analytics/lib/analytics';
import { RouteError } from '@/ui';
import { AnalyticsEvents, trackEvent } from '@/infra/analytics/lib/analytics';

interface RootErrorProps {
error: Error & { digest?: string };
Expand Down
File renamed without changes
3 changes: 2 additions & 1 deletion src/app/layout.tsx → app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AppShell } from '@/site/shell/AppShell';
import {
SITE_AUTHOR,
SITE_DESCRIPTION,
SITE_FEED_PATH,
SITE_NAME,
SITE_URL,
} from '@/site/config/site';
Expand Down Expand Up @@ -47,7 +48,7 @@ export const metadata: Metadata = {
},
alternates: {
types: {
'application/rss+xml': '/feed.xml',
'application/rss+xml': SITE_FEED_PATH,
},
},
};
Expand Down
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions src/app/resume/error.tsx → app/resume/error.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';

import { useEffect } from 'react';
import { RouteError } from '@/shared/ui';
import { AnalyticsEvents, trackEvent } from '@/platform/analytics/lib/analytics';
import { RouteError } from '@/ui';
import { AnalyticsEvents, trackEvent } from '@/infra/analytics/lib/analytics';

interface ResumeErrorProps {
error: Error & { digest?: string };
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest';
import { getSortedFeedData } from '@/blog/services/post-repository';
import { GET } from './route';

describe('/feed.xml', () => {
describe('/rss.xml', () => {
it('excludes private posts from the generated RSS feed', async () => {
const response = await GET();
const xml = await response.text();
Expand Down
26 changes: 26 additions & 0 deletions app/rss.xml/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getSortedFeedData } from '@/blog/services/post-repository';
import { createRssFeed } from '@/blog/services/rss-feed';
import {
SITE_DESCRIPTION,
SITE_FEED_URL,
SITE_NAME,
SITE_URL,
} from '@/site/config/site';

export async function GET() {
const allPosts = getSortedFeedData();
const rss = createRssFeed({
posts: allPosts,
siteUrl: SITE_URL,
siteName: SITE_NAME,
siteDescription: SITE_DESCRIPTION,
feedUrl: SITE_FEED_URL,
});

return new Response(rss, {
headers: {
'Content-Type': 'application/xml; charset=utf-8',
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=1800',
},
});
}
File renamed without changes.
7 changes: 3 additions & 4 deletions src/app/sitemap.test.ts → app/sitemap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

import { describe, expect, it } from 'vitest';
import { getSortedFeedData } from '@/blog/services/post-repository';
import { SITE_URL } from '@/site/config/site';
import sitemap from './sitemap';

const URL = 'https://eunu-log.vercel.app';

describe('sitemap', () => {
it('excludes private posts from sitemap entries', () => {
const entries = sitemap();
Expand All @@ -14,11 +13,11 @@ describe('sitemap', () => {
(post) => post.visibility === 'private'
);

expect(urls).toContain(`${URL}/blog/ctr-pipeline`);
expect(urls).toContain(`${SITE_URL}/blog/ctr-pipeline`);
expect(privatePosts.length).toBeGreaterThan(0);
for (const post of privatePosts) {
expect(urls, `${post.slug} leaked to sitemap`).not.toContain(
`${URL}/blog/${post.slug}`
`${SITE_URL}/blog/${post.slug}`
);
}
});
Expand Down
11 changes: 5 additions & 6 deletions src/app/sitemap.ts → app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import { MetadataRoute } from 'next';
import { getSortedFeedData } from '@/blog/services/post-repository';

const URL = 'https://eunu-log.vercel.app';
import { SITE_FEED_PATH, SITE_URL } from '@/site/config/site';

export default function sitemap(): MetadataRoute.Sitemap {
const feeds = getSortedFeedData();

const feedEntries = feeds.map((feed) => ({
url: `${URL}/blog/${feed.slug}`,
url: `${SITE_URL}/blog/${feed.slug}`,
lastModified: feed.date, // Use actual post date
changeFrequency: 'weekly' as const,
priority: 0.7,
}));

const routes = ['', '/engineering', '/life', '/resume', '/feed.xml'].map(
const routes = ['', '/engineering', '/life', '/resume', SITE_FEED_PATH].map(
(route) => ({
url: `${URL}${route}`,
url: `${SITE_URL}${route}`,
lastModified: new Date().toISOString().split('T')[0],
changeFrequency:
route === '/feed.xml' ? ('daily' as const) : ('monthly' as const),
route === SITE_FEED_PATH ? ('daily' as const) : ('monthly' as const),
priority: 1,
})
);
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions blog/api/view.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { cookies, headers } from 'next/headers';
import { getSupabaseServerClient } from '@/platform/integrations/supabase';
import { getSupabaseServerClient } from '@/infra/integrations/supabase';
import {
getPopularViewsInRecentDays,
getViewCount,
incrementView,
trackView,
} from './view';

vi.mock('@/platform/integrations/supabase', () => ({
vi.mock('@/infra/integrations/supabase', () => ({
getSupabaseServerClient: vi.fn(),
}));

Expand Down
Loading
Loading