diff --git a/.gitignore b/.gitignore index c121968..d06da34 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,10 @@ /.pnp .pnp.js -# testing +# generated reports and local captures /coverage /test-results/ +/output/ # next.js /.next/ @@ -18,7 +19,6 @@ # misc .DS_Store *.pem -/output/ # debug npm-debug.log* diff --git a/AGENTS.md b/AGENTS.md index 885491b..6d46d5a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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을 작성하거나 갱신한다. @@ -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 ## 개발 명령 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5ebaeeb..4cfb7ea 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,6 +1,6 @@ # ARCHITECTURE -Last updated: 2026-05-06 +Last updated: 2026-05-07 ## System Summary @@ -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 @@ -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. @@ -57,22 +57,23 @@ 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` @@ -80,8 +81,8 @@ Last updated: 2026-05-06 ### 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 @@ -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 @@ -109,10 +110,11 @@ 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 @@ -120,8 +122,8 @@ Last updated: 2026-05-06 - 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. @@ -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. diff --git a/README.md b/README.md index 82262b2..7f1c22e 100644 --- a/README.md +++ b/README.md @@ -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 # 글 본문 diff --git a/src/app/actions/view.test.ts b/blog/api/view.test.ts similarity index 98% rename from src/app/actions/view.test.ts rename to blog/api/view.test.ts index 3e72155..11cb5c1 100644 --- a/src/app/actions/view.test.ts +++ b/blog/api/view.test.ts @@ -1,6 +1,6 @@ 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, @@ -8,7 +8,7 @@ import { trackView, } from './view'; -vi.mock('@/shared/integrations/supabase', () => ({ +vi.mock('@/platform/integrations/supabase', () => ({ getSupabaseServerClient: vi.fn(), })); diff --git a/src/app/actions/view.ts b/blog/api/view.ts similarity index 99% rename from src/app/actions/view.ts rename to blog/api/view.ts index eade91c..642ca0b 100644 --- a/src/app/actions/view.ts +++ b/blog/api/view.ts @@ -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 = diff --git a/blog/index.ts b/blog/index.ts new file mode 100644 index 0000000..4e68f59 --- /dev/null +++ b/blog/index.ts @@ -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'; diff --git a/src/domains/post/model/frontmatter-schema.test.ts b/blog/model/frontmatter-schema.test.ts similarity index 100% rename from src/domains/post/model/frontmatter-schema.test.ts rename to blog/model/frontmatter-schema.test.ts diff --git a/src/domains/post/model/frontmatter-schema.ts b/blog/model/frontmatter-schema.ts similarity index 100% rename from src/domains/post/model/frontmatter-schema.ts rename to blog/model/frontmatter-schema.ts diff --git a/src/features/blog/model/series-group.test.ts b/blog/model/series-group.test.ts similarity index 97% rename from src/features/blog/model/series-group.test.ts rename to blog/model/series-group.test.ts index 5a8ff44..bf63969 100644 --- a/src/features/blog/model/series-group.test.ts +++ b/blog/model/series-group.test.ts @@ -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[] = [ diff --git a/src/features/blog/model/series-group.ts b/blog/model/series-group.ts similarity index 97% rename from src/features/blog/model/series-group.ts rename to blog/model/series-group.ts index bd9fb5f..24665de 100644 --- a/src/features/blog/model/series-group.ts +++ b/blog/model/series-group.ts @@ -1,4 +1,4 @@ -import type { FeedData } from '@/domains/post/model/types'; +import type { FeedData } from '@/blog/model/types'; export interface SeriesGroup { id: string; diff --git a/src/domains/post/model/types.ts b/blog/model/types.ts similarity index 100% rename from src/domains/post/model/types.ts rename to blog/model/types.ts diff --git a/src/features/blog/services/markdown-parser.test.ts b/blog/services/markdown-parser.test.ts similarity index 100% rename from src/features/blog/services/markdown-parser.test.ts rename to blog/services/markdown-parser.test.ts diff --git a/src/features/blog/services/markdown-parser.ts b/blog/services/markdown-parser.ts similarity index 96% rename from src/features/blog/services/markdown-parser.ts rename to blog/services/markdown-parser.ts index 5e5e02c..b43a0cf 100644 --- a/src/features/blog/services/markdown-parser.ts +++ b/blog/services/markdown-parser.ts @@ -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'; diff --git a/src/features/blog/services/post-repository.test.ts b/blog/services/post-repository.test.ts similarity index 100% rename from src/features/blog/services/post-repository.test.ts rename to blog/services/post-repository.test.ts diff --git a/src/features/blog/services/post-repository.ts b/blog/services/post-repository.ts similarity index 98% rename from src/features/blog/services/post-repository.ts rename to blog/services/post-repository.ts index 18be5c1..8aea2ec 100644 --- a/src/features/blog/services/post-repository.ts +++ b/blog/services/post-repository.ts @@ -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'; diff --git a/src/features/blog/services/publication-policy.test.ts b/blog/services/publication-policy.test.ts similarity index 97% rename from src/features/blog/services/publication-policy.test.ts rename to blog/services/publication-policy.test.ts index 5c968ca..39dda91 100644 --- a/src/features/blog/services/publication-policy.test.ts +++ b/blog/services/publication-policy.test.ts @@ -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 = [ diff --git a/src/features/blog/ui/components/CategoryFilter.test.tsx b/blog/ui/components/CategoryFilter.test.tsx similarity index 100% rename from src/features/blog/ui/components/CategoryFilter.test.tsx rename to blog/ui/components/CategoryFilter.test.tsx diff --git a/src/features/blog/ui/components/CategoryFilter/CategoryFilter.tsx b/blog/ui/components/CategoryFilter/CategoryFilter.tsx similarity index 100% rename from src/features/blog/ui/components/CategoryFilter/CategoryFilter.tsx rename to blog/ui/components/CategoryFilter/CategoryFilter.tsx diff --git a/src/features/blog/ui/components/CategoryFilter/index.ts b/blog/ui/components/CategoryFilter/index.ts similarity index 100% rename from src/features/blog/ui/components/CategoryFilter/index.ts rename to blog/ui/components/CategoryFilter/index.ts diff --git a/src/features/blog/ui/components/GiscusComments.test.tsx b/blog/ui/components/GiscusComments.test.tsx similarity index 100% rename from src/features/blog/ui/components/GiscusComments.test.tsx rename to blog/ui/components/GiscusComments.test.tsx diff --git a/src/features/blog/ui/components/GiscusComments.tsx b/blog/ui/components/GiscusComments.tsx similarity index 100% rename from src/features/blog/ui/components/GiscusComments.tsx rename to blog/ui/components/GiscusComments.tsx diff --git a/src/features/blog/ui/components/ImageGrid.test.tsx b/blog/ui/components/ImageGrid.test.tsx similarity index 100% rename from src/features/blog/ui/components/ImageGrid.test.tsx rename to blog/ui/components/ImageGrid.test.tsx diff --git a/src/features/blog/ui/components/ImageGrid.tsx b/blog/ui/components/ImageGrid.tsx similarity index 100% rename from src/features/blog/ui/components/ImageGrid.tsx rename to blog/ui/components/ImageGrid.tsx diff --git a/src/features/blog/ui/components/MermaidDiagram.test.tsx b/blog/ui/components/MermaidDiagram.test.tsx similarity index 100% rename from src/features/blog/ui/components/MermaidDiagram.test.tsx rename to blog/ui/components/MermaidDiagram.test.tsx diff --git a/src/features/blog/ui/components/MermaidDiagram.tsx b/blog/ui/components/MermaidDiagram.tsx similarity index 100% rename from src/features/blog/ui/components/MermaidDiagram.tsx rename to blog/ui/components/MermaidDiagram.tsx diff --git a/src/features/blog/ui/components/PostCard.test.tsx b/blog/ui/components/PostCard.test.tsx similarity index 97% rename from src/features/blog/ui/components/PostCard.test.tsx rename to blog/ui/components/PostCard.test.tsx index 6f78040..fef4c07 100644 --- a/src/features/blog/ui/components/PostCard.test.tsx +++ b/blog/ui/components/PostCard.test.tsx @@ -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: ({ diff --git a/src/features/blog/ui/components/PostCard/PostCard.tsx b/blog/ui/components/PostCard/PostCard.tsx similarity index 99% rename from src/features/blog/ui/components/PostCard/PostCard.tsx rename to blog/ui/components/PostCard/PostCard.tsx index 14b7e94..a811f94 100644 --- a/src/features/blog/ui/components/PostCard/PostCard.tsx +++ b/blog/ui/components/PostCard/PostCard.tsx @@ -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 { diff --git a/src/features/blog/ui/components/PostCard/index.ts b/blog/ui/components/PostCard/index.ts similarity index 100% rename from src/features/blog/ui/components/PostCard/index.ts rename to blog/ui/components/PostCard/index.ts diff --git a/src/features/blog/ui/components/PostList.test.tsx b/blog/ui/components/PostList.test.tsx similarity index 96% rename from src/features/blog/ui/components/PostList.test.tsx rename to blog/ui/components/PostList.test.tsx index 991842b..9eabbca 100644 --- a/src/features/blog/ui/components/PostList.test.tsx +++ b/blog/ui/components/PostList.test.tsx @@ -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: { diff --git a/src/features/blog/ui/components/PostList/PostList.tsx b/blog/ui/components/PostList/PostList.tsx similarity index 97% rename from src/features/blog/ui/components/PostList/PostList.tsx rename to blog/ui/components/PostList/PostList.tsx index f1a1579..457eb52 100644 --- a/src/features/blog/ui/components/PostList/PostList.tsx +++ b/blog/ui/components/PostList/PostList.tsx @@ -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, diff --git a/src/features/blog/ui/components/PostList/index.ts b/blog/ui/components/PostList/index.ts similarity index 100% rename from src/features/blog/ui/components/PostList/index.ts rename to blog/ui/components/PostList/index.ts diff --git a/src/features/blog/ui/components/ReadingProgress/ReadingProgress.test.tsx b/blog/ui/components/ReadingProgress/ReadingProgress.test.tsx similarity index 100% rename from src/features/blog/ui/components/ReadingProgress/ReadingProgress.test.tsx rename to blog/ui/components/ReadingProgress/ReadingProgress.test.tsx diff --git a/src/features/blog/ui/components/ReadingProgress/ReadingProgress.tsx b/blog/ui/components/ReadingProgress/ReadingProgress.tsx similarity index 100% rename from src/features/blog/ui/components/ReadingProgress/ReadingProgress.tsx rename to blog/ui/components/ReadingProgress/ReadingProgress.tsx diff --git a/src/features/blog/ui/components/ReadingProgress/index.ts b/blog/ui/components/ReadingProgress/index.ts similarity index 100% rename from src/features/blog/ui/components/ReadingProgress/index.ts rename to blog/ui/components/ReadingProgress/index.ts diff --git a/src/features/blog/ui/components/ScrollWorkflow.test.tsx b/blog/ui/components/ScrollWorkflow.test.tsx similarity index 100% rename from src/features/blog/ui/components/ScrollWorkflow.test.tsx rename to blog/ui/components/ScrollWorkflow.test.tsx diff --git a/src/features/blog/ui/components/ScrollWorkflow.tsx b/blog/ui/components/ScrollWorkflow.tsx similarity index 100% rename from src/features/blog/ui/components/ScrollWorkflow.tsx rename to blog/ui/components/ScrollWorkflow.tsx diff --git a/src/features/blog/ui/components/SeriesHubCard.test.tsx b/blog/ui/components/SeriesHubCard.test.tsx similarity index 96% rename from src/features/blog/ui/components/SeriesHubCard.test.tsx rename to blog/ui/components/SeriesHubCard.test.tsx index 5a02ee5..1067cbf 100644 --- a/src/features/blog/ui/components/SeriesHubCard.test.tsx +++ b/blog/ui/components/SeriesHubCard.test.tsx @@ -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', () => ({ diff --git a/src/features/blog/ui/components/SeriesHubCard.tsx b/blog/ui/components/SeriesHubCard.tsx similarity index 95% rename from src/features/blog/ui/components/SeriesHubCard.tsx rename to blog/ui/components/SeriesHubCard.tsx index d46ef43..f7d2aec 100644 --- a/src/features/blog/ui/components/SeriesHubCard.tsx +++ b/blog/ui/components/SeriesHubCard.tsx @@ -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 { diff --git a/src/features/blog/ui/components/SeriesHubList.tsx b/blog/ui/components/SeriesHubList.tsx similarity index 86% rename from src/features/blog/ui/components/SeriesHubList.tsx rename to blog/ui/components/SeriesHubList.tsx index 4670132..42e00c8 100644 --- a/src/features/blog/ui/components/SeriesHubList.tsx +++ b/blog/ui/components/SeriesHubList.tsx @@ -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 { diff --git a/src/features/blog/ui/components/SeriesNavigation.test.tsx b/blog/ui/components/SeriesNavigation.test.tsx similarity index 100% rename from src/features/blog/ui/components/SeriesNavigation.test.tsx rename to blog/ui/components/SeriesNavigation.test.tsx diff --git a/src/features/blog/ui/components/SeriesNavigation/SeriesNavigation.module.css b/blog/ui/components/SeriesNavigation/SeriesNavigation.module.css similarity index 100% rename from src/features/blog/ui/components/SeriesNavigation/SeriesNavigation.module.css rename to blog/ui/components/SeriesNavigation/SeriesNavigation.module.css diff --git a/src/features/blog/ui/components/SeriesNavigation/SeriesNavigation.tsx b/blog/ui/components/SeriesNavigation/SeriesNavigation.tsx similarity index 98% rename from src/features/blog/ui/components/SeriesNavigation/SeriesNavigation.tsx rename to blog/ui/components/SeriesNavigation/SeriesNavigation.tsx index 47b72bb..200f682 100644 --- a/src/features/blog/ui/components/SeriesNavigation/SeriesNavigation.tsx +++ b/blog/ui/components/SeriesNavigation/SeriesNavigation.tsx @@ -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 { diff --git a/src/features/blog/ui/components/SeriesNavigation/index.ts b/blog/ui/components/SeriesNavigation/index.ts similarity index 100% rename from src/features/blog/ui/components/SeriesNavigation/index.ts rename to blog/ui/components/SeriesNavigation/index.ts diff --git a/src/features/blog/ui/components/SeriesTrackedLink.test.tsx b/blog/ui/components/SeriesTrackedLink.test.tsx similarity index 97% rename from src/features/blog/ui/components/SeriesTrackedLink.test.tsx rename to blog/ui/components/SeriesTrackedLink.test.tsx index eb06cd2..1ad4c5f 100644 --- a/src/features/blog/ui/components/SeriesTrackedLink.test.tsx +++ b/blog/ui/components/SeriesTrackedLink.test.tsx @@ -30,7 +30,7 @@ vi.mock('next/link', () => ({ }, })); -vi.mock('@/shared/analytics/lib/analytics', () => ({ +vi.mock('@/platform/analytics/lib/analytics', () => ({ AnalyticsEvents: { click: 'click', }, diff --git a/src/features/blog/ui/components/SeriesTrackedLink.tsx b/blog/ui/components/SeriesTrackedLink.tsx similarity index 93% rename from src/features/blog/ui/components/SeriesTrackedLink.tsx rename to blog/ui/components/SeriesTrackedLink.tsx index 4dc8991..00694c2 100644 --- a/src/features/blog/ui/components/SeriesTrackedLink.tsx +++ b/blog/ui/components/SeriesTrackedLink.tsx @@ -3,7 +3,7 @@ import Link from 'next/link'; import type { MouseEvent, ReactNode } from 'react'; import { clsx } from 'clsx'; -import { AnalyticsEvents, trackEvent } from '@/shared/analytics/lib/analytics'; +import { AnalyticsEvents, trackEvent } from '@/platform/analytics/lib/analytics'; type SeriesTrackedTarget = 'series_hub_start' | 'series_hub_episode'; diff --git a/src/features/blog/ui/components/TableOfContents.test.tsx b/blog/ui/components/TableOfContents.test.tsx similarity index 100% rename from src/features/blog/ui/components/TableOfContents.test.tsx rename to blog/ui/components/TableOfContents.test.tsx diff --git a/src/features/blog/ui/components/TableOfContents/TableOfContents.tsx b/blog/ui/components/TableOfContents/TableOfContents.tsx similarity index 100% rename from src/features/blog/ui/components/TableOfContents/TableOfContents.tsx rename to blog/ui/components/TableOfContents/TableOfContents.tsx diff --git a/src/features/blog/ui/components/TableOfContents/index.ts b/blog/ui/components/TableOfContents/index.ts similarity index 100% rename from src/features/blog/ui/components/TableOfContents/index.ts rename to blog/ui/components/TableOfContents/index.ts diff --git a/src/features/blog/ui/components/ViewCounter.test.tsx b/blog/ui/components/ViewCounter.test.tsx similarity index 97% rename from src/features/blog/ui/components/ViewCounter.test.tsx rename to blog/ui/components/ViewCounter.test.tsx index 22d0665..3121e25 100644 --- a/src/features/blog/ui/components/ViewCounter.test.tsx +++ b/blog/ui/components/ViewCounter.test.tsx @@ -5,7 +5,7 @@ import ViewCounter from './ViewCounter'; const mockTrackView = vi.fn(); const mockGetViewCount = vi.fn(); -vi.mock('@/app/actions/view', () => ({ +vi.mock('@/blog/api/view', () => ({ getViewCount: (...args: unknown[]) => mockGetViewCount(...args), trackView: (...args: unknown[]) => mockTrackView(...args), })); diff --git a/src/features/blog/ui/components/ViewCounter.tsx b/blog/ui/components/ViewCounter.tsx similarity index 95% rename from src/features/blog/ui/components/ViewCounter.tsx rename to blog/ui/components/ViewCounter.tsx index 4ec6a5e..bdee08d 100644 --- a/src/features/blog/ui/components/ViewCounter.tsx +++ b/blog/ui/components/ViewCounter.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { getViewCount, trackView } from '@/app/actions/view'; +import { getViewCount, trackView } from '@/blog/api/view'; interface ViewCounterProps { slug: string; diff --git a/src/features/blog/ui/components/index.ts b/blog/ui/components/index.ts similarity index 100% rename from src/features/blog/ui/components/index.ts rename to blog/ui/components/index.ts diff --git a/src/features/blog/ui/mdx/components.tsx b/blog/ui/mdx/components.tsx similarity index 90% rename from src/features/blog/ui/mdx/components.tsx rename to blog/ui/mdx/components.tsx index 0132f08..b73d0a7 100644 --- a/src/features/blog/ui/mdx/components.tsx +++ b/blog/ui/mdx/components.tsx @@ -10,36 +10,36 @@ import { ImageGrid, MermaidDiagram, ScrollWorkflow, -} from '@/features/blog/ui/components'; +} from '@/blog/ui/components'; // Dynamic imports for visualization components (code splitting) const BinarySearchVisualization = dynamic(() => - import('@/components/visualization').then((mod) => ({ + import('@/shared/visualization').then((mod) => ({ default: mod.BinarySearchVisualization, })) ); const DPVisualization = dynamic(() => - import('@/components/visualization').then((mod) => ({ + import('@/shared/visualization').then((mod) => ({ default: mod.DPVisualization, })) ); const GraphTraversalVisualization = dynamic(() => - import('@/components/visualization').then((mod) => ({ + import('@/shared/visualization').then((mod) => ({ default: mod.GraphTraversalVisualization, })) ); const SlidingWindowVisualization = dynamic(() => - import('@/components/visualization').then((mod) => ({ + import('@/shared/visualization').then((mod) => ({ default: mod.SlidingWindowVisualization, })) ); const SortingVisualization = dynamic(() => - import('@/components/visualization').then((mod) => ({ + import('@/shared/visualization').then((mod) => ({ default: mod.SortingVisualization, })) ); const TwoPointerVisualization = dynamic(() => - import('@/components/visualization').then((mod) => ({ + import('@/shared/visualization').then((mod) => ({ default: mod.TwoPointerVisualization, })) ); diff --git a/src/features/blog/ui/pages/BlogListClient.tsx b/blog/ui/pages/BlogListClient.tsx similarity index 87% rename from src/features/blog/ui/pages/BlogListClient.tsx rename to blog/ui/pages/BlogListClient.tsx index 73e4d0f..0102fc9 100644 --- a/src/features/blog/ui/pages/BlogListClient.tsx +++ b/blog/ui/pages/BlogListClient.tsx @@ -1,9 +1,9 @@ 'use client'; import { useState, useMemo } from 'react'; -import { CategoryFilter, PostList } from '@/features/blog/ui/components'; -import type { Category } from '@/features/blog/ui/components'; -import type { FeedData } from '@/domains/post/model/types'; +import { CategoryFilter, PostList } from '@/blog/ui/components'; +import type { Category } from '@/blog/ui/components'; +import type { FeedData } from '@/blog/model/types'; interface BlogListClientProps { posts: FeedData[]; diff --git a/src/features/blog/ui/pages/BlogListPage.tsx b/blog/ui/pages/BlogListPage.tsx similarity index 91% rename from src/features/blog/ui/pages/BlogListPage.tsx rename to blog/ui/pages/BlogListPage.tsx index 69d159a..874c5fa 100644 --- a/src/features/blog/ui/pages/BlogListPage.tsx +++ b/blog/ui/pages/BlogListPage.tsx @@ -1,5 +1,5 @@ import { Metadata } from 'next'; -import { getSortedFeedData } from '@/features/blog/services/post-repository'; +import { getSortedFeedData } from '@/blog/services/post-repository'; import { Container } from '@/shared/layout'; import BlogListClient from './BlogListClient'; diff --git a/src/features/blog/ui/pages/BlogPostPage.tsx b/blog/ui/pages/BlogPostPage.tsx similarity index 90% rename from src/features/blog/ui/pages/BlogPostPage.tsx rename to blog/ui/pages/BlogPostPage.tsx index d567891..ca5a933 100644 --- a/src/features/blog/ui/pages/BlogPostPage.tsx +++ b/blog/ui/pages/BlogPostPage.tsx @@ -4,11 +4,11 @@ import { getFeedData, getAllFeedSlugs, getSeriesPosts, -} from '@/features/blog/services/post-repository'; +} from '@/blog/services/post-repository'; import { getMdxSource, parseHeadingsFromMdx, -} from '@/features/blog/services/markdown-parser'; +} from '@/blog/services/markdown-parser'; import { Container } from '@/shared/layout'; import { ReadingProgress, @@ -16,13 +16,13 @@ import { GiscusComments, SeriesNavigation, ViewCounter, -} from '@/features/blog/ui/components'; -import PostViewTracker from '@/shared/analytics/components/PostViewTracker'; -import DwellTimeTracker from '@/shared/analytics/components/DwellTimeTracker'; -import ScrollDepthTracker from '@/shared/analytics/components/ScrollDepthTracker'; -import JsonLd from '@/shared/seo/JsonLd'; -import { getMDXComponents } from '@/features/blog/ui/mdx/components'; -import { SITE_URL } from '@/core/config/site'; +} from '@/blog/ui/components'; +import PostViewTracker from '@/platform/analytics/components/PostViewTracker'; +import DwellTimeTracker from '@/platform/analytics/components/DwellTimeTracker'; +import ScrollDepthTracker from '@/platform/analytics/components/ScrollDepthTracker'; +import JsonLd from '@/platform/seo/JsonLd'; +import { getMDXComponents } from '@/blog/ui/mdx/components'; +import { SITE_URL } from '@/site/config/site'; export async function generateStaticParams() { return getAllFeedSlugs(); diff --git a/src/features/blog/ui/pages/EngineeringPage.test.tsx b/blog/ui/pages/EngineeringPage.test.tsx similarity index 92% rename from src/features/blog/ui/pages/EngineeringPage.test.tsx rename to blog/ui/pages/EngineeringPage.test.tsx index 40d1fb2..768282e 100644 --- a/src/features/blog/ui/pages/EngineeringPage.test.tsx +++ b/blog/ui/pages/EngineeringPage.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import type { ReactNode } from 'react'; import { describe, expect, it, vi } from 'vitest'; -import type { FeedData } from '@/domains/post/model/types'; +import type { FeedData } from '@/blog/model/types'; import EngineeringPage from './EngineeringPage'; const mockGetSortedFeedData = vi.fn<() => FeedData[]>(() => []); @@ -11,7 +11,7 @@ vi.mock('@/shared/layout', () => ({ Container: ({ children }: { children: ReactNode }) =>
{children}
, })); -vi.mock('@/features/blog/services/post-repository', () => ({ +vi.mock('@/blog/services/post-repository', () => ({ getSortedFeedData: () => mockGetSortedFeedData(), })); diff --git a/src/features/blog/ui/pages/EngineeringPage.tsx b/blog/ui/pages/EngineeringPage.tsx similarity index 92% rename from src/features/blog/ui/pages/EngineeringPage.tsx rename to blog/ui/pages/EngineeringPage.tsx index ceea957..bfcba87 100644 --- a/src/features/blog/ui/pages/EngineeringPage.tsx +++ b/blog/ui/pages/EngineeringPage.tsx @@ -1,6 +1,6 @@ import { Metadata } from 'next'; import { Suspense } from 'react'; -import { getSortedFeedData } from '@/features/blog/services/post-repository'; +import { getSortedFeedData } from '@/blog/services/post-repository'; import { Container } from '@/shared/layout'; import EngineeringPageClient from './EngineeringPageClient'; diff --git a/src/features/blog/ui/pages/EngineeringPageClient.test.tsx b/blog/ui/pages/EngineeringPageClient.test.tsx similarity index 96% rename from src/features/blog/ui/pages/EngineeringPageClient.test.tsx rename to blog/ui/pages/EngineeringPageClient.test.tsx index b03a7d8..923139b 100644 --- a/src/features/blog/ui/pages/EngineeringPageClient.test.tsx +++ b/blog/ui/pages/EngineeringPageClient.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { FeedData } from '@/domains/post/model/types'; +import type { FeedData } from '@/blog/model/types'; import EngineeringPageClient from './EngineeringPageClient'; let mockQueryString = ''; @@ -14,7 +14,7 @@ vi.mock('next/navigation', () => ({ useSearchParams: () => new URLSearchParams(mockQueryString), })); -vi.mock('@/features/blog/ui/components', () => ({ +vi.mock('@/blog/ui/components', () => ({ PostList: ({ posts }: { posts: FeedData[] }) => (
{posts.map((post) => post.slug).join(',')}
), diff --git a/src/features/blog/ui/pages/EngineeringPageClient.tsx b/blog/ui/pages/EngineeringPageClient.tsx similarity index 97% rename from src/features/blog/ui/pages/EngineeringPageClient.tsx rename to blog/ui/pages/EngineeringPageClient.tsx index 8b976ee..59f2d2d 100644 --- a/src/features/blog/ui/pages/EngineeringPageClient.tsx +++ b/blog/ui/pages/EngineeringPageClient.tsx @@ -3,8 +3,8 @@ import { useEffect, useMemo } from 'react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { clsx } from 'clsx'; -import type { FeedData } from '@/domains/post/model/types'; -import { PostList } from '@/features/blog/ui/components'; +import type { FeedData } from '@/blog/model/types'; +import { PostList } from '@/blog/ui/components'; const ARTICLE_PAGE_SIZE = 5; interface EngineeringPageClientProps { diff --git a/src/features/blog/ui/pages/EngineeringSeriesPage.test.tsx b/blog/ui/pages/EngineeringSeriesPage.test.tsx similarity index 95% rename from src/features/blog/ui/pages/EngineeringSeriesPage.test.tsx rename to blog/ui/pages/EngineeringSeriesPage.test.tsx index 6d6340a..a970ed9 100644 --- a/src/features/blog/ui/pages/EngineeringSeriesPage.test.tsx +++ b/blog/ui/pages/EngineeringSeriesPage.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import type { ReactNode } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { FeedData } from '@/domains/post/model/types'; +import type { FeedData } from '@/blog/model/types'; import EngineeringSeriesPage from './EngineeringSeriesPage'; const mockGetSortedFeedData = vi.fn<() => FeedData[]>(() => []); @@ -14,7 +14,7 @@ vi.mock('@/shared/layout', () => ({ Container: ({ children }: { children: ReactNode }) =>
{children}
, })); -vi.mock('@/features/blog/services/post-repository', () => ({ +vi.mock('@/blog/services/post-repository', () => ({ getSortedFeedData: () => mockGetSortedFeedData(), })); diff --git a/src/features/blog/ui/pages/EngineeringSeriesPage.tsx b/blog/ui/pages/EngineeringSeriesPage.tsx similarity index 95% rename from src/features/blog/ui/pages/EngineeringSeriesPage.tsx rename to blog/ui/pages/EngineeringSeriesPage.tsx index a0b3281..3f8ab9e 100644 --- a/src/features/blog/ui/pages/EngineeringSeriesPage.tsx +++ b/blog/ui/pages/EngineeringSeriesPage.tsx @@ -1,7 +1,7 @@ import Link from 'next/link'; import { notFound } from 'next/navigation'; -import { getSeriesSummaries } from '@/features/blog/model/series-group'; -import { getSortedFeedData } from '@/features/blog/services/post-repository'; +import { getSeriesSummaries } from '@/blog/model/series-group'; +import { getSortedFeedData } from '@/blog/services/post-repository'; import { Container } from '@/shared/layout'; interface EngineeringSeriesPageProps { diff --git a/src/features/blog/ui/pages/LifePage.test.tsx b/blog/ui/pages/LifePage.test.tsx similarity index 87% rename from src/features/blog/ui/pages/LifePage.test.tsx rename to blog/ui/pages/LifePage.test.tsx index 0cccbc7..bb8ec5d 100644 --- a/src/features/blog/ui/pages/LifePage.test.tsx +++ b/blog/ui/pages/LifePage.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import type { ReactNode } from 'react'; import { describe, expect, it, vi } from 'vitest'; -import type { FeedData } from '@/domains/post/model/types'; +import type { FeedData } from '@/blog/model/types'; import LifePage from './LifePage'; const mockGetSortedFeedData = vi.fn<() => FeedData[]>(() => []); @@ -11,11 +11,11 @@ vi.mock('@/shared/layout', () => ({ Container: ({ children }: { children: ReactNode }) =>
{children}
, })); -vi.mock('@/features/blog/services/post-repository', () => ({ +vi.mock('@/blog/services/post-repository', () => ({ getSortedFeedData: () => mockGetSortedFeedData(), })); -vi.mock('@/features/blog/ui/components', () => ({ +vi.mock('@/blog/ui/components', () => ({ PostList: ({ posts }: { posts: FeedData[] }) => (
posts:{posts.length}
), diff --git a/src/features/blog/ui/pages/LifePage.tsx b/blog/ui/pages/LifePage.tsx similarity index 86% rename from src/features/blog/ui/pages/LifePage.tsx rename to blog/ui/pages/LifePage.tsx index ca8b178..f032b0e 100644 --- a/src/features/blog/ui/pages/LifePage.tsx +++ b/blog/ui/pages/LifePage.tsx @@ -1,7 +1,7 @@ import { Metadata } from 'next'; -import { getSortedFeedData } from '@/features/blog/services/post-repository'; +import { getSortedFeedData } from '@/blog/services/post-repository'; import { Container } from '@/shared/layout'; -import { PostList } from '@/features/blog/ui/components'; +import { PostList } from '@/blog/ui/components'; export const metadata: Metadata = { title: 'Life', diff --git a/src/features/blog/ui/pages/SeriesPage.test.tsx b/blog/ui/pages/SeriesPage.test.tsx similarity index 90% rename from src/features/blog/ui/pages/SeriesPage.test.tsx rename to blog/ui/pages/SeriesPage.test.tsx index 8386478..c3418d1 100644 --- a/src/features/blog/ui/pages/SeriesPage.test.tsx +++ b/blog/ui/pages/SeriesPage.test.tsx @@ -1,8 +1,8 @@ import { render, screen } from '@testing-library/react'; import type { ReactNode } from 'react'; import { describe, expect, it, vi } from 'vitest'; -import type { FeedData } from '@/domains/post/model/types'; -import type { SeriesSummary } from '@/features/blog/model/series-group'; +import type { FeedData } from '@/blog/model/types'; +import type { SeriesSummary } from '@/blog/model/series-group'; import SeriesPage from './SeriesPage'; const mockGetSortedFeedData = vi.fn<() => FeedData[]>(() => []); @@ -28,14 +28,14 @@ vi.mock('@/shared/layout', () => ({ Container: ({ children }: { children: ReactNode }) =>
{children}
, })); -vi.mock('@/features/blog/services/post-repository', () => ({ +vi.mock('@/blog/services/post-repository', () => ({ getSortedFeedData: () => mockGetSortedFeedData(), })); -vi.mock('@/features/blog/model/series-group', async () => { +vi.mock('@/blog/model/series-group', async () => { const actual = await vi.importActual< - typeof import('@/features/blog/model/series-group') - >('@/features/blog/model/series-group'); + typeof import('@/blog/model/series-group') + >('@/blog/model/series-group'); return { ...actual, diff --git a/src/features/blog/ui/pages/SeriesPage.tsx b/blog/ui/pages/SeriesPage.tsx similarity index 88% rename from src/features/blog/ui/pages/SeriesPage.tsx rename to blog/ui/pages/SeriesPage.tsx index 74e2b63..53e04f6 100644 --- a/src/features/blog/ui/pages/SeriesPage.tsx +++ b/blog/ui/pages/SeriesPage.tsx @@ -1,9 +1,9 @@ import { Metadata } from 'next'; -import { getSortedFeedData } from '@/features/blog/services/post-repository'; +import { getSortedFeedData } from '@/blog/services/post-repository'; import { Container } from '@/shared/layout'; import { EmptyState } from '@/shared/ui'; -import { getSeriesSummaries } from '@/features/blog/model/series-group'; -import { SeriesHubList } from '@/features/blog/ui/components'; +import { getSeriesSummaries } from '@/blog/model/series-group'; +import { SeriesHubList } from '@/blog/ui/components'; export const metadata: Metadata = { title: 'Series', diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 25a50d1..2528f9b 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1,12 +1,12 @@ # DESIGN -Last updated: 2026-02-26 +Last updated: 2026-05-07 ## Source of Truth -1. Tokens: `src/styles/tokens.css` -2. Global style behavior: `src/styles/globals.css` -3. Reusable component patterns: `src/shared/ui/**`, `src/shared/layout/**` +1. Tokens: `styles/tokens.css` +2. Global style behavior: `styles/globals.css` +3. Reusable component patterns: `shared/ui/**`, `shared/layout/**` ## Design Guardrails diff --git a/docs/FRONTEND.md b/docs/FRONTEND.md index 71d2664..6186d69 100644 --- a/docs/FRONTEND.md +++ b/docs/FRONTEND.md @@ -1,6 +1,6 @@ # FRONTEND -Last updated: 2026-02-27 +Last updated: 2026-05-07 ## Stack @@ -11,17 +11,19 @@ Last updated: 2026-02-27 ## Frontend Architecture -1. Route layer: `src/app/**` -2. Feature layer: `src/features/**` -3. Shared layer: `src/shared/**` (+ visualization: `src/components/visualization/**`) -4. Styles/tokens: `src/styles/**` +1. Route adapter layer: `src/app/**` +2. Domain modules: `blog/**`, `resume/**`, `search/**` +3. Composition layer: `site/**` +4. Runtime/integration layer: `platform/**` +5. Domain-agnostic primitives: `shared/**` +6. Styles/tokens: `styles/**` ## Frontend Rules -1. Keep boundaries clear between feature modules (`blog`, `resume`, `search`, `home`) and shared modules (`layout`, `ui`, `analytics`, `providers`, `seo`). +1. Keep boundaries clear between domain modules (`blog`, `resume`, `search`) and composition/infrastructure modules (`site`, `platform`, `shared`). 2. Avoid duplicated global trackers/providers in root layout. -3. Use typed contracts from `src/domains/**/model/types.ts` and `src/shared/types/*`. -4. Keep MDX custom component mappings centralized in `src/features/blog/ui/mdx/components.tsx`. +3. Keep domain contracts inside the owning domain module, for example `blog/model/types.ts`. +4. Keep MDX custom component mappings centralized in `blog/ui/mdx/components.tsx`. ## Test Expectations diff --git a/docs/README.md b/docs/README.md index bef4265..2929d31 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,10 +1,12 @@ # Docs Index -Last updated: 2026-04-17 +Last updated: 2026-05-07 -This index tracks the active documentation that should stay aligned with the -current codebase. If a document is missing here, add it when the document -becomes part of the maintained workflow. +This index tracks the smallest set of documentation that must stay aligned with +the current codebase. It is not a full file inventory. + +Prefer updating an existing ADR, guide, exec plan, or README before adding a new +standalone document. ## Repository-Level Docs @@ -13,33 +15,19 @@ becomes part of the maintained workflow. ## Active Documentation -- Engineering baseline: - - `docs/FRONTEND.md` - - `docs/DESIGN.md` - - `docs/RELIABILITY.md` - - `docs/SECURITY.md` - - `docs/QUALITY_SCORE.md` - - `docs/blog-quality-guide.md` -- Delivery/process: +- Decisions: - `docs/adr/README.md` + - `docs/adr/*.md` +- Planning: - `docs/PLANS.md` - `docs/exec-plans/active/README.md` - - `docs/exec-plans/completed/README.md` - `docs/exec-plans/tech-debt-tracker.md` - - `docs/guides/agentation-workflow.md` - - `docs/guides/pr-workflow.md` - - `docs/guides/testing-guide.md` - - `docs/guides/ui-components-guide.md` -- Product/domain: - - `docs/PRODUCT_SENSE.md` - - `docs/product-specs/index.md` - - `docs/product-specs/new-user-onboarding.md` - - `docs/design-docs/index.md` - - `docs/design-docs/core-beliefs.md` -- Data/analytics: - - `docs/analytics/analytics-kpi-weekly-template.md` - - `docs/database/db-schema.md` - - `docs/database/supabase-view-count.sql` +- Maintained guides: + - `docs/guides/**` +- Domain references: + - `docs/database/**` + - `docs/product-specs/**` + - `docs/design-docs/**` ## Reference / Research @@ -56,6 +44,7 @@ becomes part of the maintained workflow. 1. Use repository-relative paths only in docs (no absolute local paths). 2. Update `Last updated` when editing policy/process docs. -3. Move stale auto-generated reports to `docs/archive/` instead of deleting context. -4. Keep commands copy-pastable from repository root. -5. Remove or replace stale links when a referenced file no longer exists. +3. Treat this file as a maintained-doc boundary, not a complete docs inventory. +4. Move stale auto-generated reports to `docs/archive/` instead of deleting context. +5. Keep commands copy-pastable from repository root. +6. Remove or replace stale links when a referenced file no longer exists. diff --git a/docs/adr/0004-preserve-feature-first-source-structure.md b/docs/adr/0004-preserve-feature-first-source-structure.md index 8ae94ea..38945b0 100644 --- a/docs/adr/0004-preserve-feature-first-source-structure.md +++ b/docs/adr/0004-preserve-feature-first-source-structure.md @@ -1,7 +1,7 @@ # 0004. Feature-first 소스 구조를 유지한다 Date: 2026-02-28 -Status: Accepted +Status: Superseded by [0011](0011-adopt-frontend-modular-monolith.md) ## 배경 diff --git a/docs/adr/0005-use-token-first-tailwind-styling.md b/docs/adr/0005-use-token-first-tailwind-styling.md index 131c80b..1c020f1 100644 --- a/docs/adr/0005-use-token-first-tailwind-styling.md +++ b/docs/adr/0005-use-token-first-tailwind-styling.md @@ -1,7 +1,10 @@ # 0005. Token-first Tailwind 스타일링을 사용한다 Date: 2026-01-27 -Status: Accepted +Status: Accepted, path amended by [0011](0011-adopt-frontend-modular-monolith.md) + +Note: ADR 0011 moves the physical style path from `src/styles/**` to `styles/**` +while preserving the token-first styling decision. ## 배경 diff --git a/docs/adr/0006-isolate-visualization-heavy-components.md b/docs/adr/0006-isolate-visualization-heavy-components.md index 610aa80..15dc8ee 100644 --- a/docs/adr/0006-isolate-visualization-heavy-components.md +++ b/docs/adr/0006-isolate-visualization-heavy-components.md @@ -1,7 +1,11 @@ # 0006. 시각화 중심 컴포넌트를 격리한다 Date: 2026-01-28 -Status: Accepted +Status: Accepted, path amended by [0011](0011-adopt-frontend-modular-monolith.md) + +Note: ADR 0011 moves the physical visualization path from +`src/components/visualization/**` to `shared/visualization/**` while preserving +the isolation decision. ## 배경 diff --git a/docs/adr/0009-use-app-shell-for-primary-navigation.md b/docs/adr/0009-use-app-shell-for-primary-navigation.md index 0982dd7..2f028f5 100644 --- a/docs/adr/0009-use-app-shell-for-primary-navigation.md +++ b/docs/adr/0009-use-app-shell-for-primary-navigation.md @@ -1,7 +1,11 @@ # 0009. 주요 탐색에 AppShell을 사용한다 Date: 2026-04-21 -Status: Accepted +Status: Accepted, path amended by [0011](0011-adopt-frontend-modular-monolith.md) + +Note: ADR 0011 moves AppShell from `src/shared/layout/AppShell/**` to +`site/shell/AppShell/**` while preserving the single app-shell navigation +decision. ## 배경 diff --git a/docs/adr/0010-use-targeted-documentation-harness.md b/docs/adr/0010-use-targeted-documentation-harness.md index 550e69f..4b11663 100644 --- a/docs/adr/0010-use-targeted-documentation-harness.md +++ b/docs/adr/0010-use-targeted-documentation-harness.md @@ -17,7 +17,7 @@ markdownlint 이슈 때문에, 현재 상태에서는 문서 변경 검증용 ha ## 결정 핵심 문서만 대상으로 하는 `npm run verify:docs`를 추가한다. 이 command는 -`internal/scripts/verify-docs.mjs`를 실행하며 다음을 검증한다. +`tooling/scripts/verify-docs.mjs`를 실행하며 다음을 검증한다. - `docs/README.md`에 등록된 명시적 문서 경로가 실제로 존재하는지 확인한다. - `AGENTS.md`와 `docs/adr/README.md`가 같은 ADR 작성 조건을 포함하는지 diff --git a/docs/adr/0011-adopt-frontend-modular-monolith.md b/docs/adr/0011-adopt-frontend-modular-monolith.md new file mode 100644 index 0000000..aa49a4d --- /dev/null +++ b/docs/adr/0011-adopt-frontend-modular-monolith.md @@ -0,0 +1,71 @@ +# 0011. Domain-first Modular Monolith를 사용한다 + +Date: 2026-05-07 +Status: Accepted + +## 배경 + +기존 구조는 `src/features`, `src/domains`, `src/core`, `src/shared` 안에서 +책임을 나눴다. 하지만 실제 변경을 검토해 보니 주요 도메인이 독립된 모듈로 +분리됐다기보다 `src` 아래 하위 디렉터리를 다시 나눈 수준에 가까웠다. + +이 프로젝트에서 원하는 경계는 Next.js 소스 폴더 내부의 분류가 아니라, +`posts/`처럼 저장소 최상단에서 주요 도메인이 눈에 보이는 구조다. 글, 이력서, +검색은 서로 다른 변경 이유와 언어를 가진다. 반면 홈, AppShell, provider, +analytics, Supabase, 공통 UI는 도메인이 아니라 조합 또는 인프라 책임이다. + +## 결정 + +DDD의 전략적 설계 개념을 차용한 domain-first modular monolith를 사용한다. +다만 Entity, Aggregate, Repository 같은 tactical DDD pattern을 모든 모듈에 +강제하지 않는다. + +최상위 모듈 경계는 다음과 같이 둔다. + +- `src/app/`: Next.js App Router route adapter, route handler, metadata entry. +- `posts/`: 블로그 원문 콘텐츠 저장소. `index.mdx`와 `meta.json` 구조를 유지한다. +- `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을 둔다. + +의존 방향은 `src/app -> site -> domain -> platform/shared`를 기본으로 한다. +`blog`는 `posts/`를 읽을 수 있는 유일한 도메인 모듈이다. `site`는 여러 도메인을 +조합할 수 있지만, 도메인 내부 정책을 대신 구현하지 않는다. + +## 결과 + +- 저장소 최상단에서 주요 도메인과 조합/인프라 책임이 구분된다. +- `src/app`은 Next.js route adapter 역할에 집중한다. +- 기존 `src/features`, `src/domains`, `src/core`, `src/shared` 경계는 이 결정으로 + 대체된다. +- 단순 폴더 이동만으로 경계가 보장되지는 않는다. import 경계는 public API와 + 테스트로 우선 관리하고, lint 기반 강제는 별도 결정으로 추가할 수 있다. +- `shared`로 올리는 코드는 도메인 언어를 포함하지 않아야 한다. + +## 검토한 대안 + +- 기존 feature-first 구조 유지: 변경량은 적지만 사용자가 기대한 최상위 도메인 + 경계를 만들지 못한다. +- 모든 코드를 `src/shared` 또는 `src/features` 안에 재분류: import는 단순하지만 + 도메인 소유권이 흐려진다. +- DDD tactical pattern 전면 적용: 작은 블로그 프로젝트에는 추상화 비용이 크고, + 실제 문제인 모듈 경계보다 형식이 앞설 수 있다. +- `home`을 독립 도메인으로 승격: 홈은 자체 도메인보다 `blog`, `resume`, `search`를 + 엮는 조합 화면이므로 `site/home`에 둔다. + +## 관련 히스토리 + +- `docs/adr/0004-preserve-feature-first-source-structure.md`: 이전 feature-first + 소스 구조 결정. 이 ADR이 대체한다. +- `docs/design-docs/adr-001-modular-monolith.md`: 최상위 도메인 경계가 아닌 + `src/features` 재분류를 modular monolith로 설명하던 초안. 이 ADR로 대체하고 + 제거한다. diff --git a/docs/adr/README.md b/docs/adr/README.md index b842157..7ed8058 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -21,13 +21,14 @@ ADR은 AI 협업 가이드와 별개의 문서다. 사람이 결정했든 AI가 | [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 소스 구조를 유지한다 | +| [0004](0004-preserve-feature-first-source-structure.md) | Superseded | 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를 사용한다 | +| [0011](0011-adopt-frontend-modular-monolith.md) | Accepted | Domain-first Modular Monolith를 사용한다 | ## 작성 조건 diff --git a/docs/database/db-schema.md b/docs/database/db-schema.md index ca597ff..afece86 100644 --- a/docs/database/db-schema.md +++ b/docs/database/db-schema.md @@ -3,9 +3,9 @@ Generated from: - `docs/database/supabase-view-count.sql` -- `src/shared/integrations/supabase.ts` +- `platform/integrations/supabase.ts` -Last updated: 2026-04-16 +Last updated: 2026-05-07 ## Table: `public.views` @@ -47,7 +47,7 @@ Last updated: 2026-04-16 ## Type Mapping (App) -`src/shared/integrations/supabase.ts` defines: +`platform/integrations/supabase.ts` defines: - Table row type for `views`. - RPC signature: diff --git a/docs/guides/testing-guide.md b/docs/guides/testing-guide.md index 949ea5e..eada71c 100644 --- a/docs/guides/testing-guide.md +++ b/docs/guides/testing-guide.md @@ -17,44 +17,39 @@ ### lib -- `src/shared/analytics/lib/analytics.test.ts` +- `platform/analytics/lib/analytics.test.ts` - GA ID 부재/부재 조건에서 이벤트 미발송 - trackEvent, trackPageView 실 발송 가드 -- `src/features/blog/services/post-repository.test.ts` +- `blog/services/post-repository.test.ts` - 정렬/시리즈 조회/SKU 슬러그 수집 -- `src/features/search/model/get-search-actions.test.ts` +- `search/model/get-search-actions.test.ts` - 키워드 생성/nullable tags 처리 -- `src/features/blog/services/markdown-parser.test.ts` +- `blog/services/markdown-parser.test.ts` - TOC ID 생성 및 중복 처리 -### app actions +### blog api -- `src/app/actions/view.test.ts` +- `blog/api/view.test.ts` - 슬러그 정규화 - Supabase 미연결 환경 fallback - RPC 실패 시 읽기 fallback 동작 ### components -- `src/shared/layout/Header/useScrollVisibility.test.ts` - - 홈/블로그 경로별 top/bottom visibility 동작, 스크롤 임계값 - - 바텀바 오프셋 CSS 변수 동기화 -- `src/shared/layout/Header/MobileBottomNav.test.tsx` +- `site/navigation/MobileBottomNav.test.tsx` - 아이템 수, active 상태, 토큰 기반 class, search action, analytics tracking -- `src/shared/layout/Header/Header.test.tsx` - - 헤더 하위 컴포넌트 연결 및 top header 상태 반영 -- `src/features/blog/ui/components/*.test.tsx` +- `blog/ui/components/*.test.tsx` - PostCard/PostList/CategoryFilter 상태별 렌더/이벤트 -- `src/shared/ui/*.test.tsx` +- `shared/ui/*.test.tsx` - Button/EmptyState/Route state 상태 검증 ### styles -- `src/styles/tokens.test.ts` -- `src/styles/globals.test.ts` +- `styles/tokens.test.ts` +- `styles/globals.test.ts` ## 문서 하네스 diff --git a/docs/references/uv-llms.txt b/docs/references/uv-llms.txt index d8d5fba..fe9814f 100644 --- a/docs/references/uv-llms.txt +++ b/docs/references/uv-llms.txt @@ -3,8 +3,8 @@ 1) Current project toolchain is Node/TypeScript-first. 2) Python is used only for specific analysis scripts: -- internal/scripts/research/build_nekaracuba_corpus.py -- internal/scripts/research/generate_toss_analysis.py +- tooling/scripts/research/build_nekaracuba_corpus.py +- tooling/scripts/research/generate_toss_analysis.py If standardizing Python tooling with `uv`: diff --git a/package.json b/package.json index 7c97e7e..1a35e8c 100644 --- a/package.json +++ b/package.json @@ -4,33 +4,33 @@ "private": true, "type": "module", "scripts": { - "dev": "node internal/scripts/dev-with-agentation.mjs", + "dev": "node tooling/scripts/dev-with-agentation.mjs", "dev:next": "next dev --webpack", "dev:agentation": "agentation-mcp server", "build": "next build --webpack", "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", + "lint:css:syntax": "node tooling/scripts/check-css-syntax.mjs", + "verify:docs": "node tooling/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:components": "vitest run blog resume search shared site platform styles", "test:coverage": "vitest run --coverage", "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", - "content:audit": "node internal/scripts/posts/audit-post-quality.mjs", - "localize:images": "node internal/scripts/posts/localize-post-images.mjs", + "test:smoke": "vitest run shared/**/*.test.ts shared/**/*.test.tsx blog/api/*.test.ts && npx playwright test --grep @smoke", + "new-post": "node tooling/scripts/posts/new-post.js", + "content:audit": "node tooling/scripts/posts/audit-post-quality.mjs", + "localize:images": "node tooling/scripts/posts/localize-post-images.mjs", "format": "prettier --write .", - "lint:md": "markdownlint-cli2 --config internal/config/.markdownlint-cli2.jsonc '**/*.md' '**/*.mdx'", - "lint:spell": "cspell --config internal/config/cspell.json '**/*'", + "lint:md": "markdownlint-cli2 --config tooling/config/.markdownlint-cli2.jsonc '**/*.md' '**/*.mdx'", + "lint:spell": "cspell --config tooling/config/cspell.json '**/*'", "prepare": "husky" }, "lint-staged": { "*.{js,jsx,ts,tsx,json,css,md,mdx}": "prettier --write", - "*.{md,mdx}": "markdownlint-cli2 --config internal/config/.markdownlint-cli2.jsonc" + "*.{md,mdx}": "markdownlint-cli2 --config tooling/config/.markdownlint-cli2.jsonc" }, "prettier": { "semi": true, diff --git a/src/shared/analytics/components/DwellTimeTracker.test.tsx b/platform/analytics/components/DwellTimeTracker.test.tsx similarity index 92% rename from src/shared/analytics/components/DwellTimeTracker.test.tsx rename to platform/analytics/components/DwellTimeTracker.test.tsx index e6df49e..8060020 100644 --- a/src/shared/analytics/components/DwellTimeTracker.test.tsx +++ b/platform/analytics/components/DwellTimeTracker.test.tsx @@ -1,9 +1,9 @@ import { act, render } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import DwellTimeTracker from './DwellTimeTracker'; -import { trackEvent } from '@/shared/analytics/lib/analytics'; +import { trackEvent } from '@/platform/analytics/lib/analytics'; -vi.mock('@/shared/analytics/lib/analytics', () => ({ +vi.mock('@/platform/analytics/lib/analytics', () => ({ trackEvent: vi.fn(), })); diff --git a/src/shared/analytics/components/DwellTimeTracker.tsx b/platform/analytics/components/DwellTimeTracker.tsx similarity index 97% rename from src/shared/analytics/components/DwellTimeTracker.tsx rename to platform/analytics/components/DwellTimeTracker.tsx index 9e9ec61..f029643 100644 --- a/src/shared/analytics/components/DwellTimeTracker.tsx +++ b/platform/analytics/components/DwellTimeTracker.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useRef } from 'react'; -import { trackEvent } from '@/shared/analytics/lib/analytics'; +import { trackEvent } from '@/platform/analytics/lib/analytics'; interface DwellTimeTrackerProps { slug: string; diff --git a/src/shared/analytics/components/PostViewTracker.tsx b/platform/analytics/components/PostViewTracker.tsx similarity index 87% rename from src/shared/analytics/components/PostViewTracker.tsx rename to platform/analytics/components/PostViewTracker.tsx index a69dd4e..8da54f6 100644 --- a/src/shared/analytics/components/PostViewTracker.tsx +++ b/platform/analytics/components/PostViewTracker.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect } from 'react'; -import { trackEvent } from '@/shared/analytics/lib/analytics'; +import { trackEvent } from '@/platform/analytics/lib/analytics'; interface PostViewTrackerProps { slug: string; diff --git a/src/shared/analytics/components/ScrollDepthTracker.tsx b/platform/analytics/components/ScrollDepthTracker.tsx similarity index 96% rename from src/shared/analytics/components/ScrollDepthTracker.tsx rename to platform/analytics/components/ScrollDepthTracker.tsx index d6431fa..5bfa8f9 100644 --- a/src/shared/analytics/components/ScrollDepthTracker.tsx +++ b/platform/analytics/components/ScrollDepthTracker.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useRef, useCallback } from 'react'; -import { trackEvent } from '@/shared/analytics/lib/analytics'; +import { trackEvent } from '@/platform/analytics/lib/analytics'; interface ScrollDepthTrackerProps { slug: string; diff --git a/src/shared/analytics/components/UmamiAnalytics.tsx b/platform/analytics/components/UmamiAnalytics.tsx similarity index 85% rename from src/shared/analytics/components/UmamiAnalytics.tsx rename to platform/analytics/components/UmamiAnalytics.tsx index 52c0b39..cec40dd 100644 --- a/src/shared/analytics/components/UmamiAnalytics.tsx +++ b/platform/analytics/components/UmamiAnalytics.tsx @@ -1,7 +1,7 @@ 'use client'; import Script from 'next/script'; -import { flushQueuedUmamiEvents } from '@/shared/analytics/lib/analytics'; +import { flushQueuedUmamiEvents } from '@/platform/analytics/lib/analytics'; const UMAMI_URL = process.env.NEXT_PUBLIC_UMAMI_URL; const UMAMI_WEBSITE_ID = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID; diff --git a/src/shared/analytics/lib/analytics.test.ts b/platform/analytics/lib/analytics.test.ts similarity index 100% rename from src/shared/analytics/lib/analytics.test.ts rename to platform/analytics/lib/analytics.test.ts diff --git a/src/shared/analytics/lib/analytics.ts b/platform/analytics/lib/analytics.ts similarity index 100% rename from src/shared/analytics/lib/analytics.ts rename to platform/analytics/lib/analytics.ts diff --git a/src/shared/devtools/AgentationOverlay.test.tsx b/platform/devtools/AgentationOverlay.test.tsx similarity index 100% rename from src/shared/devtools/AgentationOverlay.test.tsx rename to platform/devtools/AgentationOverlay.test.tsx diff --git a/src/shared/devtools/AgentationOverlay.tsx b/platform/devtools/AgentationOverlay.tsx similarity index 100% rename from src/shared/devtools/AgentationOverlay.tsx rename to platform/devtools/AgentationOverlay.tsx diff --git a/platform/index.ts b/platform/index.ts new file mode 100644 index 0000000..529a902 --- /dev/null +++ b/platform/index.ts @@ -0,0 +1,10 @@ +export { + AnalyticsEvents, + flushQueuedUmamiEvents, + trackEvent, + type AnalyticsEventName, +} from './analytics/lib/analytics'; +export { + getSupabaseServerClient, + type SupabaseDatabase, +} from './integrations/supabase'; diff --git a/src/shared/integrations/supabase.test.ts b/platform/integrations/supabase.test.ts similarity index 100% rename from src/shared/integrations/supabase.test.ts rename to platform/integrations/supabase.test.ts diff --git a/src/shared/integrations/supabase.ts b/platform/integrations/supabase.ts similarity index 100% rename from src/shared/integrations/supabase.ts rename to platform/integrations/supabase.ts diff --git a/src/shared/seo/JsonLd.tsx b/platform/seo/JsonLd.tsx similarity index 100% rename from src/shared/seo/JsonLd.tsx rename to platform/seo/JsonLd.tsx diff --git a/resume/index.ts b/resume/index.ts new file mode 100644 index 0000000..c8d6ce2 --- /dev/null +++ b/resume/index.ts @@ -0,0 +1,22 @@ +export { + activities, + certifications, + education, + experiences, + personalInfo, + personalProjects, +} from './model/resume-data'; +export { orderExperienceStages } from './model/order-experience-stages'; +export type { + Activity, + Certification, + Education, + Experience, + ExperienceProject, + ExperienceStage, + ExperienceStageKey, + PersonalInfo, + PersonalProject, + ProjectLink, + SkillGroup, +} from './model/types'; diff --git a/src/features/resume/model/order-experience-stages.test.ts b/resume/model/order-experience-stages.test.ts similarity index 96% rename from src/features/resume/model/order-experience-stages.test.ts rename to resume/model/order-experience-stages.test.ts index b94b52f..dde5e58 100644 --- a/src/features/resume/model/order-experience-stages.test.ts +++ b/resume/model/order-experience-stages.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { orderExperienceStages } from './order-experience-stages'; -import type { ExperienceStage } from '@/domains/resume/model/types'; +import type { ExperienceStage } from '@/resume/model/types'; describe('orderExperienceStages', () => { it('orders core stages by fixed priority regardless of input order', () => { diff --git a/src/features/resume/model/order-experience-stages.ts b/resume/model/order-experience-stages.ts similarity index 89% rename from src/features/resume/model/order-experience-stages.ts rename to resume/model/order-experience-stages.ts index 1196767..1c6624d 100644 --- a/src/features/resume/model/order-experience-stages.ts +++ b/resume/model/order-experience-stages.ts @@ -1,4 +1,4 @@ -import type { ExperienceStage } from '@/domains/resume/model/types'; +import type { ExperienceStage } from '@/resume/model/types'; const STAGE_PRIORITY: Record = { problem: 0, diff --git a/src/features/resume/model/resume-data.ts b/resume/model/resume-data.ts similarity index 99% rename from src/features/resume/model/resume-data.ts rename to resume/model/resume-data.ts index 35813c3..060b130 100644 --- a/src/features/resume/model/resume-data.ts +++ b/resume/model/resume-data.ts @@ -5,7 +5,7 @@ import type { Education, Activity, Certification, -} from '@/domains/resume/model/types'; +} from '@/resume/model/types'; export const personalInfo: PersonalInfo = { name: '박은우', diff --git a/src/domains/resume/model/types.ts b/resume/model/types.ts similarity index 100% rename from src/domains/resume/model/types.ts rename to resume/model/types.ts diff --git a/src/features/resume/ui/pages/ResumePage.tsx b/resume/ui/pages/ResumePage.tsx similarity index 99% rename from src/features/resume/ui/pages/ResumePage.tsx rename to resume/ui/pages/ResumePage.tsx index b3a306f..84d5f0f 100644 --- a/src/features/resume/ui/pages/ResumePage.tsx +++ b/resume/ui/pages/ResumePage.tsx @@ -8,8 +8,8 @@ import { experiences, personalInfo, personalProjects, -} from '@/features/resume/model/resume-data'; -import { orderExperienceStages } from '@/features/resume/model/order-experience-stages'; +} from '@/resume/model/resume-data'; +import { orderExperienceStages } from '@/resume/model/order-experience-stages'; export const metadata: Metadata = { title: 'CV', diff --git a/search/index.ts b/search/index.ts new file mode 100644 index 0000000..3f6c64f --- /dev/null +++ b/search/index.ts @@ -0,0 +1,2 @@ +export { getSearchActions } from './model/get-search-actions'; +export { getRecommendedSearchTerms } from './model/search-recommendations'; diff --git a/src/features/search/model/get-search-actions.test.ts b/search/model/get-search-actions.test.ts similarity index 98% rename from src/features/search/model/get-search-actions.test.ts rename to search/model/get-search-actions.test.ts index 31ead52..31345d1 100644 --- a/src/features/search/model/get-search-actions.test.ts +++ b/search/model/get-search-actions.test.ts @@ -3,7 +3,7 @@ import { getSearchActions } from './get-search-actions'; const mockTrackEvent = vi.fn(); -vi.mock('@/shared/analytics/lib/analytics', () => ({ +vi.mock('@/platform/analytics/lib/analytics', () => ({ AnalyticsEvents: { click: 'click', theme: 'theme', diff --git a/src/features/search/model/get-search-actions.ts b/search/model/get-search-actions.ts similarity index 94% rename from src/features/search/model/get-search-actions.ts rename to search/model/get-search-actions.ts index f740e4b..39c15bf 100644 --- a/src/features/search/model/get-search-actions.ts +++ b/search/model/get-search-actions.ts @@ -1,6 +1,6 @@ import { Action } from 'kbar'; -import { FeedData } from '@/domains/post/model/types'; -import { AnalyticsEvents, trackEvent } from '@/shared/analytics/lib/analytics'; +import { FeedData } from '@/blog/model/types'; +import { AnalyticsEvents, trackEvent } from '@/platform/analytics/lib/analytics'; type SearchablePost = Pick< FeedData, diff --git a/src/features/search/model/search-recommendations.test.ts b/search/model/search-recommendations.test.ts similarity index 100% rename from src/features/search/model/search-recommendations.test.ts rename to search/model/search-recommendations.test.ts diff --git a/src/features/search/model/search-recommendations.ts b/search/model/search-recommendations.ts similarity index 95% rename from src/features/search/model/search-recommendations.ts rename to search/model/search-recommendations.ts index cdac057..2fa96d7 100644 --- a/src/features/search/model/search-recommendations.ts +++ b/search/model/search-recommendations.ts @@ -1,4 +1,4 @@ -import type { FeedData } from '@/domains/post/model/types'; +import type { FeedData } from '@/blog/model/types'; type SearchRecommendationSource = Pick; diff --git a/src/features/search/ui/components/CommandPalette/CommandPalette.module.css b/search/ui/components/CommandPalette/CommandPalette.module.css similarity index 100% rename from src/features/search/ui/components/CommandPalette/CommandPalette.module.css rename to search/ui/components/CommandPalette/CommandPalette.module.css diff --git a/src/features/search/ui/components/CommandPalette/CommandPalette.tsx b/search/ui/components/CommandPalette/CommandPalette.tsx similarity index 96% rename from src/features/search/ui/components/CommandPalette/CommandPalette.tsx rename to search/ui/components/CommandPalette/CommandPalette.tsx index 3c81330..93d222f 100644 --- a/src/features/search/ui/components/CommandPalette/CommandPalette.tsx +++ b/search/ui/components/CommandPalette/CommandPalette.tsx @@ -11,9 +11,9 @@ import { useMatches, } from 'kbar'; import styles from './CommandPalette.module.css'; -import type { FeedData } from '@/domains/post/model/types'; -import { AnalyticsEvents, trackEvent } from '@/shared/analytics/lib/analytics'; -import { getRecommendedSearchTerms } from '@/features/search/model/search-recommendations'; +import type { FeedData } from '@/blog/model/types'; +import { AnalyticsEvents, trackEvent } from '@/platform/analytics/lib/analytics'; +import { getRecommendedSearchTerms } from '@/search/model/search-recommendations'; type SearchScopeId = 'all' | 'tech' | 'life' | 'resume'; diff --git a/src/features/search/ui/components/CommandPalette/index.ts b/search/ui/components/CommandPalette/index.ts similarity index 100% rename from src/features/search/ui/components/CommandPalette/index.ts rename to search/ui/components/CommandPalette/index.ts diff --git a/src/features/search/ui/components/KBarProvider.tsx b/search/ui/components/KBarProvider.tsx similarity index 69% rename from src/features/search/ui/components/KBarProvider.tsx rename to search/ui/components/KBarProvider.tsx index ea712c8..817983a 100644 --- a/src/features/search/ui/components/KBarProvider.tsx +++ b/search/ui/components/KBarProvider.tsx @@ -1,10 +1,10 @@ 'use client'; import { KBarProvider as KBarProviderLib } from 'kbar'; -import { CommandPalette } from '@/features/search/ui/components/CommandPalette'; -import { getSearchActions } from '@/features/search/model/get-search-actions'; +import { CommandPalette } from '@/search/ui/components/CommandPalette'; +import { getSearchActions } from '@/search/model/get-search-actions'; import { useMemo, type ReactNode } from 'react'; -import type { FeedData } from '@/domains/post/model/types'; +import type { FeedData } from '@/blog/model/types'; interface KBarProviderProps { children: ReactNode; diff --git a/shared/index.ts b/shared/index.ts new file mode 100644 index 0000000..b76d748 --- /dev/null +++ b/shared/index.ts @@ -0,0 +1,2 @@ +export { Container, type ContainerProps } from './layout'; +export { Button, EmptyState, RouteError } from './ui'; diff --git a/src/shared/layout/Container/Container.test.tsx b/shared/layout/Container/Container.test.tsx similarity index 100% rename from src/shared/layout/Container/Container.test.tsx rename to shared/layout/Container/Container.test.tsx diff --git a/src/shared/layout/Container/Container.tsx b/shared/layout/Container/Container.tsx similarity index 100% rename from src/shared/layout/Container/Container.tsx rename to shared/layout/Container/Container.tsx diff --git a/src/shared/layout/Container/index.ts b/shared/layout/Container/index.ts similarity index 100% rename from src/shared/layout/Container/index.ts rename to shared/layout/Container/index.ts diff --git a/shared/layout/index.ts b/shared/layout/index.ts new file mode 100644 index 0000000..7719956 --- /dev/null +++ b/shared/layout/index.ts @@ -0,0 +1,2 @@ +export { Container } from './Container'; +export type { ContainerProps } from './Container'; diff --git a/src/shared/motion/model/motion-mode.test.ts b/shared/motion/model/motion-mode.test.ts similarity index 100% rename from src/shared/motion/model/motion-mode.test.ts rename to shared/motion/model/motion-mode.test.ts diff --git a/src/shared/motion/model/motion-mode.ts b/shared/motion/model/motion-mode.ts similarity index 100% rename from src/shared/motion/model/motion-mode.ts rename to shared/motion/model/motion-mode.ts diff --git a/src/shared/motion/ui/StaggerReveal.tsx b/shared/motion/ui/StaggerReveal.tsx similarity index 100% rename from src/shared/motion/ui/StaggerReveal.tsx rename to shared/motion/ui/StaggerReveal.tsx diff --git a/src/shared/testing/dom-mocks.ts b/shared/testing/dom-mocks.ts similarity index 100% rename from src/shared/testing/dom-mocks.ts rename to shared/testing/dom-mocks.ts diff --git a/src/shared/testing/server-only.ts b/shared/testing/server-only.ts similarity index 100% rename from src/shared/testing/server-only.ts rename to shared/testing/server-only.ts diff --git a/src/shared/ui/Button.test.tsx b/shared/ui/Button.test.tsx similarity index 100% rename from src/shared/ui/Button.test.tsx rename to shared/ui/Button.test.tsx diff --git a/src/shared/ui/Button/Button.tsx b/shared/ui/Button/Button.tsx similarity index 100% rename from src/shared/ui/Button/Button.tsx rename to shared/ui/Button/Button.tsx diff --git a/src/shared/ui/Button/Button.types.ts b/shared/ui/Button/Button.types.ts similarity index 100% rename from src/shared/ui/Button/Button.types.ts rename to shared/ui/Button/Button.types.ts diff --git a/src/shared/ui/Button/index.ts b/shared/ui/Button/index.ts similarity index 100% rename from src/shared/ui/Button/index.ts rename to shared/ui/Button/index.ts diff --git a/src/shared/ui/EmptyState.test.tsx b/shared/ui/EmptyState.test.tsx similarity index 100% rename from src/shared/ui/EmptyState.test.tsx rename to shared/ui/EmptyState.test.tsx diff --git a/src/shared/ui/EmptyState/EmptyState.tsx b/shared/ui/EmptyState/EmptyState.tsx similarity index 100% rename from src/shared/ui/EmptyState/EmptyState.tsx rename to shared/ui/EmptyState/EmptyState.tsx diff --git a/src/shared/ui/EmptyState/index.ts b/shared/ui/EmptyState/index.ts similarity index 100% rename from src/shared/ui/EmptyState/index.ts rename to shared/ui/EmptyState/index.ts diff --git a/src/shared/ui/RouteError.test.tsx b/shared/ui/RouteError.test.tsx similarity index 100% rename from src/shared/ui/RouteError.test.tsx rename to shared/ui/RouteError.test.tsx diff --git a/src/shared/ui/RouteState/RouteError.tsx b/shared/ui/RouteState/RouteError.tsx similarity index 100% rename from src/shared/ui/RouteState/RouteError.tsx rename to shared/ui/RouteState/RouteError.tsx diff --git a/src/shared/ui/RouteState/index.ts b/shared/ui/RouteState/index.ts similarity index 100% rename from src/shared/ui/RouteState/index.ts rename to shared/ui/RouteState/index.ts diff --git a/src/shared/ui/ThemeToggle.test.tsx b/shared/ui/ThemeToggle.test.tsx similarity index 98% rename from src/shared/ui/ThemeToggle.test.tsx rename to shared/ui/ThemeToggle.test.tsx index 607437e..b5f1c35 100644 --- a/src/shared/ui/ThemeToggle.test.tsx +++ b/shared/ui/ThemeToggle.test.tsx @@ -57,7 +57,7 @@ vi.mock('@/shared/motion/model/motion-mode', () => ({ useEffectiveMotionMode: () => 'full', })); -vi.mock('@/shared/analytics/lib/analytics', () => ({ +vi.mock('@/platform/analytics/lib/analytics', () => ({ AnalyticsEvents: { click: 'click', theme: 'theme', diff --git a/src/shared/ui/ThemeToggle.tsx b/shared/ui/ThemeToggle.tsx similarity index 98% rename from src/shared/ui/ThemeToggle.tsx rename to shared/ui/ThemeToggle.tsx index f3a2a85..9f181e9 100644 --- a/src/shared/ui/ThemeToggle.tsx +++ b/shared/ui/ThemeToggle.tsx @@ -3,7 +3,7 @@ import { useTheme } from 'next-themes'; import { useEffect, useState } from 'react'; import { motion } from 'framer-motion'; -import { AnalyticsEvents, trackEvent } from '@/shared/analytics/lib/analytics'; +import { AnalyticsEvents, trackEvent } from '@/platform/analytics/lib/analytics'; import { useEffectiveMotionMode } from '@/shared/motion/model/motion-mode'; export const THEME_TRANSITION_ORIGIN_EVENT = 'theme-transition-origin'; diff --git a/src/shared/ui/ThemeTransitionWash.tsx b/shared/ui/ThemeTransitionWash.tsx similarity index 100% rename from src/shared/ui/ThemeTransitionWash.tsx rename to shared/ui/ThemeTransitionWash.tsx diff --git a/src/shared/ui/icons/AppSectionIcon.tsx b/shared/ui/icons/AppSectionIcon.tsx similarity index 100% rename from src/shared/ui/icons/AppSectionIcon.tsx rename to shared/ui/icons/AppSectionIcon.tsx diff --git a/src/shared/ui/index.ts b/shared/ui/index.ts similarity index 100% rename from src/shared/ui/index.ts rename to shared/ui/index.ts diff --git a/src/components/visualization/BinarySearchVisualization.tsx b/shared/visualization/BinarySearchVisualization.tsx similarity index 100% rename from src/components/visualization/BinarySearchVisualization.tsx rename to shared/visualization/BinarySearchVisualization.tsx diff --git a/src/components/visualization/DPVisualization.tsx b/shared/visualization/DPVisualization.tsx similarity index 100% rename from src/components/visualization/DPVisualization.tsx rename to shared/visualization/DPVisualization.tsx diff --git a/src/components/visualization/GraphTraversalVisualization.tsx b/shared/visualization/GraphTraversalVisualization.tsx similarity index 100% rename from src/components/visualization/GraphTraversalVisualization.tsx rename to shared/visualization/GraphTraversalVisualization.tsx diff --git a/src/components/visualization/SlidingWindowVisualization.tsx b/shared/visualization/SlidingWindowVisualization.tsx similarity index 100% rename from src/components/visualization/SlidingWindowVisualization.tsx rename to shared/visualization/SlidingWindowVisualization.tsx diff --git a/src/components/visualization/SortingVisualization.tsx b/shared/visualization/SortingVisualization.tsx similarity index 100% rename from src/components/visualization/SortingVisualization.tsx rename to shared/visualization/SortingVisualization.tsx diff --git a/src/components/visualization/TwoPointerVisualization.tsx b/shared/visualization/TwoPointerVisualization.tsx similarity index 100% rename from src/components/visualization/TwoPointerVisualization.tsx rename to shared/visualization/TwoPointerVisualization.tsx diff --git a/src/components/visualization/index.ts b/shared/visualization/index.ts similarity index 100% rename from src/components/visualization/index.ts rename to shared/visualization/index.ts diff --git a/src/core/config/site.ts b/site/config/site.ts similarity index 100% rename from src/core/config/site.ts rename to site/config/site.ts diff --git a/src/shared/layout/Footer/Footer.test.tsx b/site/footer/Footer/Footer.test.tsx similarity index 100% rename from src/shared/layout/Footer/Footer.test.tsx rename to site/footer/Footer/Footer.test.tsx diff --git a/src/shared/layout/Footer/Footer.tsx b/site/footer/Footer/Footer.tsx similarity index 100% rename from src/shared/layout/Footer/Footer.tsx rename to site/footer/Footer/Footer.tsx diff --git a/src/shared/layout/Footer/index.ts b/site/footer/Footer/index.ts similarity index 100% rename from src/shared/layout/Footer/index.ts rename to site/footer/Footer/index.ts diff --git a/src/features/home/model/home-feed.test.ts b/site/home/model/home-feed.test.ts similarity index 97% rename from src/features/home/model/home-feed.test.ts rename to site/home/model/home-feed.test.ts index 6fef3e0..06e3192 100644 --- a/src/features/home/model/home-feed.test.ts +++ b/site/home/model/home-feed.test.ts @@ -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 { buildHomeCategoryCounts, filterHomePosts, diff --git a/src/features/home/model/home-feed.ts b/site/home/model/home-feed.ts similarity index 92% rename from src/features/home/model/home-feed.ts rename to site/home/model/home-feed.ts index 3876b3f..5c44942 100644 --- a/src/features/home/model/home-feed.ts +++ b/site/home/model/home-feed.ts @@ -1,5 +1,5 @@ -import type { FeedData } from '@/domains/post/model/types'; -import type { Category } from '@/features/blog/ui/components/CategoryFilter'; +import type { FeedData } from '@/blog/model/types'; +import type { Category } from '@/blog/ui/components/CategoryFilter'; export interface HomePopularView { slug: string; diff --git a/src/features/home/ui/pages/HomePage.test.tsx b/site/home/ui/pages/HomePage.test.tsx similarity index 92% rename from src/features/home/ui/pages/HomePage.test.tsx rename to site/home/ui/pages/HomePage.test.tsx index 12db379..5045e12 100644 --- a/src/features/home/ui/pages/HomePage.test.tsx +++ b/site/home/ui/pages/HomePage.test.tsx @@ -1,16 +1,16 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import type { FeedData } from '@/domains/post/model/types'; +import type { FeedData } from '@/blog/model/types'; import HomePage from './HomePage'; const mockGetSortedFeedData = vi.fn<() => FeedData[]>(() => []); const mockGetPopularViewsInRecentDays = vi.fn(async () => []); -vi.mock('@/features/blog/services/post-repository', () => ({ +vi.mock('@/blog/services/post-repository', () => ({ getSortedFeedData: () => mockGetSortedFeedData(), })); -vi.mock('@/app/actions/view', () => ({ +vi.mock('@/blog/api/view', () => ({ getPopularViewsInRecentDays: (...args: unknown[]) => mockGetPopularViewsInRecentDays(...args), })); diff --git a/src/features/home/ui/pages/HomePage.tsx b/site/home/ui/pages/HomePage.tsx similarity index 77% rename from src/features/home/ui/pages/HomePage.tsx rename to site/home/ui/pages/HomePage.tsx index dac4ab1..67c5905 100644 --- a/src/features/home/ui/pages/HomePage.tsx +++ b/site/home/ui/pages/HomePage.tsx @@ -1,5 +1,5 @@ -import { getPopularViewsInRecentDays } from '@/app/actions/view'; -import { getSortedFeedData } from '@/features/blog/services/post-repository'; +import { getPopularViewsInRecentDays } from '@/blog/api/view'; +import { getSortedFeedData } from '@/blog/services/post-repository'; import HomePageClient from './HomePageClient'; const POPULAR_VIEW_DAYS = 30; diff --git a/src/features/home/ui/pages/HomePageClient.test.tsx b/site/home/ui/pages/HomePageClient.test.tsx similarity index 93% rename from src/features/home/ui/pages/HomePageClient.test.tsx rename to site/home/ui/pages/HomePageClient.test.tsx index 407f9a6..af131ed 100644 --- a/src/features/home/ui/pages/HomePageClient.test.tsx +++ b/site/home/ui/pages/HomePageClient.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import type { ReactNode } from 'react'; import { describe, expect, it, vi } from 'vitest'; -import type { FeedData } from '@/domains/post/model/types'; +import type { FeedData } from '@/blog/model/types'; import HomePageClient from './HomePageClient'; vi.mock('@/shared/layout', () => ({ @@ -14,7 +14,7 @@ vi.mock('@/shared/layout', () => ({ }) =>
{children}
, })); -vi.mock('@/features/blog/ui/components', () => ({ +vi.mock('@/blog/ui/components', () => ({ CategoryFilter: ({ categories, activeCategory, diff --git a/src/features/home/ui/pages/HomePageClient.tsx b/site/home/ui/pages/HomePageClient.tsx similarity index 95% rename from src/features/home/ui/pages/HomePageClient.tsx rename to site/home/ui/pages/HomePageClient.tsx index 0d1b9e9..fd73608 100644 --- a/src/features/home/ui/pages/HomePageClient.tsx +++ b/site/home/ui/pages/HomePageClient.tsx @@ -2,18 +2,18 @@ import { useMemo, useState } from 'react'; import { clsx } from 'clsx'; -import type { FeedData } from '@/domains/post/model/types'; +import type { FeedData } from '@/blog/model/types'; import { CategoryFilter, PostList, type Category, -} from '@/features/blog/ui/components'; +} from '@/blog/ui/components'; import { buildHomeCategoryCounts, filterHomePosts, type HomePopularView, type HomeSortOrder, -} from '@/features/home/model/home-feed'; +} from '@/site/home/model/home-feed'; import { Container } from '@/shared/layout'; interface HomePageClientProps { diff --git a/site/index.ts b/site/index.ts new file mode 100644 index 0000000..fdaaa64 --- /dev/null +++ b/site/index.ts @@ -0,0 +1,8 @@ +export { AppShell } from './shell/AppShell'; +export { default as AppProviders } from './providers/AppProviders'; +export { + SITE_AUTHOR, + SITE_DESCRIPTION, + SITE_NAME, + SITE_URL, +} from './config/site'; diff --git a/src/shared/layout/PageTransition.tsx b/site/layout/PageTransition.tsx similarity index 100% rename from src/shared/layout/PageTransition.tsx rename to site/layout/PageTransition.tsx diff --git a/src/shared/layout/Header/MobileBottomNav.test.tsx b/site/navigation/MobileBottomNav.test.tsx similarity index 98% rename from src/shared/layout/Header/MobileBottomNav.test.tsx rename to site/navigation/MobileBottomNav.test.tsx index e9de5bd..817297a 100644 --- a/src/shared/layout/Header/MobileBottomNav.test.tsx +++ b/site/navigation/MobileBottomNav.test.tsx @@ -20,7 +20,7 @@ vi.mock('next/link', () => ({ ), })); -vi.mock('@/shared/analytics/lib/analytics', () => { +vi.mock('@/platform/analytics/lib/analytics', () => { return { AnalyticsEvents: { click: 'click', diff --git a/src/shared/layout/Header/MobileBottomNav.tsx b/site/navigation/MobileBottomNav.tsx similarity index 99% rename from src/shared/layout/Header/MobileBottomNav.tsx rename to site/navigation/MobileBottomNav.tsx index 91a2778..8c46cbb 100644 --- a/src/shared/layout/Header/MobileBottomNav.tsx +++ b/site/navigation/MobileBottomNav.tsx @@ -3,7 +3,7 @@ import { useEffect, useMemo } from 'react'; import Link from 'next/link'; import { clsx } from 'clsx'; -import { AnalyticsEvents, trackEvent } from '@/shared/analytics/lib/analytics'; +import { AnalyticsEvents, trackEvent } from '@/platform/analytics/lib/analytics'; import { AppSectionIcon } from '@/shared/ui/icons/AppSectionIcon'; interface MobileBottomNavProps { diff --git a/src/core/providers/AppProviders.tsx b/site/providers/AppProviders.tsx similarity index 62% rename from src/core/providers/AppProviders.tsx rename to site/providers/AppProviders.tsx index 6eb08a9..035bf93 100644 --- a/src/core/providers/AppProviders.tsx +++ b/site/providers/AppProviders.tsx @@ -1,11 +1,11 @@ import type { ReactNode } from 'react'; -import ThemeProvider from '@/shared/providers/ThemeProvider'; -import KBarProvider from '@/features/search/ui/components/KBarProvider'; -import UmamiAnalytics from '@/shared/analytics/components/UmamiAnalytics'; -import { getSortedFeedData } from '@/features/blog/services/post-repository'; -import JsonLd from '@/shared/seo/JsonLd'; -import { SITE_AUTHOR, SITE_NAME, SITE_URL } from '@/core/config/site'; -import AgentationOverlay from '@/shared/devtools/AgentationOverlay'; +import ThemeProvider from '@/site/providers/ThemeProvider'; +import KBarProvider from '@/search/ui/components/KBarProvider'; +import UmamiAnalytics from '@/platform/analytics/components/UmamiAnalytics'; +import { getSortedFeedData } from '@/blog/services/post-repository'; +import JsonLd from '@/platform/seo/JsonLd'; +import { SITE_AUTHOR, SITE_NAME, SITE_URL } from '@/site/config/site'; +import AgentationOverlay from '@/platform/devtools/AgentationOverlay'; interface AppProvidersProps { children: ReactNode; diff --git a/src/shared/providers/ThemeProvider.tsx b/site/providers/ThemeProvider.tsx similarity index 100% rename from src/shared/providers/ThemeProvider.tsx rename to site/providers/ThemeProvider.tsx diff --git a/src/shared/layout/AppShell/AppShell.tsx b/site/shell/AppShell/AppShell.tsx similarity index 98% rename from src/shared/layout/AppShell/AppShell.tsx rename to site/shell/AppShell/AppShell.tsx index 13a1f4c..a53bf65 100644 --- a/src/shared/layout/AppShell/AppShell.tsx +++ b/site/shell/AppShell/AppShell.tsx @@ -6,9 +6,9 @@ import type { ReactNode } from 'react'; import { useEffect, useMemo, useState } from 'react'; import { useKBar } from 'kbar'; import { clsx } from 'clsx'; -import type { FeedData } from '@/domains/post/model/types'; -import { personalInfo } from '@/features/resume/model/resume-data'; -import MobileBottomNav from '@/shared/layout/Header/MobileBottomNav'; +import type { FeedData } from '@/blog/model/types'; +import { personalInfo } from '@/resume/model/resume-data'; +import MobileBottomNav from '@/site/navigation/MobileBottomNav'; import { AppSectionIcon } from '@/shared/ui/icons/AppSectionIcon'; import ThemeToggle from '@/shared/ui/ThemeToggle'; import ThemeTransitionWash from '@/shared/ui/ThemeTransitionWash'; diff --git a/src/shared/layout/AppShell/index.ts b/site/shell/AppShell/index.ts similarity index 100% rename from src/shared/layout/AppShell/index.ts rename to site/shell/AppShell/index.ts diff --git a/src/app/blog/[slug]/error.tsx b/src/app/blog/[slug]/error.tsx index b0d6106..f80a81e 100644 --- a/src/app/blog/[slug]/error.tsx +++ b/src/app/blog/[slug]/error.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { RouteError } from '@/shared/ui'; -import { AnalyticsEvents, trackEvent } from '@/shared/analytics/lib/analytics'; +import { AnalyticsEvents, trackEvent } from '@/platform/analytics/lib/analytics'; interface BlogPostErrorProps { error: Error & { digest?: string }; diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx index ec1ccc6..108ccca 100644 --- a/src/app/blog/[slug]/page.tsx +++ b/src/app/blog/[slug]/page.tsx @@ -2,4 +2,4 @@ export { generateStaticParams, generateMetadata, default, -} from '@/features/blog/ui/pages/BlogPostPage'; +} from '@/blog/ui/pages/BlogPostPage'; diff --git a/src/app/blog/error.tsx b/src/app/blog/error.tsx index 836502c..87e75f4 100644 --- a/src/app/blog/error.tsx +++ b/src/app/blog/error.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { RouteError } from '@/shared/ui'; -import { AnalyticsEvents, trackEvent } from '@/shared/analytics/lib/analytics'; +import { AnalyticsEvents, trackEvent } from '@/platform/analytics/lib/analytics'; interface BlogErrorProps { error: Error & { digest?: string }; diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx index d3a577c..f69e5ed 100644 --- a/src/app/blog/page.tsx +++ b/src/app/blog/page.tsx @@ -1 +1 @@ -export { metadata, default } from '@/features/blog/ui/pages/BlogListPage'; +export { metadata, default } from '@/blog/ui/pages/BlogListPage'; diff --git a/src/app/engineering/page.tsx b/src/app/engineering/page.tsx index d2db93c..532ec69 100644 --- a/src/app/engineering/page.tsx +++ b/src/app/engineering/page.tsx @@ -1 +1 @@ -export { metadata, default } from '@/features/blog/ui/pages/EngineeringPage'; +export { metadata, default } from '@/blog/ui/pages/EngineeringPage'; diff --git a/src/app/engineering/series/[seriesId]/page.tsx b/src/app/engineering/series/[seriesId]/page.tsx index a1634ce..68275c1 100644 --- a/src/app/engineering/series/[seriesId]/page.tsx +++ b/src/app/engineering/series/[seriesId]/page.tsx @@ -1,4 +1,4 @@ -import EngineeringSeriesPage from '@/features/blog/ui/pages/EngineeringSeriesPage'; +import EngineeringSeriesPage from '@/blog/ui/pages/EngineeringSeriesPage'; export default async function EngineeringSeriesRoutePage({ params, diff --git a/src/app/error.tsx b/src/app/error.tsx index c4d2eca..17eefd0 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { RouteError } from '@/shared/ui'; -import { AnalyticsEvents, trackEvent } from '@/shared/analytics/lib/analytics'; +import { AnalyticsEvents, trackEvent } from '@/platform/analytics/lib/analytics'; interface RootErrorProps { error: Error & { digest?: string }; diff --git a/src/app/feed.xml/route.test.ts b/src/app/feed.xml/route.test.ts index 47fd9c5..603d98e 100644 --- a/src/app/feed.xml/route.test.ts +++ b/src/app/feed.xml/route.test.ts @@ -1,7 +1,7 @@ // @vitest-environment node import { describe, expect, it } from 'vitest'; -import { getSortedFeedData } from '@/features/blog/services/post-repository'; +import { getSortedFeedData } from '@/blog/services/post-repository'; import { GET } from './route'; describe('/feed.xml', () => { diff --git a/src/app/feed.xml/route.ts b/src/app/feed.xml/route.ts index 6dba13c..7d5cc0a 100644 --- a/src/app/feed.xml/route.ts +++ b/src/app/feed.xml/route.ts @@ -1,4 +1,4 @@ -import { getSortedFeedData } from '@/features/blog/services/post-repository'; +import { getSortedFeedData } from '@/blog/services/post-repository'; const SITE_URL = 'https://eunu-log.vercel.app'; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6f83731..034d7e7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,15 +3,15 @@ import type { ReactNode } from 'react'; import '@/styles/globals.css'; import '@/styles/tossface.css'; -import AppProviders from '@/core/providers/AppProviders'; -import { getSortedFeedData } from '@/features/blog/services/post-repository'; -import { AppShell } from '@/shared/layout'; +import AppProviders from '@/site/providers/AppProviders'; +import { getSortedFeedData } from '@/blog/services/post-repository'; +import { AppShell } from '@/site/shell/AppShell'; import { SITE_AUTHOR, SITE_DESCRIPTION, SITE_NAME, SITE_URL, -} from '@/core/config/site'; +} from '@/site/config/site'; export const metadata: Metadata = { metadataBase: new URL(SITE_URL), diff --git a/src/app/life/page.tsx b/src/app/life/page.tsx index b05e910..ba07a64 100644 --- a/src/app/life/page.tsx +++ b/src/app/life/page.tsx @@ -1 +1 @@ -export { metadata, default } from '@/features/blog/ui/pages/LifePage'; +export { metadata, default } from '@/blog/ui/pages/LifePage'; diff --git a/src/app/page.tsx b/src/app/page.tsx index 7c24c78..5a40793 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1 +1 @@ -export { default } from '@/features/home/ui/pages/HomePage'; +export { default } from '@/site/home/ui/pages/HomePage'; diff --git a/src/app/resume/error.tsx b/src/app/resume/error.tsx index ad96db6..c416587 100644 --- a/src/app/resume/error.tsx +++ b/src/app/resume/error.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { RouteError } from '@/shared/ui'; -import { AnalyticsEvents, trackEvent } from '@/shared/analytics/lib/analytics'; +import { AnalyticsEvents, trackEvent } from '@/platform/analytics/lib/analytics'; interface ResumeErrorProps { error: Error & { digest?: string }; diff --git a/src/app/resume/page.tsx b/src/app/resume/page.tsx index 1d7b66a..e55fd76 100644 --- a/src/app/resume/page.tsx +++ b/src/app/resume/page.tsx @@ -1 +1 @@ -export { metadata, default } from '@/features/resume/ui/pages/ResumePage'; +export { metadata, default } from '@/resume/ui/pages/ResumePage'; diff --git a/src/app/series/page.tsx b/src/app/series/page.tsx index 06efe8f..58d42ed 100644 --- a/src/app/series/page.tsx +++ b/src/app/series/page.tsx @@ -1 +1 @@ -export { metadata, default } from '@/features/blog/ui/pages/SeriesPage'; +export { metadata, default } from '@/blog/ui/pages/SeriesPage'; diff --git a/src/app/sitemap.test.ts b/src/app/sitemap.test.ts index 23326bb..e5453a9 100644 --- a/src/app/sitemap.test.ts +++ b/src/app/sitemap.test.ts @@ -1,7 +1,7 @@ // @vitest-environment node import { describe, expect, it } from 'vitest'; -import { getSortedFeedData } from '@/features/blog/services/post-repository'; +import { getSortedFeedData } from '@/blog/services/post-repository'; import sitemap from './sitemap'; const URL = 'https://eunu-log.vercel.app'; diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 58f1ef3..2c7b700 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,5 +1,5 @@ import { MetadataRoute } from 'next'; -import { getSortedFeedData } from '@/features/blog/services/post-repository'; +import { getSortedFeedData } from '@/blog/services/post-repository'; const URL = 'https://eunu-log.vercel.app'; diff --git a/src/app/template.tsx b/src/app/template.tsx index b95b983..2079614 100644 --- a/src/app/template.tsx +++ b/src/app/template.tsx @@ -1,4 +1,4 @@ -import PageTransition from '@/shared/layout/PageTransition'; +import PageTransition from '@/site/layout/PageTransition'; export default function Template({ children }: { children: React.ReactNode }) { return {children}; diff --git a/src/shared/layout/index.ts b/src/shared/layout/index.ts deleted file mode 100644 index 0502b4d..0000000 --- a/src/shared/layout/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Layout Components -export { Footer } from './Footer'; -export { Container } from './Container'; -export { AppShell } from './AppShell'; -export type { ContainerProps } from './Container'; diff --git a/src/styles/globals.css b/styles/globals.css similarity index 100% rename from src/styles/globals.css rename to styles/globals.css diff --git a/src/styles/globals.test.ts b/styles/globals.test.ts similarity index 92% rename from src/styles/globals.test.ts rename to styles/globals.test.ts index 1fc0baf..23b23ab 100644 --- a/src/styles/globals.test.ts +++ b/styles/globals.test.ts @@ -2,9 +2,9 @@ import fs from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; -const globalsPath = path.resolve(process.cwd(), 'src/styles/globals.css'); +const globalsPath = path.resolve(process.cwd(), 'styles/globals.css'); const globalsContent = fs.readFileSync(globalsPath, 'utf8'); -const tokensPath = path.resolve(process.cwd(), 'src/styles/tokens.css'); +const tokensPath = path.resolve(process.cwd(), 'styles/tokens.css'); const tokensContent = fs.readFileSync(tokensPath, 'utf8'); describe('globals styles', () => { diff --git a/src/styles/tokens.css b/styles/tokens.css similarity index 100% rename from src/styles/tokens.css rename to styles/tokens.css diff --git a/src/styles/tokens.test.ts b/styles/tokens.test.ts similarity index 94% rename from src/styles/tokens.test.ts rename to styles/tokens.test.ts index 19d20a1..d5f763a 100644 --- a/src/styles/tokens.test.ts +++ b/styles/tokens.test.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; -const tokensPath = path.resolve(process.cwd(), 'src/styles/tokens.css'); +const tokensPath = path.resolve(process.cwd(), 'styles/tokens.css'); const tokensContent = fs.readFileSync(tokensPath, 'utf8'); describe('TDS token definitions', () => { diff --git a/src/styles/tossface.css b/styles/tossface.css similarity index 100% rename from src/styles/tossface.css rename to styles/tossface.css diff --git a/internal/config/.markdownlint-cli2.jsonc b/tooling/config/.markdownlint-cli2.jsonc similarity index 100% rename from internal/config/.markdownlint-cli2.jsonc rename to tooling/config/.markdownlint-cli2.jsonc diff --git a/internal/config/cspell.json b/tooling/config/cspell.json similarity index 100% rename from internal/config/cspell.json rename to tooling/config/cspell.json diff --git a/internal/scripts/check-css-syntax.mjs b/tooling/scripts/check-css-syntax.mjs similarity index 72% rename from internal/scripts/check-css-syntax.mjs rename to tooling/scripts/check-css-syntax.mjs index e0651d9..cd5d1be 100644 --- a/internal/scripts/check-css-syntax.mjs +++ b/tooling/scripts/check-css-syntax.mjs @@ -3,7 +3,7 @@ import path from 'path'; import postcss from 'postcss'; const ROOT = process.cwd(); -const TARGET_DIR = path.join(ROOT, 'src'); +const TARGET_DIRS = ['src', 'blog', 'search', 'shared', 'site', 'styles']; async function collectCssFiles(dirPath) { const entries = await fs.readdir(dirPath, { withFileTypes: true }); @@ -42,10 +42,27 @@ async function validateCssFile(filePath) { } async function main() { - const cssFiles = await collectCssFiles(TARGET_DIR); + const existingTargetDirs = await Promise.all( + TARGET_DIRS.map(async (targetDir) => { + const fullPath = path.join(ROOT, targetDir); + try { + const stats = await fs.stat(fullPath); + return stats.isDirectory() ? fullPath : null; + } catch { + return null; + } + }) + ); + const cssFiles = ( + await Promise.all( + existingTargetDirs + .filter((entry) => entry !== null) + .map((targetDir) => collectCssFiles(targetDir)) + ) + ).flat(); if (cssFiles.length === 0) { - console.log('No CSS files found under src/.'); + console.log('No CSS files found under configured source directories.'); return; } diff --git a/internal/scripts/dev-with-agentation.mjs b/tooling/scripts/dev-with-agentation.mjs similarity index 100% rename from internal/scripts/dev-with-agentation.mjs rename to tooling/scripts/dev-with-agentation.mjs diff --git a/internal/scripts/posts/audit-post-quality.mjs b/tooling/scripts/posts/audit-post-quality.mjs similarity index 100% rename from internal/scripts/posts/audit-post-quality.mjs rename to tooling/scripts/posts/audit-post-quality.mjs diff --git a/internal/scripts/posts/localize-post-images.mjs b/tooling/scripts/posts/localize-post-images.mjs similarity index 100% rename from internal/scripts/posts/localize-post-images.mjs rename to tooling/scripts/posts/localize-post-images.mjs diff --git a/internal/scripts/posts/new-post.js b/tooling/scripts/posts/new-post.js similarity index 100% rename from internal/scripts/posts/new-post.js rename to tooling/scripts/posts/new-post.js diff --git a/internal/scripts/research/build_nekaracuba_corpus.py b/tooling/scripts/research/build_nekaracuba_corpus.py similarity index 100% rename from internal/scripts/research/build_nekaracuba_corpus.py rename to tooling/scripts/research/build_nekaracuba_corpus.py diff --git a/internal/scripts/research/generate_toss_analysis.py b/tooling/scripts/research/generate_toss_analysis.py similarity index 100% rename from internal/scripts/research/generate_toss_analysis.py rename to tooling/scripts/research/generate_toss_analysis.py diff --git a/internal/scripts/verify-docs.mjs b/tooling/scripts/verify-docs.mjs similarity index 97% rename from internal/scripts/verify-docs.mjs rename to tooling/scripts/verify-docs.mjs index 93000be..a2e71bc 100644 --- a/internal/scripts/verify-docs.mjs +++ b/tooling/scripts/verify-docs.mjs @@ -88,7 +88,7 @@ function runMarkdownlint() { path.join(ROOT, 'node_modules', '.bin', 'markdownlint-cli2'), [ '--config', - path.join('internal', 'config', '.markdownlint-cli2.jsonc'), + path.join('tooling', 'config', '.markdownlint-cli2.jsonc'), ...markdownlintTargets, ], { diff --git a/tsconfig.json b/tsconfig.json index 0f96f2a..bcdde96 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,13 +13,14 @@ "isolatedModules": true, "jsx": "react-jsx", "incremental": true, + "baseUrl": ".", "plugins": [ { "name": "next" } ], "paths": { - "@/*": ["./src/*"] + "@/*": ["./*"] } }, "include": [ diff --git a/vitest.config.ts b/vitest.config.ts index 769dc89..d812491 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,30 +4,50 @@ import path from 'path'; export default defineConfig({ plugins: [react()], - test: { - environment: 'jsdom', - globals: true, - setupFiles: './vitest.setup.ts', - include: ['src/**/*.test.ts', 'src/**/*.test.tsx'], + resolve: { alias: { - '@': path.resolve(__dirname, './src'), + '@': path.resolve(__dirname, '.'), 'server-only': path.resolve( __dirname, - './src/shared/testing/server-only.ts' + './shared/testing/server-only.ts' ), }, + }, + test: { + environment: 'jsdom', + globals: true, + setupFiles: './vitest.setup.ts', + include: [ + 'blog/**/*.test.ts', + 'blog/**/*.test.tsx', + 'platform/**/*.test.ts', + 'platform/**/*.test.tsx', + 'resume/**/*.test.ts', + 'resume/**/*.test.tsx', + 'search/**/*.test.ts', + 'search/**/*.test.tsx', + 'shared/**/*.test.ts', + 'shared/**/*.test.tsx', + 'site/**/*.test.ts', + 'site/**/*.test.tsx', + 'src/**/*.test.ts', + 'src/**/*.test.tsx', + 'styles/**/*.test.ts', + ], coverage: { provider: 'v8', include: [ - 'src/core/**/*.{ts,tsx}', - 'src/domains/**/*.{ts,tsx}', - 'src/features/**/*.{ts,tsx}', - 'src/shared/**/*.{ts,tsx}', - 'src/styles/**/*.{ts,tsx}', - 'src/components/visualization/**/*.{ts,tsx}', + 'blog/**/*.{ts,tsx}', + 'platform/**/*.{ts,tsx}', + 'resume/**/*.{ts,tsx}', + 'search/**/*.{ts,tsx}', + 'shared/**/*.{ts,tsx}', + 'site/**/*.{ts,tsx}', + 'src/app/**/*.{ts,tsx}', + 'styles/**/*.{ts,tsx}', ], exclude: [ - 'src/shared/testing/**', + 'shared/testing/**', '**/*.types.ts', '**/index.ts', '**/index.tsx',