計画: B-119 ディレクトリ構造リファクタリング実施計画
B-119 ディレクトリ構造リファクタリング実施計画
1. ゴール
誰のために
プロジェクトのコードを読み書きするAIエージェント自身の生産性向上のため。現在、1つのフィーチャーに関するファイルが3-4箇所に散在しており、機能追加・変更・バグ修正時に関連ファイルの把握に時間がかかる。とくにgames(153ファイルが4箇所)が深刻。
この作業によって提供する価値
- フィーチャー単位の凝集度向上により、機能追加・修正・削除が1ディレクトリ内で完結する
- 新しいフィーチャー追加時のテンプレートが明確になる
- コードベースの見通しが良くなり、AIエージェントの作業精度と速度が向上する
- 結果としてサイトの品質改善サイクルが加速し、PV向上に寄与する
完成の定義
- すべてのフィーチャーが src/features/ 配下にコロケーションされていること
- src/components/ には共有コンポーネント(common/)のみが残ること
- src/lib/ にはフィーチャー固有でない共有ユーティリティのみが残ること
- src/data/ ディレクトリが廃止されていること
- npm run typecheck, npm run test, npm run build がすべてパスすること
- pre-commit hook(prettier, eslint, tsc, memo-lint)がすべてパスすること
- 既存のURL構造に変更がないこと
- path alias @/* -> ./src/* の設定が変更されていないこと
2. 構造案の最終決定
決定: 案Bフィーチャーベース完全統合型を採用(一部修正あり)
リサーチャーの案Bを基本的に採用するが、以下の修正を加える。
修正点
修正1: 共有データ(src/data/)の配置
- ゲーム専用データ(*-schedule.json, nakamawake-data.json)は features/games/ 配下の各ゲームに配置
- dictionary/gamesの両方から参照されるデータ(kanji-data.json, yoji-data.json, traditional-colors.json)は src/data/ に残す。ただし名前を src/shared-data/ に変更して「意図的な共有データ」であることを明示する
理由: features/dictionary/data/ に配置すると features/games/ から features/dictionary/ への依存が生まれ、フィーチャー間の結合度が上がる。共有データは明示的な共有レイヤーに置くほうがフィーチャーの独立性が高い。
修正2: blog/content/ の配置
- ブログのMarkdownファイルは src/content/blog/ のまま移動しない。features/blog/ 内にはlib, componentsのみを配置する
- 理由: Markdownコンテンツファイルはコードではなくデータであり、features/blog/content/ に置くとコードとデータの境界が曖昧になる。また lib/blog.ts のパスが process.cwd() ベースでハードコードされているので、パスの一貫性を保つため
修正3: searchは features/ に移動しない
- search は複数フィーチャーを横断する基盤機能であり、独立した「フィーチャー」ではない
- src/lib/search/ と src/components/search/ はそのまま残す(共有レイヤーとして扱う)
最終ディレクトリ構造
src/
app/ # ルーティング層(変更なし)
features/ # フィーチャーごとの完全なコード
tools/
[各ツール32個]/ # Component.tsx, Component.module.css, logic.ts, meta.ts, __tests__/
components/ # src/components/tools/ から移動
ToolCard.tsx, ToolCard.module.css
ToolLayout.tsx, ToolLayout.module.css
ToolsGrid.tsx, ToolsGrid.module.css
ToolsListView.tsx, ToolsListView.module.css
RelatedTools.tsx, RelatedTools.module.css
RelatedBlogPosts.tsx, RelatedBlogPosts.module.css
ErrorBoundary.tsx
__tests__/
registry.ts
types.ts
cheatsheets/
[各チートシート3個]/ # Component.tsx, meta.ts
components/ # src/components/cheatsheets/ から移動
CheatsheetCard.tsx, ...
CheatsheetLayout.tsx, ...
CodeBlock.tsx, ...
RelatedCheatsheets.tsx, ...
TableOfContents.tsx, ...
__tests__/
registry.ts
types.ts
games/
kanji-kanaru/
components/ # src/components/games/kanji-kanaru/ から移動
lib/ # src/lib/games/kanji-kanaru/ から移動
data/ # ゲーム固有データ(puzzle-schedule.json)
yoji-kimeru/
components/ # src/components/games/yoji-kimeru/ から移動
lib/ # src/lib/games/yoji-kimeru/ から移動
data/ # ゲーム固有データ(yoji-schedule.json)
nakamawake/
components/ # src/components/games/nakamawake/ から移動
lib/ # src/lib/games/nakamawake/ から移動
data/ # ゲーム固有データ(nakamawake-data.json, nakamawake-schedule.json)
irodori/
components/ # src/components/games/irodori/ から移動
lib/ # src/lib/games/irodori/ から移動
data/ # ゲーム固有データ(irodori-schedule.json)
shared/
components/ # src/components/games/shared/ から移動
lib/ # src/lib/games/shared/ から移動
registry.ts # src/lib/games/registry.ts から移動
types.ts # src/lib/games/types.ts から移動
quiz/
components/ # src/components/quiz/ から移動
data/ # src/lib/quiz/data/ から移動
lib/ # scoring.ts を配置
registry.ts
types.ts
__tests__/ # src/lib/quiz/__tests__/ から移動
dictionary/
components/ # src/components/dictionary/ から移動
lib/ # src/lib/dictionary/ から移動(kanji.ts, yoji.ts, colors.ts, index.ts, types.ts)
blog/
components/ # src/components/blog/ から移動
lib.ts # src/lib/blog.ts から移動
memos/
components/ # src/components/memos/ から移動
lib.ts # src/lib/memos.ts から移動
lib-shared.ts # src/lib/memos-shared.ts から移動
content/ # コンテンツデータ(変更なし)
blog/ # ブログMarkdownファイル
shared-data/ # src/data/ をリネーム(辞典とゲーム両方から参照される共有データ)
kanji-data.json
yoji-data.json
traditional-colors.json
components/ # 共有コンポーネントのみ
common/ # Header, Footer, Breadcrumb, Pagination 等(変更なし)
lib/ # 共有ユーティリティのみ
constants.ts
cross-links.ts
date.ts
feed.ts
feed-memos.ts
markdown.ts
ogp-image.tsx
pagination.ts
search/ # 検索機能(フィーチャー横断のため共有層に残す)
seo.ts
__tests__/ # 共有lib用テスト(constants, date, markdown, pagination, ogp-image, seo 等)
types/ # サードパーティ型定義(変更なし)
test/ # テストセットアップ(変更なし)
各フィーチャーの配置先詳細
| 移動元 | 移動先 |
|---|---|
| src/tools/ | src/features/tools/(ツール定義をそのまま移動) |
| src/components/tools/ | src/features/tools/components/ |
| src/cheatsheets/ | src/features/cheatsheets/(定義をそのまま移動) |
| src/components/cheatsheets/ | src/features/cheatsheets/components/ |
| src/lib/games/ | src/features/games/ 配下に分散配置 |
| src/components/games/ | src/features/games/ 配下に分散配置 |
| src/data/*-schedule.json | src/features/games/各ゲーム/data/ |
| src/data/nakamawake-data.json | src/features/games/nakamawake/data/ |
| src/data/kanji-data.json, yoji-data.json, traditional-colors.json | src/shared-data/ |
| src/lib/quiz/ | src/features/quiz/ |
| src/components/quiz/ | src/features/quiz/components/ |
| src/lib/dictionary/ | src/features/dictionary/lib/ |
| src/components/dictionary/ | src/features/dictionary/components/ |
| src/lib/blog.ts | src/features/blog/lib.ts |
| src/components/blog/ | src/features/blog/components/ |
| src/lib/memos.ts | src/features/memos/lib.ts |
| src/lib/memos-shared.ts | src/features/memos/lib-shared.ts |
| src/components/memos/ | src/features/memos/components/ |
共有ライブラリ(lib/)の整理方針
以下のファイルはフィーチャー固有ではないため src/lib/ に残す:
- constants.ts(全体で使用)
- cross-links.ts(blog + memos を横断)
- date.ts(共通ユーティリティ)
- feed.ts, feed-memos.ts(全体で使用)
- markdown.ts(共通ユーティリティ)
- ogp-image.tsx(全体で使用)
- pagination.ts(共通ユーティリティ)
- search/(フィーチャー横断の基盤機能)
- seo.ts(フィーチャー横断の基盤機能)
cross-links.ts のインポートパスは @/lib/blog -> @/features/blog/lib, @/lib/memos -> @/features/memos/lib に更新する。 seo.ts のインポートは @/tools/types -> @/features/tools/types, @/cheatsheets/types -> @/features/cheatsheets/types, @/lib/quiz/types -> @/features/quiz/types に更新する。 search/build-index.ts のインポートは全フィーチャーのregistryパスに更新する。
3. 移行計画
移行の原則
- 1フェーズ = 1フィーチャーの移行
- 各フェーズは独立したコミットとする
- 各フェーズ完了後に必ず typecheck + test + build を実行して健全性を確認
- フェーズ間の依存がないよう、各フェーズ内で完結するようにする
フェーズ0: 準備(共有データの分離)
作業内容:
- src/data/ を src/shared-data/ にリネーム
- src/shared-data/ には kanji-data.json, yoji-data.json, traditional-colors.json のみを残す
- ゲーム固有データ(*-schedule.json, nakamawake-data.json)は一旦 src/shared-data/ に含めておく(フェーズ2で各ゲームに移動)
- 全ファイルの @/data/ インポートを @/shared-data/ に更新(6ファイル)
影響ファイル:
- src/components/games/kanji-kanaru/GameContainer.tsx
- src/components/games/yoji-kimeru/GameContainer.tsx
- src/components/games/nakamawake/GameContainer.tsx
- src/components/games/irodori/GameContainer.tsx
- src/lib/dictionary/kanji.ts
- src/lib/dictionary/yoji.ts
- src/lib/dictionary/colors.ts
検証: typecheck + test + build
フェーズ1: games の移行(最優先・最大規模)
games は4箇所に153ファイルが散在しており、最も改善効果が大きい。
作業内容:
- src/features/games/ ディレクトリを作成
- 各ゲームごとに以下を移動:
- src/lib/games/{game}/ -> src/features/games/{game}/lib/
- src/components/games/{game}/ -> src/features/games/{game}/components/
- src/shared-data/{game}-schedule.json -> src/features/games/{game}/data/
- nakamawake-data.json -> src/features/games/nakamawake/data/
- 共有を移動:
- src/lib/games/shared/ -> src/features/games/shared/lib/
- src/components/games/shared/ -> src/features/games/shared/components/
- registry/types を移動:
- src/lib/games/registry.ts -> src/features/games/registry.ts
- src/lib/games/types.ts -> src/features/games/types.ts
- 全インポートパスを更新:
- @/lib/games/* -> @/features/games/*
- @/components/games/* -> @/features/games//components/
- @/shared-data/-schedule.json -> @/features/games//data/*-schedule.json
- @/shared-data/nakamawake-data.json -> @/features/games/nakamawake/data/nakamawake-data.json
- src/app/games/ 内のインポートを更新
- search/build-index.ts の games registry インポートを更新
- 空になった src/lib/games/, src/components/games/ を削除
影響ファイルの概算: 約80ファイルのインポートパス変更
検証: typecheck + test + build
フェーズ2: tools の移行
tools は既に src/tools/ でコロケーションされているため、比較的単純。主な作業は src/components/tools/ の統合。
作業内容:
- src/tools/ を src/features/tools/ に移動
- src/components/tools/ を src/features/tools/components/ に移動
- 全インポートパスを更新:
- @/tools/* -> @/features/tools/*
- @/components/tools/* -> @/features/tools/components/*
- src/app/tools/ 内のインポートを更新
- src/lib/seo.ts, search/build-index.ts のインポートを更新
- src/components/cheatsheets/CheatsheetLayout.tsx の allToolMetas インポートを更新
- 空になった旧ディレクトリを削除
影響ファイルの概算: 約50ファイルのインポートパス変更
検証: typecheck + test + build
フェーズ3: cheatsheets の移行
作業内容:
- src/cheatsheets/ を src/features/cheatsheets/ に移動
- src/components/cheatsheets/ を src/features/cheatsheets/components/ に移動
- 全インポートパスを更新:
- @/cheatsheets/* -> @/features/cheatsheets/*
- @/components/cheatsheets/* -> @/features/cheatsheets/components/*
- src/app/cheatsheets/ 内のインポートを更新
- src/lib/seo.ts, search/build-index.ts のインポートを更新
- 空になった旧ディレクトリを削除
影響ファイルの概算: 約20ファイルのインポートパス変更
検証: typecheck + test + build
フェーズ4: quiz の移行
作業内容:
- src/features/quiz/ ディレクトリを作成
- src/lib/quiz/ の内容を src/features/quiz/ に移動:
- registry.ts, types.ts, scoring.ts -> src/features/quiz/
- data/ -> src/features/quiz/data/
- tests/ -> src/features/quiz/tests/
- src/components/quiz/ を src/features/quiz/components/ に移動
- 全インポートパスを更新:
- @/lib/quiz/* -> @/features/quiz/*
- @/components/quiz/* -> @/features/quiz/components/*
- src/app/quiz/ 内のインポートを更新
- src/lib/seo.ts, search/build-index.ts のインポートを更新
- 空になった旧ディレクトリを削除
影響ファイルの概算: 約15ファイルのインポートパス変更
検証: typecheck + test + build
フェーズ5: dictionary の移行
作業内容:
- src/features/dictionary/ ディレクトリを作成
- src/lib/dictionary/ を src/features/dictionary/lib/ に移動
- src/components/dictionary/ を src/features/dictionary/components/ に移動
- 全インポートパスを更新:
- @/lib/dictionary/* -> @/features/dictionary/lib/*
- @/components/dictionary/* -> @/features/dictionary/components/*
- src/app/dictionary/ と src/app/colors/ 内のインポートを更新
- search/build-index.ts のインポートを更新
- 空になった旧ディレクトリを削除
注意: shared-data/ のJSONファイル(kanji-data.json等)はそのまま。features/dictionary/lib/kanji.ts は @/shared-data/kanji-data.json を参照する。
影響ファイルの概算: 約25ファイルのインポートパス変更
検証: typecheck + test + build
フェーズ6: blog の移行
作業内容:
- src/features/blog/ ディレクトリを作成
- src/lib/blog.ts を src/features/blog/lib.ts に移動
- src/components/blog/ を src/features/blog/components/ に移動
- src/features/blog/lib.ts 内の BLOG_DIR パスは変更なし(process.cwd() + 'src/content/blog' のまま)
- src/content/blog/ はそのまま(Markdownファイルは移動しない)
- 全インポートパスを更新:
- @/lib/blog -> @/features/blog/lib
- @/components/blog/* -> @/features/blog/components/*
- src/lib/cross-links.ts のインポートを更新
- src/lib/search/build-index.ts のインポートを更新
- 空になった旧ディレクトリを削除
影響ファイルの概算: 約15ファイルのインポートパス変更
検証: typecheck + test + build
フェーズ7: memos の移行
作業内容:
- src/features/memos/ ディレクトリを作成
- src/lib/memos.ts を src/features/memos/lib.ts に移動
- src/lib/memos-shared.ts を src/features/memos/lib-shared.ts に移動
- src/components/memos/ を src/features/memos/components/ に移動
- 全インポートパスを更新:
- @/lib/memos -> @/features/memos/lib
- @/lib/memos-shared -> @/features/memos/lib-shared
- @/components/memos/* -> @/features/memos/components/*
- src/lib/cross-links.ts, src/lib/feed-memos.ts のインポートを更新
- src/app/memos/ 内のインポートを更新
- 空になった旧ディレクトリを削除
影響ファイルの概算: 約15ファイルのインポートパス変更
検証: typecheck + test + build
フェーズ8: 共有レイヤーのクリーンアップと最終検証
作業内容:
- src/lib/tests/ のテストファイルで移動済みフィーチャー関連のものを適切な場所に移動:
- blog-series.test.ts -> src/features/blog/tests/
- memos.test.ts, memos-shared.test.ts -> src/features/memos/tests/
- seo-cheatsheet.test.ts -> そのまま残す(seo.ts自体はlib/に残すため)
- seo.test.ts -> そのまま残す
- constants.test.ts, date.test.ts, markdown.test.ts, pagination.test.ts, ogp-image.test.tsx -> そのまま残す
- src/components/ 配下に common/ 以外のディレクトリが残っていないことを確認
- src/lib/ 配下にフィーチャー固有のディレクトリが残っていないことを確認
- search のインポートパスがすべて @/features/* を正しく参照していることを確認
最終検証:
- npm run typecheck(型チェック)
- npm run test(全テスト実行)
- npm run build(ビルド成功確認)
- npm run lint(ESLint)
- npm run format:check(Prettier)
4. リスクと対策
リスク1: インポートパスの変更漏れ
- 対策: 各フェーズ完了後に必ず tsc --noEmit を実行。TypeScriptの型チェッカーが未解決のインポートを全て検出する
- 対策: grep で旧パス(例: @/tools/, @/lib/games/)が残っていないことを確認するステップを全フェーズに含める
リスク2: テスト破損
- 対策: 各フェーズで npm run test を実行。相対パスインポートのテストは移動先でも正常に動作するが、@/ エイリアスパスのテストは更新が必要
- 対策: vitest.config.mts は vite-tsconfig-paths を使用しているため、tsconfig.json の paths 設定(@/* -> ./src/*)が変わらない限り自動解決される
リスク3: ビルド失敗
- 対策: 各フェーズで npm run build を実行
- 対策: 動的インポート(tools/registry.ts の componentImport)は相対パスを使用しているため、registry.ts の移動に伴って自動的に正しく解決される
リスク4: 共有データの参照ミス
- 対策: フェーズ0で共有データの移動を先に行い、以降のフェーズではデータパスが安定している状態で作業する
- 対策: kanji-data.json等の共有データを features/ 内に置かないことで、フィーチャー間の暗黙的な依存を回避する
リスク5: git diff の大きさ
- 対策: フェーズごとにコミットすることで、各コミットのdiffを管理可能なサイズに保つ
- 対策: git mv を使ってファイルの移動を行い、git がリネームとして認識できるようにする(内容変更を伴うファイルは少なくする)
リスク6: next.config.ts のリダイレクト
- 影響なし: redirects はURLベースであり、ファイル構造には依存しない。確認は必要だが変更は不要
リスク7: scripts/generate-puzzle-schedule.ts のパス参照
- 対策: このスクリプトは src/data/ のパスをハードコードしている可能性がある。フェーズ0またはフェーズ1で確認・修正する
5. 検証計画
各フェーズ後の確認項目(全フェーズ共通)
- 旧パス残存チェック: grep で移動元のパス(@/tools/, @/lib/games/ 等)がソースコード中に残っていないことを確認
- 型チェック: npm run typecheck
- テスト実行: npm run test
- ビルド確認: npm run build
- フォーマット確認: npm run format:check
最終リグレッションチェック(フェーズ8完了後)
- 上記5項目の全実行
- ESLint: npm run lint
- ディレクトリ構造の確認: src/ 配下に意図しないファイルが残っていないこと
- src/components/ に common/ のみが存在すること
- src/lib/ にフィーチャー固有ディレクトリが存在しないこと
6. 作業分割と実行方針
各フェーズをそれぞれ独立したビルダータスクとして実行する。各タスクの粒度:
- フェーズ0(準備): 1タスク — 小規模(7ファイルのインポート更新のみ)
- フェーズ1(games): 1タスク — 大規模(約80ファイル)。ゲームごとに4つのサブタスクに分割してもよい
- フェーズ2(tools): 1タスク — 中規模(約50ファイル)
- フェーズ3(cheatsheets): 1タスク — 小規模(約20ファイル)
- フェーズ4(quiz): 1タスク — 小規模(約15ファイル)
- フェーズ5(dictionary): 1タスク — 小規模(約25ファイル)
- フェーズ6(blog): 1タスク — 小規模(約15ファイル)
- フェーズ7(memos): 1タスク — 小規模(約15ファイル)
- フェーズ8(クリーンアップ): 1タスク — 小規模
フェーズは順番に直列で実行する。前のフェーズが完了し検証をパスしてから次のフェーズに進む。
各フェーズ完了後にレビュアーによるレビューは不要(機械的なリファクタリングのため)。ただし、フェーズ8の最終検証後に全体レビューを実施する。
7. 補足: 共有データ配置の代替案について
リサーチャーは「features/dictionary/data/ に配置しgamesから @/features/dictionary/data/ で参照」を提案したが、本計画では src/shared-data/ に配置する案を採用した。
理由:
- dictionaryは「データの権威ある出典」かもしれないが、ゲームがdictionaryフィーチャーに依存する構造は、フィーチャーベース設計の「各フィーチャーが独立」という原則に反する
- shared-data/ という名前にすることで「これは複数フィーチャーから共有されるデータである」という意図が明確になる
- 将来、辞典とゲーム以外のフィーチャー(例: 漢字に関するクイズ)からも利用される可能性がある
- 共有データファイルは3つだけなので、独立ディレクトリとしてのオーバーヘッドは小さい