diff --git a/AGENTS.md b/AGENTS.md index 6c75bd7..885491b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,38 +1,90 @@ # Repository Guidelines -## Always-Follow Rules -- Keep changes minimal and scoped to the task. Do not edit unrelated files. -- Preserve the current MDX pipeline. MDX must stay on the custom webpack rule with `@mdx-js/loader` in `next.config.mjs`; do not switch to Next.js built-in MDX unless the whole content pipeline is intentionally migrated. -- Keep visualization-heavy UI in `src/components/visualization/`. Do not move that code to other folders without a clear architectural reason. -- Never use `any`. Use concrete types or `unknown` with narrowing. -- Never use arbitrary Tailwind values such as `p-[13px]`. Use standard utilities, shared tokens, and existing style patterns. -- Do not replace feature-first structure with flat shared folders. Keep code in `src/features/`, `src/shared/`, and `src/domains/` by responsibility. -- Preserve blog content structure as `posts/**/index.mdx` with nearby `meta.json`, including nested series directories when present. - -## Project Structure -- `src/app/` — Next.js App Router pages, layouts, handlers -- `src/core/` — app-level providers and configuration composition -- `src/domains/` — cross-feature contracts and schemas -- `src/features/` — feature modules such as blog, home, resume, and search -- `src/shared/` — reusable UI, layout, analytics, SEO, and providers -- `src/components/visualization/` — animation and visualization-heavy components -- `src/styles/` — design tokens and global styles -- `posts/` — blog content, series entries, and metadata managed as nested `index.mdx` + `meta.json` -- `tests/` — Playwright end-to-end coverage -- `internal/` — scripts and tool configuration - -## Development Commands -- `npm run dev` — run the local dev server with webpack -- `npm run build` — create the production build -- `npm run lint` — run ESLint on source files -- `npm run lint:css:syntax` — check CSS syntax rules -- `npm run test:unit` — run Vitest unit tests -- `npm run test:components` — run component-focused Vitest tests -- `npm run test:e2e` — run Playwright scenarios -- `npm run test:ci` — run the main CI-equivalent validation set - -## Style & Testing -Use TypeScript with 2-space indentation, semicolons, single quotes, trailing commas (`es5`), and 80-column width; Prettier enforces this. Name components in `PascalCase`, hooks in `camelCase` with a `use` prefix, and tests as `*.test.ts` or `*.test.tsx`. Add targeted Vitest or Playwright coverage when changing logic, UI behavior, parsers, or app actions. Before opening a PR, run `npm run build` and the most relevant test command for the change. - -## Commits & PRs -Use commit messages like `type(scope): concise description`, for example `fix(home): preview 배포 타입 오류 수정`. Common types include `feat`, `fix`, `refactor`, `test`, and `chore`. Use branch names like `codex/`. Follow `.github/pull_request_template.md`, link related issues or PRs, and include screenshots when UI changes are visible. Confirm mobile/desktop and dark/light behavior when layout or navigation changes. +## 항상 지킬 규칙 + +- 변경은 작업 범위에 맞게 최소화한다. 관련 없는 파일은 수정하지 않는다. +- 현재 MDX 파이프라인을 보존한다. MDX는 `next.config.mjs`의 `@mdx-js/loader` + 기반 커스텀 webpack rule에 남겨둔다. 전체 콘텐츠 파이프라인을 의도적으로 + 마이그레이션하지 않는 한 Next.js 내장 MDX로 바꾸지 않는다. +- 시각화 중심 UI는 `src/components/visualization/`에 둔다. 명확한 + 아키텍처 이유 없이 다른 폴더로 옮기지 않는다. +- `any`를 사용하지 않는다. 구체 타입을 쓰거나 `unknown`과 narrowing을 + 사용한다. +- `p-[13px]` 같은 arbitrary Tailwind value를 사용하지 않는다. 표준 utility, + shared token, 기존 스타일 패턴을 사용한다. +- feature-first 구조를 flat shared folder 구조로 바꾸지 않는다. + `src/features/`, `src/shared/`, `src/domains/`를 책임별로 유지한다. +- 블로그 콘텐츠 구조는 `posts/**/index.mdx`와 주변 `meta.json`으로 유지한다. + 중첩된 series 디렉터리도 보존한다. +- 변경이 아래 ADR 작성 조건에 걸리면 반드시 ADR을 작성하거나 갱신한다. + +## ADR 작성 조건 + +아래 조건 중 하나라도 해당하면 `docs/adr/`에 ADR을 작성하거나 갱신한다. + +- 선택지가 2개 이상이고 트레이드오프가 존재한 경우. +- 반복적으로 따라야 할 규칙이나 경계를 정의한 경우. +- 테스트 전략이나 검증 방식이 결정의 핵심이었던 경우. + +ADR 작성 기준: + +- `docs/adr/` 아래에 번호가 붙은 파일을 만들고 `docs/adr/README.md`를 + 갱신한다. +- 배경, 결정, 결과, 검토한 대안, 가능하면 관련 커밋 히스토리를 기록한다. +- 과거를 지우기 위해 기존 ADR을 고쳐 쓰지 않는다. 이전 결정을 대체하는 새 + ADR을 작성한다. +- ADR은 AI 협업 노트와 분리한다. 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 +- `posts/`: 중첩 가능한 `index.mdx`와 `meta.json` 기반 블로그 콘텐츠 +- `tests/`: Playwright E2E 테스트 +- `internal/`: script와 tool configuration + +## 개발 명령 + +- `npm run dev`: webpack 기반 로컬 개발 서버 실행 +- `npm run build`: production build 생성 +- `npm run lint`: source file ESLint 실행 +- `npm run lint:css:syntax`: CSS syntax rule 검사 +- `npm run verify:docs`: ADR과 핵심 문서 하네스 검사 +- `npm run test:unit`: Vitest unit test 실행 +- `npm run test:components`: component 중심 Vitest test 실행 +- `npm run test:e2e`: Playwright scenario 실행 +- `npm run test:ci`: 주요 CI-equivalent validation set 실행 + +## 스타일과 테스트 + +TypeScript를 사용하고 2-space indentation, semicolon, single quote, trailing +comma(`es5`), 80-column width를 따른다. Prettier가 이를 강제한다. Component는 +`PascalCase`, hook은 `use` prefix가 붙은 `camelCase`, test는 `*.test.ts` +또는 `*.test.tsx`로 작성한다. Logic, UI behavior, parser, app action을 바꿀 +때는 대상 Vitest 또는 Playwright coverage를 추가한다. PR 전에는 +`npm run build`와 변경에 가장 관련 있는 test command를 실행한다. + +## AI 작업 운영 + +- AI가 제안했더라도 이 저장소의 구조, ADR, 테스트 규칙을 우선한다. +- 사용자나 다른 작업자가 만든 변경을 명시 요청 없이 되돌리지 않는다. +- 작업 전 관련 문서와 현재 구현을 먼저 확인하고, 추측으로 구조를 바꾸지 + 않는다. +- 작업 후 변경 내용, 이유, 검증 방법을 짧게 요약한다. +- 반복되는 AI 작업 절차는 새 중복 가이드보다 `AGENTS.md`, ADR, 실행 계획, + 또는 기존 guide 중 가장 직접적인 문서에 흡수한다. + +## 커밋과 PR + +커밋 메시지는 `type(scope): concise description` 형식을 사용한다. 예: +`fix(home): preview 배포 타입 오류 수정`. 주로 쓰는 type은 `feat`, `fix`, +`refactor`, `test`, `chore`다. Branch name은 `codex/` 형태를 사용한다. +`.github/pull_request_template.md`를 따르고 관련 issue나 PR을 연결한다. UI 변경이 +보이면 screenshot을 포함한다. Layout이나 navigation이 바뀌면 mobile/desktop, +dark/light 동작을 확인한다. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c5bcc6e..5ebaeeb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,6 +1,6 @@ # ARCHITECTURE -Last updated: 2026-02-27 +Last updated: 2026-05-06 ## System Summary @@ -9,7 +9,7 @@ Last updated: 2026-02-27 - File-based MDX content under `posts/**`. - Static generation for blog routes. - Server-side view counting via Supabase RPC. -- Client-side analytics via GA4 trackers. +- Client-side analytics via Umami trackers. - Token-driven UI styling (Tailwind + CSS variables). ## Runtime Topology @@ -80,7 +80,7 @@ Last updated: 2026-02-27 ### Analytics and SEO -- GA4 event helpers in `src/shared/analytics/lib/analytics.ts`. +- Umami event helpers in `src/shared/analytics/lib/analytics.ts`. - Trackers in `src/shared/analytics/components/*`. - Structured data via `JsonLd` component in layout and post page. @@ -127,7 +127,10 @@ Last updated: 2026-02-27 ## Decisions to Preserve +복원한 아키텍처 결정의 상세 기록은 `docs/adr/README.md`에 둔다. + 1. Keep folder-based content (`posts/**`) with `meta.json + index.mdx`. 2. Keep Zod schema validation in content ingestion path. 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. diff --git a/docs/README.md b/docs/README.md index 684987b..bef4265 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,7 @@ becomes part of the maintained workflow. - `docs/QUALITY_SCORE.md` - `docs/blog-quality-guide.md` - Delivery/process: + - `docs/adr/README.md` - `docs/PLANS.md` - `docs/exec-plans/active/README.md` - `docs/exec-plans/completed/README.md` @@ -28,7 +29,6 @@ becomes part of the maintained workflow. - `docs/guides/agentation-workflow.md` - `docs/guides/pr-workflow.md` - `docs/guides/testing-guide.md` - - `docs/guides/ai-collaboration.md` - `docs/guides/ui-components-guide.md` - Product/domain: - `docs/PRODUCT_SENSE.md` diff --git a/docs/adr/0001-use-nextjs-app-router.md b/docs/adr/0001-use-nextjs-app-router.md new file mode 100644 index 0000000..b131910 --- /dev/null +++ b/docs/adr/0001-use-nextjs-app-router.md @@ -0,0 +1,42 @@ +# 0001. Next.js App Router를 사용한다 + +Date: 2026-01-20 +Status: Accepted + +## 배경 + +이 프로젝트는 개인 블로그와 이력서 사이트로 시작했다. 첫 번째로 오래 남은 +런타임 결정은 정적 사이트 생성기나 클라이언트 전용 React 앱이 아니라 +Next.js 애플리케이션으로 만든다는 선택이었다. + +이후 동적 블로그 라우트, 사이트맵과 피드 라우트, 라우트 단위 메타데이터, +조회수 집계를 위한 서버 액션, edge OG 이미지 엔드포인트가 추가됐다. 이 +흐름은 별도 백엔드보다 App Router의 기능에 의존한다. + +## 결정 + +애플리케이션 런타임으로 Next.js App Router를 사용한다. 라우트 진입점은 +`src/app/**`에 두고, 실제 기능 구현은 feature 모듈로 위임한다. + +## 결과 + +- 정적 블로그 페이지는 `generateStaticParams`를 사용할 수 있다. +- 라우트 핸들러로 `feed.xml`, OG 이미지, 통합 엔드포인트를 제공할 수 + 있다. +- 조회수 집계 같은 변경 작업은 서버 액션이 소유할 수 있다. +- 라우트 파일에 기능 구현이 과하게 쌓이지 않도록 경계를 유지해야 한다. + +## 검토한 대안 + +- 정적 사이트 생성기: 배포는 단순하지만 라우트 핸들러, 서버 액션, 런타임 + 통합에는 약하다. +- 클라이언트 전용 React 앱: 렌더링 모델은 단순하지만 글 중심 사이트에 + 필요한 SEO와 피드 지원에 불리하다. + +## 관련 히스토리 + +- `521eae7` (2026-01-20): 초기 프로젝트 설정. +- `eaed3bf` (2026-01-20): Markdown 기반 블로그 시스템. +- `b49aa5b` (2026-01-23): 사이트맵과 기본 메타데이터. +- `1df701f` (2026-01-27): Next.js 16 업그레이드. +- `2c92fdc` (2026-02-26): 아키텍처 기준 문서 추가. diff --git a/docs/adr/0002-keep-custom-mdx-webpack-pipeline.md b/docs/adr/0002-keep-custom-mdx-webpack-pipeline.md new file mode 100644 index 0000000..fc57e90 --- /dev/null +++ b/docs/adr/0002-keep-custom-mdx-webpack-pipeline.md @@ -0,0 +1,45 @@ +# 0002. 커스텀 MDX webpack 파이프라인을 유지한다 + +Date: 2026-01-27 +Status: Accepted + +## 배경 + +블로그는 Markdown 파일에서 MDX로 이동했다. 글 안에서 더 풍부한 콘텐츠, +커스텀 컴포넌트, 코드 하이라이팅, heading, 인터랙티브 위젯을 렌더링하기 +위해서였다. 현재 파이프라인은 `next.config.mjs`에서 `@mdx-js/loader`, +`remark-gfm`, `rehype-slug`, `rehype-pretty-code`를 직접 연결한다. + +이 파이프라인은 콘텐츠 시스템의 일부다. Next.js 내장 MDX로 교체하면 +heading 생성, 코드 렌더링, 컴포넌트 매핑, 글 import 방식이 달라질 수 +있다. + +## 결정 + +MDX는 `next.config.mjs`의 커스텀 webpack rule로 유지한다. 커스텀 +컴포넌트 매핑은 `src/features/blog/ui/mdx/components.tsx`에 집중한다. + +## 결과 + +- MDX 동작을 하나의 Next.js 설정 파일에서 명시적으로 검토할 수 있다. +- 코드 하이라이팅과 slug 동작이 글 전체에서 안정적으로 유지된다. +- Next.js 빌드는 webpack 경로를 계속 사용해야 한다. +- 향후 MDX를 바꾸려면 패키지 교체가 아니라 콘텐츠 파이프라인 마이그레이션 + 작업으로 다뤄야 한다. + +## 검토한 대안 + +- Next.js 내장 MDX: 로컬 설정은 줄어들지만 현재 글 렌더링 계약을 바꿀 + 위험이 있다. +- Plain Markdown: 수집은 단순하지만 React 컴포넌트와 인터랙티브 시각화 + 지원을 잃는다. + +## 관련 히스토리 + +- `e97330d` (2026-01-27): 피드 콘텐츠를 MDX로 마이그레이션. +- `fc500c6` (2026-01-27): MDX 처리 인프라 추가. +- `90a22b0` (2026-01-27): 피드 페이지를 MDX 렌더링으로 전환. +- `cf1c9cb` (2026-01-28): Next 설정을 `next.config.mjs`로 이동. +- `5be50b5` (2026-02-28): 소스 구조 개편 중 커스텀 MDX 유지. +- `9545057` (2026-03-30): 현재 저장소 규칙과 충돌하던 구조 평탄화 계획 + 폐기. diff --git a/docs/adr/0003-store-posts-as-folder-mdx-and-meta.md b/docs/adr/0003-store-posts-as-folder-mdx-and-meta.md new file mode 100644 index 0000000..cee9a75 --- /dev/null +++ b/docs/adr/0003-store-posts-as-folder-mdx-and-meta.md @@ -0,0 +1,40 @@ +# 0003. 글을 폴더형 MDX와 메타데이터로 저장한다 + +Date: 2026-02-09 +Status: Accepted + +## 배경 + +콘텐츠 모델은 flat Markdown 파일에서 글 단위 폴더 구조로 발전했다. 현재 +저장소는 `posts/**` 아래에 글을 저장하고, 유효한 글 폴더는 본문 +`index.mdx`와 주변 메타데이터 `meta.json`을 함께 가진다. + +중첩된 시리즈 디렉터리도 모델의 일부다. repository 계층은 폴더를 재귀적으로 +탐색하고, 라우트, 피드, 사이트맵, 검색에 글을 노출하기 전에 Zod로 +메타데이터를 검증한다. + +## 결정 + +`posts/**/index.mdx`와 `posts/**/meta.json`을 표준 콘텐츠 형식으로 사용한다. +시리즈는 단일 목록으로 평탄화하지 않고 중첩 폴더로 표현한다. + +## 결과 + +- 본문과 메타데이터가 가까운 위치에 유지된다. +- 시리즈를 디렉터리 구조로 표현할 수 있다. +- repository 계층은 재귀 탐색과 검증을 보존해야 한다. +- 새 글 생성 도구는 두 파일을 모두 만들어야 한다. + +## 검토한 대안 + +- flat `posts/*.mdx`: 탐색은 단순하지만 시리즈와 주변 자산 관리에 약하다. +- frontmatter-only MDX: 파일 수는 줄지만 메타데이터 검증과 정책 검사가 + 약해진다. +- 외부 CMS: 편집 UI는 좋아지지만 이 프로젝트에는 운영 비용이 과하다. + +## 관련 히스토리 + +- `1e5355d` (2026-02-09): SEO 친화적 콘텐츠 폴더와 slug 도입. +- `1cd4e06` (2026-02-13): 시리즈와 조회수 통합 확장. +- `5be50b5` (2026-02-28): 표준 콘텐츠 루트를 `content/`에서 `posts/`로 이동. +- `21a4a6a` (2026-04-14): 메타데이터 정책과 글 스캐폴딩 변경. diff --git a/docs/adr/0004-preserve-feature-first-source-structure.md b/docs/adr/0004-preserve-feature-first-source-structure.md new file mode 100644 index 0000000..8ae94ea --- /dev/null +++ b/docs/adr/0004-preserve-feature-first-source-structure.md @@ -0,0 +1,47 @@ +# 0004. Feature-first 소스 구조를 유지한다 + +Date: 2026-02-28 +Status: Accepted + +## 배경 + +소스 트리는 여러 차례 구조 변경을 거쳤다. 초기 코드는 route-local 구조와 +일반 컴포넌트 폴더에 섞여 있었다. 이후 라우트, feature 모듈, shared +인프라, domain 계약, app-level provider를 분리했다. + +나중에 일반적인 Next.js 구조로 평탄화하자는 실행 계획도 있었지만, 저장소 +가이드와 충돌했기 때문에 superseded 상태로 정리됐다. + +## 결정 + +다음 feature-first 구조를 유지한다. + +- `src/app/**`: App Router 진입점. +- `src/features/**`: 기능 구현. +- `src/domains/**`: feature 사이에서 공유되는 계약과 스키마. +- `src/shared/**`: 재사용 가능한 layout, UI, analytics, SEO, provider, + integration. +- `src/core/**`: 앱 수준 설정과 provider 조합. + +## 결과 + +- flat shared component 디렉터리보다 소유권이 명확하다. +- 라우트 파일은 얇게 유지하고 feature page를 조합할 수 있다. +- shared 코드로 올릴 때는 feature-local 코드보다 높은 재사용 기준이 필요하다. +- 리팩터링 시 import와 테스트를 이 경계에 맞춰 유지해야 한다. + +## 검토한 대안 + +- route-local 구현: 작은 앱에는 유용하지만 blog, resume, search, home + 기능이 커지면서 중복이 늘었다. +- flat `components/`, `lib/`, `types/`: 처음에는 단순하지만 feature 소유권과 + 계약이 흐려진다. +- 일반적인 Next.js 평탄화: 실행 계획 아카이브에서 명시적으로 superseded + 처리됐다. + +## 관련 히스토리 + +- `e17fe0f` (2026-01-25): 첫 feature 기반 컴포넌트 재구성. +- `5be50b5` (2026-02-28): FSD 스타일 마이그레이션 정리. +- `2c92fdc` (2026-02-26): 아키텍처 기준 문서 추가. +- `9545057` (2026-03-30): 평탄화 계획을 superseded 상태로 completed에 이동. diff --git a/docs/adr/0005-use-token-first-tailwind-styling.md b/docs/adr/0005-use-token-first-tailwind-styling.md new file mode 100644 index 0000000..131c80b --- /dev/null +++ b/docs/adr/0005-use-token-first-tailwind-styling.md @@ -0,0 +1,44 @@ +# 0005. Token-first Tailwind 스타일링을 사용한다 + +Date: 2026-01-27 +Status: Accepted + +## 배경 + +초기 UI는 CSS Modules와 global CSS를 사용했다. 1월 리디자인 과정에서 +프로젝트는 Next.js 16, Tailwind CSS v4, 로컬 폰트, CSS variables로 +이동했다. 이후 TDS 영향을 받은 작업을 통해 shared token과 스타일 규칙이 +강화됐다. + +현재 가이드는 일회성 값보다 표준 Tailwind utility, CSS variables, token을 +우선한다. + +## 결정 + +Tailwind CSS와 CSS variables를 주요 스타일링 시스템으로 사용한다. 디자인 +token은 `src/styles/tokens.css`에 두고, 전역 base style은 +`src/styles/globals.css`에 둔다. + +## 결과 + +- 시각적 선택을 검토하고 재사용하기 쉬워진다. +- 스타일 시스템을 의도적으로 확장하는 경우가 아니라면 arbitrary Tailwind + value를 피해야 한다. +- global CSS는 token과 base style 중심으로 유지해야 한다. +- 컴포넌트별 스타일은 기존 utility와 shared token 패턴을 따라야 한다. + +## 검토한 대안 + +- CSS Modules 중심 시스템: 익숙하고 scope가 명확하지만 리디자인 과정에서 + 이미 주요 경로에서 제거됐다. +- inline style: 빠른 일회성 구현에는 좋지만 통제와 테스트가 어렵다. +- 컴포넌트 라이브러리 도입: 커스텀 시각 방향을 가진 개인 블로그에는 + 지나치게 무겁다. + +## 관련 히스토리 + +- `1df701f` (2026-01-27): Tailwind v4 업그레이드와 사이트 리디자인. +- `c987764` (2026-01-28): CSS Modules를 Tailwind로 전환. +- `e3bee05` (2026-01-28): 스타일링 접근 표준화. +- `420a5bb` (2026-02-07): TDS token 설정 추가. +- `5be50b5` (2026-02-28): 문서와 token 구조 정리. diff --git a/docs/adr/0006-isolate-visualization-heavy-components.md b/docs/adr/0006-isolate-visualization-heavy-components.md new file mode 100644 index 0000000..610aa80 --- /dev/null +++ b/docs/adr/0006-isolate-visualization-heavy-components.md @@ -0,0 +1,44 @@ +# 0006. 시각화 중심 컴포넌트를 격리한다 + +Date: 2026-01-28 +Status: Accepted + +## 배경 + +알고리즘과 인터랙티브 시각화 컴포넌트는 기술 글을 더 풍부하게 설명하기 위해 +추가됐다. 이 컴포넌트들은 일반 UI와 다른 관심사를 가진다. 애니메이션 상태, +canvas나 SVG 스타일 렌더링, 상호작용 타이밍, 더 무거운 클라이언트 동작이 +포함된다. + +이후 구조 개편에서도 이 컴포넌트들은 generic shared UI로 흡수되지 않고 +전용 위치에 보존됐다. + +## 결정 + +시각화 중심 UI는 `src/components/visualization/**`에 둔다. MDX 컴포넌트 +매핑은 이 컴포넌트를 글에 노출할 수 있지만, 시각화 코드 자체는 격리된 +위치에 유지한다. + +## 결과 + +- 시각화 코드는 shared UI 추상화를 오염시키지 않고 발전할 수 있다. +- 블로그 MDX 렌더링은 풍부한 인터랙티브 위젯을 import할 명확한 위치를 + 가진다. +- shared UI는 재사용 가능한 인터페이스 primitive에 집중한다. +- 시각화 코드를 옮기려면 구체적인 아키텍처 이유가 필요하다. + +## 검토한 대안 + +- `src/shared/ui` 아래에 두기: post-specific하고 무거운 동작을 reusable UI + primitive처럼 보이게 만든다. +- 각 post 옆에 두기: locality는 좋아지지만 패턴이 중복되고 테스트가 + 어려워진다. +- route-local에 두기: 한 글에는 충분하지만 재사용 가능한 기술 설명에는 + 맞지 않는다. + +## 관련 히스토리 + +- `8787466` (2026-01-28): 알고리즘 시각화 컴포넌트 추가. +- `5fc05c0` (2026-01-29): 시각화 컴포넌트 복구와 통합. +- `5be50b5` (2026-02-28): FSD 마이그레이션 중 시각화 코드 분리 유지. +- `b43600b` (2026-03-30): 시각화 컴포넌트의 품질 게이트 위반 정리. diff --git a/docs/adr/0007-use-umami-and-supabase-view-counts.md b/docs/adr/0007-use-umami-and-supabase-view-counts.md new file mode 100644 index 0000000..c17b05d --- /dev/null +++ b/docs/adr/0007-use-umami-and-supabase-view-counts.md @@ -0,0 +1,42 @@ +# 0007. Umami와 Supabase 조회수를 사용한다 + +Date: 2026-03-03 +Status: Accepted + +## 배경 + +분석 스택은 두 단계로 변했다. 먼저 Supabase 조회수와 GA4 이벤트 추적이 +추가됐다. 이후 self-hosted Umami 분석이 들어왔고, 처음에는 GA4와 함께 +동작했다. 뒤따른 리팩터링에서 GA4는 제거됐고 Umami가 유일한 클라이언트 +분석 provider가 됐다. + +조회수는 분석 이벤트와 분리되어 있다. 조회수는 사용자에게 보이는 글 +메타데이터이며 서버 측 중복 방지가 필요하기 때문이다. + +## 결정 + +클라이언트 분석에는 Umami를 사용하고, 공개 글 조회수 저장에는 Supabase +RPC 기반 저장소를 사용한다. + +## 결과 + +- 클라이언트 분석은 `NEXT_PUBLIC_UMAMI_URL`과 + `NEXT_PUBLIC_UMAMI_WEBSITE_ID`로 제어되는 더 가벼운 경로가 된다. +- 분석 이벤트는 Umami 준비 전까지 큐에 쌓을 수 있다. +- 조회수는 Supabase 환경 변수에 의존하며, 누락 시 graceful degradation이 + 필요하다. +- 조회수 중복 방지 동작은 RPC가 소유한다. + +## 검토한 대안 + +- GA4-only 분석: 중복 추적과 외부 의존면을 줄이기 위해 제거했다. +- 보이는 조회수까지 Umami에 의존: 공개 카운터에는 애플리케이션이 소유한 + read와 dedupe semantics가 필요해 부족하다. +- DB-only 분석: 가벼운 페이지와 인터랙션 이벤트 추적을 잃는다. + +## 관련 히스토리 + +- `1cd4e06` (2026-02-13): Supabase 블로그 조회수와 분석 문서 추가. +- `42e7592` (2026-02-27): self-hosted Umami 분석 추가. +- `243e32e` (2026-03-03): GA4 제거와 Umami-only 전환. +- `b4ae7c2` (2026-04-21): 24시간 조회수 중복 방지 추가. diff --git a/docs/adr/0008-enforce-publication-policy-in-content-ingestion.md b/docs/adr/0008-enforce-publication-policy-in-content-ingestion.md new file mode 100644 index 0000000..1ec64ce --- /dev/null +++ b/docs/adr/0008-enforce-publication-policy-in-content-ingestion.md @@ -0,0 +1,42 @@ +# 0008. 콘텐츠 수집 경로에서 공개 정책을 강제한다 + +Date: 2026-04-14 +Status: Accepted + +## 배경 + +블로그는 “유효한 모든 글은 공개”라는 방식에서 명시적인 공개 정책으로 +이동했다. 메타데이터에는 이제 visibility와 quality review 필드가 포함된다. +repository 함수, 피드 생성, 사이트맵 생성, 글 스캐폴딩도 private 또는 +품질 기준을 통과하지 못한 콘텐츠가 실수로 노출되지 않도록 수정됐다. + +이 결정은 수집, 라우팅, 검색, 피드, 릴리스 안전성에 영향을 주기 때문에 +콘텐츠 아키텍처 결정이다. + +## 결정 + +공개 상태를 `meta.json`에 표현하고, repository 계층에서 기본적으로 public +visibility를 강제한다. 품질 정책은 `docs/blog-quality-guide.md`에 문서화하고 +대상 테스트로 보호한다. + +## 결과 + +- private 글은 저장소에 존재해도 public route, feed, sitemap에 노출되지 + 않는다. +- 새 글은 메타데이터가 다르게 지정하지 않는 한 public을 기본값으로 가진다. +- 품질 기준은 기억에 의존한 수동 리뷰가 아니라 테스트 가능한 정책이 된다. +- `post-repository`를 우회하면 미공개 콘텐츠가 노출될 위험이 있다. + +## 검토한 대안 + +- draft 콘텐츠를 git branch로 분리: main에서 draft를 제외할 수 있지만 + 오래 걸리는 작성과 리뷰 흐름이 불편해진다. +- `_draft` 같은 파일명 convention: 놓치기 쉽고 검증하기 어렵다. +- feed나 sitemap에서 수동 필터링: 각 출력 경로가 같은 규칙을 기억해야 해서 + 취약하다. + +## 관련 히스토리 + +- `21a4a6a` (2026-04-14): 공개 정책, 메타데이터, 테스트 추가. +- `b4ae7c2` (2026-04-21): 공개 정책과 품질 기준 정리. +- `c480f2f` (2026-04-22): 공개 콘텐츠 제목과 문체 정리. diff --git a/docs/adr/0009-use-app-shell-for-primary-navigation.md b/docs/adr/0009-use-app-shell-for-primary-navigation.md new file mode 100644 index 0000000..0982dd7 --- /dev/null +++ b/docs/adr/0009-use-app-shell-for-primary-navigation.md @@ -0,0 +1,42 @@ +# 0009. 주요 탐색에 AppShell을 사용한다 + +Date: 2026-04-21 +Status: Accepted + +## 배경 + +탐색과 홈 경험은 여러 번 바뀌었다. header 기반 블로그 탐색, 모바일 bottom +navigation, Engineering/Life IA, 이후 Linear 스타일 shell이 있었다. 정리 +후 살아남은 장기 결정은 AppShell 레이아웃 모델이다. + +AppShell이 실제 렌더링 구조가 된 뒤에는 예전 header, home section, logo, +motion toggle이 제거됐다. + +## 결정 + +앱 경험의 주요 레이아웃과 탐색 프레임으로 `src/shared/layout/AppShell/**`을 +사용한다. feature page는 navigation chrome을 다시 만들지 않고 이 shell +안에 조합된다. + +## 결과 + +- 탐색 동작은 하나의 주요 소유자를 가진다. +- 모바일과 데스크톱 레이아웃 결정을 shell 기준으로 테스트할 수 있다. +- feature page는 콘텐츠에 집중하고 app chrome 중복을 피해야 한다. +- legacy header나 home section 코드는 새 ADR 또는 명시적인 결정 반전 없이 + 다시 도입하지 않는다. + +## 검토한 대안 + +- 페이지별 탐색: 유연하지만 페이지마다 동작이 중복되고 회귀가 생기기 쉽다. +- header-only 탐색: 단순하지만 현재 app-like IA와 모바일 상호작용에는 + 덜 맞는다. +- 마케팅 스타일 home section: product-like shell이 기본 경험이 된 뒤 + 제거됐다. + +## 관련 히스토리 + +- `6acdcac` (2026-04-21): Linear 스타일 AppShell 도입. +- `d10d2b1` (2026-04-21): 블로그 shell과 탐색 동작 개선. +- `8d2f78f` (2026-04-22): 모바일 탐색과 레이아웃 문제 수정. +- `6c962cc` (2026-04-23): 사용하지 않는 예전 레이아웃 코드 제거. diff --git a/docs/adr/0010-use-targeted-documentation-harness.md b/docs/adr/0010-use-targeted-documentation-harness.md new file mode 100644 index 0000000..550e69f --- /dev/null +++ b/docs/adr/0010-use-targeted-documentation-harness.md @@ -0,0 +1,48 @@ +# 0010. Targeted Documentation Harness를 사용한다 + +Date: 2026-05-07 +Status: Accepted + +## 배경 + +ADR 작성 조건이 `AGENTS.md`와 `docs/adr/README.md`에 추가되면서, 문서 규칙도 +반복적으로 검증할 필요가 생겼다. 하지만 기존 `npm run lint:md`는 전체 +`**/*.md`와 `**/*.mdx`를 검사한다. 과거 글과 일부 guide에 남아 있는 +markdownlint 이슈 때문에, 현재 상태에서는 문서 변경 검증용 harness로 쓰기 +어렵다. + +문서 작업마다 전체 markdownlint 실패를 무시하면, 실제로 중요한 ADR/규칙 문서 +오류도 함께 묻힌다. + +## 결정 + +핵심 문서만 대상으로 하는 `npm run verify:docs`를 추가한다. 이 command는 +`internal/scripts/verify-docs.mjs`를 실행하며 다음을 검증한다. + +- `docs/README.md`에 등록된 명시적 문서 경로가 실제로 존재하는지 확인한다. +- `AGENTS.md`와 `docs/adr/README.md`가 같은 ADR 작성 조건을 포함하는지 + 확인한다. +- `docs/adr/*.md` 파일이 ADR 인덱스에 등록되어 있는지 확인한다. +- `AGENTS.md`, `ARCHITECTURE.md`, `docs/README.md`, `docs/adr/*.md`에 + markdownlint를 실행한다. + +`npm run test:ci`에도 `npm run verify:docs`를 포함한다. + +## 결과 + +- ADR과 저장소 규칙 문서는 전체 Markdown 부채와 분리해 안정적으로 검증된다. +- ADR 파일을 추가하고 인덱스 갱신을 잊는 실수를 줄인다. +- 문서 인덱스의 stale path를 더 빨리 발견할 수 있다. +- 전체 Markdown lint 부채는 별도 정리 과제로 남는다. + +## 검토한 대안 + +- 기존 `npm run lint:md`만 사용: 범위가 넓고 현재 실패가 많아 문서 변경용 + harness로 신뢰하기 어렵다. +- 모든 Markdown lint 이슈를 먼저 정리: 이상적이지만 이번 목표보다 범위가 + 크고 글 콘텐츠까지 광범위하게 건드린다. +- PR 리뷰 체크리스트에만 의존: 자동 검증이 없어 반복 실수를 줄이기 어렵다. + +## 관련 히스토리 + +- `ee9cb14` (2026-05-07): ADR 문서 체계와 ADR 작성 조건 추가. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..b842157 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,47 @@ +# Architecture Decision Records + +Last updated: 2026-05-07 + +이 디렉터리는 커밋 히스토리에서 복원한 아키텍처 의사결정을 기록한다. +ADR은 AI 협업 가이드와 별개의 문서다. 사람이 결정했든 AI가 초안을 +도왔든, ADR의 목적은 제품과 엔지니어링 선택의 맥락을 남기는 것이다. + +## 범위 + +- 검토한 히스토리: 2026-01-20 `521eae7`부터 2026-04-28 `939601a`까지. +- 글 발행, 문장 수정, 단일 버그 수정은 오래 유지될 시스템 제약을 만들지 + 않는 한 ADR로 승격하지 않는다. +- 현재도 유효한 결정인지 확인하기 위해 `ARCHITECTURE.md`, `AGENTS.md`, + 기존 문서, 현재 소스 구조를 함께 대조했다. + +## 기록 + +| ID | 상태 | 결정 | +| --- | --- | --- | +| [0001](0001-use-nextjs-app-router.md) | Accepted | 블로그 런타임으로 Next.js App Router를 사용한다 | +| [0002](0002-keep-custom-mdx-webpack-pipeline.md) | Accepted | 커스텀 MDX webpack 파이프라인을 유지한다 | +| [0003](0003-store-posts-as-folder-mdx-and-meta.md) | Accepted | 글을 중첩 가능한 `index.mdx`와 `meta.json` 폴더로 저장한다 | +| [0004](0004-preserve-feature-first-source-structure.md) | Accepted | feature-first 소스 구조를 유지한다 | +| [0005](0005-use-token-first-tailwind-styling.md) | Accepted | token-first Tailwind 스타일링을 사용한다 | +| [0006](0006-isolate-visualization-heavy-components.md) | Accepted | 시각화 중심 컴포넌트를 격리한다 | +| [0007](0007-use-umami-and-supabase-view-counts.md) | Accepted | Umami 분석과 Supabase 조회수를 함께 사용한다 | +| [0008](0008-enforce-publication-policy-in-content-ingestion.md) | Accepted | 콘텐츠 수집 경로에서 공개 정책을 강제한다 | +| [0009](0009-use-app-shell-for-primary-navigation.md) | Accepted | 주요 탐색과 레이아웃에 AppShell을 사용한다 | +| [0010](0010-use-targeted-documentation-harness.md) | Accepted | 핵심 문서 검증에 targeted documentation harness를 사용한다 | + +## 작성 조건 + +아래 조건 중 하나라도 해당하면 ADR을 작성하거나 갱신한다. + +1. 선택지가 2개 이상이고 트레이드오프가 존재한 경우. +2. 반복적으로 따라야 할 규칙이나 경계를 정의한 경우. +3. 테스트 전략이나 검증 방식이 결정의 핵심이었던 경우. + +## 유지 규칙 + +1. 오래 유지될 제약을 만들거나 뒤집는 변경에는 새 ADR을 추가한다. +2. 과거를 지우기 위해 기존 ADR을 고쳐 쓰지 않는다. 이전 결정을 대체하는 + 새 ADR을 작성한다. +3. `Related History`에 커밋 해시를 남겨 이후 독자가 원 변경을 확인할 수 + 있게 한다. +4. ADR을 추가하면 이 인덱스와 `docs/README.md`를 함께 갱신한다. diff --git a/docs/guides/ai-collaboration.md b/docs/guides/ai-collaboration.md deleted file mode 100644 index 85e528f..0000000 --- a/docs/guides/ai-collaboration.md +++ /dev/null @@ -1,41 +0,0 @@ -# AI Collaboration Guide - -## Why This Exists - -This repository now relies on the Codex harness for generic agent behavior. -This guide keeps only repository-specific rules that are easy to miss. - -## Repository-Specific Rules - -1. Post data lives in `posts/[slug]/index.mdx + meta.json`. -2. Internal tooling lives in `internal/`: - - `internal/scripts/**` - - `internal/config/**` -3. Visualization-heavy UI belongs in `src/components/visualization/**`. -4. Do not use arbitrary Tailwind values like `p-[13px]`. -5. Do not add `any` type; use concrete types or `unknown` with narrowing. - -## Execution Checklist - -1. Keep scope minimal and avoid unrelated file changes. -2. Do not revert user-authored changes unless explicitly asked. -3. Run relevant checks after changes: - - Base: `npm run build` - - Tests when logic changes: `npm run test:unit` or targeted Vitest runs -4. Summarize what changed, why, and how it was verified. - -## Useful Commands - -- `npm run dev` -- `npm run build` -- `npm run test:unit` -- `npm run test:e2e` -- `npm run lint` -- `npm run lint:css:syntax` - -## Related Docs - -- `docs/FRONTEND.md` -- `docs/DESIGN.md` -- `docs/guides/testing-guide.md` -- `docs/guides/pr-workflow.md` diff --git a/docs/guides/testing-guide.md b/docs/guides/testing-guide.md index b392693..949ea5e 100644 --- a/docs/guides/testing-guide.md +++ b/docs/guides/testing-guide.md @@ -11,6 +11,7 @@ - `npm run test:components` : UI 컴포넌트 중심 테스트 실행 - `npm run test:smoke` : 유닛 smoke + Playwright smoke - `npm run test:e2e` : 전체 Playwright 시나리오 +- `npm run verify:docs` : ADR과 핵심 문서 하네스 검사 ## 단위 테스트 체크리스트 @@ -55,6 +56,24 @@ - `src/styles/tokens.test.ts` - `src/styles/globals.test.ts` +## 문서 하네스 + +`npm run verify:docs`는 전체 Markdown 문서를 한 번에 검사하지 않는다. 현재 +저장소에는 과거 글과 일부 guide에 남은 markdownlint 이슈가 있기 때문이다. +대신 의사결정과 작업 경계를 잡는 핵심 문서만 안정적으로 검증한다. + +검증 범위: + +1. `docs/README.md`에 명시된 문서 경로가 실제로 존재하는지 확인한다. +2. `AGENTS.md`와 `docs/adr/README.md`가 같은 ADR 작성 조건을 포함하는지 + 확인한다. +3. `docs/adr/*.md` 파일이 ADR 인덱스에 등록되어 있는지 확인한다. +4. `AGENTS.md`, `ARCHITECTURE.md`, `docs/README.md`, `docs/adr/*.md`에 + markdownlint를 실행한다. + +문서 변경이 ADR, 저장소 규칙, 문서 인덱스에 닿으면 최소 검증으로 +`npm run verify:docs`를 실행한다. + ## Playwright 체크리스트 ### 1) 모바일 내비 회귀 diff --git a/internal/scripts/verify-docs.mjs b/internal/scripts/verify-docs.mjs new file mode 100644 index 0000000..93000be --- /dev/null +++ b/internal/scripts/verify-docs.mjs @@ -0,0 +1,121 @@ +import fs from 'fs'; +import path from 'path'; +import { spawnSync } from 'child_process'; + +const ROOT = process.cwd(); +const ADR_DIR = path.join(ROOT, 'docs', 'adr'); +const DOCS_INDEX = path.join(ROOT, 'docs', 'README.md'); +const ADR_INDEX = path.join(ADR_DIR, 'README.md'); +const AGENTS = path.join(ROOT, 'AGENTS.md'); + +const adrTriggerRules = [ + '선택지가 2개 이상이고 트레이드오프가 존재한 경우.', + '반복적으로 따라야 할 규칙이나 경계를 정의한 경우.', + '테스트 전략이나 검증 방식이 결정의 핵심이었던 경우.', +]; + +const markdownlintTargets = [ + 'AGENTS.md', + 'ARCHITECTURE.md', + 'docs/README.md', + ...fs + .readdirSync(ADR_DIR) + .filter((fileName) => fileName.endsWith('.md')) + .sort() + .map((fileName) => path.join('docs', 'adr', fileName)), +]; + +function readText(filePath) { + return fs.readFileSync(filePath, 'utf8'); +} + +function assertCondition(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +function assertFileExists(relativePath) { + assertCondition( + fs.existsSync(path.join(ROOT, relativePath)), + `Missing docs index target: ${relativePath}` + ); +} + +function verifyDocsIndexTargets() { + const docsIndex = readText(DOCS_INDEX); + const linkedPaths = [...docsIndex.matchAll(/`([^`]+)`/g)] + .map((match) => match[1]) + .filter((entry) => !entry.includes('*')) + .filter((entry) => entry.endsWith('.md') || entry.endsWith('.sql')); + + linkedPaths.forEach(assertFileExists); +} + +function verifyAdrTriggerRules() { + const agents = readText(AGENTS); + const adrIndex = readText(ADR_INDEX); + + adrTriggerRules.forEach((rule) => { + assertCondition( + agents.includes(rule), + `AGENTS.md is missing ADR trigger rule: ${rule}` + ); + assertCondition( + adrIndex.includes(rule), + `docs/adr/README.md is missing ADR trigger rule: ${rule}` + ); + }); +} + +function verifyAdrIndexEntries() { + const adrIndex = readText(ADR_INDEX); + const adrFiles = fs + .readdirSync(ADR_DIR) + .filter((fileName) => /^\d{4}-.+\.md$/.test(fileName)) + .sort(); + + adrFiles.forEach((fileName) => { + assertCondition( + adrIndex.includes(`](${fileName})`), + `docs/adr/README.md does not list ${fileName}` + ); + }); +} + +function runMarkdownlint() { + const result = spawnSync( + path.join(ROOT, 'node_modules', '.bin', 'markdownlint-cli2'), + [ + '--config', + path.join('internal', 'config', '.markdownlint-cli2.jsonc'), + ...markdownlintTargets, + ], + { + cwd: ROOT, + stdio: 'inherit', + } + ); + + assertCondition( + result.status === 0, + `markdownlint failed with exit code ${result.status ?? 'unknown'}` + ); +} + +function main() { + verifyDocsIndexTargets(); + verifyAdrTriggerRules(); + verifyAdrIndexEntries(); + runMarkdownlint(); + console.log('Documentation verification passed.'); +} + +try { + main(); +} catch (error) { + console.error( + error instanceof Error ? error.message : 'Documentation verification failed.' + ); + process.exit(1); +} diff --git a/package.json b/package.json index 76403f7..e7e3091 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,13 @@ "start": "next start", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "lint:css:syntax": "node internal/scripts/check-css-syntax.mjs", + "verify:docs": "node internal/scripts/verify-docs.mjs", "analyze": "ANALYZE=true next build --webpack", "test": "vitest", "test:unit": "vitest run", "test:components": "vitest run src/features/**/*.test.ts src/features/**/*.test.tsx src/shared/**/*.test.ts src/shared/**/*.test.tsx", "test:coverage": "vitest run --coverage", - "test:ci": "npm run lint && npm run lint:css:syntax && npm run test:unit && npm run test:e2e", + "test:ci": "npm run lint && npm run lint:css:syntax && npm run verify:docs && npm run test:unit && npm run test:e2e", "test:e2e": "npx playwright test", "test:smoke": "vitest run src/shared/**/*.test.ts src/app/actions/*.test.ts && npx playwright test --grep @smoke", "new-post": "node internal/scripts/posts/new-post.js",