|
| 1 | +# アーキテクチャガイド |
| 2 | + |
| 3 | +## 1. 3層ディレクトリモデル |
| 4 | + |
| 5 | +コードの配置先を **routes / features / lib** の3層で判断する。 |
| 6 | + |
| 7 | +```text |
| 8 | +src/ |
| 9 | +├── routes/ # ルート定義。薄く保つ |
| 10 | +├── features/ # 機能スコープのコード(1つの機能ドメインに閉じるもの) |
| 11 | +└── lib/ # 真に共通のコード(2つ以上の機能から使われるもの) |
| 12 | +``` |
| 13 | + |
| 14 | +### 判定基準 |
| 15 | + |
| 16 | +```text |
| 17 | +このコードはどの機能ドメインで使われるか? |
| 18 | +│ |
| 19 | +├── 1つの機能ドメインに属する |
| 20 | +│ → features/{feature}/ に配置 |
| 21 | +│ |
| 22 | +└── 複数の機能ドメインで使う |
| 23 | + → lib/ に配置 |
| 24 | +``` |
| 25 | + |
| 26 | +`routes/` にはルーティングファイル(`+page.svelte`, `+page.server.ts`, `+layout.svelte` 等)のみ置く。 |
| 27 | + |
| 28 | +**例:** |
| 29 | + |
| 30 | +- `WorkBookForm.svelte` → workbooks ドメイン → `features/workbooks/` |
| 31 | +- `TaskTable.svelte` → tasks ドメイン(problems ページで表示) → `features/tasks/` |
| 32 | +- `GradeLabel.svelte` → problems・workbooks・admin で使う → `lib/components/` |
| 33 | +- `UserService` → auth・admin・users で使う → `lib/server/services/` |
| 34 | + |
| 35 | +--- |
| 36 | + |
| 37 | +## 2. src/features/ の構造 |
| 38 | + |
| 39 | +feature ディレクトリの内部は技術スタックベースで整理する。`components/` はページ単位でサブディレクトリに分割し、テストはファイル隣接で配置する。 |
| 40 | + |
| 41 | +```text |
| 42 | +src/features/ |
| 43 | +├── workbooks/ |
| 44 | +│ ├── components/ |
| 45 | +│ │ ├── list/ # 一覧ページ |
| 46 | +│ │ │ ├── WorkBookList.svelte |
| 47 | +│ │ │ ├── WorkBookList.test.ts |
| 48 | +│ │ │ └── WorkBookBaseTable.svelte |
| 49 | +│ │ ├── detail/ # 詳細ページ |
| 50 | +│ │ │ ├── CommentAndHint.svelte |
| 51 | +│ │ │ └── PublicationStatusLabel.svelte |
| 52 | +│ │ ├── form/ # 作成・編集ページ |
| 53 | +│ │ │ ├── WorkBookForm.svelte |
| 54 | +│ │ │ ├── WorkBookForm.test.ts |
| 55 | +│ │ │ └── WorkBookInputFields.svelte |
| 56 | +│ │ └── shared/ # feature 内で複数ページから使うもの |
| 57 | +│ ├── fixtures/ # テスト用データ |
| 58 | +│ ├── services/ |
| 59 | +│ │ ├── workbooks.ts |
| 60 | +│ │ └── workbooks.test.ts |
| 61 | +│ ├── stores/ |
| 62 | +│ │ └── active_workbook_tab.ts |
| 63 | +│ ├── types/ |
| 64 | +│ │ └── workbook_form.ts |
| 65 | +│ └── utils/ |
| 66 | +│ ├── workbook.ts |
| 67 | +│ └── workbook.test.ts |
| 68 | +├── tasks/ |
| 69 | +│ ├── components/ |
| 70 | +│ │ ├── contest-table/ # コンテスト別 |
| 71 | +│ │ ├── grade-list/ # グレード別 |
| 72 | +│ │ ├── detail/ |
| 73 | +│ │ │ ├── statistics/ # AtCoder Problems の difficulty、JOI 難易度、AC人数 など |
| 74 | +│ │ │ ├── votes/ # AtCoder NoviSteps の難易度、タグを投票 |
| 75 | +│ │ │ └── comments/ # (要確認) 管理者: 解説、一般ユーザ: コメント |
| 76 | +│ │ └── shared/ |
| 77 | +├── admin/ |
| 78 | +│ ├── components/ |
| 79 | +│ │ ├── account_transfer/ |
| 80 | +│ │ ├── tasks-for-import/ |
| 81 | +│ │ └── shared/ |
| 82 | +│ ... |
| 83 | +├── auth/ |
| 84 | +└── user-profile/ |
| 85 | +``` |
| 86 | + |
| 87 | +**components/ のサブディレクトリ規約:** |
| 88 | + |
| 89 | +- `list/` — 一覧ページ用コンポーネント |
| 90 | +- `detail/` — 詳細ページ用コンポーネント |
| 91 | +- `form/` — 作成・編集フォーム用コンポーネント |
| 92 | +- `shared/` — feature 内で複数ページから使うコンポーネント |
| 93 | + |
| 94 | +### Feature 分割案 |
| 95 | + |
| 96 | +現在の `src/lib/` からの抽出候補: |
| 97 | + |
| 98 | +| Feature | 抽出対象 | |
| 99 | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | |
| 100 | +| **workbooks** | `WorkBook/*`, `WorkBooks/*`, `services/workbooks.ts`, `utils/workbook*.ts`, `stores/active_workbook_tab.ts` | |
| 101 | +| **tasks** | `TaskTables/*`, `TaskGrades/*`, `stores/active_problem_list_tab.svelte.ts`(※ `TaskGradeList`, `TaskList` 等は複数ドメインで使うため `lib/` に残す) | |
| 102 | +| **admin** | `TagForm`, `TagListForEdit`, `TaskForm`, `TaskListForEdit`, `services/tags.ts`, `services/task_tags.ts` | |
| 103 | +| **auth** | `AuthForm`, `utils/auth_forms.ts`, `types/auth_forms.ts` | |
| 104 | +| **user-profile** | `UserProfile`, `UserAccountDeletionForm` | |
| 105 | + |
| 106 | +### Feature 間の依存ルール |
| 107 | + |
| 108 | +- `features/` → `lib/` の参照: OK |
| 109 | +- `features/A/` → `features/B/` の参照: NG(共通化が必要なら `lib/` に移す) |
| 110 | +- `routes/` → `features/` の参照: OK |
| 111 | +- `routes/` → `lib/` の参照: OK |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +## 3. src/lib/ に残すもの |
| 116 | + |
| 117 | +2つ以上の feature から使われるコードのみ配置する。 |
| 118 | + |
| 119 | +```text |
| 120 | +src/lib/ |
| 121 | +├── actions/ # SvelteKit アクション |
| 122 | +├── clients/ # 外部 API クライアント(AtCoder Problems, AOJ) |
| 123 | +├── components/ # 共通 UI コンポーネント(GradeLabel, TaskGradeList, TaskList, FormWrapper 等) |
| 124 | +├── constants/ # アプリ定数 |
| 125 | +├── server/ # サーバー専用コード |
| 126 | +│ ├── auth.ts |
| 127 | +│ ├── database.ts |
| 128 | +│ └── services/ # 複数 feature で使うビジネスロジック |
| 129 | +├── stores/ # 共通ストア(error_message 等) |
| 130 | +├── types/ # 共通型定義 |
| 131 | +├── utils/ # 共通ユーティリティ |
| 132 | +└── zod/ # バリデーションスキーマ |
| 133 | +``` |
| 134 | + |
| 135 | +`lib/server/` 内のコードはクライアントからインポートできない(SvelteKit が自動でビルド時エラーにする)。 |
| 136 | + |
| 137 | +--- |
| 138 | + |
| 139 | +## 4. テストコロケーション |
| 140 | + |
| 141 | +### 方針: ファイル隣接配置 |
| 142 | + |
| 143 | +テストは対象ファイルの隣に置く。変更時にテストが目に入り、更新忘れを防ぐ。 |
| 144 | + |
| 145 | +```text |
| 146 | +features/workbooks/services/ |
| 147 | +├── workbooks.ts |
| 148 | +└── workbooks.test.ts ← 隣接 |
| 149 | +``` |
| 150 | + |
| 151 | +### 共通テスト資産は src/test/ に残す |
| 152 | + |
| 153 | +- テストファクトリ(`@quramy/prisma-fabbrica`) |
| 154 | +- テストヘルパー・フィクスチャ |
| 155 | +- E2E テスト(`tests/`) |
| 156 | + |
| 157 | +### Vitest 設定 |
| 158 | + |
| 159 | +`vite.config.ts` の `include` パターンを更新する: |
| 160 | + |
| 161 | +```ts |
| 162 | +include: [ |
| 163 | + 'src/test/**/*.test.ts', // 既存テスト(段階移行) |
| 164 | + 'src/features/**/*.test.ts', // feature 内テスト |
| 165 | +]; |
| 166 | +``` |
| 167 | + |
| 168 | +--- |
| 169 | + |
| 170 | +## 5. パスエイリアス |
| 171 | + |
| 172 | +`svelte.config.js` でカスタムエイリアスを設定する: |
| 173 | + |
| 174 | +```js |
| 175 | +kit: { |
| 176 | + alias: { |
| 177 | + $lib: './src/lib', |
| 178 | + $features: './src/features', |
| 179 | + }, |
| 180 | +} |
| 181 | +``` |
| 182 | + |
| 183 | +```ts |
| 184 | +// インポート例 |
| 185 | +import WorkBookForm from '$features/workbooks/components/WorkBookForm.svelte'; |
| 186 | +import { formatDate } from '$lib/utils/format'; |
| 187 | +import type { PageServerLoad } from './$types'; // フレームワーク型は ./$types から |
| 188 | +``` |
| 189 | + |
| 190 | +`$types` は SvelteKit が `.svelte-kit/types/` に自動生成するフレームワーク型(`PageServerLoad` 等)。ビジネスドメイン型は `$lib/types/` に置く。 |
| 191 | + |
| 192 | +--- |
| 193 | + |
| 194 | +## 6. 参考資料 |
| 195 | + |
| 196 | +### SvelteKit 公式 |
| 197 | + |
| 198 | +- [プロジェクト構造](https://svelte.dev/docs/kit/project-structure) |
| 199 | +- [ルーティング](https://svelte.dev/docs/kit/routing) |
| 200 | +- [高度なルーティング](https://svelte.dev/docs/kit/advanced-routing) |
| 201 | +- [サーバー専用モジュール](https://svelte.dev/docs/kit/server-only-modules) |
| 202 | + |
| 203 | +### 設計の参考 |
| 204 | + |
| 205 | +- [Next.js App Router アーキテクチャ設計ガイド](https://zenn.dev/yukionishi/articles/cd79e39ea6c172) — `src/features/` パターン、ESLint によるアーキテクチャ境界の自動強制 |
| 206 | +- [サンプルリポジトリ](https://github.com/YukiOnishi1129/next-app-router-architecture) — 上記記事の実装例 |
| 207 | +- [Immich](https://github.com/immich-app/immich/tree/main/web/src) — 大規模 SvelteKit アプリの実例(`lib/components/{domain}/` パターン) |
| 208 | + |
| 209 | +### 関連 Issue |
| 210 | + |
| 211 | +- [#601: ディレクトリ構造&コロケーション](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/601) |
0 commit comments