@einja/dev-cli 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/README.md +179 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +49 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/init.d.ts +3 -0
  7. package/dist/commands/init.d.ts.map +1 -0
  8. package/dist/commands/init.js +243 -0
  9. package/dist/commands/init.js.map +1 -0
  10. package/dist/commands/list.d.ts +2 -0
  11. package/dist/commands/list.d.ts.map +1 -0
  12. package/dist/commands/list.js +23 -0
  13. package/dist/commands/list.js.map +1 -0
  14. package/dist/commands/sync.d.ts +7 -0
  15. package/dist/commands/sync.d.ts.map +1 -0
  16. package/dist/commands/sync.js +294 -0
  17. package/dist/commands/sync.js.map +1 -0
  18. package/dist/commands/sync.test.d.ts +2 -0
  19. package/dist/commands/sync.test.d.ts.map +1 -0
  20. package/dist/commands/sync.test.js +593 -0
  21. package/dist/commands/sync.test.js.map +1 -0
  22. package/dist/commands/task-loop.d.ts +11 -0
  23. package/dist/commands/task-loop.d.ts.map +1 -0
  24. package/dist/commands/task-loop.js +81 -0
  25. package/dist/commands/task-loop.js.map +1 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +3 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/lib/file-system.d.ts +39 -0
  31. package/dist/lib/file-system.d.ts.map +1 -0
  32. package/dist/lib/file-system.js +79 -0
  33. package/dist/lib/file-system.js.map +1 -0
  34. package/dist/lib/mcp-config.d.ts +43 -0
  35. package/dist/lib/mcp-config.d.ts.map +1 -0
  36. package/dist/lib/mcp-config.js +109 -0
  37. package/dist/lib/mcp-config.js.map +1 -0
  38. package/dist/lib/mcp-config.test.d.ts +2 -0
  39. package/dist/lib/mcp-config.test.d.ts.map +1 -0
  40. package/dist/lib/mcp-config.test.js +285 -0
  41. package/dist/lib/mcp-config.test.js.map +1 -0
  42. package/dist/lib/merger.d.ts +41 -0
  43. package/dist/lib/merger.d.ts.map +1 -0
  44. package/dist/lib/merger.js +164 -0
  45. package/dist/lib/merger.js.map +1 -0
  46. package/dist/lib/preset-update/cli-repo-detector.d.ts +35 -0
  47. package/dist/lib/preset-update/cli-repo-detector.d.ts.map +1 -0
  48. package/dist/lib/preset-update/cli-repo-detector.js +83 -0
  49. package/dist/lib/preset-update/cli-repo-detector.js.map +1 -0
  50. package/dist/lib/preset-update/cli-repo-detector.test.d.ts +2 -0
  51. package/dist/lib/preset-update/cli-repo-detector.test.d.ts.map +1 -0
  52. package/dist/lib/preset-update/cli-repo-detector.test.js +120 -0
  53. package/dist/lib/preset-update/cli-repo-detector.test.js.map +1 -0
  54. package/dist/lib/preset-update/file-copier.d.ts +59 -0
  55. package/dist/lib/preset-update/file-copier.d.ts.map +1 -0
  56. package/dist/lib/preset-update/file-copier.js +220 -0
  57. package/dist/lib/preset-update/file-copier.js.map +1 -0
  58. package/dist/lib/preset-update/file-copier.test.d.ts +2 -0
  59. package/dist/lib/preset-update/file-copier.test.d.ts.map +1 -0
  60. package/dist/lib/preset-update/file-copier.test.js +297 -0
  61. package/dist/lib/preset-update/file-copier.test.js.map +1 -0
  62. package/dist/lib/preset-update/preset-finder.d.ts +39 -0
  63. package/dist/lib/preset-update/preset-finder.d.ts.map +1 -0
  64. package/dist/lib/preset-update/preset-finder.js +92 -0
  65. package/dist/lib/preset-update/preset-finder.js.map +1 -0
  66. package/dist/lib/preset-update/preset-finder.test.d.ts +2 -0
  67. package/dist/lib/preset-update/preset-finder.test.d.ts.map +1 -0
  68. package/dist/lib/preset-update/preset-finder.test.js +128 -0
  69. package/dist/lib/preset-update/preset-finder.test.js.map +1 -0
  70. package/dist/lib/preset.d.ts +14 -0
  71. package/dist/lib/preset.d.ts.map +1 -0
  72. package/dist/lib/preset.js +52 -0
  73. package/dist/lib/preset.js.map +1 -0
  74. package/dist/lib/sync/backup-manager.d.ts +50 -0
  75. package/dist/lib/sync/backup-manager.d.ts.map +1 -0
  76. package/dist/lib/sync/backup-manager.js +117 -0
  77. package/dist/lib/sync/backup-manager.js.map +1 -0
  78. package/dist/lib/sync/backup-manager.test.d.ts +2 -0
  79. package/dist/lib/sync/backup-manager.test.d.ts.map +1 -0
  80. package/dist/lib/sync/backup-manager.test.js +155 -0
  81. package/dist/lib/sync/backup-manager.test.js.map +1 -0
  82. package/dist/lib/sync/batch-processor.d.ts +27 -0
  83. package/dist/lib/sync/batch-processor.d.ts.map +1 -0
  84. package/dist/lib/sync/batch-processor.js +46 -0
  85. package/dist/lib/sync/batch-processor.js.map +1 -0
  86. package/dist/lib/sync/batch-processor.test.d.ts +2 -0
  87. package/dist/lib/sync/batch-processor.test.d.ts.map +1 -0
  88. package/dist/lib/sync/batch-processor.test.js +110 -0
  89. package/dist/lib/sync/batch-processor.test.js.map +1 -0
  90. package/dist/lib/sync/category-validator.d.ts +36 -0
  91. package/dist/lib/sync/category-validator.d.ts.map +1 -0
  92. package/dist/lib/sync/category-validator.js +46 -0
  93. package/dist/lib/sync/category-validator.js.map +1 -0
  94. package/dist/lib/sync/category-validator.test.d.ts +2 -0
  95. package/dist/lib/sync/category-validator.test.d.ts.map +1 -0
  96. package/dist/lib/sync/category-validator.test.js +89 -0
  97. package/dist/lib/sync/category-validator.test.js.map +1 -0
  98. package/dist/lib/sync/conflict-reporter.d.ts +57 -0
  99. package/dist/lib/sync/conflict-reporter.d.ts.map +1 -0
  100. package/dist/lib/sync/conflict-reporter.js +81 -0
  101. package/dist/lib/sync/conflict-reporter.js.map +1 -0
  102. package/dist/lib/sync/conflict-reporter.test.d.ts +2 -0
  103. package/dist/lib/sync/conflict-reporter.test.d.ts.map +1 -0
  104. package/dist/lib/sync/conflict-reporter.test.js +132 -0
  105. package/dist/lib/sync/conflict-reporter.test.js.map +1 -0
  106. package/dist/lib/sync/diff-engine.d.ts +28 -0
  107. package/dist/lib/sync/diff-engine.d.ts.map +1 -0
  108. package/dist/lib/sync/diff-engine.js +118 -0
  109. package/dist/lib/sync/diff-engine.js.map +1 -0
  110. package/dist/lib/sync/diff-engine.test.d.ts +2 -0
  111. package/dist/lib/sync/diff-engine.test.d.ts.map +1 -0
  112. package/dist/lib/sync/diff-engine.test.js +133 -0
  113. package/dist/lib/sync/diff-engine.test.js.map +1 -0
  114. package/dist/lib/sync/file-filter.d.ts +40 -0
  115. package/dist/lib/sync/file-filter.d.ts.map +1 -0
  116. package/dist/lib/sync/file-filter.js +171 -0
  117. package/dist/lib/sync/file-filter.js.map +1 -0
  118. package/dist/lib/sync/file-filter.test.d.ts +2 -0
  119. package/dist/lib/sync/file-filter.test.d.ts.map +1 -0
  120. package/dist/lib/sync/file-filter.test.js +179 -0
  121. package/dist/lib/sync/file-filter.test.js.map +1 -0
  122. package/dist/lib/sync/hash-cache.d.ts +34 -0
  123. package/dist/lib/sync/hash-cache.d.ts.map +1 -0
  124. package/dist/lib/sync/hash-cache.js +51 -0
  125. package/dist/lib/sync/hash-cache.js.map +1 -0
  126. package/dist/lib/sync/hash-cache.test.d.ts +2 -0
  127. package/dist/lib/sync/hash-cache.test.d.ts.map +1 -0
  128. package/dist/lib/sync/hash-cache.test.js +110 -0
  129. package/dist/lib/sync/hash-cache.test.js.map +1 -0
  130. package/dist/lib/sync/integration.test.d.ts +2 -0
  131. package/dist/lib/sync/integration.test.d.ts.map +1 -0
  132. package/dist/lib/sync/integration.test.js +317 -0
  133. package/dist/lib/sync/integration.test.js.map +1 -0
  134. package/dist/lib/sync/marker-processor.d.ts +54 -0
  135. package/dist/lib/sync/marker-processor.d.ts.map +1 -0
  136. package/dist/lib/sync/marker-processor.js +208 -0
  137. package/dist/lib/sync/marker-processor.js.map +1 -0
  138. package/dist/lib/sync/marker-processor.test.d.ts +2 -0
  139. package/dist/lib/sync/marker-processor.test.d.ts.map +1 -0
  140. package/dist/lib/sync/marker-processor.test.js +245 -0
  141. package/dist/lib/sync/marker-processor.test.js.map +1 -0
  142. package/dist/lib/sync/metadata-manager.d.ts +46 -0
  143. package/dist/lib/sync/metadata-manager.d.ts.map +1 -0
  144. package/dist/lib/sync/metadata-manager.js +129 -0
  145. package/dist/lib/sync/metadata-manager.js.map +1 -0
  146. package/dist/lib/sync/metadata-manager.test.d.ts +2 -0
  147. package/dist/lib/sync/metadata-manager.test.d.ts.map +1 -0
  148. package/dist/lib/sync/metadata-manager.test.js +137 -0
  149. package/dist/lib/sync/metadata-manager.test.js.map +1 -0
  150. package/dist/lib/sync/performance.test.d.ts +2 -0
  151. package/dist/lib/sync/performance.test.d.ts.map +1 -0
  152. package/dist/lib/sync/performance.test.js +126 -0
  153. package/dist/lib/sync/performance.test.js.map +1 -0
  154. package/dist/types/index.d.ts +59 -0
  155. package/dist/types/index.d.ts.map +1 -0
  156. package/dist/types/index.js +2 -0
  157. package/dist/types/index.js.map +1 -0
  158. package/dist/types/preset-update.d.ts +106 -0
  159. package/dist/types/preset-update.d.ts.map +1 -0
  160. package/dist/types/preset-update.js +5 -0
  161. package/dist/types/preset-update.js.map +1 -0
  162. package/dist/types/sync.d.ts +169 -0
  163. package/dist/types/sync.d.ts.map +1 -0
  164. package/dist/types/sync.js +19 -0
  165. package/dist/types/sync.js.map +1 -0
  166. package/package.json +72 -0
  167. package/presets/minimal/.claude/agents/einja/docs/docs-updater.md +161 -0
  168. package/presets/minimal/.claude/agents/einja/frontend/design-engineer.md +685 -0
  169. package/presets/minimal/.claude/agents/einja/frontend/frontend-architect.md +747 -0
  170. package/presets/minimal/.claude/agents/einja/frontend/frontend-coder.md +441 -0
  171. package/presets/minimal/.claude/agents/einja/git/conflict-resolver.md +148 -0
  172. package/presets/minimal/.claude/agents/einja/specs/spec-design-generator.md +462 -0
  173. package/presets/minimal/.claude/agents/einja/specs/spec-qa-generator.md +466 -0
  174. package/presets/minimal/.claude/agents/einja/specs/spec-requirements-generator.md +416 -0
  175. package/presets/minimal/.claude/agents/einja/specs/spec-tasks-generator.md +608 -0
  176. package/presets/minimal/.claude/agents/einja/task/task-committer.md +82 -0
  177. package/presets/minimal/.claude/agents/einja/task/task-executer.md +352 -0
  178. package/presets/minimal/.claude/agents/einja/task/task-modification-analyzer.md +369 -0
  179. package/presets/minimal/.claude/agents/einja/task/task-qa.md +74 -0
  180. package/presets/minimal/.claude/agents/einja/task/task-reviewer.md +169 -0
  181. package/presets/minimal/.claude/commands/einja/frontend-implement.md +322 -0
  182. package/presets/minimal/.claude/commands/einja/spec-create.md +254 -0
  183. package/presets/minimal/.claude/commands/einja/start-dev.md +98 -0
  184. package/presets/minimal/.claude/commands/einja/sync-cursor-commands.md +203 -0
  185. package/presets/minimal/.claude/commands/einja/task-exec.md +390 -0
  186. package/presets/minimal/.claude/commands/einja/update-docs-by-task-specs.md +448 -0
  187. package/presets/minimal/.claude/hooks/einja/biome-format.sh +49 -0
  188. package/presets/minimal/.claude/hooks/einja/design-doc-check.sh +61 -0
  189. package/presets/minimal/.claude/hooks/einja/detect-secrets.sh +62 -0
  190. package/presets/minimal/.claude/hooks/einja/large-file-warning.sh +42 -0
  191. package/presets/minimal/.claude/hooks/einja/playwright-resize.sh +36 -0
  192. package/presets/minimal/.claude/hooks/einja/typecheck.sh +37 -0
  193. package/presets/minimal/.claude/hooks/einja/unset-volta-recursion.sh +32 -0
  194. package/presets/minimal/.claude/hooks/einja/validate-git-commit.sh +239 -0
  195. package/presets/minimal/.claude/hooks/einja/warn-index-ts.sh +34 -0
  196. package/presets/minimal/.claude/hooks/einja/warn-relative-import.sh +48 -0
  197. package/presets/minimal/.claude/settings.json +174 -0
  198. package/presets/minimal/.claude/skills/einja/api-development/SKILL.md +14 -0
  199. package/presets/minimal/.claude/skills/einja/backend-architecture/SKILL.md +14 -0
  200. package/presets/minimal/.claude/skills/einja/coding-standards/SKILL.md +120 -0
  201. package/presets/minimal/.claude/skills/einja/coding-standards/reference/naming-conventions.md +107 -0
  202. package/presets/minimal/.claude/skills/einja/coding-standards/reference/prohibited-patterns.md +169 -0
  203. package/presets/minimal/.claude/skills/einja/coding-standards/reference/typescript-rules.md +247 -0
  204. package/presets/minimal/.claude/skills/einja/component-design/SKILL.md +109 -0
  205. package/presets/minimal/.claude/skills/einja/component-design/reference/directory-structure.md +117 -0
  206. package/presets/minimal/.claude/skills/einja/component-design/reference/props-patterns.md +159 -0
  207. package/presets/minimal/.claude/skills/einja/component-design/reference/styling-guide.md +200 -0
  208. package/presets/minimal/.claude/skills/einja/conflict-resolver/SKILL.md +190 -0
  209. package/presets/minimal/.claude/skills/einja/frontend-development/SKILL.md +14 -0
  210. package/presets/minimal/.claude/skills/einja/general-context-loader/SKILL.md +254 -0
  211. package/presets/minimal/.claude/skills/einja/output-format/SKILL.md +137 -0
  212. package/presets/minimal/.claude/skills/einja/spec-context-loader/SKILL.md +177 -0
  213. package/presets/minimal/.claude/skills/einja/task-commit/SKILL.md +269 -0
  214. package/presets/minimal/.claude/skills/einja/task-qa/SKILL.md +306 -0
  215. package/presets/minimal/.claude/skills/einja/task-qa/reference/failure-patterns.md +69 -0
  216. package/presets/minimal/.claude/skills/einja/task-qa/reference/troubleshooting.md +65 -0
  217. package/presets/minimal/.claude/skills/einja/task-qa/reference/usage-patterns.md +52 -0
  218. package/presets/minimal/.claude/skills/einja/task-qa/templates/qa-test-template.md +128 -0
  219. package/presets/minimal/preset.yaml +111 -0
  220. package/presets/minimal/symlinks.json +45 -0
  221. package/scaffolds/.mcp.json +45 -0
  222. package/scaffolds/CLAUDE.md.template +318 -0
  223. package/scaffolds/steering/README.md +170 -0
  224. package/scaffolds/steering/acceptance-criteria-and-qa-guide.md +415 -0
  225. package/scaffolds/steering/architecture.md +481 -0
  226. package/scaffolds/steering/branch-strategy.md +362 -0
  227. package/scaffolds/steering/commit-rules.md +217 -0
  228. package/scaffolds/steering/db-schema-design.md +609 -0
  229. package/scaffolds/steering/development/api-development.md +783 -0
  230. package/scaffolds/steering/development/backend-architecture.md +731 -0
  231. package/scaffolds/steering/development/frontend-development.md +1537 -0
  232. package/scaffolds/steering/development/review-guidelines.md +365 -0
  233. package/scaffolds/steering/development/testing-strategy.md +819 -0
  234. package/scaffolds/steering/development-workflow.md +429 -0
  235. package/scaffolds/steering/infrastructure/deployment.md +277 -0
  236. package/scaffolds/steering/infrastructure/environment-variables.md +298 -0
  237. package/scaffolds/steering/product.md +540 -0
  238. package/scaffolds/steering/task-management.md +367 -0
  239. package/templates/README.md +159 -0
  240. package/templates/design-simple.md.template +172 -0
  241. package/templates/design.md.template +327 -0
  242. package/templates/qa-test.md.template +125 -0
  243. package/templates/requirements.md.template +254 -0
@@ -0,0 +1,1537 @@
1
+ # フロントエンド開発ガイド
2
+
3
+ ## 概要
4
+
5
+ このドキュメントでは、Next.js 14 App Routerを使用したフロントエンド開発のベストプラクティスと実装ガイドラインを説明します。
6
+
7
+ Tanstack Query、React Hook Form、Hono Clientを活用した型安全で保守性の高いフロントエンド開発を実現します。
8
+
9
+ ### 関連ドキュメント
10
+
11
+ - **[API開発ガイド](api-development.md)** - Hono API実装、Server Actionsとの使い分け
12
+ - **[バックエンドアーキテクチャ](backend-architecture.md)** - 4層アーキテクチャ、Repository/Result型
13
+
14
+ > **📌 Server Actions vs Hono Client + Tanstack Query の使い分け**
15
+ >
16
+ > フロントエンドからAPIを呼び出す方法は2パターンあります。使い分けの基準は **[API開発ガイド セクション7](api-development.md#7-フロントエンド統合パターン)** を参照してください。
17
+ >
18
+ > - **Server Actions**: シンプルなフォーム送信、単発のミューテーション
19
+ > - **Hono Client + Tanstack Query**: 複雑なデータフェッチ、キャッシュ管理、リアルタイム更新(本ドキュメントで解説)
20
+
21
+ ---
22
+
23
+ ## 目次
24
+
25
+ 1. [ディレクトリ構造](#1-ディレクトリ構造)
26
+ 2. [技術スタック](#2-技術スタック)
27
+ 3. [Hono Client統合(API呼び出し)](#3-hono-client統合api呼び出し)
28
+ 4. [Server ComponentとClient Component](#4-server-componentとclient-component)
29
+ 5. [Tanstack Query(サーバー状態管理)](#5-tanstack-queryサーバー状態管理)
30
+ 6. [React Hook Form(フォーム処理)](#6-react-hook-formフォーム処理)
31
+ 7. [コンポーネント設計](#7-コンポーネント設計)
32
+ 8. [App Router構成](#8-app-router構成)
33
+ 9. [状態管理戦略](#9-状態管理戦略)
34
+ 10. [エラーハンドリング](#10-エラーハンドリング)
35
+ 11. [実装例](#11-実装例)
36
+
37
+ ---
38
+
39
+ ## 1. ディレクトリ構造
40
+
41
+ ### Webアプリケーション(apps/web)
42
+
43
+ ```
44
+ apps/web/
45
+ ├── app/ # Next.js App Router
46
+ │ ├── (auth)/ # ルートグループ: 認証関連
47
+ │ │ ├── login/
48
+ │ │ │ └── page.tsx
49
+ │ │ └── register/
50
+ │ │ └── page.tsx
51
+ │ ├── (dashboard)/ # ルートグループ: ダッシュボード
52
+ │ │ ├── posts/
53
+ │ │ │ ├── page.tsx # 投稿一覧
54
+ │ │ │ ├── new/
55
+ │ │ │ │ └── page.tsx # 投稿作成
56
+ │ │ │ └── [id]/
57
+ │ │ │ └── page.tsx # 投稿詳細
58
+ │ │ └── profile/
59
+ │ │ └── page.tsx
60
+ │ ├── api/ # API Routes
61
+ │ │ └── [[...route]]/
62
+ │ │ └── route.ts # Honoエントリーポイント
63
+ │ ├── layout.tsx # ルートレイアウト
64
+ │ └── page.tsx # トップページ
65
+ ├── components/ # UIコンポーネント
66
+ │ ├── ui/ # 基本コンポーネント
67
+ │ │ ├── button.tsx
68
+ │ │ ├── card.tsx
69
+ │ │ ├── input.tsx
70
+ │ │ ├── modal.tsx
71
+ │ │ └── ...
72
+ │ └── features/ # 機能別コンポーネント
73
+ │ ├── posts/
74
+ │ │ ├── PostList.tsx
75
+ │ │ ├── PostCard.tsx
76
+ │ │ ├── PostCreateForm.tsx
77
+ │ │ └── PostDetail.tsx
78
+ │ └── auth/
79
+ │ ├── LoginForm.tsx
80
+ │ └── RegisterForm.tsx
81
+ ├── lib/ # ユーティリティ
82
+ │ ├── api-client.ts # Hono Client設定
83
+ │ ├── query-client.ts # Tanstack Query設定
84
+ │ └── utils.ts # 共通ユーティリティ
85
+ ├── hooks/ # カスタムフック
86
+ │ ├── use-posts.ts # Postデータフック
87
+ │ └── use-auth.ts # 認証フック
88
+ ├── public/ # 静的ファイル
89
+ ├── styles/ # グローバルスタイル
90
+ │ └── globals.css
91
+ ├── next.config.js
92
+ ├── package.json
93
+ ├── tsconfig.json
94
+ └── tailwind.config.ts
95
+ ```
96
+
97
+ ### 管理画面(apps/admin)
98
+
99
+ ```
100
+ apps/admin/
101
+ ├── app/
102
+ │ ├── (protected)/ # ルートグループ: 認証必須
103
+ │ │ ├── admin/
104
+ │ │ │ ├── users/
105
+ │ │ │ │ └── page.tsx
106
+ │ │ │ ├── posts/
107
+ │ │ │ │ └── page.tsx
108
+ │ │ │ └── analytics/
109
+ │ │ │ └── page.tsx
110
+ │ ├── api/
111
+ │ │ └── [[...route]]/
112
+ │ │ └── route.ts
113
+ │ ├── layout.tsx
114
+ │ └── page.tsx
115
+ ├── components/
116
+ │ ├── ui/
117
+ │ └── features/
118
+ │ └── admin/
119
+ │ ├── UserTable.tsx
120
+ │ └── PostStatusManager.tsx
121
+ ├── lib/
122
+ ├── hooks/
123
+ └── ...
124
+ ```
125
+
126
+ **設計ポイント**:
127
+ - **ルートグループ**: `(auth)`, `(dashboard)`, `(protected)` でルートを論理的にグループ化
128
+ - **コロケーション**: 機能別にコンポーネント、フック、ユーティリティを配置
129
+ - **共通コンポーネント**: ui/に再利用可能な基本コンポーネント、features/に機能別コンポーネント
130
+
131
+ ---
132
+
133
+ ## 2. 技術スタック
134
+
135
+ | カテゴリ | ライブラリ | バージョン | 用途 |
136
+ |---------|-----------|----------|------|
137
+ | フレームワーク | Next.js | 14.x | App Router、SSR/SSG |
138
+ | UI | React | 18.x | コンポーネントライブラリ |
139
+ | 状態管理 | Tanstack Query | 5.x | サーバー状態管理 |
140
+ | フォーム | React Hook Form | 7.x | フォーム処理 |
141
+ | バリデーション | Zod | 3.x | スキーマバリデーション |
142
+ | API Client | Hono Client | 4.x | 型安全なAPI呼び出し |
143
+ | スタイリング | Tailwind CSS | 3.x | ユーティリティファースト |
144
+ | 型チェック | TypeScript | 5.x | 静的型付け |
145
+
146
+ ---
147
+
148
+ ## 3. Hono Client統合(API呼び出し)
149
+
150
+ ### セットアップ
151
+
152
+ **Hono Clientの初期化**:
153
+
154
+ ```typescript
155
+ // apps/web/lib/api-client.ts
156
+ import { hc } from 'hono/client'
157
+ import type { AppType } from '@/app/api/[[...route]]/route'
158
+
159
+ export const apiClient = hc<AppType>('/api')
160
+ ```
161
+
162
+ **型定義のエクスポート**:
163
+
164
+ ```typescript
165
+ // apps/web/app/api/[[...route]]/route.ts
166
+ import { Hono } from 'hono'
167
+ import { handle } from 'hono/vercel'
168
+ import postRoutes from '@/server/routes/postRoutes'
169
+
170
+ const app = new Hono().basePath('/api')
171
+
172
+ app.route('/', postRoutes)
173
+
174
+ export const GET = handle(app)
175
+ export const POST = handle(app)
176
+ export const PUT = handle(app)
177
+ export const DELETE = handle(app)
178
+
179
+ // 型のエクスポート(フロントエンドで使用)
180
+ export type AppType = typeof app
181
+ ```
182
+
183
+ ### API呼び出しパターン
184
+
185
+ **GET リクエスト**:
186
+
187
+ ```typescript
188
+ // 投稿一覧取得
189
+ const response = await apiClient.posts.$get({
190
+ query: { page: '1', limit: '10' }
191
+ })
192
+ const data = await response.json() // 型推論: { posts: Post[], total: number }
193
+ ```
194
+
195
+ **POST リクエスト**:
196
+
197
+ ```typescript
198
+ // 投稿作成
199
+ const response = await apiClient.posts.$post({
200
+ json: { title: 'New Post', content: 'Content' }
201
+ })
202
+ const data = await response.json() // 型推論: { post: Post }
203
+ ```
204
+
205
+ **GET リクエスト(パスパラメータ)**:
206
+
207
+ ```typescript
208
+ // 投稿詳細取得
209
+ const response = await apiClient.posts[':id'].$get({
210
+ param: { id: '123' }
211
+ })
212
+ const data = await response.json() // 型推論: { post: Post }
213
+ ```
214
+
215
+ **PUT リクエスト**:
216
+
217
+ ```typescript
218
+ // 投稿更新
219
+ const response = await apiClient.posts[':id'].$put({
220
+ param: { id: '123' },
221
+ json: { title: 'Updated Title' }
222
+ })
223
+ const data = await response.json() // 型推論: { post: Post }
224
+ ```
225
+
226
+ **DELETE リクエスト**:
227
+
228
+ ```typescript
229
+ // 投稿削除
230
+ const response = await apiClient.posts[':id'].$delete({
231
+ param: { id: '123' }
232
+ })
233
+ const data = await response.json() // 型推論: { success: true }
234
+ ```
235
+
236
+ **エンドツーエンド型推論のメリット**:
237
+ - バックエンドのAPI変更が自動的にフロントエンドに反映
238
+ - 型エラーでAPI仕様の不一致を早期発見
239
+ - IDEの補完機能でAPI仕様を確認可能
240
+
241
+ ---
242
+
243
+ ## 4. Server ComponentとClient Component
244
+
245
+ ### 🚨 基本原則(最重要)
246
+
247
+ このプロジェクトでは、以下の原則を**必ず守ってください**:
248
+
249
+ ✅ **可能な限りServer Componentを使用する**
250
+ - すべてのコンポーネントはデフォルトでServer Component
251
+ - インタラクティブ性が必要な部分のみClient Component化
252
+
253
+ ❌ **page.tsxでの`'use client'`使用は禁止**
254
+ - ページコンポーネント(`app/**/page.tsx`)はServer Componentとして実装
255
+ - データフェッチ、認証チェックはServer Componentで実行
256
+ - インタラクティブな部分は別コンポーネントに分離してClient Component化
257
+
258
+ ### Server ComponentとClient Componentの違い
259
+
260
+ #### Server Component(デフォルト)
261
+
262
+ **特徴**:
263
+ - サーバー側でのみレンダリング
264
+ - JavaScriptバンドルに含まれない
265
+ - データベースやAPIに直接アクセス可能
266
+ - 機密情報(APIキー、トークン)を安全に扱える
267
+
268
+ **制限**:
269
+ - `useState`, `useEffect`などのReactフックは使用不可
270
+ - ブラウザAPI(`window`, `document`)は使用不可
271
+ - イベントハンドラー(`onClick`等)は使用不可
272
+
273
+ **例**:
274
+ ```typescript
275
+ // app/posts/page.tsx (Server Component - デフォルト)
276
+ import { PostList } from '@/components/features/posts/PostList'
277
+ import { apiClient } from '@/lib/api-client'
278
+
279
+ export default async function PostsPage() {
280
+ // サーバー側でデータフェッチ
281
+ const response = await apiClient.posts.$get({
282
+ query: { page: '1', limit: '10' }
283
+ })
284
+ const data = await response.json()
285
+
286
+ return (
287
+ <div>
288
+ <h1>投稿一覧</h1>
289
+ <PostList initialData={data} /> {/* Client Componentにデータを渡す */}
290
+ </div>
291
+ )
292
+ }
293
+ ```
294
+
295
+ #### Client Component
296
+
297
+ **特徴**:
298
+ - `'use client'`ディレクティブで明示的に宣言
299
+ - サーバー側でプリレンダリング後、クライアント側でハイドレーション
300
+ - Reactフック、イベントハンドラー使用可能
301
+ - JavaScriptバンドルに含まれる
302
+
303
+ **用途**:
304
+ - インタラクティブ性(ボタンクリック、入力処理)
305
+ - ステート管理(`useState`, `useReducer`)
306
+ - ライフサイクル(`useEffect`, `useLayoutEffect`)
307
+ - カスタムフック(`useQuery`, `useForm`)
308
+ - ブラウザAPI(`localStorage`, `window`)
309
+
310
+ **例**:
311
+ ```typescript
312
+ // components/features/posts/PostList.tsx (Client Component)
313
+ 'use client'
314
+
315
+ import { useState } from 'react'
316
+ import { PostCard } from './PostCard'
317
+ import type { Post } from '@repo/server-core/domain/entities/Post'
318
+
319
+ interface PostListProps {
320
+ initialData: { posts: Post[], total: number }
321
+ }
322
+
323
+ export function PostList({ initialData }: PostListProps) {
324
+ const [posts, setPosts] = useState(initialData.posts)
325
+
326
+ const handleSort = (field: string) => {
327
+ // ソート処理
328
+ const sorted = [...posts].sort(/* ... */)
329
+ setPosts(sorted)
330
+ }
331
+
332
+ return (
333
+ <div>
334
+ <button onClick={() => handleSort('title')}>タイトルでソート</button>
335
+ {posts.map(post => <PostCard key={post.id} post={post} />)}
336
+ </div>
337
+ )
338
+ }
339
+ ```
340
+
341
+ ### 判断フローチャート
342
+
343
+ コンポーネント作成時の判断基準:
344
+
345
+ ```
346
+ 新しいコンポーネントを作成
347
+
348
+ 質問1: page.tsxか?
349
+ YES → Server Component(絶対)
350
+ NO → 質問2へ
351
+
352
+ 質問2: イベントハンドラー(onClick等)が必要か?
353
+ YES → Client Component
354
+ NO → 質問3へ
355
+
356
+ 質問3: Reactフック(useState, useEffect等)が必要か?
357
+ YES → Client Component
358
+ NO → 質問4へ
359
+
360
+ 質問4: ブラウザAPI(window, localStorage等)が必要か?
361
+ YES → Client Component
362
+ NO → 質問5へ
363
+
364
+ 質問5: カスタムフック(useQuery, useForm等)が必要か?
365
+ YES → Client Component
366
+ NO → Server Component(デフォルト)
367
+ ```
368
+
369
+ ### `'use client'`境界の最適化
370
+
371
+ #### ❌ 非推奨パターン
372
+
373
+ ```typescript
374
+ // ❌ page.tsx全体をClient Component化(禁止)
375
+ 'use client'
376
+
377
+ import { useState } from 'react'
378
+
379
+ export default function PostsPage() {
380
+ const [filter, setFilter] = useState('')
381
+
382
+ return (
383
+ <div>
384
+ <Header /> {/* 静的 */}
385
+ <Sidebar /> {/* 静的 */}
386
+ <FilterInput value={filter} onChange={setFilter} /> {/* インタラクティブ */}
387
+ <PostList filter={filter} />
388
+ </div>
389
+ )
390
+ }
391
+ ```
392
+
393
+ **問題点**:
394
+ - ページ全体がJavaScriptバンドルに含まれる
395
+ - バンドルサイズが肥大化
396
+ - First Contentful Paintが遅くなる
397
+ - Server Componentのメリットを失う
398
+
399
+ #### ✅ 推奨パターン
400
+
401
+ ```typescript
402
+ // ✅ page.tsxはServer Component('use client'なし)
403
+ import { Header } from '@/components/features/Header'
404
+ import { Sidebar } from '@/components/features/Sidebar'
405
+ import { PostListContainer } from '@/components/features/posts/PostListContainer'
406
+ import { apiClient } from '@/lib/api-client'
407
+
408
+ export default async function PostsPage() {
409
+ // サーバー側でデータフェッチ
410
+ const response = await apiClient.posts.$get()
411
+ const data = await response.json()
412
+
413
+ return (
414
+ <div>
415
+ <Header /> {/* Server Component */}
416
+ <Sidebar /> {/* Server Component */}
417
+ <PostListContainer initialData={data} /> {/* Client Component */}
418
+ </div>
419
+ )
420
+ }
421
+ ```
422
+
423
+ ```typescript
424
+ // components/features/posts/PostListContainer.tsx
425
+ 'use client'
426
+
427
+ import { useState } from 'react'
428
+ import { FilterInput } from './FilterInput'
429
+ import { PostList } from './PostList'
430
+
431
+ export function PostListContainer({ initialData }) {
432
+ const [filter, setFilter] = useState('')
433
+
434
+ return (
435
+ <div>
436
+ <FilterInput value={filter} onChange={setFilter} />
437
+ <PostList data={initialData} filter={filter} />
438
+ </div>
439
+ )
440
+ }
441
+ ```
442
+
443
+ **メリット**:
444
+ - バンドルサイズの最小化
445
+ - 静的コンテンツは即座に表示
446
+ - インタラクティブ部分のみハイドレーション
447
+ - パフォーマンスの最適化
448
+
449
+ ### コンポーネント間のデータ受け渡しパターン
450
+
451
+ #### パターン1: Server Component → Client Component(props)
452
+
453
+ ```typescript
454
+ // app/posts/[id]/page.tsx (Server Component)
455
+ import { PostDetail } from '@/components/features/posts/PostDetail'
456
+ import { apiClient } from '@/lib/api-client'
457
+
458
+ export default async function PostDetailPage({ params }: { params: { id: string } }) {
459
+ // サーバー側でデータフェッチ
460
+ const response = await apiClient.posts[':id'].$get({
461
+ param: { id: params.id }
462
+ })
463
+ const { post } = await response.json()
464
+
465
+ // Client Componentにpropsとして渡す
466
+ return <PostDetail post={post} />
467
+ }
468
+ ```
469
+
470
+ ```typescript
471
+ // components/features/posts/PostDetail.tsx (Client Component)
472
+ 'use client'
473
+
474
+ import { useState } from 'react'
475
+ import type { Post } from '@repo/server-core/domain/entities/Post'
476
+
477
+ interface PostDetailProps {
478
+ post: Post // Server Componentから受け取ったデータ
479
+ }
480
+
481
+ export function PostDetail({ post }: PostDetailProps) {
482
+ const [isEditing, setIsEditing] = useState(false)
483
+
484
+ return (
485
+ <div>
486
+ <h1>{post.title}</h1>
487
+ <p>{post.content}</p>
488
+ <button onClick={() => setIsEditing(true)}>編集</button>
489
+ </div>
490
+ )
491
+ }
492
+ ```
493
+
494
+ #### パターン2: Server ComponentをClient Componentの子として渡す(children)
495
+
496
+ ```typescript
497
+ // app/posts/layout.tsx (Server Component)
498
+ import { PostSidebar } from '@/components/features/posts/PostSidebar'
499
+ import { PostContainer } from '@/components/features/posts/PostContainer'
500
+
501
+ export default function PostLayout({ children }: { children: React.ReactNode }) {
502
+ return (
503
+ <PostContainer>
504
+ <PostSidebar /> {/* Server Component */}
505
+ {children} {/* Server Component */}
506
+ </PostContainer>
507
+ )
508
+ }
509
+ ```
510
+
511
+ ```typescript
512
+ // components/features/posts/PostContainer.tsx (Client Component)
513
+ 'use client'
514
+
515
+ import { useState } from 'react'
516
+ import type { ReactNode } from 'react'
517
+
518
+ interface PostContainerProps {
519
+ children: ReactNode
520
+ }
521
+
522
+ export function PostContainer({ children }: PostContainerProps) {
523
+ const [sidebarOpen, setSidebarOpen] = useState(true)
524
+
525
+ return (
526
+ <div className={sidebarOpen ? 'with-sidebar' : 'no-sidebar'}>
527
+ <button onClick={() => setSidebarOpen(!sidebarOpen)}>
528
+ サイドバー切替
529
+ </button>
530
+ {children} {/* Server Componentが表示される */}
531
+ </div>
532
+ )
533
+ }
534
+ ```
535
+
536
+ ### パフォーマンス考慮事項
537
+
538
+ #### バンドルサイズへの影響
539
+
540
+ | コンポーネントタイプ | JavaScriptバンドル | 初期表示速度 | SEO |
541
+ |------------------|-----------------|------------|-----|
542
+ | Server Component | 含まれない | ⚡ 高速 | ✅ 優秀 |
543
+ | Client Component | 含まれる | 🐢 遅い | ⚠️ 要対策 |
544
+
545
+ #### コードスプリッティング戦略
546
+
547
+ ```typescript
548
+ // ✅ 複数のClient Componentエントリーポイントで分割
549
+ // components/features/posts/PostCreateForm.tsx
550
+ 'use client'
551
+ export function PostCreateForm() { /* ... */ }
552
+
553
+ // components/features/posts/PostList.tsx
554
+ 'use client'
555
+ export function PostList() { /* ... */ }
556
+
557
+ // components/features/posts/PostDetail.tsx
558
+ 'use client'
559
+ export function PostDetail() { /* ... */ }
560
+ ```
561
+
562
+ それぞれ独立したバンドルとなり、必要なページでのみロードされます。
563
+
564
+ #### 動的インポートの活用
565
+
566
+ 大きなClient Componentは動的インポートで遅延ロード:
567
+
568
+ ```typescript
569
+ // app/posts/page.tsx
570
+ import dynamic from 'next/dynamic'
571
+
572
+ const PostEditor = dynamic(
573
+ () => import('@/components/features/posts/PostEditor'),
574
+ {
575
+ loading: () => <div>エディタを読み込み中...</div>,
576
+ ssr: false // クライアント側のみでレンダリング
577
+ }
578
+ )
579
+
580
+ export default function PostsPage() {
581
+ return (
582
+ <div>
583
+ <h1>投稿編集</h1>
584
+ <PostEditor />
585
+ </div>
586
+ )
587
+ }
588
+ ```
589
+
590
+ ### チェックリスト
591
+
592
+ 新しいコンポーネントを作成する際のチェックリスト:
593
+
594
+ - [ ] **page.tsxか?** → YESなら必ずServer Component
595
+ - [ ] **インタラクティブ性が必要か?**(onClick, onChange等)
596
+ - [ ] **Reactフックを使用するか?**(useState, useEffect等)
597
+ - [ ] **ブラウザAPIを使用するか?**(window, localStorage等)
598
+ - [ ] **カスタムフックを使用するか?**(useQuery, useForm等)
599
+ - [ ] **バンドルサイズへの影響を考慮したか?**
600
+ - [ ] **`'use client'`境界を最小限に抑えたか?**
601
+ - [ ] **静的コンテンツをServer Componentとして分離したか?**
602
+
603
+ すべてNOならServer Component、1つでもYESならClient Componentを検討してください。
604
+
605
+ ---
606
+
607
+ ## 5. Tanstack Query(サーバー状態管理)
608
+
609
+ > **⚠️ 重要**: Tanstack Query(`useQuery`, `useMutation`等)は**Client Componentでのみ使用できます**。詳細は[4. Server ComponentとClient Component](#4-server-componentとclient-component)を参照してください。
610
+
611
+ ### QueryClientの設定
612
+
613
+ ```typescript
614
+ // apps/web/lib/query-client.ts
615
+ import { QueryClient } from '@tanstack/react-query'
616
+
617
+ export const queryClient = new QueryClient({
618
+ defaultOptions: {
619
+ queries: {
620
+ staleTime: 1000 * 60 * 5, // 5分間キャッシュを新鮮とみなす
621
+ cacheTime: 1000 * 60 * 10, // 10分間キャッシュを保持
622
+ refetchOnWindowFocus: false, // ウィンドウフォーカス時の再取得を無効化
623
+ retry: 1, // 失敗時1回リトライ
624
+ },
625
+ },
626
+ })
627
+ ```
628
+
629
+ **Providerの設定**:
630
+
631
+ ```typescript
632
+ // apps/web/app/layout.tsx
633
+ 'use client'
634
+
635
+ import { QueryClientProvider } from '@tanstack/react-query'
636
+ import { queryClient } from '@/lib/query-client'
637
+
638
+ export default function RootLayout({ children }) {
639
+ return (
640
+ <html lang="ja">
641
+ <body>
642
+ <QueryClientProvider client={queryClient}>
643
+ {children}
644
+ </QueryClientProvider>
645
+ </body>
646
+ </html>
647
+ )
648
+ }
649
+ ```
650
+
651
+ ### useQuery - データ取得
652
+
653
+ **基本パターン**:
654
+
655
+ ```typescript
656
+ import { useQuery } from '@tanstack/react-query'
657
+ import { apiClient } from '@/lib/api-client'
658
+
659
+ export function usePostList(page: number, limit: number) {
660
+ return useQuery({
661
+ queryKey: ['posts', page, limit], // キャッシュキー
662
+ queryFn: async () => {
663
+ const response = await apiClient.posts.$get({
664
+ query: { page: String(page), limit: String(limit) }
665
+ })
666
+ if (!response.ok) {
667
+ throw new Error('Failed to fetch posts')
668
+ }
669
+ return response.json()
670
+ },
671
+ enabled: true, // 自動実行を有効化
672
+ })
673
+ }
674
+ ```
675
+
676
+ **コンポーネントでの使用**:
677
+
678
+ ```typescript
679
+ export function PostList() {
680
+ const { data, isLoading, error } = usePostList(1, 10)
681
+
682
+ if (isLoading) return <div>読み込み中...</div>
683
+ if (error) return <div>エラー: {error.message}</div>
684
+
685
+ return (
686
+ <div>
687
+ {data.posts.map(post => (
688
+ <PostCard key={post.id} post={post} />
689
+ ))}
690
+ </div>
691
+ )
692
+ }
693
+ ```
694
+
695
+ ### useMutation - データ更新
696
+
697
+ **基本パターン**:
698
+
699
+ ```typescript
700
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
701
+ import { apiClient } from '@/lib/api-client'
702
+ import type { CreatePostInput } from '@repo/server-core/domain/validators/post'
703
+
704
+ export function useCreatePost() {
705
+ const queryClient = useQueryClient()
706
+
707
+ return useMutation({
708
+ mutationFn: async (data: CreatePostInput) => {
709
+ const response = await apiClient.posts.$post({ json: data })
710
+ if (!response.ok) {
711
+ throw new Error('Failed to create post')
712
+ }
713
+ return response.json()
714
+ },
715
+ onSuccess: () => {
716
+ // 投稿一覧のキャッシュを無効化(再取得をトリガー)
717
+ queryClient.invalidateQueries({ queryKey: ['posts'] })
718
+ },
719
+ onError: (error) => {
720
+ console.error('投稿作成エラー:', error)
721
+ },
722
+ })
723
+ }
724
+ ```
725
+
726
+ **コンポーネントでの使用**:
727
+
728
+ ```typescript
729
+ export function PostCreateButton() {
730
+ const createPost = useCreatePost()
731
+
732
+ const handleCreate = () => {
733
+ createPost.mutate({
734
+ title: 'New Post',
735
+ content: 'Content',
736
+ status: 'draft',
737
+ })
738
+ }
739
+
740
+ return (
741
+ <button onClick={handleCreate} disabled={createPost.isPending}>
742
+ {createPost.isPending ? '作成中...' : '投稿を作成'}
743
+ </button>
744
+ )
745
+ }
746
+ ```
747
+
748
+ ### QueryKeyパターン
749
+
750
+ **推奨されるキー構造**:
751
+
752
+ ```typescript
753
+ // 投稿一覧
754
+ ['posts'] // すべての投稿
755
+ ['posts', { page: 1, limit: 10 }] // ページング付き投稿一覧
756
+
757
+ // 投稿詳細
758
+ ['posts', postId] // 特定の投稿
759
+
760
+ // ユーザー関連
761
+ ['users'] // ユーザー一覧
762
+ ['users', userId] // 特定のユーザー
763
+ ['users', userId, 'posts'] // 特定ユーザーの投稿
764
+ ```
765
+
766
+ **キャッシュ無効化のパターン**:
767
+
768
+ ```typescript
769
+ // 特定のクエリのみ無効化
770
+ queryClient.invalidateQueries({ queryKey: ['posts', postId] })
771
+
772
+ // プレフィックスマッチで無効化
773
+ queryClient.invalidateQueries({ queryKey: ['posts'] }) // ['posts', ...] すべて無効化
774
+
775
+ // 完全一致で無効化
776
+ queryClient.invalidateQueries({ queryKey: ['posts'], exact: true })
777
+ ```
778
+
779
+ ---
780
+
781
+ ## 6. React Hook Form(フォーム処理)
782
+
783
+ > **⚠️ 重要**: React Hook Form(`useForm`, `Controller`等)は**Client Componentでのみ使用できます**。詳細は[4. Server ComponentとClient Component](#4-server-componentとclient-component)を参照してください。
784
+
785
+ ### Zodスキーマとの統合
786
+
787
+ **Zodスキーマの定義** (共有スキーマを使用):
788
+
789
+ ```typescript
790
+ // @repo/server-core/domain/validators/post.ts
791
+ import { z } from 'zod'
792
+
793
+ export const createPostSchema = z.object({
794
+ title: z.string().min(1, 'タイトルは必須です').max(200, 'タイトルは200文字以内です'),
795
+ content: z.string().min(1, '本文は必須です'),
796
+ status: z.enum(['draft', 'published']).default('draft'),
797
+ })
798
+
799
+ export type CreatePostInput = z.infer<typeof createPostSchema>
800
+ ```
801
+
802
+ ### フォームの実装
803
+
804
+ ```typescript
805
+ import { useForm } from 'react-hook-form'
806
+ import { zodResolver } from '@hookform/resolvers/zod'
807
+ import { createPostSchema, type CreatePostInput } from '@repo/server-core/domain/validators/post'
808
+ import { useCreatePost } from '@/hooks/use-posts'
809
+
810
+ export function PostCreateForm() {
811
+ const {
812
+ register,
813
+ handleSubmit,
814
+ formState: { errors, isSubmitting },
815
+ reset,
816
+ } = useForm<CreatePostInput>({
817
+ resolver: zodResolver(createPostSchema),
818
+ defaultValues: {
819
+ status: 'draft',
820
+ },
821
+ })
822
+
823
+ const createPost = useCreatePost()
824
+
825
+ const onSubmit = async (data: CreatePostInput) => {
826
+ try {
827
+ await createPost.mutateAsync(data)
828
+ alert('投稿を作成しました')
829
+ reset() // フォームをリセット
830
+ } catch (error) {
831
+ console.error('投稿作成エラー:', error)
832
+ }
833
+ }
834
+
835
+ return (
836
+ <form onSubmit={handleSubmit(onSubmit)}>
837
+ <div>
838
+ <label>タイトル</label>
839
+ <input {...register('title')} />
840
+ {errors.title && <p className="error">{errors.title.message}</p>}
841
+ </div>
842
+
843
+ <div>
844
+ <label>本文</label>
845
+ <textarea {...register('content')} />
846
+ {errors.content && <p className="error">{errors.content.message}</p>}
847
+ </div>
848
+
849
+ <div>
850
+ <label>ステータス</label>
851
+ <select {...register('status')}>
852
+ <option value="draft">下書き</option>
853
+ <option value="published">公開</option>
854
+ </select>
855
+ </div>
856
+
857
+ <button type="submit" disabled={isSubmitting || createPost.isPending}>
858
+ {isSubmitting || createPost.isPending ? '作成中...' : '投稿を作成'}
859
+ </button>
860
+ </form>
861
+ )
862
+ }
863
+ ```
864
+
865
+ **設計ポイント**:
866
+ - **zodResolver**: Zodスキーマとの統合によりバリデーションロジックを一元化
867
+ - **型安全性**: `CreatePostInput`型でフォームデータの型を保証
868
+ - **エラー表示**: `formState.errors`でリアルタイムエラー表示
869
+ - **defaultValues**: デフォルト値の設定
870
+ - **reset()**: 送信成功後にフォームをクリア
871
+
872
+ ---
873
+
874
+ ## 7. コンポーネント設計
875
+
876
+ ### UIコンポーネント(components/ui/)
877
+
878
+ **基本コンポーネントの役割**:
879
+ - 再利用可能な汎用UIパーツ
880
+ - プロジェクト全体で使用
881
+ - ビジネスロジックを含まない
882
+
883
+ **例**:
884
+
885
+ ```typescript
886
+ // components/ui/button.tsx
887
+ import type { ReactNode } from 'react'
888
+
889
+ interface ButtonProps {
890
+ children: ReactNode
891
+ onClick?: () => void
892
+ disabled?: boolean
893
+ variant?: 'primary' | 'secondary' | 'danger'
894
+ }
895
+
896
+ export function Button({ children, onClick, disabled, variant = 'primary' }: ButtonProps) {
897
+ const baseStyles = 'px-4 py-2 rounded font-medium'
898
+ const variantStyles = {
899
+ primary: 'bg-blue-500 text-white hover:bg-blue-600',
900
+ secondary: 'bg-gray-500 text-white hover:bg-gray-600',
901
+ danger: 'bg-red-500 text-white hover:bg-red-600',
902
+ }
903
+
904
+ return (
905
+ <button
906
+ onClick={onClick}
907
+ disabled={disabled}
908
+ className={`${baseStyles} ${variantStyles[variant]} disabled:opacity-50`}
909
+ >
910
+ {children}
911
+ </button>
912
+ )
913
+ }
914
+ ```
915
+
916
+ ### 機能別コンポーネント(components/features/)
917
+
918
+ **機能別コンポーネントの役割**:
919
+ - 特定の機能に特化したコンポーネント
920
+ - ビジネスロジックを含む
921
+ - UIコンポーネントを組み合わせて構築
922
+
923
+ **例**:
924
+
925
+ ```typescript
926
+ // components/features/posts/PostCard.tsx
927
+ import Link from 'next/link'
928
+ import { Card } from '@/components/ui/card'
929
+ import { Button } from '@/components/ui/button'
930
+ import type { Post } from '@repo/server-core/domain/entities/Post'
931
+
932
+ interface PostCardProps {
933
+ post: Post
934
+ }
935
+
936
+ export function PostCard({ post }: PostCardProps) {
937
+ return (
938
+ <Card>
939
+ <h3>{post.title}</h3>
940
+ <p>{post.content.substring(0, 100)}...</p>
941
+ <div className="meta">
942
+ <span>{post.status}</span>
943
+ <span>{new Date(post.createdAt).toLocaleDateString()}</span>
944
+ </div>
945
+ <Link href={`/posts/${post.id}`}>
946
+ <Button variant="primary">詳細を見る</Button>
947
+ </Link>
948
+ </Card>
949
+ )
950
+ }
951
+ ```
952
+
953
+ ### カスタムフック(hooks/)
954
+
955
+ **カスタムフックの役割**:
956
+ - データ取得ロジックの抽象化
957
+ - 状態管理ロジックの再利用
958
+ - コンポーネントをシンプルに保つ
959
+
960
+ **例**:
961
+
962
+ ```typescript
963
+ // hooks/use-posts.ts
964
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
965
+ import { apiClient } from '@/lib/api-client'
966
+ import type { CreatePostInput, UpdatePostInput } from '@repo/server-core/domain/validators/post'
967
+
968
+ // 投稿一覧取得
969
+ export function usePostList(page: number, limit: number) {
970
+ return useQuery({
971
+ queryKey: ['posts', page, limit],
972
+ queryFn: async () => {
973
+ const response = await apiClient.posts.$get({
974
+ query: { page: String(page), limit: String(limit) }
975
+ })
976
+ return response.json()
977
+ },
978
+ })
979
+ }
980
+
981
+ // 投稿詳細取得
982
+ export function usePost(id: string) {
983
+ return useQuery({
984
+ queryKey: ['posts', id],
985
+ queryFn: async () => {
986
+ const response = await apiClient.posts[':id'].$get({ param: { id } })
987
+ return response.json()
988
+ },
989
+ })
990
+ }
991
+
992
+ // 投稿作成
993
+ export function useCreatePost() {
994
+ const queryClient = useQueryClient()
995
+
996
+ return useMutation({
997
+ mutationFn: async (data: CreatePostInput) => {
998
+ const response = await apiClient.posts.$post({ json: data })
999
+ return response.json()
1000
+ },
1001
+ onSuccess: () => {
1002
+ queryClient.invalidateQueries({ queryKey: ['posts'] })
1003
+ },
1004
+ })
1005
+ }
1006
+
1007
+ // 投稿更新
1008
+ export function useUpdatePost(id: string) {
1009
+ const queryClient = useQueryClient()
1010
+
1011
+ return useMutation({
1012
+ mutationFn: async (data: UpdatePostInput) => {
1013
+ const response = await apiClient.posts[':id'].$put({
1014
+ param: { id },
1015
+ json: data
1016
+ })
1017
+ return response.json()
1018
+ },
1019
+ onSuccess: () => {
1020
+ queryClient.invalidateQueries({ queryKey: ['posts', id] })
1021
+ queryClient.invalidateQueries({ queryKey: ['posts'] })
1022
+ },
1023
+ })
1024
+ }
1025
+
1026
+ // 投稿削除
1027
+ export function useDeletePost() {
1028
+ const queryClient = useQueryClient()
1029
+
1030
+ return useMutation({
1031
+ mutationFn: async (id: string) => {
1032
+ const response = await apiClient.posts[':id'].$delete({ param: { id } })
1033
+ return response.json()
1034
+ },
1035
+ onSuccess: () => {
1036
+ queryClient.invalidateQueries({ queryKey: ['posts'] })
1037
+ },
1038
+ })
1039
+ }
1040
+ ```
1041
+
1042
+ ---
1043
+
1044
+ ## 8. App Router構成
1045
+
1046
+ ### ルートグループ
1047
+
1048
+ **認証グループ** (`(auth)/`):
1049
+ - 認証関連のページをグループ化
1050
+ - URLパスに影響しない(`/login`となり、`/(auth)/login`とはならない)
1051
+ - 共通のレイアウトを適用可能
1052
+
1053
+ **ダッシュボードグループ** (`(dashboard)/`):
1054
+ - ログイン後のページをグループ化
1055
+ - 認証チェックミドルウェアを適用
1056
+
1057
+ ### レイアウト
1058
+
1059
+ **ルートレイアウト** (`app/layout.tsx`):
1060
+
1061
+ ```typescript
1062
+ import type { Metadata } from 'next'
1063
+ import { QueryClientProvider } from '@tanstack/react-query'
1064
+ import { queryClient } from '@/lib/query-client'
1065
+ import './globals.css'
1066
+
1067
+ export const metadata: Metadata = {
1068
+ title: 'プロジェクト名',
1069
+ description: 'プロジェクトの説明',
1070
+ }
1071
+
1072
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
1073
+ return (
1074
+ <html lang="ja">
1075
+ <body>
1076
+ <QueryClientProvider client={queryClient}>
1077
+ {children}
1078
+ </QueryClientProvider>
1079
+ </body>
1080
+ </html>
1081
+ )
1082
+ }
1083
+ ```
1084
+
1085
+ **グループレイアウト** (`app/(dashboard)/layout.tsx`):
1086
+
1087
+ ```typescript
1088
+ import { Header } from '@/components/features/Header'
1089
+ import { Sidebar } from '@/components/features/Sidebar'
1090
+
1091
+ export default function DashboardLayout({ children }: { children: React.ReactNode }) {
1092
+ return (
1093
+ <div className="dashboard">
1094
+ <Header />
1095
+ <div className="flex">
1096
+ <Sidebar />
1097
+ <main className="flex-1">{children}</main>
1098
+ </div>
1099
+ </div>
1100
+ )
1101
+ }
1102
+ ```
1103
+
1104
+ ### ページコンポーネント
1105
+
1106
+ **一覧ページ** (`app/posts/page.tsx`):
1107
+
1108
+ ```typescript
1109
+ // ✅ page.tsxはServer Component('use client'なし)
1110
+ import { PostListContainer } from '@/components/features/posts/PostListContainer'
1111
+ import { Button } from '@/components/ui/button'
1112
+ import { apiClient } from '@/lib/api-client'
1113
+ import Link from 'next/link'
1114
+
1115
+ export default async function PostsPage() {
1116
+ // サーバー側でデータフェッチ
1117
+ const response = await apiClient.posts.$get({
1118
+ query: { page: '1', limit: '10' }
1119
+ })
1120
+ const data = await response.json()
1121
+
1122
+ return (
1123
+ <div>
1124
+ <div className="header">
1125
+ <h1>投稿一覧</h1>
1126
+ <Link href="/posts/new">
1127
+ <Button variant="primary">新規作成</Button>
1128
+ </Link>
1129
+ </div>
1130
+
1131
+ {/* Client Componentにデータを渡す */}
1132
+ <PostListContainer initialData={data} />
1133
+ </div>
1134
+ )
1135
+ }
1136
+ ```
1137
+
1138
+ ```typescript
1139
+ // components/features/posts/PostListContainer.tsx (Client Component)
1140
+ 'use client'
1141
+
1142
+ import { PostCard } from '@/components/features/posts/PostCard'
1143
+ import type { Post } from '@repo/server-core/domain/entities/Post'
1144
+
1145
+ interface PostListContainerProps {
1146
+ initialData: { posts: Post[], total: number }
1147
+ }
1148
+
1149
+ export function PostListContainer({ initialData }: PostListContainerProps) {
1150
+ return (
1151
+ <div className="grid">
1152
+ {initialData.posts.map(post => (
1153
+ <PostCard key={post.id} post={post} />
1154
+ ))}
1155
+ </div>
1156
+ )
1157
+ }
1158
+ ```
1159
+
1160
+ **作成ページ** (`app/posts/new/page.tsx`):
1161
+
1162
+ ```typescript
1163
+ // ✅ page.tsxはServer Component('use client'なし)
1164
+ import { PostCreateForm } from '@/components/features/posts/PostCreateForm'
1165
+
1166
+ export default function PostNewPage() {
1167
+ return (
1168
+ <div>
1169
+ <h1>新規投稿作成</h1>
1170
+ {/* PostCreateFormはClient Component */}
1171
+ <PostCreateForm />
1172
+ </div>
1173
+ )
1174
+ }
1175
+ ```
1176
+
1177
+ **詳細ページ** (`app/posts/[id]/page.tsx`):
1178
+
1179
+ ```typescript
1180
+ // ✅ page.tsxはServer Component('use client'なし)
1181
+ import { PostDetail } from '@/components/features/posts/PostDetail'
1182
+ import { apiClient } from '@/lib/api-client'
1183
+
1184
+ export default async function PostDetailPage({ params }: { params: { id: string } }) {
1185
+ // サーバー側でデータフェッチ
1186
+ const response = await apiClient.posts[':id'].$get({
1187
+ param: { id: params.id }
1188
+ })
1189
+ const { post } = await response.json()
1190
+
1191
+ // Client Componentにデータを渡す
1192
+ return <PostDetail post={post} />
1193
+ }
1194
+ ```
1195
+
1196
+ ---
1197
+
1198
+ ## 9. 状態管理戦略
1199
+
1200
+ ### サーバー状態 vs クライアント状態
1201
+
1202
+ **サーバー状態**(Tanstack Queryで管理):
1203
+ - APIから取得したデータ
1204
+ - キャッシュ、再取得、無効化が必要
1205
+ - 例: 投稿一覧、ユーザー情報
1206
+
1207
+ **クライアント状態**(Reactの状態管理で管理):
1208
+ - UIの状態(モーダルの開閉、タブの選択など)
1209
+ - フォームの入力値(未送信)
1210
+ - 例: サイドバーの開閉状態、テーマ設定
1211
+
1212
+ ### 状態管理の選択基準
1213
+
1214
+ | データの種類 | 管理方法 | ツール |
1215
+ |-------------|---------|--------|
1216
+ | サーバーから取得したデータ | Tanstack Query | useQuery, useMutation |
1217
+ | フォームの入力値 | React Hook Form | useForm |
1218
+ | UIの状態(ローカル) | useState | useState |
1219
+ | UIの状態(グローバル) | Context API | useContext, createContext |
1220
+ | 認証状態 | Context API + Tanstack Query | 組み合わせ |
1221
+
1222
+ ---
1223
+
1224
+ ## 10. エラーハンドリング
1225
+
1226
+ ### API エラーハンドリング
1227
+
1228
+ ```typescript
1229
+ export function usePostList(page: number, limit: number) {
1230
+ return useQuery({
1231
+ queryKey: ['posts', page, limit],
1232
+ queryFn: async () => {
1233
+ const response = await apiClient.posts.$get({
1234
+ query: { page: String(page), limit: String(limit) }
1235
+ })
1236
+
1237
+ if (!response.ok) {
1238
+ const error = await response.json()
1239
+ throw new Error(error.error?.message || 'データの取得に失敗しました')
1240
+ }
1241
+
1242
+ return response.json()
1243
+ },
1244
+ retry: (failureCount, error) => {
1245
+ // 4xx エラーはリトライしない
1246
+ if (error.message.includes('401') || error.message.includes('404')) {
1247
+ return false
1248
+ }
1249
+ return failureCount < 2
1250
+ },
1251
+ })
1252
+ }
1253
+ ```
1254
+
1255
+ ### エラー境界(Error Boundary)
1256
+
1257
+ ```typescript
1258
+ // components/ErrorBoundary.tsx
1259
+ 'use client'
1260
+
1261
+ import { Component, type ReactNode } from 'react'
1262
+
1263
+ interface Props {
1264
+ children: ReactNode
1265
+ }
1266
+
1267
+ interface State {
1268
+ hasError: boolean
1269
+ error?: Error
1270
+ }
1271
+
1272
+ export class ErrorBoundary extends Component<Props, State> {
1273
+ constructor(props: Props) {
1274
+ super(props)
1275
+ this.state = { hasError: false }
1276
+ }
1277
+
1278
+ static getDerivedStateFromError(error: Error) {
1279
+ return { hasError: true, error }
1280
+ }
1281
+
1282
+ render() {
1283
+ if (this.state.hasError) {
1284
+ return (
1285
+ <div className="error-boundary">
1286
+ <h2>エラーが発生しました</h2>
1287
+ <p>{this.state.error?.message}</p>
1288
+ <button onClick={() => window.location.reload()}>
1289
+ ページを再読み込み
1290
+ </button>
1291
+ </div>
1292
+ )
1293
+ }
1294
+
1295
+ return this.props.children
1296
+ }
1297
+ }
1298
+ ```
1299
+
1300
+ ---
1301
+
1302
+ ## 11. 実装例
1303
+
1304
+ ### 完全な投稿一覧ページ実装
1305
+
1306
+ ```typescript
1307
+ // app/posts/page.tsx
1308
+ // ✅ page.tsxはServer Component('use client'なし)
1309
+ import { PostListWithPagination } from '@/components/features/posts/PostListWithPagination'
1310
+ import { Button } from '@/components/ui/button'
1311
+ import { apiClient } from '@/lib/api-client'
1312
+ import Link from 'next/link'
1313
+
1314
+ interface PostsPageProps {
1315
+ searchParams: { page?: string }
1316
+ }
1317
+
1318
+ export default async function PostsPage({ searchParams }: PostsPageProps) {
1319
+ const page = Number(searchParams.page) || 1
1320
+ const limit = 10
1321
+
1322
+ // サーバー側でデータフェッチ
1323
+ const response = await apiClient.posts.$get({
1324
+ query: { page: String(page), limit: String(limit) }
1325
+ })
1326
+ const data = await response.json()
1327
+
1328
+ return (
1329
+ <div className="container mx-auto py-8">
1330
+ <div className="flex justify-between items-center mb-8">
1331
+ <h1 className="text-3xl font-bold">投稿一覧</h1>
1332
+ <Link href="/posts/new">
1333
+ <Button variant="primary">新規作成</Button>
1334
+ </Link>
1335
+ </div>
1336
+
1337
+ {/* Client Componentにデータを渡す */}
1338
+ <PostListWithPagination
1339
+ initialData={data}
1340
+ currentPage={page}
1341
+ limit={limit}
1342
+ />
1343
+ </div>
1344
+ )
1345
+ }
1346
+ ```
1347
+
1348
+ ```typescript
1349
+ // components/features/posts/PostListWithPagination.tsx (Client Component)
1350
+ 'use client'
1351
+
1352
+ import { useRouter, usePathname } from 'next/navigation'
1353
+ import { PostCard } from '@/components/features/posts/PostCard'
1354
+ import { Button } from '@/components/ui/button'
1355
+ import type { Post } from '@repo/server-core/domain/entities/Post'
1356
+
1357
+ interface PostListWithPaginationProps {
1358
+ initialData: { posts: Post[], total: number }
1359
+ currentPage: number
1360
+ limit: number
1361
+ }
1362
+
1363
+ export function PostListWithPagination({
1364
+ initialData,
1365
+ currentPage,
1366
+ limit
1367
+ }: PostListWithPaginationProps) {
1368
+ const router = useRouter()
1369
+ const pathname = usePathname()
1370
+ const totalPages = Math.ceil(initialData.total / limit)
1371
+
1372
+ const handlePageChange = (newPage: number) => {
1373
+ router.push(`${pathname}?page=${newPage}`)
1374
+ }
1375
+
1376
+ return (
1377
+ <>
1378
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
1379
+ {initialData.posts.map(post => (
1380
+ <PostCard key={post.id} post={post} />
1381
+ ))}
1382
+ </div>
1383
+
1384
+ {totalPages > 1 && (
1385
+ <div className="flex justify-center gap-2 mt-8">
1386
+ <Button
1387
+ onClick={() => handlePageChange(Math.max(1, currentPage - 1))}
1388
+ disabled={currentPage === 1}
1389
+ >
1390
+ 前へ
1391
+ </Button>
1392
+ <span className="px-4 py-2">
1393
+ {currentPage} / {totalPages}
1394
+ </span>
1395
+ <Button
1396
+ onClick={() => handlePageChange(Math.min(totalPages, currentPage + 1))}
1397
+ disabled={currentPage === totalPages}
1398
+ >
1399
+ 次へ
1400
+ </Button>
1401
+ </div>
1402
+ )}
1403
+ </>
1404
+ )
1405
+ }
1406
+ ```
1407
+
1408
+ ### 完全な投稿作成フォーム実装
1409
+
1410
+ ```typescript
1411
+ // components/features/posts/PostCreateForm.tsx
1412
+ 'use client'
1413
+
1414
+ import { useForm } from 'react-hook-form'
1415
+ import { zodResolver } from '@hookform/resolvers/zod'
1416
+ import { useRouter } from 'next/navigation'
1417
+ import { useCreatePost } from '@/hooks/use-posts'
1418
+ import { createPostSchema, type CreatePostInput } from '@repo/server-core/domain/validators/post'
1419
+ import { Button } from '@/components/ui/button'
1420
+ import { Input } from '@/components/ui/input'
1421
+
1422
+ export function PostCreateForm() {
1423
+ const router = useRouter()
1424
+ const createPost = useCreatePost()
1425
+
1426
+ const {
1427
+ register,
1428
+ handleSubmit,
1429
+ formState: { errors, isSubmitting },
1430
+ } = useForm<CreatePostInput>({
1431
+ resolver: zodResolver(createPostSchema),
1432
+ defaultValues: {
1433
+ status: 'draft',
1434
+ },
1435
+ })
1436
+
1437
+ const onSubmit = async (data: CreatePostInput) => {
1438
+ try {
1439
+ await createPost.mutateAsync(data)
1440
+ alert('投稿を作成しました')
1441
+ router.push('/posts')
1442
+ } catch (error) {
1443
+ console.error('投稿作成エラー:', error)
1444
+ alert('投稿の作成に失敗しました')
1445
+ }
1446
+ }
1447
+
1448
+ return (
1449
+ <form onSubmit={handleSubmit(onSubmit)} className="max-w-2xl mx-auto space-y-6">
1450
+ <div>
1451
+ <label className="block text-sm font-medium mb-2">タイトル</label>
1452
+ <Input {...register('title')} placeholder="投稿のタイトルを入力" />
1453
+ {errors.title && (
1454
+ <p className="text-red-500 text-sm mt-1">{errors.title.message}</p>
1455
+ )}
1456
+ </div>
1457
+
1458
+ <div>
1459
+ <label className="block text-sm font-medium mb-2">本文</label>
1460
+ <textarea
1461
+ {...register('content')}
1462
+ rows={10}
1463
+ className="w-full border rounded px-3 py-2"
1464
+ placeholder="投稿の本文を入力"
1465
+ />
1466
+ {errors.content && (
1467
+ <p className="text-red-500 text-sm mt-1">{errors.content.message}</p>
1468
+ )}
1469
+ </div>
1470
+
1471
+ <div>
1472
+ <label className="block text-sm font-medium mb-2">ステータス</label>
1473
+ <select
1474
+ {...register('status')}
1475
+ className="w-full border rounded px-3 py-2"
1476
+ >
1477
+ <option value="draft">下書き</option>
1478
+ <option value="published">公開</option>
1479
+ </select>
1480
+ {errors.status && (
1481
+ <p className="text-red-500 text-sm mt-1">{errors.status.message}</p>
1482
+ )}
1483
+ </div>
1484
+
1485
+ <div className="flex gap-4">
1486
+ <Button
1487
+ type="submit"
1488
+ variant="primary"
1489
+ disabled={isSubmitting || createPost.isPending}
1490
+ >
1491
+ {isSubmitting || createPost.isPending ? '作成中...' : '投稿を作成'}
1492
+ </Button>
1493
+ <Button
1494
+ type="button"
1495
+ variant="secondary"
1496
+ onClick={() => router.back()}
1497
+ >
1498
+ キャンセル
1499
+ </Button>
1500
+ </div>
1501
+ </form>
1502
+ )
1503
+ }
1504
+ ```
1505
+
1506
+ ---
1507
+
1508
+ ## まとめ
1509
+
1510
+ このフロントエンド開発ガイドに従うことで、以下を実現できます:
1511
+
1512
+ 1. **型安全性**: Hono Client + Zodによるエンドツーエンドの型推論
1513
+ 2. **パフォーマンス**:
1514
+ - Tanstack Queryによる効率的なデータキャッシング
1515
+ - Server Componentによる最小限のJavaScriptバンドル
1516
+ - 高速な初期表示(First Contentful Paint)
1517
+ 3. **保守性**:
1518
+ - コンポーネント分割とカスタムフックによる関心の分離
1519
+ - Server/Client Componentの適切な使い分け
1520
+ 4. **開発効率**: React Hook Formによる宣言的なフォーム処理
1521
+ 5. **スケーラビリティ**: App Routerとルートグループによる明確な構造
1522
+
1523
+ ### 🚨 必須原則
1524
+
1525
+ すべてのフロントエンド実装は、以下の原則を**厳守**してください:
1526
+
1527
+ ✅ **可能な限りServer Componentを使用する**
1528
+ - すべてのコンポーネントはデフォルトでServer Component
1529
+ - データフェッチはサーバー側で実行
1530
+ - インタラクティブ部分のみClient Component化
1531
+
1532
+ ❌ **page.tsxでの`'use client'`使用は禁止**
1533
+ - ページコンポーネントは必ずServer Componentとして実装
1534
+ - インタラクティブな機能は別コンポーネントに分離
1535
+ - バンドルサイズを最小限に抑える
1536
+
1537
+ このガイドラインに従うことで、高速で保守性の高いモダンなWebアプリケーションを構築できます。