create-einja-app 0.1.1

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 (235) hide show
  1. package/README.md +307 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +1041 -0
  4. package/dist/cli.js.map +1 -0
  5. package/package.json +62 -0
  6. package/templates/turborepo-pandacss/.biomeignore +15 -0
  7. package/templates/turborepo-pandacss/.claude/hooks/einja/biome-format.sh +49 -0
  8. package/templates/turborepo-pandacss/.claude/hooks/einja/design-doc-check.sh +61 -0
  9. package/templates/turborepo-pandacss/.claude/hooks/einja/detect-secrets.sh +62 -0
  10. package/templates/turborepo-pandacss/.claude/hooks/einja/large-file-warning.sh +42 -0
  11. package/templates/turborepo-pandacss/.claude/hooks/einja/playwright-resize.sh +36 -0
  12. package/templates/turborepo-pandacss/.claude/hooks/einja/typecheck.sh +37 -0
  13. package/templates/turborepo-pandacss/.claude/hooks/einja/unset-volta-recursion.sh +32 -0
  14. package/templates/turborepo-pandacss/.claude/hooks/einja/validate-git-commit.sh +239 -0
  15. package/templates/turborepo-pandacss/.claude/hooks/einja/warn-index-ts.sh +34 -0
  16. package/templates/turborepo-pandacss/.claude/hooks/einja/warn-relative-import.sh +48 -0
  17. package/templates/turborepo-pandacss/.claude/settings.json +174 -0
  18. package/templates/turborepo-pandacss/.claude/skills/create-einja-app-release/SKILL.md +186 -0
  19. package/templates/turborepo-pandacss/.claude/skills/dev-cli-release/SKILL.md +173 -0
  20. package/templates/turborepo-pandacss/.cursor/commands/spec-create.md +227 -0
  21. package/templates/turborepo-pandacss/.cursor/commands/start-dev.md +98 -0
  22. package/templates/turborepo-pandacss/.cursor/commands/task-exec.md +287 -0
  23. package/templates/turborepo-pandacss/.cursor/commands/task-vibe-kanban-loop.md +532 -0
  24. package/templates/turborepo-pandacss/.cursor/commands/update-docs-by-task-specs.md +448 -0
  25. package/templates/turborepo-pandacss/.cursor/mcp.json +45 -0
  26. package/templates/turborepo-pandacss/.cursor/rules/api-rules.mdc +171 -0
  27. package/templates/turborepo-pandacss/.cursor/rules/api-test-rules.mdc +181 -0
  28. package/templates/turborepo-pandacss/.cursor/rules/base-code.mdc +70 -0
  29. package/templates/turborepo-pandacss/.cursor/rules/base-commit-rules.mdc +174 -0
  30. package/templates/turborepo-pandacss/.cursor/rules/base-design.mdc +12 -0
  31. package/templates/turborepo-pandacss/.cursor/rules/base-rules.mdc +231 -0
  32. package/templates/turborepo-pandacss/.cursor/rules/error-handling-rules.mdc +188 -0
  33. package/templates/turborepo-pandacss/.cursor/rules/refactor-rules.mdc +93 -0
  34. package/templates/turborepo-pandacss/.dockerignore +126 -0
  35. package/templates/turborepo-pandacss/.einja-sync.json +35 -0
  36. package/templates/turborepo-pandacss/.env.ci +25 -0
  37. package/templates/turborepo-pandacss/.env.example +35 -0
  38. package/templates/turborepo-pandacss/.env.personal.example +27 -0
  39. package/templates/turborepo-pandacss/.envrc +4 -0
  40. package/templates/turborepo-pandacss/.gitattributes +5 -0
  41. package/templates/turborepo-pandacss/.husky/pre-commit +1 -0
  42. package/templates/turborepo-pandacss/.lintstagedrc.js +24 -0
  43. package/templates/turborepo-pandacss/.mcp.json +45 -0
  44. package/templates/turborepo-pandacss/.node-version +1 -0
  45. package/templates/turborepo-pandacss/.templateignore +60 -0
  46. package/templates/turborepo-pandacss/.vscode/extensions.json +3 -0
  47. package/templates/turborepo-pandacss/CLAUDE.md +415 -0
  48. package/templates/turborepo-pandacss/README.md +322 -0
  49. package/templates/turborepo-pandacss/apps/web/middleware.ts +28 -0
  50. package/templates/turborepo-pandacss/apps/web/next.config.ts +10 -0
  51. package/templates/turborepo-pandacss/apps/web/package.json +80 -0
  52. package/templates/turborepo-pandacss/apps/web/panda.config.ts +114 -0
  53. package/templates/turborepo-pandacss/apps/web/postcss.config.cjs +6 -0
  54. package/templates/turborepo-pandacss/apps/web/public/file.svg +1 -0
  55. package/templates/turborepo-pandacss/apps/web/public/globe.svg +1 -0
  56. package/templates/turborepo-pandacss/apps/web/public/next.svg +1 -0
  57. package/templates/turborepo-pandacss/apps/web/public/vercel.svg +1 -0
  58. package/templates/turborepo-pandacss/apps/web/public/window.svg +1 -0
  59. package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/dashboard/page.tsx +79 -0
  60. package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/data/_components/UserTable.tsx +203 -0
  61. package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/data/page.tsx +57 -0
  62. package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/layout-client.tsx +31 -0
  63. package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/layout.tsx +17 -0
  64. package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/profile/page.tsx +59 -0
  65. package/templates/turborepo-pandacss/apps/web/src/app/api/auth/[...nextauth]/route.ts +3 -0
  66. package/templates/turborepo-pandacss/apps/web/src/app/api/auth/signup/route.ts +70 -0
  67. package/templates/turborepo-pandacss/apps/web/src/app/error.tsx +106 -0
  68. package/templates/turborepo-pandacss/apps/web/src/app/favicon.ico +0 -0
  69. package/templates/turborepo-pandacss/apps/web/src/app/global-error.tsx +110 -0
  70. package/templates/turborepo-pandacss/apps/web/src/app/globals.css +121 -0
  71. package/templates/turborepo-pandacss/apps/web/src/app/layout.tsx +28 -0
  72. package/templates/turborepo-pandacss/apps/web/src/app/not-found.tsx +54 -0
  73. package/templates/turborepo-pandacss/apps/web/src/app/page.module.css +165 -0
  74. package/templates/turborepo-pandacss/apps/web/src/app/page.test.tsx +52 -0
  75. package/templates/turborepo-pandacss/apps/web/src/app/page.tsx +284 -0
  76. package/templates/turborepo-pandacss/apps/web/src/app/signin/page.tsx +296 -0
  77. package/templates/turborepo-pandacss/apps/web/src/app/signup/page.tsx +395 -0
  78. package/templates/turborepo-pandacss/apps/web/src/application/use-cases/UserUseCases.test.ts +229 -0
  79. package/templates/turborepo-pandacss/apps/web/src/application/use-cases/UserUseCases.ts +115 -0
  80. package/templates/turborepo-pandacss/apps/web/src/components/auth/login-button.tsx +35 -0
  81. package/templates/turborepo-pandacss/apps/web/src/components/auth/logout-button.tsx +24 -0
  82. package/templates/turborepo-pandacss/apps/web/src/components/auth/user-avatar.test.tsx +68 -0
  83. package/templates/turborepo-pandacss/apps/web/src/components/auth/user-avatar.tsx +43 -0
  84. package/templates/turborepo-pandacss/apps/web/src/components/dashboard/dashboard-stats.tsx +128 -0
  85. package/templates/turborepo-pandacss/apps/web/src/components/providers/query-provider.tsx +30 -0
  86. package/templates/turborepo-pandacss/apps/web/src/components/providers/session-provider.tsx +12 -0
  87. package/templates/turborepo-pandacss/apps/web/src/components/shared/Sidebar.tsx +175 -0
  88. package/templates/turborepo-pandacss/apps/web/src/components/shared/header.tsx +166 -0
  89. package/templates/turborepo-pandacss/apps/web/src/components/ui/accordion.tsx +64 -0
  90. package/templates/turborepo-pandacss/apps/web/src/components/ui/alert-dialog.tsx +135 -0
  91. package/templates/turborepo-pandacss/apps/web/src/components/ui/alert.tsx +60 -0
  92. package/templates/turborepo-pandacss/apps/web/src/components/ui/aspect-ratio.tsx +9 -0
  93. package/templates/turborepo-pandacss/apps/web/src/components/ui/avatar.tsx +41 -0
  94. package/templates/turborepo-pandacss/apps/web/src/components/ui/badge.tsx +39 -0
  95. package/templates/turborepo-pandacss/apps/web/src/components/ui/breadcrumb.tsx +101 -0
  96. package/templates/turborepo-pandacss/apps/web/src/components/ui/button.tsx +56 -0
  97. package/templates/turborepo-pandacss/apps/web/src/components/ui/card.tsx +75 -0
  98. package/templates/turborepo-pandacss/apps/web/src/components/ui/checkbox.tsx +29 -0
  99. package/templates/turborepo-pandacss/apps/web/src/components/ui/data-table.tsx +189 -0
  100. package/templates/turborepo-pandacss/apps/web/src/components/ui/dialog-hook.tsx +210 -0
  101. package/templates/turborepo-pandacss/apps/web/src/components/ui/dialog.tsx +129 -0
  102. package/templates/turborepo-pandacss/apps/web/src/components/ui/drawer.tsx +124 -0
  103. package/templates/turborepo-pandacss/apps/web/src/components/ui/dropdown-menu.tsx +228 -0
  104. package/templates/turborepo-pandacss/apps/web/src/components/ui/form.tsx +152 -0
  105. package/templates/turborepo-pandacss/apps/web/src/components/ui/hover-card.tsx +38 -0
  106. package/templates/turborepo-pandacss/apps/web/src/components/ui/input.tsx +21 -0
  107. package/templates/turborepo-pandacss/apps/web/src/components/ui/label.tsx +21 -0
  108. package/templates/turborepo-pandacss/apps/web/src/components/ui/pagination.tsx +105 -0
  109. package/templates/turborepo-pandacss/apps/web/src/components/ui/popover.tsx +42 -0
  110. package/templates/turborepo-pandacss/apps/web/src/components/ui/progress.tsx +28 -0
  111. package/templates/turborepo-pandacss/apps/web/src/components/ui/select.tsx +170 -0
  112. package/templates/turborepo-pandacss/apps/web/src/components/ui/separator.tsx +28 -0
  113. package/templates/turborepo-pandacss/apps/web/src/components/ui/skeleton.tsx +13 -0
  114. package/templates/turborepo-pandacss/apps/web/src/components/ui/sonner.tsx +25 -0
  115. package/templates/turborepo-pandacss/apps/web/src/components/ui/table.tsx +92 -0
  116. package/templates/turborepo-pandacss/apps/web/src/components/ui/tabs.tsx +54 -0
  117. package/templates/turborepo-pandacss/apps/web/src/components/ui/textarea.tsx +18 -0
  118. package/templates/turborepo-pandacss/apps/web/src/components/ui/tooltip.tsx +57 -0
  119. package/templates/turborepo-pandacss/apps/web/src/components/ui/typography.tsx +158 -0
  120. package/templates/turborepo-pandacss/apps/web/src/lib/auth/guard.ts +36 -0
  121. package/templates/turborepo-pandacss/apps/web/src/lib/auth/index.ts +22 -0
  122. package/templates/turborepo-pandacss/apps/web/src/lib/prisma.ts +3 -0
  123. package/templates/turborepo-pandacss/apps/web/src/lib/utils.ts +6 -0
  124. package/templates/turborepo-pandacss/apps/web/test/globals.d.ts +1 -0
  125. package/templates/turborepo-pandacss/apps/web/test/matchers.d.ts +1 -0
  126. package/templates/turborepo-pandacss/apps/web/test/setup.ts +22 -0
  127. package/templates/turborepo-pandacss/apps/web/tsconfig.json +37 -0
  128. package/templates/turborepo-pandacss/apps/web/vitest.config.ts +20 -0
  129. package/templates/turborepo-pandacss/apps/web/vitest.d.ts +2 -0
  130. package/templates/turborepo-pandacss/biome.json +60 -0
  131. package/templates/turborepo-pandacss/components.json +21 -0
  132. package/templates/turborepo-pandacss/docker-compose.yml +27 -0
  133. package/templates/turborepo-pandacss/middleware.ts +32 -0
  134. package/templates/turborepo-pandacss/next.config.ts +10 -0
  135. package/templates/turborepo-pandacss/package-lock.json +9346 -0
  136. package/templates/turborepo-pandacss/package.json +64 -0
  137. package/templates/turborepo-pandacss/packages/config/package.json +41 -0
  138. package/templates/turborepo-pandacss/packages/config/panda.config.ts +114 -0
  139. package/templates/turborepo-pandacss/packages/config/src/index.ts +24 -0
  140. package/templates/turborepo-pandacss/packages/config/src/worktree-config-loader.ts +129 -0
  141. package/templates/turborepo-pandacss/packages/config/src/worktree-config.ts +75 -0
  142. package/templates/turborepo-pandacss/packages/config/tsconfig.build.json +19 -0
  143. package/templates/turborepo-pandacss/packages/config/tsconfig.json +24 -0
  144. package/templates/turborepo-pandacss/packages/config/typescript/base.json +19 -0
  145. package/templates/turborepo-pandacss/packages/front-core/package.json +24 -0
  146. package/templates/turborepo-pandacss/packages/front-core/src/auth/config.ts +84 -0
  147. package/templates/turborepo-pandacss/packages/front-core/src/auth/index.ts +5 -0
  148. package/templates/turborepo-pandacss/packages/front-core/src/auth/types/next-auth.d.ts +20 -0
  149. package/templates/turborepo-pandacss/packages/front-core/src/auth/utils.ts +29 -0
  150. package/templates/turborepo-pandacss/packages/front-core/src/context/index.ts +2 -0
  151. package/templates/turborepo-pandacss/packages/front-core/src/hooks/index.ts +2 -0
  152. package/templates/turborepo-pandacss/packages/front-core/src/index.ts +4 -0
  153. package/templates/turborepo-pandacss/packages/front-core/src/utils/index.ts +2 -0
  154. package/templates/turborepo-pandacss/packages/front-core/tsconfig.json +14 -0
  155. package/templates/turborepo-pandacss/packages/server-core/package.json +32 -0
  156. package/templates/turborepo-pandacss/packages/server-core/prisma/schema.prisma +102 -0
  157. package/templates/turborepo-pandacss/packages/server-core/prisma/seed.ts +67 -0
  158. package/templates/turborepo-pandacss/packages/server-core/prisma.config.ts +8 -0
  159. package/templates/turborepo-pandacss/packages/server-core/src/__generated__/fabbrica/index.d.ts +270 -0
  160. package/templates/turborepo-pandacss/packages/server-core/src/__generated__/fabbrica/index.js +484 -0
  161. package/templates/turborepo-pandacss/packages/server-core/src/core/result.test.ts +78 -0
  162. package/templates/turborepo-pandacss/packages/server-core/src/core/result.ts +53 -0
  163. package/templates/turborepo-pandacss/packages/server-core/src/domain/.gitkeep +0 -0
  164. package/templates/turborepo-pandacss/packages/server-core/src/domain/entities/User.test.ts +232 -0
  165. package/templates/turborepo-pandacss/packages/server-core/src/domain/entities/User.ts +105 -0
  166. package/templates/turborepo-pandacss/packages/server-core/src/domain/repository-interfaces/IUserRepository.ts +101 -0
  167. package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/client.ts +15 -0
  168. package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/mappers/UserMapper.test.ts +278 -0
  169. package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/mappers/UserMapper.ts +103 -0
  170. package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/repositories/UserRepository.test.ts +317 -0
  171. package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/repositories/UserRepository.ts +169 -0
  172. package/templates/turborepo-pandacss/packages/server-core/src/testing/factories/index.ts +22 -0
  173. package/templates/turborepo-pandacss/packages/server-core/src/testing/factories/user.factory.ts +123 -0
  174. package/templates/turborepo-pandacss/packages/server-core/src/testing/fixtures/users.ts +92 -0
  175. package/templates/turborepo-pandacss/packages/server-core/src/testing/helpers/date.ts +49 -0
  176. package/templates/turborepo-pandacss/packages/server-core/src/testing/helpers/password.ts +50 -0
  177. package/templates/turborepo-pandacss/packages/server-core/src/testing/index.ts +26 -0
  178. package/templates/turborepo-pandacss/packages/server-core/tsconfig.json +14 -0
  179. package/templates/turborepo-pandacss/packages/server-core/vitest.config.ts +15 -0
  180. package/templates/turborepo-pandacss/packages/ui/package.json +37 -0
  181. package/templates/turborepo-pandacss/packages/ui/src/accordion.tsx +66 -0
  182. package/templates/turborepo-pandacss/packages/ui/src/alert-dialog.tsx +157 -0
  183. package/templates/turborepo-pandacss/packages/ui/src/alert.tsx +66 -0
  184. package/templates/turborepo-pandacss/packages/ui/src/aspect-ratio.tsx +11 -0
  185. package/templates/turborepo-pandacss/packages/ui/src/avatar.tsx +53 -0
  186. package/templates/turborepo-pandacss/packages/ui/src/badge.tsx +46 -0
  187. package/templates/turborepo-pandacss/packages/ui/src/breadcrumb.tsx +108 -0
  188. package/templates/turborepo-pandacss/packages/ui/src/button.tsx +59 -0
  189. package/templates/turborepo-pandacss/packages/ui/src/card.tsx +92 -0
  190. package/templates/turborepo-pandacss/packages/ui/src/checkbox.tsx +32 -0
  191. package/templates/turborepo-pandacss/packages/ui/src/data-table.tsx +216 -0
  192. package/templates/turborepo-pandacss/packages/ui/src/dialog-hook.tsx +226 -0
  193. package/templates/turborepo-pandacss/packages/ui/src/dialog.tsx +143 -0
  194. package/templates/turborepo-pandacss/packages/ui/src/drawer.tsx +135 -0
  195. package/templates/turborepo-pandacss/packages/ui/src/dropdown-menu.tsx +257 -0
  196. package/templates/turborepo-pandacss/packages/ui/src/form.tsx +168 -0
  197. package/templates/turborepo-pandacss/packages/ui/src/hover-card.tsx +44 -0
  198. package/templates/turborepo-pandacss/packages/ui/src/input.tsx +21 -0
  199. package/templates/turborepo-pandacss/packages/ui/src/label.tsx +24 -0
  200. package/templates/turborepo-pandacss/packages/ui/src/lib/utils.ts +6 -0
  201. package/templates/turborepo-pandacss/packages/ui/src/pagination.tsx +126 -0
  202. package/templates/turborepo-pandacss/packages/ui/src/popover.tsx +48 -0
  203. package/templates/turborepo-pandacss/packages/ui/src/progress.tsx +31 -0
  204. package/templates/turborepo-pandacss/packages/ui/src/select.tsx +185 -0
  205. package/templates/turborepo-pandacss/packages/ui/src/separator.tsx +28 -0
  206. package/templates/turborepo-pandacss/packages/ui/src/skeleton.tsx +13 -0
  207. package/templates/turborepo-pandacss/packages/ui/src/sonner.tsx +25 -0
  208. package/templates/turborepo-pandacss/packages/ui/src/table.tsx +116 -0
  209. package/templates/turborepo-pandacss/packages/ui/src/tabs.tsx +66 -0
  210. package/templates/turborepo-pandacss/packages/ui/src/textarea.tsx +18 -0
  211. package/templates/turborepo-pandacss/packages/ui/src/tooltip.tsx +61 -0
  212. package/templates/turborepo-pandacss/packages/ui/src/typography.tsx +187 -0
  213. package/templates/turborepo-pandacss/packages/ui/tsconfig.json +20 -0
  214. package/templates/turborepo-pandacss/panda.config.ts +114 -0
  215. package/templates/turborepo-pandacss/pnpm-lock.yaml +9032 -0
  216. package/templates/turborepo-pandacss/pnpm-workspace.yaml +11 -0
  217. package/templates/turborepo-pandacss/postcss.config.cjs +6 -0
  218. package/templates/turborepo-pandacss/prisma/schema.prisma +82 -0
  219. package/templates/turborepo-pandacss/public/file.svg +1 -0
  220. package/templates/turborepo-pandacss/public/globe.svg +1 -0
  221. package/templates/turborepo-pandacss/public/next.svg +1 -0
  222. package/templates/turborepo-pandacss/public/vercel.svg +1 -0
  223. package/templates/turborepo-pandacss/public/window.svg +1 -0
  224. package/templates/turborepo-pandacss/scripts/cli-template-update.ts +387 -0
  225. package/templates/turborepo-pandacss/scripts/env-show.ts +129 -0
  226. package/templates/turborepo-pandacss/scripts/env.ts +555 -0
  227. package/templates/turborepo-pandacss/scripts/init.sh +92 -0
  228. package/templates/turborepo-pandacss/scripts/setup-dev.ts +640 -0
  229. package/templates/turborepo-pandacss/scripts/template-update.ts +277 -0
  230. package/templates/turborepo-pandacss/scripts/worktree/dev.ts +872 -0
  231. package/templates/turborepo-pandacss/test/globals.d.ts +1 -0
  232. package/templates/turborepo-pandacss/test/setup.ts +22 -0
  233. package/templates/turborepo-pandacss/tsconfig.json +46 -0
  234. package/templates/turborepo-pandacss/turbo.json +57 -0
  235. package/templates/turborepo-pandacss/vitest.config.ts +20 -0
@@ -0,0 +1,229 @@
1
+ import { failure, isFailure, isSuccess, success } from "{{packageName}}/server-core/core/result";
2
+ import { User } from "{{packageName}}/server-core/domain/entities/User";
3
+ import type { PaginatedResult } from "{{packageName}}/server-core/domain/repository-interfaces/IUserRepository";
4
+ import { buildUserProps, initialize } from "{{packageName}}/server-core/testing";
5
+ import { beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { userUseCases } from "./UserUseCases";
7
+
8
+ // UserRepositoryをモック
9
+ vi.mock("@repo/server-core/infrastructure/database/repositories/UserRepository", () => ({
10
+ userRepository: {
11
+ search: vi.fn(),
12
+ findById: vi.fn(),
13
+ findByEmail: vi.fn(),
14
+ updateLastLogin: vi.fn(),
15
+ },
16
+ }));
17
+
18
+ import { userRepository } from "{{packageName}}/server-core/infrastructure/database/repositories/UserRepository";
19
+
20
+ describe("UserUseCases", () => {
21
+ beforeEach(() => {
22
+ // ユースケーステストではモックリポジトリを使用するため、空のオブジェクトを渡す
23
+ initialize({ prisma: {} as unknown as Parameters<typeof initialize>[0]["prisma"] });
24
+ vi.clearAllMocks();
25
+ });
26
+
27
+ describe("list", () => {
28
+ it("ユーザー一覧をDTO形式で取得できる", async () => {
29
+ // Given
30
+ const mockUsers = [
31
+ new User(await buildUserProps({ id: "user-1", email: "user1@example.com" })),
32
+ new User(await buildUserProps({ id: "user-2", email: "user2@example.com" })),
33
+ ];
34
+ const mockPaginatedResult: PaginatedResult<User> = {
35
+ items: mockUsers,
36
+ total: 25,
37
+ page: 1,
38
+ limit: 10,
39
+ totalPages: 3,
40
+ };
41
+ vi.mocked(userRepository.search).mockResolvedValue(success(mockPaginatedResult));
42
+
43
+ // When
44
+ const result = await userUseCases.list();
45
+
46
+ // Then
47
+ expect(isSuccess(result)).toBe(true);
48
+ if (isSuccess(result)) {
49
+ expect(result.value.items).toHaveLength(2);
50
+ expect(result.value.items[0].id).toBe("user-1");
51
+ expect(result.value.items[0].email).toBe("user1@example.com");
52
+ expect(result.value.items[0].status).toBe("active");
53
+ expect(result.value.items[0].role).toBe("user");
54
+ expect(result.value.total).toBe(25);
55
+ expect(result.value.page).toBe(1);
56
+ expect(result.value.limit).toBe(10);
57
+ expect(result.value.totalPages).toBe(3);
58
+ }
59
+ });
60
+
61
+ it("nameがnullの場合、空文字に変換される", async () => {
62
+ // Given
63
+ const mockUser = new User(await buildUserProps({ id: "user-3", name: null }));
64
+ const mockPaginatedResult: PaginatedResult<User> = {
65
+ items: [mockUser],
66
+ total: 1,
67
+ page: 1,
68
+ limit: 10,
69
+ totalPages: 1,
70
+ };
71
+ vi.mocked(userRepository.search).mockResolvedValue(success(mockPaginatedResult));
72
+
73
+ // When
74
+ const result = await userUseCases.list();
75
+
76
+ // Then
77
+ expect(isSuccess(result)).toBe(true);
78
+ if (isSuccess(result)) {
79
+ expect(result.value.items[0].name).toBe("");
80
+ }
81
+ });
82
+
83
+ it("lastLoginがnullの場合、nullのまま返される", async () => {
84
+ // Given
85
+ const mockUser = new User(await buildUserProps({ id: "user-4", lastLogin: null }));
86
+ const mockPaginatedResult: PaginatedResult<User> = {
87
+ items: [mockUser],
88
+ total: 1,
89
+ page: 1,
90
+ limit: 10,
91
+ totalPages: 1,
92
+ };
93
+ vi.mocked(userRepository.search).mockResolvedValue(success(mockPaginatedResult));
94
+
95
+ // When
96
+ const result = await userUseCases.list();
97
+
98
+ // Then
99
+ expect(isSuccess(result)).toBe(true);
100
+ if (isSuccess(result)) {
101
+ expect(result.value.items[0].lastLogin).toBeNull();
102
+ }
103
+ });
104
+
105
+ it("検索条件とページネーションがリポジトリに渡される", async () => {
106
+ // Given
107
+ const criteria = { status: "active" as const };
108
+ const pagination = { page: 2, limit: 20 };
109
+ vi.mocked(userRepository.search).mockResolvedValue(
110
+ success({
111
+ items: [],
112
+ total: 0,
113
+ page: 2,
114
+ limit: 20,
115
+ totalPages: 0,
116
+ })
117
+ );
118
+
119
+ // When
120
+ await userUseCases.list(criteria, pagination);
121
+
122
+ // Then
123
+ expect(userRepository.search).toHaveBeenCalledWith(criteria, pagination);
124
+ });
125
+
126
+ it("Repository失敗時にfailure Resultを返す", async () => {
127
+ // Given
128
+ vi.mocked(userRepository.search).mockResolvedValue(failure(new Error("DB error")));
129
+
130
+ // When
131
+ const result = await userUseCases.list();
132
+
133
+ // Then
134
+ expect(isFailure(result)).toBe(true);
135
+ });
136
+ });
137
+
138
+ describe("getById", () => {
139
+ it("IDでユーザーをDTO形式で取得できる", async () => {
140
+ // Given
141
+ const mockUser = new User(await buildUserProps({ id: "user-123" }));
142
+ vi.mocked(userRepository.findById).mockResolvedValue(success(mockUser));
143
+
144
+ // When
145
+ const result = await userUseCases.getById("user-123");
146
+
147
+ // Then
148
+ expect(isSuccess(result)).toBe(true);
149
+ if (isSuccess(result)) {
150
+ expect(result.value?.id).toBeDefined();
151
+ expect(result.value?.email).toBeDefined();
152
+ }
153
+ });
154
+
155
+ it("見つからない場合はnullを返す", async () => {
156
+ // Given
157
+ vi.mocked(userRepository.findById).mockResolvedValue(success(null));
158
+
159
+ // When
160
+ const result = await userUseCases.getById("nonexistent");
161
+
162
+ // Then
163
+ expect(isSuccess(result)).toBe(true);
164
+ if (isSuccess(result)) {
165
+ expect(result.value).toBeNull();
166
+ }
167
+ });
168
+ });
169
+
170
+ describe("getByEmail", () => {
171
+ it("メールアドレスでユーザーをDTO形式で取得できる", async () => {
172
+ // Given
173
+ const mockUser = new User(
174
+ await buildUserProps({ id: "user-456", email: "test@example.com" })
175
+ );
176
+ vi.mocked(userRepository.findByEmail).mockResolvedValue(success(mockUser));
177
+
178
+ // When
179
+ const result = await userUseCases.getByEmail("test@example.com");
180
+
181
+ // Then
182
+ expect(isSuccess(result)).toBe(true);
183
+ if (isSuccess(result)) {
184
+ expect(result.value?.email).toBe("test@example.com");
185
+ }
186
+ });
187
+
188
+ it("見つからない場合はnullを返す", async () => {
189
+ // Given
190
+ vi.mocked(userRepository.findByEmail).mockResolvedValue(success(null));
191
+
192
+ // When
193
+ const result = await userUseCases.getByEmail("notfound@example.com");
194
+
195
+ // Then
196
+ expect(isSuccess(result)).toBe(true);
197
+ if (isSuccess(result)) {
198
+ expect(result.value).toBeNull();
199
+ }
200
+ });
201
+ });
202
+
203
+ describe("updateLastLogin", () => {
204
+ it("最終ログイン日時を更新できる", async () => {
205
+ // Given
206
+ vi.mocked(userRepository.updateLastLogin).mockResolvedValue(success(undefined));
207
+
208
+ // When
209
+ const result = await userUseCases.updateLastLogin("user-123");
210
+
211
+ // Then
212
+ expect(isSuccess(result)).toBe(true);
213
+ expect(userRepository.updateLastLogin).toHaveBeenCalledWith("user-123", expect.any(Date));
214
+ });
215
+
216
+ it("Repository失敗時にfailure Resultを返す", async () => {
217
+ // Given
218
+ vi.mocked(userRepository.updateLastLogin).mockResolvedValue(
219
+ failure(new Error("Update failed"))
220
+ );
221
+
222
+ // When
223
+ const result = await userUseCases.updateLastLogin("user-123");
224
+
225
+ // Then
226
+ expect(isFailure(result)).toBe(true);
227
+ });
228
+ });
229
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * UserUseCases
3
+ *
4
+ * ユーザー関連のユースケース。
5
+ * Domain層とInfrastructure層を組み合わせてビジネスロジックを実行。
6
+ */
7
+
8
+ import { type Result, failure, success } from "{{packageName}}/server-core/core/result";
9
+ import type { User } from "{{packageName}}/server-core/domain/entities/User";
10
+ import type {
11
+ PaginatedResult,
12
+ PaginationOptions,
13
+ UserSearchCriteria,
14
+ } from "{{packageName}}/server-core/domain/repository-interfaces/IUserRepository";
15
+ import { userRepository } from "{{packageName}}/server-core/infrastructure/database/repositories/UserRepository";
16
+
17
+ /**
18
+ * ユーザー一覧表示用のDTO
19
+ */
20
+ export interface UserListItem {
21
+ readonly id: string;
22
+ readonly name: string;
23
+ readonly email: string;
24
+ readonly status: "active" | "inactive" | "pending";
25
+ readonly role: "admin" | "user" | "moderator";
26
+ readonly createdAt: string;
27
+ readonly lastLogin: string | null;
28
+ }
29
+
30
+ /**
31
+ * ページネーション付きユーザー一覧のDTO
32
+ */
33
+ export interface PaginatedUserList {
34
+ readonly items: readonly UserListItem[];
35
+ readonly total: number;
36
+ readonly page: number;
37
+ readonly limit: number;
38
+ readonly totalPages: number;
39
+ }
40
+
41
+ /**
42
+ * User Entity を UserListItem DTO に変換
43
+ */
44
+ function toUserListItem(user: User): UserListItem {
45
+ return {
46
+ id: user.id,
47
+ name: user.name ?? "",
48
+ email: user.email,
49
+ status: user.status,
50
+ role: user.role,
51
+ createdAt: user.createdAt.toISOString(),
52
+ lastLogin: user.lastLogin?.toISOString() ?? null,
53
+ };
54
+ }
55
+
56
+ /**
57
+ * PaginatedResult<User> を PaginatedUserList に変換
58
+ */
59
+ function toPaginatedUserList(result: PaginatedResult<User>): PaginatedUserList {
60
+ return {
61
+ items: result.items.map(toUserListItem),
62
+ total: result.total,
63
+ page: result.page,
64
+ limit: result.limit,
65
+ totalPages: result.totalPages,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * ユーザー関連のユースケース
71
+ */
72
+ export const userUseCases = {
73
+ /**
74
+ * ユーザー一覧を取得
75
+ */
76
+ async list(
77
+ criteria?: UserSearchCriteria,
78
+ pagination?: PaginationOptions
79
+ ): Promise<Result<PaginatedUserList, Error>> {
80
+ const result = await userRepository.search(criteria ?? {}, pagination);
81
+ if (!result.isSuccess) {
82
+ return failure(result.error);
83
+ }
84
+ return success(toPaginatedUserList(result.value));
85
+ },
86
+
87
+ /**
88
+ * IDでユーザーを取得
89
+ */
90
+ async getById(id: string): Promise<Result<UserListItem | null, Error>> {
91
+ const result = await userRepository.findById(id);
92
+ if (!result.isSuccess) {
93
+ return failure(result.error);
94
+ }
95
+ return success(result.value ? toUserListItem(result.value) : null);
96
+ },
97
+
98
+ /**
99
+ * メールアドレスでユーザーを取得
100
+ */
101
+ async getByEmail(email: string): Promise<Result<UserListItem | null, Error>> {
102
+ const result = await userRepository.findByEmail(email);
103
+ if (!result.isSuccess) {
104
+ return failure(result.error);
105
+ }
106
+ return success(result.value ? toUserListItem(result.value) : null);
107
+ },
108
+
109
+ /**
110
+ * 最終ログイン日時を更新
111
+ */
112
+ async updateLastLogin(id: string): Promise<Result<void, Error>> {
113
+ return userRepository.updateLastLogin(id, new Date());
114
+ },
115
+ };
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { signIn } from "next-auth/react";
4
+
5
+ interface LoginButtonProps {
6
+ provider?: "credentials"; // | "google" | "github" - add when OAuth is enabled
7
+ children?: React.ReactNode;
8
+ className?: string;
9
+ }
10
+
11
+ export function LoginButton({
12
+ provider = "credentials",
13
+ children,
14
+ className = "",
15
+ }: LoginButtonProps) {
16
+ const handleSignIn = async () => {
17
+ await signIn(provider, { callbackUrl: "/" });
18
+ };
19
+
20
+ const defaultText = {
21
+ credentials: "Sign in with Email",
22
+ // google: "Sign in with Google", // add when needed
23
+ // github: "Sign in with GitHub", // add when needed
24
+ };
25
+
26
+ return (
27
+ <button
28
+ type="button"
29
+ onClick={handleSignIn}
30
+ className={`px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors ${className}`}
31
+ >
32
+ {children || defaultText[provider]}
33
+ </button>
34
+ );
35
+ }
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ import { signOut } from "next-auth/react";
4
+
5
+ interface LogoutButtonProps {
6
+ children?: React.ReactNode;
7
+ className?: string;
8
+ }
9
+
10
+ export function LogoutButton({ children = "Sign out", className = "" }: LogoutButtonProps) {
11
+ const handleSignOut = async () => {
12
+ await signOut({ callbackUrl: "/" });
13
+ };
14
+
15
+ return (
16
+ <button
17
+ type="button"
18
+ onClick={handleSignOut}
19
+ className={`px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors ${className}`}
20
+ >
21
+ {children}
22
+ </button>
23
+ );
24
+ }
@@ -0,0 +1,68 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import type { Session } from "next-auth";
3
+ import React from "react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { UserAvatar } from "./user-avatar";
6
+
7
+ // next-authのuseSessionをモック
8
+ vi.mock("next-auth/react", () => ({
9
+ useSession: vi.fn(),
10
+ }));
11
+
12
+ // LoginButtonコンポーネントをモック
13
+ vi.mock("./login-button", () => ({
14
+ LoginButton: () => <button type="button">ログイン</button>,
15
+ }));
16
+
17
+ // LogoutButtonコンポーネントをモック
18
+ vi.mock("./logout-button", () => ({
19
+ LogoutButton: () => <button type="button">ログアウト</button>,
20
+ }));
21
+
22
+ describe("UserAvatar", () => {
23
+ it("認証されていない場合ログインボタンを表示", async () => {
24
+ const { useSession } = await import("next-auth/react");
25
+ vi.mocked(useSession).mockReturnValue({
26
+ data: null,
27
+ status: "unauthenticated",
28
+ update: vi.fn(),
29
+ });
30
+
31
+ render(<UserAvatar />);
32
+
33
+ expect(screen.getByText("ログイン")).toBeInTheDocument();
34
+ });
35
+
36
+ it("認証されている場合ユーザー情報を表示", async () => {
37
+ const { useSession } = await import("next-auth/react");
38
+ vi.mocked(useSession).mockReturnValue({
39
+ data: {
40
+ user: {
41
+ name: "テストユーザー",
42
+ email: "test@example.com",
43
+ },
44
+ } as Session,
45
+ status: "authenticated",
46
+ update: vi.fn(),
47
+ });
48
+
49
+ render(<UserAvatar />);
50
+
51
+ expect(screen.getByText("テストユーザー")).toBeInTheDocument();
52
+ expect(screen.getByText("test@example.com")).toBeInTheDocument();
53
+ });
54
+
55
+ it("ローディング状態でローディング表示をする", async () => {
56
+ const { useSession } = await import("next-auth/react");
57
+ vi.mocked(useSession).mockReturnValue({
58
+ data: null,
59
+ status: "loading",
60
+ update: vi.fn(),
61
+ });
62
+
63
+ const { container } = render(<UserAvatar />);
64
+
65
+ const loadingElement = container.querySelector(".animate-pulse");
66
+ expect(loadingElement).toBeInTheDocument();
67
+ });
68
+ });
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import { useSession } from "next-auth/react";
4
+ import React from "react";
5
+ import { LoginButton } from "./login-button";
6
+ import { LogoutButton } from "./logout-button";
7
+
8
+ interface UserAvatarProps {
9
+ className?: string;
10
+ }
11
+
12
+ export function UserAvatar({ className = "" }: UserAvatarProps) {
13
+ const { data: session, status } = useSession();
14
+
15
+ if (status === "loading") {
16
+ return <div className={`animate-pulse bg-gray-300 rounded-full w-8 h-8 ${className}`} />;
17
+ }
18
+
19
+ if (!session) {
20
+ return (
21
+ <div className={`flex gap-2 ${className}`}>
22
+ <LoginButton provider="credentials" />
23
+ </div>
24
+ );
25
+ }
26
+
27
+ return (
28
+ <div className={`flex items-center gap-3 ${className}`}>
29
+ {session.user?.image && (
30
+ <img
31
+ src={session.user.image}
32
+ alt={session.user.name || "User"}
33
+ className="w-8 h-8 rounded-full"
34
+ />
35
+ )}
36
+ <div className="flex flex-col">
37
+ <span className="text-sm font-medium">{session.user?.name || "User"}</span>
38
+ <span className="text-xs text-gray-500">{session.user?.email}</span>
39
+ </div>
40
+ <LogoutButton />
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,128 @@
1
+ import { css } from "../../../styled-system/css";
2
+ import { hstack, vstack } from "../../../styled-system/patterns";
3
+
4
+ const stats = [
5
+ {
6
+ title: "総プロジェクト数",
7
+ value: "24",
8
+ change: "+12%",
9
+ changeType: "increase" as const,
10
+ icon: "📊",
11
+ },
12
+ {
13
+ title: "アクティブユーザー",
14
+ value: "1,234",
15
+ change: "+23%",
16
+ changeType: "increase" as const,
17
+ icon: "👥",
18
+ },
19
+ {
20
+ title: "今月の売上",
21
+ value: "¥2,480,000",
22
+ change: "+8%",
23
+ changeType: "increase" as const,
24
+ icon: "💰",
25
+ },
26
+ {
27
+ title: "システム稼働率",
28
+ value: "99.9%",
29
+ change: "-0.1%",
30
+ changeType: "decrease" as const,
31
+ icon: "⚡",
32
+ },
33
+ ];
34
+
35
+ export function DashboardStats() {
36
+ return (
37
+ <div
38
+ className={css({
39
+ display: "grid",
40
+ gridTemplateColumns: {
41
+ base: "1fr",
42
+ sm: "repeat(2, 1fr)",
43
+ lg: "repeat(4, 1fr)",
44
+ },
45
+ gap: "1.5rem",
46
+ })}
47
+ >
48
+ {stats.map((stat) => (
49
+ <div
50
+ key={stat.title}
51
+ className={css({
52
+ background: "white",
53
+ borderRadius: "lg",
54
+ padding: { base: "1.25rem", md: "1.5rem", lg: "1.75rem" },
55
+ boxShadow: "sm",
56
+ border: "1px solid {colors.gray.200}",
57
+ transition: "all 0.2s",
58
+ _hover: {
59
+ boxShadow: "md",
60
+ transform: "translateY(-2px)",
61
+ },
62
+ })}
63
+ >
64
+ <div className={vstack({ gap: "1rem", alignItems: "flex-start" })}>
65
+ {/* アイコンとタイトル */}
66
+ <div className={hstack({ gap: "0.75rem", alignItems: "center" })}>
67
+ <span
68
+ className={css({
69
+ fontSize: "1.5rem",
70
+ })}
71
+ >
72
+ {stat.icon}
73
+ </span>
74
+ <h3
75
+ className={css({
76
+ fontSize: "sm",
77
+ fontWeight: "medium",
78
+ color: "{colors.gray.600}",
79
+ lineHeight: "tight",
80
+ })}
81
+ >
82
+ {stat.title}
83
+ </h3>
84
+ </div>
85
+
86
+ {/* 値と変化率 */}
87
+ <div className={vstack({ gap: "0.5rem", alignItems: "flex-start" })}>
88
+ <p
89
+ className={css({
90
+ fontSize: "2xl",
91
+ fontWeight: "bold",
92
+ color: "{colors.gray.900}",
93
+ lineHeight: "none",
94
+ })}
95
+ >
96
+ {stat.value}
97
+ </p>
98
+ <div className={hstack({ gap: "0.25rem", alignItems: "center" })}>
99
+ <span
100
+ className={css({
101
+ fontSize: "xs",
102
+ fontWeight: "medium",
103
+ color:
104
+ stat.changeType === "increase" ? "{colors.green.600}" : "{colors.red.600}",
105
+ background:
106
+ stat.changeType === "increase" ? "{colors.green.100}" : "{colors.red.100}",
107
+ padding: "0.125rem 0.375rem",
108
+ borderRadius: "full",
109
+ })}
110
+ >
111
+ {stat.changeType === "increase" ? "↗" : "↘"} {stat.change}
112
+ </span>
113
+ <span
114
+ className={css({
115
+ fontSize: "xs",
116
+ color: "{colors.gray.500}",
117
+ })}
118
+ >
119
+ 前月比
120
+ </span>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ ))}
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
+ import type { ReactNode } from "react";
5
+ import { useState } from "react";
6
+
7
+ interface QueryProviderProps {
8
+ children: ReactNode;
9
+ }
10
+
11
+ export function QueryProvider({ children }: QueryProviderProps) {
12
+ const [queryClient] = useState(
13
+ () =>
14
+ new QueryClient({
15
+ defaultOptions: {
16
+ queries: {
17
+ staleTime: 5 * 60 * 1000, // 5分
18
+ gcTime: 10 * 60 * 1000, // 10分
19
+ retry: 1,
20
+ refetchOnWindowFocus: false,
21
+ },
22
+ mutations: {
23
+ retry: false,
24
+ },
25
+ },
26
+ })
27
+ );
28
+
29
+ return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
30
+ }
@@ -0,0 +1,12 @@
1
+ "use client";
2
+
3
+ import { SessionProvider } from "next-auth/react";
4
+ import type { ReactNode } from "react";
5
+
6
+ interface AuthProviderProps {
7
+ children: ReactNode;
8
+ }
9
+
10
+ export function AuthProvider({ children }: AuthProviderProps) {
11
+ return <SessionProvider>{children}</SessionProvider>;
12
+ }