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
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
/.pnp
.pnp.js

# testing
# generated reports and local captures
/coverage
/test-results/
/output/

# next.js
/.next/
Expand All @@ -18,7 +19,6 @@
# misc
.DS_Store
*.pem
/output/

# debug
npm-debug.log*
Expand Down
29 changes: 17 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
- 현재 MDX 파이프라인을 보존한다. MDX는 `next.config.mjs`의 `@mdx-js/loader`
기반 커스텀 webpack rule에 남겨둔다. 전체 콘텐츠 파이프라인을 의도적으로
마이그레이션하지 않는 한 Next.js 내장 MDX로 바꾸지 않는다.
- 시각화 중심 UI는 `src/components/visualization/`에 둔다. 명확한
아키텍처 이유 없이 다른 폴더로 옮기지 않는다.
- 시각화 중심 UI는 `shared/visualization/`에 둔다. 블로그 MDX가 재사용하는
시각화 컴포넌트는 도메인 코드가 아니라 shared 시각화 모듈로 관리한다.
- `any`를 사용하지 않는다. 구체 타입을 쓰거나 `unknown`과 narrowing을
사용한다.
- `p-[13px]` 같은 arbitrary Tailwind value를 사용하지 않는다. 표준 utility,
shared token, 기존 스타일 패턴을 사용한다.
- feature-first 구조를 flat shared folder 구조로 바꾸지 않는다.
`src/features/`, `src/shared/`, `src/domains/`를 책임별로 유지한다.
- DDD의 전략적 설계 개념을 차용한 domain-first modular monolith 구조를
유지한다. 주요 도메인은 `src`와 같은 최상위 모듈로 분리하고, `src/app`은
Next.js route adapter로 제한한다.
- 블로그 콘텐츠 구조는 `posts/**/index.mdx`와 주변 `meta.json`으로 유지한다.
중첩된 series 디렉터리도 보존한다.
- 변경이 아래 ADR 작성 조건에 걸리면 반드시 ADR을 작성하거나 갱신한다.
Expand All @@ -38,16 +39,20 @@ ADR 작성 기준:

## 프로젝트 구조

- `src/app/`: Next.js App Router 페이지, 레이아웃, 핸들러
- `src/core/`: 앱 수준 provider와 설정 조합
- `src/domains/`: feature 사이에서 공유되는 계약과 스키마
- `src/features/`: blog, home, resume, search 같은 feature 모듈
- `src/shared/`: 재사용 가능한 UI, layout, analytics, SEO, provider
- `src/components/visualization/`: animation과 시각화 중심 컴포넌트
- `src/styles/`: design token과 global style
- `src/app/`: Next.js App Router route adapter, route handler, metadata entry
- `blog/`: 글 도메인. post schema, repository, publication policy, series,
blog UI, 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
- `styles/`: design token과 global style
- `posts/`: 중첩 가능한 `index.mdx`와 `meta.json` 기반 블로그 콘텐츠
- `tests/`: Playwright E2E 테스트
- `internal/`: script와 tool configuration
- `tooling/`: script와 tool configuration

## 개발 명령

Expand Down
51 changes: 27 additions & 24 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ARCHITECTURE

Last updated: 2026-05-06
Last updated: 2026-05-07

## System Summary

Expand All @@ -21,7 +21,7 @@ Last updated: 2026-05-06
- Rendering mix:
- Static prerendered route params via `generateStaticParams` for post pages.
- Server route handlers for feed and OG image.
- Server actions for view counting.
- Domain-owned server actions for view counting.

### Edge Runtime

Expand All @@ -40,12 +40,12 @@ Last updated: 2026-05-06
- Route handlers:
- `/feed.xml` (`src/app/feed.xml/route.ts`)
- `/api/og` (`src/app/api/og/route.tsx`)
- Server action:
- `src/app/actions/view.ts` for increment/read view count.
- `src/app` imports page adapters from top-level domain modules and does not own
domain policy.

### Content Layer (`posts/**` + `src/features/blog/services`)
### Content Layer (`posts/**` + `blog/services`)

- `src/features/blog/services/post-repository.ts` recursively discovers valid post folders (`index.mdx` + `meta.json`).
- `blog/services/post-repository.ts` recursively discovers valid post folders (`index.mdx` + `meta.json`).
- `meta.json` is validated with Zod (`FeedFrontmatterSchema`).
- MDX is loaded by dynamic import per folder path.
- Reading time is auto-derived from MDX when metadata omits it.
Expand All @@ -57,31 +57,32 @@ Last updated: 2026-05-06
- `remark-gfm`
- `rehype-slug`
- `rehype-pretty-code`
- `src/features/blog/services/markdown-parser.ts` parses MDX headings for TOC data.
- `src/features/blog/ui/mdx/components.tsx` maps MDX nodes to UI components and interactive visualization widgets.
- `blog/services/markdown-parser.ts` parses MDX headings for TOC data.
- `blog/ui/mdx/components.tsx` maps MDX nodes to UI components and interactive visualization widgets.

### Feature/Shared Layer (`src/features` + `src/shared` + `src/styles`)
### Domain-first Modular Monolith

- Feature-first organization:
- `src/features/blog`, `src/features/resume`, `src/features/search`, `src/features/home`
- `src/shared/analytics`, `src/shared/layout`, `src/shared/ui`, `src/shared/providers`, `src/shared/seo`
- `src/components/visualization` is intentionally preserved for heavy visualization widgets.
- Design tokens in `src/styles/tokens.css`.
- Global base styles and typography in `src/styles/globals.css`.
- Theming via `next-themes` provider.
- `blog/`: post schema, repository, publication policy, series, blog UI, view-count use case.
- `resume/`: resume data, ordering, resume UI.
- `search/`: command palette, search action, search recommendation.
- `site/`: home composition, AppShell, navigation, providers, site config.
- `platform/`: Supabase integration, Umami analytics, SEO helper, devtools.
- `shared/`: domain-agnostic UI, layout primitive, motion helper, testing helper, visualization widget.
- `styles/`: design tokens, global base styles, local font CSS.
- Theming via `next-themes` provider in `site/providers`.

### Data and Integrations

- Supabase client setup in `src/shared/integrations/supabase.ts`.
- Supabase client setup in `platform/integrations/supabase.ts`.
- View count data model:
- table: `public.views`
- rpc: `increment_view(slug_input text) -> bigint`
- SQL provisioning script: `docs/database/supabase-view-count.sql`.

### Analytics and SEO

- Umami event helpers in `src/shared/analytics/lib/analytics.ts`.
- Trackers in `src/shared/analytics/components/*`.
- Umami event helpers in `platform/analytics/lib/analytics.ts`.
- Trackers in `platform/analytics/components/*`.
- Structured data via `JsonLd` component in layout and post page.

## Request/Data Flows
Expand All @@ -92,7 +93,7 @@ Last updated: 2026-05-06
2. Request to `/blog/[slug]` resolves post via `getFeedData(slug)`.
3. MDX source is parsed for heading structure (TOC).
4. MDX component renders with mapped custom components.
5. Client tracker records post view; server action can persist counter in Supabase.
5. Client tracker records post view; `blog/api/view.ts` can persist counter in Supabase.

### Feed Flow

Expand All @@ -109,19 +110,20 @@ Last updated: 2026-05-06

## Testing and Quality

- Unit/component tests: Vitest + Testing Library (`src/**/*.test.ts(x)`).
- Unit/component tests: Vitest + Testing Library across top-level modules and `src/app`.
- E2E tests: Playwright mobile-focused projects (`tests/e2e/**/*.spec.ts`).
- Linting/formatting: ESLint, Prettier, markdownlint, cspell.
- Coverage focus includes `src/features`, `src/shared`, `src/styles`, and selected route domains.
- Coverage focus includes `blog`, `resume`, `search`, `site`, `platform`,
`shared`, `styles`, and selected route adapters.

## Current Architecture Risks

1. Docs/runtime drift:
- AGENTS and README mention older stack assumptions; package versions are newer.
2. Provider boundary drift:
- Route/layout changes can reintroduce duplicated tracker mounts if `AppProviders` is bypassed.
3. Content/animation docs drift:
- Documented `src/components/animations` path does not exist in current tree.
3. Boundary enforcement drift:
- Module boundaries are physical and documented, but lint-level enforcement is not yet configured.
4. SEO endpoint mismatch risk:
- Post JSON-LD image URL differs from the actual OG route path/domain conventions.

Expand All @@ -134,3 +136,4 @@ Last updated: 2026-05-06
3. Keep token-first styling and avoid one-off visual constants where possible.
4. Keep route-level separation for feed, OG, and view-count concerns.
5. Keep Umami analytics separate from Supabase-backed public view counts.
6. Keep top-level domain modules as documented in ADR 0011.
26 changes: 11 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,23 +75,19 @@
```text
eunu.log/
├── 📁 src/
│ ├── 📁 app/ # 라우트 엔트리 전용 (Next App Router)
│ ├── 📁 core/ # 앱 전역 설정/프로바이더 조합
│ ├── 📁 domains/ # 도메인 계약/타입/스키마
│ ├── 📁 features/ # 기능 모듈(ui/model/services)
│ │ ├── 📁 blog/
│ │ ├── 📁 resume/
│ │ ├── 📁 search/
│ │ └── 📁 home/
│ ├── 📁 shared/ # 공용 모듈(analytics/integrations/layout/seo/testing/ui/types)
│ ├── 📁 components/
│ │ └── 📁 visualization/ # 인터랙티브 알고리즘 시각화 전용
│ └── 📁 styles/ # 전역 스타일과 토큰
│ └── 📁 app/ # Next.js route adapter
├── 📁 blog/ # 글 도메인
├── 📁 resume/ # 이력서 도메인
├── 📁 search/ # 검색 도메인
├── 📁 site/ # 홈, AppShell, provider, site config
├── 📁 platform/ # Supabase, Umami, SEO, devtool integration
├── 📁 shared/ # 도메인 지식 없는 UI/모션/테스트/시각화
├── 📁 styles/ # 전역 스타일과 토큰
├── 📁 tests/
│ └── 📁 e2e/ # Playwright E2E 테스트
├── 📁 internal/
│ ├── 📁 config/ # 내부 lint/spell 설정
│ └── 📁 scripts/ # 내부 자동화/유틸 스크립트
├── 📁 tooling/
│ ├── 📁 config/ # lint/spell 설정
│ └── 📁 scripts/ # 자동화/유틸 스크립트
├── 📁 posts/ # 블로그 글(MDX + 메타데이터)
│ └── 📁 [slug]/ # 글 단위 폴더
│ ├── index.mdx # 글 본문
Expand Down
4 changes: 2 additions & 2 deletions src/app/actions/view.test.ts → 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 '@/shared/integrations/supabase';
import { getSupabaseServerClient } from '@/platform/integrations/supabase';
import {
getPopularViewsInRecentDays,
getViewCount,
incrementView,
trackView,
} from './view';

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

Expand Down
2 changes: 1 addition & 1 deletion src/app/actions/view.ts → blog/api/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { createHash, randomUUID } from 'node:crypto';
import { cookies, headers } from 'next/headers';
import { getSupabaseServerClient } from '@/shared/integrations/supabase';
import { getSupabaseServerClient } from '@/platform/integrations/supabase';

const VIEW_DEDUPE_WINDOW_SECONDS = 60 * 60 * 24;
const VIEW_FINGERPRINT_SALT =
Expand Down
33 changes: 33 additions & 0 deletions blog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export {
calculateReadingTime,
getAllFeedSlugs,
getFeedData,
getFolderSlug,
getSeriesPosts,
getSortedFeedData,
type FeedQueryOptions,
type TocItem,
} from './services/post-repository';
export {
formatSeriesDate,
getSeriesGroups,
getSeriesSummaries,
type SeriesGroup,
type SeriesSummary,
} from './model/series-group';
export {
getPopularViewsInRecentDays,
getViewCount,
incrementView,
trackView,
type PopularViewEntry,
} from './api/view';
export type {
Feed,
FeedData,
FeedFrontmatter,
PostCategory,
PostVisibility,
QualityReview,
QualityScore,
} from './model/types';
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import type { FeedData } from '@/domains/post/model/types';
import type { FeedData } from '@/blog/model/types';
import { getSeriesGroups, getSeriesSummaries } from './series-group';

const posts: FeedData[] = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FeedData } from '@/domains/post/model/types';
import type { FeedData } from '@/blog/model/types';

export interface SeriesGroup {
id: string;
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getFolderSlug, type TocItem } from '@/features/blog/services/post-repository';
import { getFolderSlug, type TocItem } from '@/blog/services/post-repository';
import fs from 'fs';
import path from 'path';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';
import type { FeedData, Feed, FeedFrontmatter } from '@/domains/post/model/types';
import { FeedFrontmatterSchema } from '@/domains/post/model/frontmatter-schema';
import type { FeedData, Feed, FeedFrontmatter } from '@/blog/model/types';
import { FeedFrontmatterSchema } from '@/blog/model/frontmatter-schema';

const postsDirectory = path.join(process.cwd(), 'posts');
const isProduction = process.env.NODE_ENV === 'production';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @vitest-environment node

import { describe, expect, it } from 'vitest';
import type { FeedData, QualityReview } from '@/domains/post/model/types';
import type { FeedData, QualityReview } from '@/blog/model/types';
import { getAllFeedSlugs, getSortedFeedData } from './post-repository';

const FEATURED_SLUGS = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
import type { ReactNode } from 'react';
import { describe, expect, it } from 'vitest';
import PostCard from './PostCard/PostCard';
import type { FeedData } from '@/domains/post/model/types';
import type { FeedData } from '@/blog/model/types';

vi.mock('next/link', () => ({
default: ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Link from 'next/link';
import { clsx } from 'clsx';
import { FeedData } from '@/domains/post/model/types';
import { FeedData } from '@/blog/model/types';
import { CategoryIcon } from '@/shared/ui/icons/AppSectionIcon';

interface PostCardProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import PostList from './PostList/PostList';
import type { FeedData } from '@/domains/post/model/types';
import type { FeedData } from '@/blog/model/types';

vi.mock('framer-motion', () => ({
motion: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { motion } from 'framer-motion';
import type { Variants } from 'framer-motion';
import { PostCard } from '../PostCard';
import { EmptyState } from '@/shared/ui';
import type { FeedData } from '@/domains/post/model/types';
import type { FeedData } from '@/blog/model/types';
import {
useEffectiveMotionMode,
type EffectiveMotionMode,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type { SeriesSummary } from '@/features/blog/model/series-group';
import type { SeriesSummary } from '@/blog/model/series-group';
import SeriesHubCard from './SeriesHubCard';

vi.mock('./SeriesTrackedLink', () => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SeriesSummary } from '@/features/blog/model/series-group';
import { formatSeriesDate } from '@/features/blog/model/series-group';
import type { SeriesSummary } from '@/blog/model/series-group';
import { formatSeriesDate } from '@/blog/model/series-group';
import SeriesTrackedLink from './SeriesTrackedLink';

interface SeriesHubCardProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SeriesSummary } from '@/features/blog/model/series-group';
import type { SeriesSummary } from '@/blog/model/series-group';
import SeriesHubCard from './SeriesHubCard';

interface SeriesHubListProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import Link from 'next/link';
import { useState } from 'react';
import { FeedData } from '@/domains/post/model/types';
import { FeedData } from '@/blog/model/types';
import styles from './SeriesNavigation.module.css';

interface SeriesNavigationProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ vi.mock('next/link', () => ({
},
}));

vi.mock('@/shared/analytics/lib/analytics', () => ({
vi.mock('@/platform/analytics/lib/analytics', () => ({
AnalyticsEvents: {
click: 'click',
},
Expand Down
Loading
Loading