├── .eslintrc.js
├── .github
└── CODEOWNERS
├── .gitignore
├── .node-version
├── .prettierrc.mjs
├── README.md
├── apps
├── api
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── cli
│ ├── package.json
│ ├── src
│ │ ├── developer
│ │ │ ├── add-developer.ts
│ │ │ ├── index.ts
│ │ │ └── remove-developer.ts
│ │ ├── employee
│ │ │ ├── add-employee.ts
│ │ │ ├── edit-employee.ts
│ │ │ ├── index.ts
│ │ │ └── remove-employee.ts
│ │ ├── index.ts
│ │ ├── init.ts
│ │ └── team
│ │ │ ├── create-team.ts
│ │ │ ├── disband-team.ts
│ │ │ ├── edit-team.ts
│ │ │ ├── index.ts
│ │ │ └── list-team.ts
│ └── tsconfig.json
└── web
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── README.md
│ ├── next.config.js
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ ├── next.svg
│ └── vercel.svg
│ ├── src
│ ├── app
│ │ ├── (product)
│ │ │ └── [product]
│ │ │ │ ├── [project]
│ │ │ │ ├── _common
│ │ │ │ │ └── task-list.tsx
│ │ │ │ ├── not-found.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ ├── sprint-backlog
│ │ │ │ │ └── page.tsx
│ │ │ │ └── team
│ │ │ │ │ ├── edit
│ │ │ │ │ ├── actions.tsx
│ │ │ │ │ ├── delete-form.tsx
│ │ │ │ │ ├── form.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── page.tsx
│ │ │ │ │ ├── stats.tsx
│ │ │ │ │ └── team.tsx
│ │ │ │ ├── _common
│ │ │ │ └── empty-team.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── not-found.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── project-list.tsx
│ │ ├── (root)
│ │ │ ├── employees
│ │ │ │ ├── actions.ts
│ │ │ │ ├── add-form.tsx
│ │ │ │ ├── delete-form.tsx
│ │ │ │ ├── edit-form.tsx
│ │ │ │ ├── employee-list.tsx
│ │ │ │ ├── employee.tsx
│ │ │ │ └── page.tsx
│ │ │ └── layout.tsx
│ │ ├── actions.ts
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── initial-form.tsx
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── components
│ │ ├── common
│ │ │ ├── error-message
│ │ │ │ ├── error-message.tsx
│ │ │ │ └── index.ts
│ │ │ └── submit-button
│ │ │ │ ├── index.ts
│ │ │ │ └── submit-button.tsx
│ │ ├── feature
│ │ │ └── reset-db-form
│ │ │ │ ├── actions.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── reset-db-form.tsx
│ │ ├── global
│ │ │ └── toast
│ │ │ │ ├── index.ts
│ │ │ │ ├── toast.tsx
│ │ │ │ └── use-toast.tsx
│ │ └── layout
│ │ │ ├── breadcrumb
│ │ │ ├── breadcrumb-container.tsx
│ │ │ ├── breadcrumb.tsx
│ │ │ └── index.ts
│ │ │ ├── search-bar.tsx
│ │ │ ├── sidebar
│ │ │ ├── index.ts
│ │ │ ├── sidebar-container.tsx
│ │ │ └── sidebar.tsx
│ │ │ └── user-name.tsx
│ ├── hooks
│ │ └── index.ts
│ └── utils.ts
│ ├── tailwind.config.ts
│ ├── todo.md
│ └── tsconfig.json
├── docs
├── chatgpt-model.md
├── data-model.md
├── employee-page.png
├── model.md
├── todo.md
├── top-page.png
└── user-story.md
├── package.json
├── packages
├── config
│ ├── base.json
│ ├── build.mjs
│ ├── jest.config.js
│ ├── package.json
│ └── pnpm-lock.yaml
├── core
│ ├── index.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── src
│ │ ├── common
│ │ │ ├── command.ts
│ │ │ ├── id.ts
│ │ │ ├── index.ts
│ │ │ ├── tests
│ │ │ │ └── id.test.ts
│ │ │ └── time.ts
│ │ ├── company
│ │ │ ├── employee.ts
│ │ │ ├── employee
│ │ │ │ ├── command.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ └── tests
│ │ │ │ └── employee.test.ts
│ │ ├── index.ts
│ │ └── scrum
│ │ │ ├── artifact
│ │ │ ├── artifact.ts
│ │ │ ├── increment.ts
│ │ │ ├── index.ts
│ │ │ ├── product-backlog.ts
│ │ │ ├── sprint-backlog.ts
│ │ │ └── user-story.ts
│ │ │ ├── index.ts
│ │ │ ├── product
│ │ │ ├── command.ts
│ │ │ ├── index.ts
│ │ │ ├── product.ts
│ │ │ └── tests
│ │ │ │ └── product.test.ts
│ │ │ ├── scrum-event
│ │ │ ├── daily-scrum.ts
│ │ │ ├── index.ts
│ │ │ ├── scrum-event.ts
│ │ │ ├── sprint-planning.ts
│ │ │ ├── sprint-retrospective.ts
│ │ │ ├── sprint-review.ts
│ │ │ └── sprint.ts
│ │ │ └── team
│ │ │ ├── command.ts
│ │ │ ├── index.ts
│ │ │ ├── project.ts
│ │ │ ├── project
│ │ │ ├── command.ts
│ │ │ └── index.ts
│ │ │ ├── scrum-team.ts
│ │ │ └── tests
│ │ │ ├── project.test.ts
│ │ │ └── scrum-team.test.ts
│ └── tsconfig.json
├── gateway
│ ├── README.md
│ ├── index.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── src
│ │ ├── adapter
│ │ │ ├── .gitkeep
│ │ │ ├── cli
│ │ │ │ ├── .gitkeep
│ │ │ │ ├── employee-command.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── init-scenario-command.ts
│ │ │ │ ├── product-command.ts
│ │ │ │ ├── project-command.ts
│ │ │ │ ├── scrum-team-command.ts
│ │ │ │ └── tests
│ │ │ │ │ ├── employee-command.test.ts
│ │ │ │ │ ├── init-scenario-command.test.ts
│ │ │ │ │ ├── product-command.test.ts
│ │ │ │ │ ├── project-command.test.ts
│ │ │ │ │ └── scrum-team-command.test.ts
│ │ │ └── web
│ │ │ │ ├── .gitkeep
│ │ │ │ ├── employee-command.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── init-scenario-command.ts
│ │ │ │ ├── product-command.ts
│ │ │ │ ├── project-command.ts
│ │ │ │ ├── scrum-team-command.ts
│ │ │ │ └── tests
│ │ │ │ ├── employee-command.test.ts
│ │ │ │ ├── init-scenario-command.test.ts
│ │ │ │ ├── product-command.test.ts
│ │ │ │ ├── project-command.test.ts
│ │ │ │ └── scrum-team-command.test.ts
│ │ ├── common.ts
│ │ ├── external
│ │ │ ├── .gitkeep
│ │ │ └── lowdb
│ │ │ │ ├── .gitkeep
│ │ │ │ ├── database.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── schema.ts
│ │ ├── index.ts
│ │ ├── repository
│ │ │ ├── .gitkeep
│ │ │ └── json
│ │ │ │ ├── .gitkeep
│ │ │ │ ├── employee-repository.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── json-repository.ts
│ │ │ │ ├── product-repository.ts
│ │ │ │ ├── project-repository.ts
│ │ │ │ ├── scrum-team-repository.ts
│ │ │ │ └── tests
│ │ │ │ ├── .gitkeep
│ │ │ │ ├── employee-repository.test.ts
│ │ │ │ ├── helper
│ │ │ │ └── database.ts
│ │ │ │ ├── json-repository.test.ts
│ │ │ │ ├── product-repository.test.ts
│ │ │ │ ├── project-repository.test.ts
│ │ │ │ └── scrum-team-repository.test.ts
│ │ └── types.ts
│ └── tsconfig.json
└── use-case
│ ├── .scaffdog
│ ├── cli-scenario.md
│ ├── config.js
│ └── web-query-service.md
│ ├── index.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── src
│ ├── application
│ │ ├── query-service
│ │ │ ├── cli
│ │ │ │ ├── add-developer-query-service.ts
│ │ │ │ ├── create-scrum-team-query-service.ts
│ │ │ │ ├── disband-scrum-team-query-service.ts
│ │ │ │ ├── edit-employee-query-service.ts
│ │ │ │ ├── edit-scrum-team-query-service.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── list-scrum-team-query-service.ts
│ │ │ │ ├── remove-developer-query-service.ts
│ │ │ │ └── remove-employee-query-service.ts
│ │ │ ├── index.ts
│ │ │ └── web
│ │ │ │ ├── breadcrumb-query-service.ts
│ │ │ │ ├── employee-list-query-service.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── project-list-query-service.ts
│ │ │ │ ├── scrum-team-edit-query-service.ts
│ │ │ │ ├── scrum-team-query-service.ts
│ │ │ │ ├── sidebar-query-service.ts
│ │ │ │ ├── top-page-query-service.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ ├── scenario
│ │ │ └── init
│ │ │ │ ├── index.ts
│ │ │ │ └── scenario.ts
│ │ └── use-case
│ │ │ ├── employee
│ │ │ ├── index.ts
│ │ │ └── use-case.ts
│ │ │ ├── product
│ │ │ ├── index.ts
│ │ │ └── use-case.ts
│ │ │ ├── project
│ │ │ ├── index.ts
│ │ │ └── use-case.ts
│ │ │ └── scrum-team
│ │ │ ├── index.ts
│ │ │ └── use-case.ts
│ └── index.ts
│ └── tsconfig.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | ignorePatterns: ['**/*.js', '**/*.d.ts'],
4 | settings: {
5 | next: {
6 | rootDir: 'apps/web/',
7 | },
8 | },
9 | extends: [
10 | 'eslint:recommended',
11 | 'plugin:@typescript-eslint/recommended',
12 | 'plugin:import/recommended',
13 | 'plugin:import/typescript',
14 | // 競合を避けるため、prettierは一番最後に書く
15 | 'prettier',
16 | ],
17 | plugins: ['@typescript-eslint', 'import', 'unused-imports'],
18 | parserOptions: {
19 | ecmaVersion: 2020,
20 | sourceType: 'module',
21 | ecmaFeatures: {
22 | jsx: true,
23 | },
24 | },
25 | rules: {
26 | // 絶対パスでのモジュールの読み込みをokにする
27 | 'import/no-unresolved': 'off',
28 | // importの順番を整理する
29 | 'import/order': [
30 | 'error',
31 | {
32 | pathGroups: [
33 | {
34 | pattern: '~/**',
35 | group: 'external',
36 | position: 'after',
37 | },
38 | ],
39 | 'newlines-between': 'always',
40 | alphabetize: {
41 | order: 'asc',
42 | caseInsensitive: false,
43 | },
44 | },
45 | ],
46 | // 「import Link from 'next/link'」で引っかかるため無効化
47 | 'no-submodule-imports': 'off',
48 | // if文内のcontinueをokにする
49 | 'no-continue': 'off',
50 | // for (const a of A) を許可
51 | 'no-restricted-syntax': 'off',
52 | // console.errorを許容する
53 | 'no-console': ['error', { allow: ['info', 'warn', 'error'] }],
54 | // 未使用のimportの削除
55 | 'unused-imports/no-unused-imports': 'error',
56 | // any を ok にする
57 | '@typescript-eslint/no-explicit-any': 'off',
58 | },
59 | }
60 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | @Panda-Program-Web
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 | lerna-debug.log*
10 | .pnpm-debug.log*
11 |
12 | # Diagnostic reports (https://nodejs.org/api/report.html)
13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
14 |
15 | # Runtime data
16 | pids
17 | *.pid
18 | *.seed
19 | *.pid.lock
20 |
21 | # Directory for instrumented libs generated by jscoverage/JSCover
22 | lib-cov
23 |
24 | # Coverage directory used by tools like istanbul
25 | coverage
26 | *.lcov
27 |
28 | # nyc test coverage
29 | .nyc_output
30 |
31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
32 | .grunt
33 |
34 | # Bower dependency directory (https://bower.io/)
35 | bower_components
36 |
37 | # node-waf configuration
38 | .lock-wscript
39 |
40 | # Compiled binary addons (https://nodejs.org/api/addons.html)
41 | build/Release
42 |
43 | # Dependency directories
44 | node_modules/
45 | jspm_packages/
46 |
47 | # Snowpack dependency directory (https://snowpack.dev/)
48 | web_modules/
49 |
50 | # TypeScript cache
51 | *.tsbuildinfo
52 |
53 | # Optional npm cache directory
54 | .npm
55 |
56 | # Optional eslint cache
57 | .eslintcache
58 |
59 | # Optional stylelint cache
60 | .stylelintcache
61 |
62 | # Microbundle cache
63 | .rpt2_cache/
64 | .rts2_cache_cjs/
65 | .rts2_cache_es/
66 | .rts2_cache_umd/
67 |
68 | # Optional REPL history
69 | .node_repl_history
70 |
71 | # Output of 'npm pack'
72 | *.tgz
73 |
74 | # Yarn Integrity file
75 | .yarn-integrity
76 |
77 | # dotenv environment variable files
78 | .env
79 | .env.development.local
80 | .env.test.local
81 | .env.production.local
82 | .env.local
83 |
84 | # parcel-bundler cache (https://parceljs.org/)
85 | .cache
86 | .parcel-cache
87 |
88 | # Next.js build output
89 | .next
90 | out
91 |
92 | # Nuxt.js build / generate output
93 | .nuxt
94 | dist
95 |
96 | # Gatsby files
97 | .cache/
98 | # Comment in the public line in if your project uses Gatsby and not Next.js
99 | # https://nextjs.org/blog/next-9-1#public-directory-support
100 | # public
101 |
102 | # vuepress build output
103 | .vuepress/dist
104 |
105 | # vuepress v2.x temp and cache directory
106 | .temp
107 | .cache
108 |
109 | # Docusaurus cache and generated files
110 | .docusaurus
111 |
112 | # Serverless directories
113 | .serverless/
114 |
115 | # FuseBox cache
116 | .fusebox/
117 |
118 | # DynamoDB Local files
119 | .dynamodb/
120 |
121 | # TernJS port file
122 | .tern-port
123 |
124 | # Stores VSCode versions used for testing VSCode extensions
125 | .vscode-test
126 |
127 | # yarn v2
128 | .yarn/cache
129 | .yarn/unplugged
130 | .yarn/build-state.yml
131 | .yarn/install-state.gz
132 | .pnp.*
133 |
134 | .idea
135 | .turbo
136 | db.json
137 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 20.10.0
2 |
--------------------------------------------------------------------------------
/.prettierrc.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import("prettier").Config} */
2 | const config = {
3 | trailingComma: 'es5',
4 | tabWidth: 2,
5 | semi: false,
6 | singleQuote: true,
7 | printWidth: 120,
8 | }
9 |
10 | export default config
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # scrum
2 |
3 | このレポジトリは、スクラムのチームを管理するアプリケーションです。Web からも CLI からも動かせます。
4 |
5 | ```mermaid
6 | flowchart TD
7 | %% UI Layer
8 | subgraph "User Interfaces"
9 | CLI["CLI Application"]:::ui
10 | Web["Web Application"]:::ui
11 | end
12 |
13 | %% Application Layer
14 | subgraph "Application Layer"
15 | UC["Use-Case Layer"]:::app
16 | end
17 |
18 | %% Domain Layer
19 | subgraph "Core Domain"
20 | Core["Core Domain Layer"]:::domain
21 | end
22 |
23 | %% Infrastructure Layer
24 | subgraph "Infrastructure"
25 | Repo["Adapters/Repositories"]:::infra
26 | end
27 |
28 | %% Connections
29 | CLI -->|"command_call"| UC
30 | Web -->|"web_query"| UC
31 | UC -->|"business_logic"| Core
32 | UC -->|"persist_data"| Repo
33 | Repo -->|"data_access"| Core
34 |
35 | %% Click Events
36 | click CLI "https://github.com/kushibikimashu/scrum/tree/main/apps/cli"
37 | click Web "https://github.com/kushibikimashu/scrum/tree/main/apps/web"
38 | click Core "https://github.com/kushibikimashu/scrum/tree/main/packages/core"
39 | click UC "https://github.com/kushibikimashu/scrum/tree/main/packages/use-case"
40 | click Repo "https://github.com/kushibikimashu/scrum/tree/main/packages/use-case/src/gateway"
41 |
42 | %% Styles
43 | classDef ui fill:#a6cee3,stroke:#1f78b4,stroke-width:2px;
44 | classDef app fill:#b2df8a,stroke:#33a02c,stroke-width:2px;
45 | classDef domain fill:#fb9a99,stroke:#e31a1c,stroke-width:2px;
46 | classDef infra fill:#fdbf6f,stroke:#ff7f00,stroke-width:2px;
47 | ```
48 |
49 | ## Quick Start
50 |
51 | アプリケーションを動かす事前準備をします。
52 |
53 | ```
54 | $ pnpm install
55 | $ pnpm build
56 | ```
57 |
58 | ### CLI
59 |
60 | CLI は `pnpm scrum [command]` で動かせます。
61 |
62 | ```
63 | $ cd apps/cli/
64 | $ pnpm scrum help
65 | Usage: index [options] [command]
66 |
67 | Options:
68 | -h, --help display help for command
69 |
70 | Commands:
71 | init 最初の設定をします
72 | add-employee [options] 社員を追加します。 -m, --multiple 複数の社員を追加します
73 | edit-employee 社員の名前を変更します
74 | remove-employee 社員を削除します
75 | create-team スクラムチームを作成します
76 | list-team スクラムチームのメンバーを表示します
77 | edit-team [options] スクラムチームのPOかSMを変更します。-po, --product-owner | -sm, --scrum-master
78 | disband-team スクラムチームを解散します
79 | add-developer スクラムチームの開発者を追加します
80 | remove-developer スクラムチームから開発者を除外します
81 | help [command] display help for command
82 | ```
83 |
84 | DB を作成し、社員を追加してください。
85 |
86 | ```
87 | $ pnpm scrum init
88 | 最初の設定を開始します
89 | ? 開発するプロダクトの名前は? sample
90 | ? プロジェクト名は? sample-project
91 | 初期設定を完了しました
92 | ```
93 |
94 | ```
95 | $ pnpm scrum add-employee
96 | ? スクラムチームに参加する社員の名前は?(姓名は半角スペース区切り) foo bar
97 | 社員を登録しました: foo bar
98 | ```
99 |
100 | 社員を2名以上登録すると、`create-team` コマンドでスクラムチームを作成できます。
101 |
102 | ### API
103 |
104 | Hono を使った API サーバーでも同じユースケースを利用できます。
105 | 各エンドポイントでは zod を用いてリクエストをバリデートしています。
106 |
107 | ```bash
108 | $ cd apps/api/
109 | $ pnpm build && pnpm start
110 | ```
111 |
112 | ### Web
113 |
114 | > [!NOTE]
115 | > `db.json`が root にあれば、このファイルを削除してください。
116 | > 削除しなくても以下のコマンドは実行でき、アプリケーションも問題なく動作します。
117 | > ただ、以下の説明は db.json がない状態を想定した説明になっていますのでご注意ください。
118 |
119 | Next.js を開発者モードで動かします。
120 |
121 | ```
122 | $ cd apps/web/
123 | $ pnpm next:dev
124 | ```
125 |
126 | http://localhost:3000 にアクセスすると、以下の画面が表示されます。
127 |
128 | 
129 |
130 | プロジェクト名とプロダクト名を送信すると以下の画面に遷移します。
131 |
132 | 
133 |
134 | 社員を2名以上登録して、サイドメニューのスクラムチームのページを見たり、遊んでみてください。
135 |
--------------------------------------------------------------------------------
/apps/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@panda-project/api",
3 | "version": "1.0.0",
4 | "main": "dist/index.js",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "node ../../packages/config/build.mjs",
8 | "build": "esbuild src/index.ts --bundle --outfile=dist/index.js --platform=node --format=esm",
9 | "start": "node dist/index.js",
10 | "type-check": "tspc --noEmit"
11 | },
12 | "dependencies": {
13 | "@panda-project/gateway": "workspace:*",
14 | "@panda-project/use-case": "workspace:*",
15 | "hono": "^3.11.0",
16 | "zod": "^3.25.8"
17 | },
18 | "devDependencies": {
19 | "@hono/node-server": "^1.14.3",
20 | "@panda-project/config": "workspace:*",
21 | "@types/node": "^20.8.2"
22 | },
23 | "license": "MIT"
24 | }
25 |
--------------------------------------------------------------------------------
/apps/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@panda-project/config/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "~/*": ["./src/*"],
7 | "@/*": ["../../packages/use-case/src/*"]
8 | },
9 | "outDir": "./dist"
10 | },
11 | "include": ["src"],
12 | "exclude": ["**/*.test.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/apps/cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@panda-project/cli",
3 | "version": "1.0.0",
4 | "main": "dist/index.js",
5 | "scripts": {
6 | "scrum": "node dist/index.js",
7 | "dev": "node ../../packages/config/build.mjs",
8 | "build": "esbuild src/index.ts --bundle --outfile=dist/index.js --platform=node",
9 | "type-check": "tspc --noEmit"
10 | },
11 | "dependencies": {
12 | "@inquirer/prompts": "^3.1.2",
13 | "@panda-project/gateway": "workspace:*",
14 | "@panda-project/use-case": "workspace:*",
15 | "commander": "^11.0.0"
16 | },
17 | "devDependencies": {
18 | "@types/commander": "^2.12.2",
19 | "@types/inquirer": "^9.0.3",
20 | "@panda-project/config": "workspace:*"
21 | },
22 | "license": "MIT"
23 | }
24 |
--------------------------------------------------------------------------------
/apps/cli/src/developer/add-developer.ts:
--------------------------------------------------------------------------------
1 | import * as console from 'console'
2 |
3 | import { confirm, select } from '@inquirer/prompts'
4 | import { AddDeveloperCliCommand } from '@panda-project/gateway'
5 | import { AddDeveloperQueryService, AddDeveloperQueryServiceDto, ScrumTeamUseCase } from '@panda-project/use-case'
6 | import { Command } from 'commander'
7 |
8 | type SelectDeveloper = (arg: AddDeveloperQueryServiceDto['candidateEmployees']) => Promise<{ developerId: number }>
9 |
10 | // developer add。loop で複数 select + confirm で抜ける
11 | export const addAddDeveloperCommand = (program: Command) => {
12 | program
13 | .command('add-developer')
14 | .description('スクラムチームの開発者を追加します')
15 | .action(async () => {
16 | const selectDeveloper: SelectDeveloper = async (candidates) => {
17 | const developerId = await select({
18 | message: '追加する開発者を選択してください',
19 | choices: candidates.map((v) => ({
20 | name: `${v.id}: ${v.name}`,
21 | value: v.id,
22 | })),
23 | })
24 | return { developerId }
25 | }
26 | const continueToSelect = async () => await confirm({ message: '他の開発者を追加しますか?' })
27 |
28 | try {
29 | let shouldContinueLoop = true
30 | const i = 0
31 |
32 | while (shouldContinueLoop) {
33 | // ユーザー入力に基づいてループを終了する条件
34 | if (i >= 1 && !(await continueToSelect())) {
35 | shouldContinueLoop = false
36 | break
37 | }
38 |
39 | const { candidateEmployees } = await new AddDeveloperQueryService().exec()
40 | if (candidateEmployees.length === 0) {
41 | console.info('開発者としてスクラムチームに参加できる社員はもういません')
42 | break
43 | }
44 |
45 | const { developerId } = await selectDeveloper(candidateEmployees)
46 | const command = new AddDeveloperCliCommand(developerId)
47 | await new ScrumTeamUseCase().addDeveloper(command)
48 | }
49 |
50 | // TODO: output を作る
51 | // console.info(result)
52 | } catch (e: any) {
53 | console.error(e?.message)
54 | }
55 | })
56 | }
57 |
--------------------------------------------------------------------------------
/apps/cli/src/developer/index.ts:
--------------------------------------------------------------------------------
1 | export * from './add-developer'
2 | export * from './remove-developer'
3 |
--------------------------------------------------------------------------------
/apps/cli/src/developer/remove-developer.ts:
--------------------------------------------------------------------------------
1 | import { confirm, select } from '@inquirer/prompts'
2 | import { RemoveDeveloperCliCommand } from '@panda-project/gateway'
3 | import { RemoveDeveloperQueryService, RemoveDeveloperQueryServiceDto, ScrumTeamUseCase } from '@panda-project/use-case'
4 | import { Command } from 'commander'
5 |
6 | type SelectDeveloper = (arg: RemoveDeveloperQueryServiceDto['developers']) => Promise<{ developerId: number }>
7 |
8 | // developer remove. loop で複数 select + confirm で抜ける
9 | export const addRemoveDeveloperCommand = (program: Command) => {
10 | program
11 | .command('remove-developer')
12 | .description('スクラムチームから開発者を除外します')
13 | .action(async () => {
14 | const selectDeveloper: SelectDeveloper = async (developers) => {
15 | const developerId = await select({
16 | message: 'チームから除外する開発者を選択してください',
17 | choices: developers.map((v) => ({
18 | name: `${v.id}: ${v.name}`,
19 | value: v.id,
20 | })),
21 | })
22 | return { developerId }
23 | }
24 | const continueToSelect = async () => await confirm({ message: '他の開発者を除外しますか?' })
25 |
26 | try {
27 | let shouldContinueLoop = true
28 | const i = 0
29 |
30 | while (shouldContinueLoop) {
31 | // ユーザー入力に基づいてループを終了する条件
32 | if (i >= 1 && !(await continueToSelect())) {
33 | shouldContinueLoop = false
34 | break
35 | }
36 |
37 | const { developers } = await new RemoveDeveloperQueryService().exec()
38 | const { developerId } = await selectDeveloper(developers)
39 | const command = new RemoveDeveloperCliCommand(developerId)
40 | await new ScrumTeamUseCase().removeDeveloper(command)
41 | }
42 | } catch (e: any) {
43 | console.error(e?.message)
44 | }
45 | })
46 | }
47 |
--------------------------------------------------------------------------------
/apps/cli/src/employee/add-employee.ts:
--------------------------------------------------------------------------------
1 | import * as console from 'console'
2 |
3 | import { input } from '@inquirer/prompts'
4 | import { CreateEmployeeCliCommand, CreateMultipleEmployeeCliCommand } from '@panda-project/gateway'
5 | import { EmployeeUseCase } from '@panda-project/use-case'
6 | import { Command } from 'commander'
7 |
8 | export const addAddEmployeeCommand = (program: Command) => {
9 | program
10 | .command('add-employee')
11 | .description('社員を追加します。 -m, --multiple 複数の社員を追加します')
12 | .option('-m, --multiple', '複数の社員を登録する')
13 | .action(async (option) => {
14 | if (option.multiple) {
15 | const useInput = async () => {
16 | const employees = await input({
17 | message: '複数の社員の名前をカンマ区切りで入力してください(姓名は半角スペース区切り)',
18 | })
19 | return { employees }
20 | }
21 |
22 | try {
23 | const { employees } = await useInput()
24 | const command = new CreateMultipleEmployeeCliCommand(employees)
25 | await new EmployeeUseCase().createMultiple(command)
26 | // TODO: 後から output adapter を使う形で実装する
27 | console.info(`社員を登録しました: 合計${employees.length}名`)
28 | } catch (e: any) {
29 | console.error(e?.message)
30 | }
31 | } else {
32 | const useInput = async () => {
33 | const name = await input({
34 | message: 'スクラムチームに参加する社員の名前は?(姓名は半角スペース区切り)',
35 | })
36 | return { name }
37 | }
38 |
39 | try {
40 | const { name } = await useInput()
41 | const command = new CreateEmployeeCliCommand(name)
42 | await new EmployeeUseCase().create(command)
43 | // TODO: output portで対応する
44 | console.info(`社員を登録しました: ${name}`)
45 | } catch (e: any) {
46 | console.error(e?.message)
47 | }
48 | }
49 | })
50 | }
51 |
--------------------------------------------------------------------------------
/apps/cli/src/employee/edit-employee.ts:
--------------------------------------------------------------------------------
1 | import { input, select } from '@inquirer/prompts'
2 | import { EditEmployeeCliCommand } from '@panda-project/gateway'
3 | import { EditEmployeeQueryService, EditEmployeeQueryServiceDto, EmployeeUseCase } from '@panda-project/use-case'
4 | import { Command } from 'commander'
5 |
6 | type UserInput = (arg: EditEmployeeQueryServiceDto) => Promise<{ employeeId: number; newEmployeeName: string }>
7 |
8 | export const addEditEmployeeCommand = (program: Command) => {
9 | program
10 | .command('edit-employee')
11 | .description('社員の名前を変更します')
12 | .action(async () => {
13 | const userInput: UserInput = async (names) => {
14 | const employeeId = await select({
15 | message: '名前を変更する社員を選択してください',
16 | choices: names.map((v: { id: number; name: string }) => ({
17 | name: `${v.id}: ${v.name}`,
18 | value: v.id,
19 | })),
20 | })
21 | const employee = await input({
22 | message: '新しい名前を入力してください',
23 | })
24 | return { employeeId, newEmployeeName: employee }
25 | }
26 |
27 | try {
28 | const employees = await new EditEmployeeQueryService().exec()
29 | const { employeeId, newEmployeeName } = await userInput(employees)
30 | const command = new EditEmployeeCliCommand(employeeId, newEmployeeName)
31 | await new EmployeeUseCase().edit(command)
32 | } catch (e: any) {
33 | console.error(e?.message)
34 | }
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/apps/cli/src/employee/index.ts:
--------------------------------------------------------------------------------
1 | export * from 'src/employee/add-employee'
2 | export * from './edit-employee'
3 | export * from './remove-employee'
4 |
--------------------------------------------------------------------------------
/apps/cli/src/employee/remove-employee.ts:
--------------------------------------------------------------------------------
1 | import { select } from '@inquirer/prompts'
2 | import { RemoveEmployeeCliCommand } from '@panda-project/gateway'
3 | import { EmployeeUseCase, RemoveEmployeeQueryService } from '@panda-project/use-case'
4 | import { Command } from 'commander'
5 |
6 | import { RemoveEmployeeQueryServiceDto } from '@/application/query-service'
7 |
8 | type UserInput = (arg: RemoveEmployeeQueryServiceDto) => Promise<{ employeeId: number }>
9 |
10 | export const addRemoveEmployeeCommand = (program: Command) => {
11 | program
12 | .command('remove-employee')
13 | .description('社員を削除します')
14 | .action(async () => {
15 | const userInput: UserInput = async (employees) => {
16 | const employeeId = await select({
17 | message: '削除する社員名を選択してください',
18 | choices: employees.map((v) => ({
19 | name: `${v.id}: ${v.name}`,
20 | value: v.id,
21 | })),
22 | })
23 | return { employeeId }
24 | }
25 |
26 | try {
27 | const employees = await new RemoveEmployeeQueryService().exec()
28 | const { employeeId } = await userInput(employees)
29 | const command = new RemoveEmployeeCliCommand(employeeId)
30 | await new EmployeeUseCase().remove(command)
31 |
32 | // TODO: output port & presenter を作る
33 | const output = `社員ID ${employeeId} を削除しました`
34 | console.info(output)
35 | } catch (e: any) {
36 | console.error(e?.message)
37 | }
38 | })
39 | }
40 |
--------------------------------------------------------------------------------
/apps/cli/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createDb, dbFileExists } from '@panda-project/gateway'
2 | import { Command } from 'commander'
3 |
4 | import { addAddDeveloperCommand, addRemoveDeveloperCommand } from '~/developer'
5 | import { addAddEmployeeCommand, addEditEmployeeCommand, addRemoveEmployeeCommand } from '~/employee'
6 | import { addInitCommand } from '~/init'
7 | import { addCreateTeamCommand, addDisbandTeamCommand, addEditTeamCommand, addListTeamCommand } from '~/team'
8 |
9 | const program = new Command()
10 |
11 | type AddCommand = (program: Command) => void
12 | const commands: AddCommand[] = [
13 | addInitCommand,
14 | // employee
15 | addAddEmployeeCommand,
16 | addEditEmployeeCommand,
17 | addRemoveEmployeeCommand,
18 | // team
19 | addCreateTeamCommand,
20 | addListTeamCommand,
21 | addEditTeamCommand,
22 | addDisbandTeamCommand,
23 | // developer
24 | addAddDeveloperCommand,
25 | addRemoveDeveloperCommand,
26 | ]
27 |
28 | for (const command of commands) {
29 | command(program)
30 | }
31 |
32 | ;(async () => {
33 | if (!dbFileExists()) {
34 | await createDb()
35 | }
36 | })()
37 |
38 | program.parse(process.argv)
39 |
--------------------------------------------------------------------------------
/apps/cli/src/init.ts:
--------------------------------------------------------------------------------
1 | import { input } from '@inquirer/prompts'
2 | import { InitCliCommand } from '@panda-project/gateway'
3 | import { InitScenario } from '@panda-project/use-case'
4 | import { Command } from 'commander'
5 |
6 | export const addInitCommand = (program: Command) => {
7 | program
8 | .command('init')
9 | .description('最初の設定をします')
10 | .action(async () => {
11 | const useInput = async () => {
12 | const productName = await input({
13 | message: '開発するプロダクトの名前は?',
14 | })
15 | const projectName = await input({ message: 'プロジェクト名は?' })
16 | return { productName, projectName }
17 | }
18 |
19 | console.info('最初の設定を開始します')
20 | try {
21 | const { productName, projectName } = await useInput()
22 | const command = new InitCliCommand(productName, projectName)
23 | await new InitScenario().exec(command)
24 | console.info('初期設定を完了しました')
25 | } catch (e: any) {
26 | console.error(e?.message)
27 | }
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/apps/cli/src/team/create-team.ts:
--------------------------------------------------------------------------------
1 | import { select } from '@inquirer/prompts'
2 | import { CreateScrumTeamCliCommand } from '@panda-project/gateway'
3 | import {
4 | EditScrumTeamQueryService,
5 | EditScrumTeamQueryServiceDto,
6 | EditScrumTeamQueryServiceInput,
7 | ScrumTeamUseCase,
8 | } from '@panda-project/use-case'
9 | import { Command } from 'commander'
10 |
11 | // TODO: EditScrumTeamQueryService になってるけど、これ CreateScrumTeamQueryService じゃない?
12 |
13 | type SelectProductOwner = (
14 | arg: EditScrumTeamQueryServiceDto['candidateEmployees']
15 | ) => Promise<{ newProductOwnerId: number }>
16 | type SelectScrumMaster = (
17 | arg: EditScrumTeamQueryServiceDto['candidateEmployees']
18 | ) => Promise<{ newScrumMasterId: number }>
19 |
20 | export const addCreateTeamCommand = (program: Command) => {
21 | program
22 | .command('create-team')
23 | .description('スクラムチームを作成します')
24 | .action(async () => {
25 | const selectProductOwner: SelectProductOwner = async (candidates) => {
26 | const newProductOwnerId = await select({
27 | message: 'プロダクトオーナーを選択してください',
28 | choices: candidates.map((v) => ({
29 | name: `${v.id}: ${v.name}`,
30 | value: v.id,
31 | })),
32 | })
33 | return { newProductOwnerId }
34 | }
35 | const selectScrumMaster: SelectScrumMaster = async (candidates) => {
36 | const newScrumMasterId = await select({
37 | message: 'スクラムマスターを選択してください',
38 | choices: candidates.map((v) => ({
39 | name: `${v.id}: ${v.name}`,
40 | value: v.id,
41 | })),
42 | })
43 | return { newScrumMasterId }
44 | }
45 |
46 | try {
47 | const { candidateEmployees: productOwnerCandidates } = await new EditScrumTeamQueryService().exec()
48 | const { newProductOwnerId } = await selectProductOwner(productOwnerCandidates)
49 |
50 | const input = new EditScrumTeamQueryServiceInput([newProductOwnerId])
51 | const { candidateEmployees: scrumMasterCandidates } = await new EditScrumTeamQueryService().exec(input)
52 | const { newScrumMasterId } = await selectScrumMaster(scrumMasterCandidates)
53 |
54 | const command = new CreateScrumTeamCliCommand(newProductOwnerId, newScrumMasterId)
55 | await new ScrumTeamUseCase().create(command)
56 | } catch (e: any) {
57 | console.error(e?.message)
58 | }
59 | })
60 | }
61 |
--------------------------------------------------------------------------------
/apps/cli/src/team/disband-team.ts:
--------------------------------------------------------------------------------
1 | import { confirm } from '@inquirer/prompts'
2 | import { DisbandScrumTeamCliCommand } from '@panda-project/gateway'
3 | import { DisbandScrumTeamQueryService, ScrumTeamUseCase } from '@panda-project/use-case'
4 | import { Command } from 'commander'
5 |
6 | export const addDisbandTeamCommand = (program: Command) => {
7 | program
8 | .command('disband-team')
9 | .description('スクラムチームを解散します')
10 | .action(async () => {
11 | const answer = await confirm({
12 | message: '本当にスクラムチームを解散しますか?',
13 | })
14 | if (answer) {
15 | try {
16 | const { scrumTeamId } = await new DisbandScrumTeamQueryService().exec()
17 | const command = new DisbandScrumTeamCliCommand(scrumTeamId)
18 |
19 | await new ScrumTeamUseCase().disband(command)
20 | } catch (e: any) {
21 | console.error(e?.message)
22 | }
23 | }
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/apps/cli/src/team/edit-team.ts:
--------------------------------------------------------------------------------
1 | import { select } from '@inquirer/prompts'
2 | import { EditScrumTeamCliCommand } from '@panda-project/gateway'
3 | import { EditScrumTeamQueryService, EditScrumTeamQueryServiceDto, ScrumTeamUseCase } from '@panda-project/use-case'
4 | import { Command } from 'commander'
5 |
6 | type SelectProductOwner = (
7 | args: EditScrumTeamQueryServiceDto['candidateEmployees']
8 | ) => Promise<{ newProductOwnerId: number }>
9 | type SelectScrumMaster = (
10 | args: EditScrumTeamQueryServiceDto['candidateEmployees']
11 | ) => Promise<{ newScrumMasterId: number }>
12 |
13 | // team-edit product owner を変更する
14 | // team-edit scrum master を変更する
15 | export const addEditTeamCommand = (program: Command) => {
16 | program
17 | .command('edit-team')
18 | .description('スクラムチームのPOかSMを変更します。-po, --product-owner | -sm, --scrum-master')
19 | .option('-po, --product-owner', 'プロダクトオーナーを変更する')
20 | .option('-sm, --scrum-master', 'スクラムマスターを変更する')
21 | .action(async (option) => {
22 | if (!option.productOwner && !option.scrumMaster) {
23 | console.error('オプションを指定してください: -po, --product-owner | -sm, --scrum-master')
24 | return
25 | }
26 |
27 | if (option.productOwner) {
28 | const selectProductOwner: SelectProductOwner = async (candidates) => {
29 | const newProductOwnerId = await select({
30 | message: 'プロダクトオーナーを選択してください',
31 | choices: candidates.map((v) => ({
32 | name: `${v.id}: ${v.name}`,
33 | value: v.id,
34 | })),
35 | })
36 | return { newProductOwnerId }
37 | }
38 | try {
39 | const dto = await new EditScrumTeamQueryService().exec()
40 | const { newProductOwnerId } = await selectProductOwner(dto.candidateEmployees)
41 | const command = new EditScrumTeamCliCommand(newProductOwnerId, dto.scumMasterId, dto.developerIds)
42 | await new ScrumTeamUseCase().edit(command)
43 | } catch (e: any) {
44 | console.error(e?.message)
45 | return
46 | }
47 | }
48 |
49 | if (option.scrumMaster) {
50 | const selectScrumMaster: SelectScrumMaster = async (candidates) => {
51 | const newScrumMasterId = await select({
52 | message: 'スクラムマスターを選択してください',
53 | choices: candidates.map((v) => ({
54 | name: `${v.id}: ${v.name}`,
55 | value: v.id,
56 | })),
57 | })
58 | return { newScrumMasterId }
59 | }
60 |
61 | try {
62 | const dto = await new EditScrumTeamQueryService().exec()
63 | const { newScrumMasterId } = await selectScrumMaster(dto.candidateEmployees)
64 | const command = new EditScrumTeamCliCommand(dto.productOwnerId, newScrumMasterId, dto.developerIds)
65 | await new ScrumTeamUseCase().edit(command)
66 | } catch (e: any) {
67 | console.error(e?.message)
68 | return
69 | }
70 | }
71 | })
72 | }
73 |
--------------------------------------------------------------------------------
/apps/cli/src/team/index.ts:
--------------------------------------------------------------------------------
1 | export * from './create-team'
2 | export * from './list-team'
3 | export * from './edit-team'
4 | export * from './disband-team'
5 |
--------------------------------------------------------------------------------
/apps/cli/src/team/list-team.ts:
--------------------------------------------------------------------------------
1 | import { ListScrumTeamQueryService, ListScrumTeamPresenter } from '@panda-project/use-case'
2 | import { Command } from 'commander'
3 |
4 | export const addListTeamCommand = (program: Command) => {
5 | program
6 | .command('list-team')
7 | .description('スクラムチームのメンバーを表示します')
8 | .action(async () => {
9 | try {
10 | const result = await new ListScrumTeamQueryService().exec()
11 |
12 | const output = new ListScrumTeamPresenter().exec(result)
13 | console.info(output)
14 | } catch (e: any) {
15 | console.error(e?.message)
16 | }
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/apps/cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@panda-project/config/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "~/*": ["./src/*"],
7 | "@/*": ["../../packages/use-case/src/*"]
8 | },
9 | "outDir": "./dist"
10 | },
11 | "include": ["src"],
12 | "exclude": ["**/*.test.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/apps/web/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/apps/web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/apps/web/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/apps/web/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | }
5 |
6 | module.exports = nextConfig
7 |
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "0.1.0",
4 | "scripts": {
5 | "next:dev": "next dev",
6 | "next:build": "next build",
7 | "next:start": "next start",
8 | "lint": "next lint",
9 | "type-check": "tspc --noEmit"
10 | },
11 | "dependencies": {
12 | "@headlessui/react": "^1.7.17",
13 | "@heroicons/react": "^2.0.18",
14 | "@panda-project/gateway": "workspace:*",
15 | "@panda-project/use-case": "workspace:*",
16 | "@tailwindcss/forms": "^0.5.6",
17 | "next": "^14.0.3",
18 | "react": "18.2.0",
19 | "react-dom": "18.2.0",
20 | "zod": "^3.22.4"
21 | },
22 | "devDependencies": {
23 | "@types/node": "^20",
24 | "@types/react": "18.2.30",
25 | "@types/react-dom": "18.2.14",
26 | "autoprefixer": "^10",
27 | "eslint": "^8",
28 | "eslint-config-next": "13.5.4",
29 | "postcss": "^8",
30 | "tailwindcss": "^3",
31 | "typescript": "^5"
32 | },
33 | "license": "MIT"
34 | }
35 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/apps/web/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/src/app/(product)/[product]/[project]/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 |
3 | export default function NotFound() {
4 | return (
5 |
6 |
Not Found
7 |
Could not find requested resource
8 |
Return Home
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/apps/web/src/app/(product)/[product]/[project]/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from 'next/navigation'
2 |
3 | type Props = {
4 | params: {
5 | project: string
6 | product: string
7 | }
8 | }
9 |
10 | export default async function ProductPage({ params }: Props) {
11 | redirect(`/${params.product}/${params.project}/team`)
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/src/app/(product)/[product]/[project]/sprint-backlog/page.tsx:
--------------------------------------------------------------------------------
1 | import { ScrumTeamQueryService } from '@panda-project/use-case'
2 |
3 | import { EmptyTeam } from '~/app/(product)/[product]/_common/empty-team'
4 | import { BreadcrumbContainer } from '~/components/layout/breadcrumb'
5 |
6 | import TaskList from '../_common/task-list'
7 |
8 | export default async function SprintBacklogPage() {
9 | const { data } = await new ScrumTeamQueryService().exec()
10 |
11 | const today = new Date()
12 | const sprintStart = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 4)
13 | const sprintEnd = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
14 | const toYearMonthDay = (date: Date) => `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
15 |
16 | const prevSprintStart = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 12)
17 | const prevSprintEnd = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 5)
18 |
19 | return (
20 |
21 |
22 |
23 | {data!.scrumTeam === null ? (
24 |
{}
25 | ) : (
26 | <>
27 |
28 |
29 |
スプリント8
30 |
31 | ({toYearMonthDay(sprintStart)} ~ {toYearMonthDay(sprintEnd)})
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
スプリント7
42 |
43 | ({toYearMonthDay(prevSprintStart)} ~ {toYearMonthDay(prevSprintEnd)})
44 |
45 |
46 |
47 |
48 |
49 |
50 | >
51 | )}
52 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/apps/web/src/app/(product)/[product]/[project]/team/edit/actions.tsx:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { CreateScrumTeamWebCommand, DisbandScrumTeamWebCommand, EditScrumTeamWebCommand } from '@panda-project/gateway'
4 | import { ScrumTeamUseCase } from '@panda-project/use-case'
5 | import { redirect } from 'next/navigation'
6 | import { z } from 'zod'
7 |
8 | export const updateTeam = async (_: any, formData: FormData) => {
9 | const schema = z.object({
10 | scrumTeamId: z.string(),
11 | productOwnerId: z.string(),
12 | scrumMasterId: z.string(),
13 | developerIds: z.array(z.string()).min(0).max(10),
14 | })
15 |
16 | try {
17 | const parsed = schema.parse({
18 | scrumTeamId: formData.get('scrum-team-id'),
19 | productOwnerId: formData.get('product-owner-id'),
20 | scrumMasterId: formData.get('scrum-master-id'),
21 | developerIds: formData.getAll('developers'),
22 | })
23 |
24 | const isCreate = parsed.scrumTeamId === ''
25 | if (isCreate) {
26 | const command = new CreateScrumTeamWebCommand(parsed.productOwnerId, parsed.scrumMasterId, parsed.developerIds)
27 | await new ScrumTeamUseCase().create(command)
28 | } else {
29 | const command = new EditScrumTeamWebCommand(parsed.productOwnerId, parsed.scrumMasterId, parsed.developerIds)
30 | await new ScrumTeamUseCase().edit(command)
31 | }
32 | } catch (e: unknown) {
33 | if (e instanceof z.ZodError) {
34 | return {
35 | errors: {
36 | ...e.formErrors.fieldErrors,
37 | },
38 | }
39 | }
40 |
41 | return {
42 | errors: e instanceof Error ? [e.message] : [],
43 | }
44 | }
45 |
46 | redirect('./')
47 | }
48 |
49 | export const deleteTeam = async (prevState: any, formData: FormData) => {
50 | const schema = z.object({
51 | teamId: z.string(),
52 | })
53 |
54 | try {
55 | const parsed = schema.parse({
56 | teamId: formData.get('team-id'),
57 | })
58 |
59 | const command = new DisbandScrumTeamWebCommand(parsed.teamId)
60 | await new ScrumTeamUseCase().disband(command)
61 | } catch (e: unknown) {
62 | if (e instanceof z.ZodError) {
63 | return {
64 | errors: {
65 | type: 'error',
66 | ...e.formErrors.fieldErrors,
67 | },
68 | }
69 | }
70 |
71 | return {
72 | type: 'error',
73 | errors: e instanceof Error ? [e.message] : [],
74 | }
75 | }
76 | redirect('./')
77 | }
78 |
--------------------------------------------------------------------------------
/apps/web/src/app/(product)/[product]/[project]/team/edit/delete-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect } from 'react'
4 | import { useFormState } from 'react-dom'
5 |
6 | import { ErrorMessage } from '~/components/common/error-message'
7 | import { useToastDispatch } from '~/components/global/toast'
8 |
9 | import { deleteTeam } from './actions'
10 |
11 | const initialState: {
12 | type: null | 'success' | 'error'
13 | errors: null | {
14 | teamId: string[]
15 | }
16 | } = {
17 | type: null,
18 | errors: null,
19 | }
20 |
21 | type Props = {
22 | teamId: number
23 | }
24 |
25 | export default function DeleteForm({ teamId }: Props) {
26 | const { showToast } = useToastDispatch()
27 | const [state, action] = useFormState(deleteTeam, initialState)
28 |
29 | const onSubmit = async (formData: FormData) => {
30 | const answer = confirm('本当にチームを解散しますか?')
31 | if (answer) {
32 | await action(formData)
33 | }
34 | }
35 |
36 | useEffect(() => {
37 | if (state.type === 'error') {
38 | showToast({
39 | icon: 'error',
40 | heading: state.errors,
41 | })
42 | }
43 | }, [state, showToast])
44 |
45 | return (
46 |
47 |
56 |
57 |
58 |
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/apps/web/src/app/(product)/[product]/[project]/team/edit/page.tsx:
--------------------------------------------------------------------------------
1 | import { UserIcon } from '@heroicons/react/20/solid'
2 | import { ScrumTeamEditQueryService } from '@panda-project/use-case'
3 | import Link from 'next/link'
4 |
5 | import { BreadcrumbContainer } from '~/components/layout/breadcrumb'
6 |
7 | import DeleteForm from './delete-form'
8 | import { TeamForm } from './form'
9 |
10 | function EmployeeEmpty() {
11 | return (
12 |
13 |
17 |
18 |
19 |
20 |
21 |
社員を登録する
22 |
チームを作成するためには、社員を3名以上登録してください。
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | export default async function TeamEditPage() {
30 | const { data } = await new ScrumTeamEditQueryService().exec()
31 |
32 | if (data === null) {
33 | return サーバーエラーです
34 | }
35 |
36 | return (
37 |
38 |
39 |
40 |
41 | {data.employees.length <= 2 ? (
42 |
43 | ) : (
44 |
45 |
46 |
47 |
48 |
49 |
53 | 社員を登録する
54 |
55 |
56 |
57 |
58 | {data.scrumTeam !== null &&
}
59 |
60 | )}
61 |
62 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/apps/web/src/app/(product)/[product]/[project]/team/page.tsx:
--------------------------------------------------------------------------------
1 | import { ScrumTeamQueryService } from '@panda-project/use-case'
2 | import Link from 'next/link'
3 |
4 | import { EmptyTeam } from '~/app/(product)/[product]/_common/empty-team'
5 | import { BreadcrumbContainer } from '~/components/layout/breadcrumb'
6 |
7 | import TaskList from '../_common/task-list'
8 |
9 | import Stats from './stats'
10 | import Team from './team'
11 |
12 | export default async function TeamPage() {
13 | const { data } = await new ScrumTeamQueryService().exec()
14 |
15 | if (data === null || data.scrumTeam === null) {
16 | return (
17 |
23 | )
24 | }
25 |
26 | const { scrumTeam } = data
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {/* sidebar */}
44 |
45 |
46 |
47 | 編集する
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/apps/web/src/app/(product)/[product]/[project]/team/stats.tsx:
--------------------------------------------------------------------------------
1 | const stats = [
2 | { name: '消化済みポイント', stat: '11(61.1%)' },
3 | { name: 'キャパシティ', stat: '18' },
4 | { name: 'ベロシティ', stat: '17.8' },
5 | ]
6 |
7 | export default function Stats() {
8 | const today = new Date()
9 | const sprintStart = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 4)
10 | const sprintEnd = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
11 | const toYearMonthDay = (date: Date) => `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
12 | return (
13 |
14 |
15 |
スプリント8
16 |
17 | ({toYearMonthDay(sprintStart)} ~ {toYearMonthDay(sprintEnd)})
18 |
19 |
20 |
21 | {stats.map((item) => (
22 |
23 |
- {item.name}
24 | - {item.stat}
25 |
26 | ))}
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/apps/web/src/app/(product)/[product]/[project]/team/team.tsx:
--------------------------------------------------------------------------------
1 | import { ScrumTeamQueryServiceDto } from '@panda-project/use-case'
2 |
3 | type Props = { scrumTeam: NonNullable }
4 |
5 | type Developer = Props['scrumTeam']['developers'][number]
6 |
7 | export default function Team({ scrumTeam }: Props) {
8 | const isPoDeveloper = scrumTeam.developers.some(
9 | (developer: Developer) => developer.employeeId === scrumTeam.productOwner.employeeId
10 | )
11 | const isSmDeveloper = scrumTeam.developers.some(
12 | (developer: Developer) => developer.employeeId === scrumTeam.scrumMaster.employeeId
13 | )
14 | const filteredDevelopers = scrumTeam.developers.filter(
15 | (developer: Developer) =>
16 | developer.employeeId !== scrumTeam.productOwner.employeeId &&
17 | developer.employeeId !== scrumTeam.scrumMaster.employeeId
18 | )
19 |
20 | return (
21 |
22 | -
23 |
24 |
25 |
プロダクトオーナー{isPoDeveloper && ' / 開発者'}
26 |
27 |
28 |
29 |
{scrumTeam.productOwner.name}
30 |
31 |
32 | -
33 |
34 |
35 |
スクラムマスター{isSmDeveloper && ' / 開発者'}
36 |
37 |
38 |
39 |
{scrumTeam.scrumMaster.name}
40 |
41 |
42 |
43 | {filteredDevelopers.map((developer: Developer, i: number) => (
44 | -
45 |
50 |
51 |
{developer.name}
52 |
53 |
54 | ))}
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/apps/web/src/app/(product)/[product]/_common/empty-team.tsx:
--------------------------------------------------------------------------------
1 | import { UsersIcon } from '@heroicons/react/20/solid'
2 | import Link from 'next/link'
3 |
4 | type Props = {
5 | href: string
6 | }
7 |
8 | export function EmptyTeam({ href }: Props) {
9 | return (
10 |
11 |
15 |
16 |
17 |
18 |
19 |
スクラムチームを作成する
20 |
21 |
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/apps/web/src/app/(product)/[product]/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import SearchBar from '~/components/layout/search-bar'
4 | import { SidebarContainer } from '~/components/layout/sidebar'
5 | import UserName from '~/components/layout/user-name'
6 |
7 | export default async function Layout({ children }: { children: React.ReactNode }) {
8 | return (
9 |
10 | {/* Left Menu */}
11 |
12 |
13 | {/* Nav Bar */}
14 |
22 | {/* Main */}
23 | {children}
24 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/apps/web/src/app/(product)/[product]/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 |
3 | export default function NotFound() {
4 | return (
5 |
6 |
Not Found
7 |
Could not find requested resource
8 |
Return Home
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/apps/web/src/app/(product)/[product]/page.tsx:
--------------------------------------------------------------------------------
1 | import { ProjectList } from './project-list'
2 |
3 | export default async function ProductPage() {
4 | return (
5 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/apps/web/src/app/(root)/employees/actions.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { CreateEmployeeWebCommand, EditEmployeeWebCommand, RemoveEmployeeWebCommand } from '@panda-project/gateway'
4 | import { EmployeeUseCase } from '@panda-project/use-case'
5 | import { revalidatePath } from 'next/cache'
6 | import { z } from 'zod'
7 |
8 | export const createEmployee = async (_: any, formData: FormData) => {
9 | const schema = z.object({
10 | familyName: z.string().min(1, '1文字以上入力してください'),
11 | firstName: z.string().min(1, '1文字以上入力してください'),
12 | })
13 |
14 | try {
15 | const parsed = schema.parse({
16 | familyName: formData.get('family-name'),
17 | firstName: formData.get('first-name'),
18 | })
19 |
20 | const command = new CreateEmployeeWebCommand(parsed.familyName, parsed.firstName)
21 | await new EmployeeUseCase().create(command)
22 | revalidatePath('/employees')
23 | return { errors: null }
24 | } catch (e: unknown) {
25 | if (e instanceof z.ZodError) {
26 | return {
27 | errors: {
28 | ...e.formErrors.fieldErrors,
29 | },
30 | }
31 | }
32 |
33 | return {
34 | errors: null,
35 | }
36 | }
37 | }
38 |
39 | export const editEmployee = async (_: any, formData: FormData) => {
40 | const schema = z.object({
41 | employeeId: z.string(),
42 | familyName: z.string().min(1, '1文字以上入力してください'),
43 | firstName: z.string().min(1, '1文字以上入力してください'),
44 | })
45 |
46 | try {
47 | const parsed = schema.parse({
48 | employeeId: formData.get('employee-id'),
49 | familyName: formData.get('family-name'),
50 | firstName: formData.get('first-name'),
51 | })
52 |
53 | const command = new EditEmployeeWebCommand(
54 | Number.parseInt(parsed.employeeId, 10),
55 | parsed.familyName,
56 | parsed.firstName
57 | )
58 | await new EmployeeUseCase().edit(command)
59 | revalidatePath('/employees')
60 | return { errors: null }
61 | } catch (e: unknown) {
62 | if (e instanceof z.ZodError) {
63 | return {
64 | errors: {
65 | ...e.formErrors.fieldErrors,
66 | },
67 | }
68 | }
69 |
70 | return {
71 | errors: null,
72 | }
73 | }
74 | }
75 |
76 | export const deleteEmployee = async (_: any, formData: FormData) => {
77 | const schema = z.object({
78 | employeeId: z.string(),
79 | })
80 |
81 | try {
82 | const parsed = schema.parse({
83 | employeeId: formData.get('employee-id'),
84 | })
85 |
86 | const command = new RemoveEmployeeWebCommand(Number.parseInt(parsed.employeeId, 10))
87 | await new EmployeeUseCase().remove(command)
88 | revalidatePath('/employees')
89 | return { type: 'success', errors: null }
90 | } catch (e) {
91 | return { type: 'error', errors: e instanceof Error ? e.message : null }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/apps/web/src/app/(root)/employees/add-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState } from 'react'
4 | import { useFormState, useFormStatus } from 'react-dom'
5 |
6 | import { ErrorMessage } from '~/components/common/error-message'
7 | import { SubmitButton } from '~/components/common/submit-button'
8 | import { useToastDispatch } from '~/components/global/toast'
9 |
10 | import { createEmployee } from './actions'
11 |
12 | export const createEmployeeState: {
13 | errors: {
14 | familyName: string[]
15 | firstName: string[]
16 | } | null
17 | } = {
18 | errors: null,
19 | }
20 |
21 | const Submit = () => {
22 | const { pending } = useFormStatus()
23 | return
24 | }
25 |
26 | export default function AddForm() {
27 | const { showToast } = useToastDispatch()
28 |
29 | const [state, action] = useFormState(createEmployee, createEmployeeState)
30 | // action 実行時に form を reset したいのだが、まだ正式な方法がないみたい...
31 | const [familyName, setFamilyName] = useState('')
32 | const [firstName, setFirstName] = useState('')
33 | const handleSubmit = async (data: FormData) => {
34 | await action(data)
35 | setFamilyName('')
36 | setFirstName('')
37 |
38 | showToast({
39 | icon: 'success',
40 | heading: '社員を登録しました',
41 | })
42 | }
43 |
44 | return (
45 |
91 | )
92 | }
93 |
--------------------------------------------------------------------------------
/apps/web/src/app/(root)/employees/delete-form.tsx:
--------------------------------------------------------------------------------
1 | import { classNames } from '~/utils'
2 |
3 | type Props = {
4 | employeeId: number
5 | active: boolean
6 | onSubmit: (formData: FormData) => Promise
7 | }
8 |
9 | export default function DeleteForm({ employeeId, active, onSubmit }: Props) {
10 | return (
11 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/apps/web/src/app/(root)/employees/edit-form.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { useFormState, useFormStatus } from 'react-dom'
3 |
4 | import { ErrorMessage } from '~/components/common/error-message'
5 | import { SubmitButton } from '~/components/common/submit-button'
6 | import { useToastDispatch } from '~/components/global/toast'
7 |
8 | import { editEmployee } from './actions'
9 |
10 | export const editEmployeeState = {
11 | errors: null,
12 | }
13 |
14 | type Props = {
15 | employeeName: string
16 | employeeId: number
17 | onSave: () => void
18 | onCancel: () => void
19 | }
20 |
21 | function Submit() {
22 | const { pending } = useFormStatus()
23 | return
24 | }
25 |
26 | function CancelButton({ onCancel }: Pick) {
27 | return
28 | }
29 |
30 | export default function EditForm({ employeeName, employeeId, onSave, onCancel }: Props) {
31 | const { showToast } = useToastDispatch()
32 | const [state, action] = useFormState(editEmployee, editEmployeeState)
33 |
34 | const [_familyName, ...rest] = employeeName.split(' ')
35 | const [familyName, setFamilyName] = useState(_familyName as string)
36 | const [firstName, setFirstName] = useState(rest.join(' ') as string)
37 |
38 | const handleSubmit = async (data: FormData) => {
39 | await action(data)
40 | setFamilyName('')
41 | setFirstName('')
42 | onSave()
43 | showToast({
44 | icon: 'success',
45 | heading: '社員名を更新しました',
46 | })
47 | }
48 |
49 | return (
50 |
97 | )
98 | }
99 |
--------------------------------------------------------------------------------
/apps/web/src/app/(root)/employees/employee-list.tsx:
--------------------------------------------------------------------------------
1 | import { UserIcon } from '@heroicons/react/20/solid'
2 | import { EmployeeListQueryService } from '@panda-project/use-case'
3 |
4 | import { assertDefined } from '~/utils'
5 |
6 | import Employee from './employee'
7 |
8 | function EmployeeEmpty() {
9 | return (
10 |
11 |
12 |
13 |
14 |
社員がまだ登録されていません
15 |
社員の名前を登録してください。
16 |
17 | )
18 | }
19 |
20 | export default async function EmployeeList() {
21 | const { data } = await new EmployeeListQueryService().exec()
22 | assertDefined(data?.employees)
23 |
24 | return (
25 |
26 |
社員一覧
27 |
28 |
29 | {data.employees.length === 0 ? (
30 |
31 | ) : (
32 |
33 | {data.employees.map((employee, i) => (
34 |
35 | ))}
36 |
37 | )}
38 |
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/apps/web/src/app/(root)/employees/page.tsx:
--------------------------------------------------------------------------------
1 | import AddForm from './add-form'
2 | import EmployeeList from './employee-list'
3 |
4 | export default async function EmployeesPage() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/apps/web/src/app/(root)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { SidebarContainer } from '~/components/layout/sidebar'
2 | import UserName from '~/components/layout/user-name'
3 |
4 | export default async function Layout({ children }: { children: React.ReactNode }) {
5 | return (
6 |
7 |
8 |
9 |
10 | {/* Nav Bar */}
11 |
18 | {/* Main */}
19 |
{children}
20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/apps/web/src/app/actions.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { InitWebCommand } from '@panda-project/gateway'
4 | import { InitScenario } from '@panda-project/use-case'
5 | import { redirect } from 'next/navigation'
6 | import { z } from 'zod'
7 |
8 | export const createProductAndProject = async (prevState: any, formData: FormData) => {
9 | const validation = z
10 | .string()
11 | .min(1, '1文字以上入力してください')
12 | .max(30, '30文字以下で入力してください')
13 | .regex(/^[a-zA-Z0-9_-]*$/, '半角英数字と-_のみ使えます')
14 | const schema = z.object({
15 | productName: validation,
16 | projectName: validation,
17 | })
18 |
19 | const productName = formData.get('product-name')
20 | const projectName = formData.get('project-name')
21 |
22 | try {
23 | const parsed = schema.parse({ productName, projectName })
24 | const command = new InitWebCommand(parsed.productName, parsed.projectName)
25 | await new InitScenario().exec(command)
26 | } catch (e) {
27 | if (e instanceof z.ZodError) {
28 | return {
29 | errors: {
30 | ...e.formErrors.fieldErrors,
31 | },
32 | }
33 | }
34 |
35 | return {
36 | errors: e instanceof Error ? [e.message] : [],
37 | }
38 | }
39 |
40 | redirect(`/employees`)
41 | }
42 |
--------------------------------------------------------------------------------
/apps/web/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | body {
12 | background: rgb(var(--background-start-rgb));
13 | }
14 |
--------------------------------------------------------------------------------
/apps/web/src/app/initial-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useFormState, useFormStatus } from 'react-dom'
4 |
5 | import { createProductAndProject } from '~/app/actions'
6 | import { ErrorMessage } from '~/components/common/error-message'
7 | import { SubmitButton } from '~/components/common/submit-button'
8 |
9 | function Submit() {
10 | const { pending } = useFormStatus()
11 |
12 | return
13 | }
14 |
15 | const initialState: {
16 | errors:
17 | | {
18 | productName: string[]
19 | projectName: string[]
20 | }
21 | | string[]
22 | | null
23 | } = {
24 | errors: null,
25 | }
26 |
27 | export function InitialForm() {
28 | const [state, action] = useFormState(createProductAndProject, initialState)
29 |
30 | return (
31 |
32 |
33 |
Scrum Management
34 |
35 | 本ソフトウェアはスクラムチームのタスク管理ツール(デモ用)です
36 |
37 |
38 |
73 |
74 | )
75 | }
76 |
--------------------------------------------------------------------------------
/apps/web/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import './globals.css'
2 | import { Inter } from 'next/font/google'
3 |
4 | import { Toast, ToastProviderContainer } from '~/components/global/toast'
5 | import { classNames } from '~/utils'
6 |
7 | const inter = Inter({ subsets: ['latin'] })
8 |
9 | export default function RootLayout({ children }: { children: React.ReactNode }) {
10 | return (
11 |
12 |
13 |
14 | {children}
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { TopPageQueryService } from '@panda-project/use-case'
2 | import { redirect } from 'next/navigation'
3 |
4 | import { InitialForm } from '~/app/initial-form'
5 |
6 | export default async function Page() {
7 | const { data } = await new TopPageQueryService().exec()
8 |
9 | if (data !== null && data.productName !== null) {
10 | redirect(`/${data.productName}`)
11 | }
12 |
13 | return (
14 |
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/src/components/common/error-message/error-message.tsx:
--------------------------------------------------------------------------------
1 | type Props = {
2 | messages: string[] | null | undefined
3 | }
4 |
5 | export function ErrorMessage({ messages }: Props) {
6 | if (messages === null || messages === undefined || messages.length === 0) {
7 | return null
8 | }
9 |
10 | return (
11 |
12 | {messages.map((message: string) => (
13 | -
14 | {message}
15 |
16 | ))}
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/apps/web/src/components/common/error-message/index.ts:
--------------------------------------------------------------------------------
1 | export * from './error-message'
2 |
--------------------------------------------------------------------------------
/apps/web/src/components/common/submit-button/index.ts:
--------------------------------------------------------------------------------
1 | export * from './submit-button'
2 |
--------------------------------------------------------------------------------
/apps/web/src/components/common/submit-button/submit-button.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonHTMLAttributes } from 'react'
2 |
3 | type Props = {
4 | label: string
5 | pending?: boolean
6 | } & ButtonHTMLAttributes
7 |
8 | export function SubmitButton({ label, pending, type = 'button', ...props }: Props) {
9 | return (
10 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/src/components/feature/reset-db-form/actions.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { resetDb } from '@panda-project/gateway'
4 |
5 | export const resetDbAction = async () => {
6 | try {
7 | await resetDb()
8 | } catch (e) {
9 | return { message: 'DBをリセットできませんでした', success: false }
10 | }
11 |
12 | return {
13 | message: 'DBをリセットしました',
14 | success: true,
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/apps/web/src/components/feature/reset-db-form/index.ts:
--------------------------------------------------------------------------------
1 | export * from './reset-db-form'
2 |
--------------------------------------------------------------------------------
/apps/web/src/components/feature/reset-db-form/reset-db-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { redirect } from 'next/navigation'
4 | import { useFormState } from 'react-dom'
5 |
6 | import { useToastDispatch } from '~/components/global/toast'
7 |
8 | import { resetDbAction } from './actions'
9 |
10 | const initialState: {
11 | message: string
12 | success: boolean | null
13 | } = {
14 | message: '',
15 | success: null,
16 | }
17 |
18 | export function ResetDbForm() {
19 | const [state, action] = useFormState(resetDbAction, initialState)
20 | const { showToast } = useToastDispatch()
21 |
22 | const handleSubmit = async () => {
23 | const answer = confirm('本当にDBをリセットしますか?')
24 | if (answer) {
25 | await action()
26 | }
27 | }
28 |
29 | if (state.success !== null) {
30 | showToast({
31 | icon: state.success ? 'success' : 'error',
32 | heading: state.message,
33 | })
34 | }
35 |
36 | if (state.success) {
37 | redirect('/')
38 | }
39 |
40 | return (
41 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/apps/web/src/components/global/toast/index.ts:
--------------------------------------------------------------------------------
1 | export * from './toast'
2 | export * from './use-toast'
3 |
--------------------------------------------------------------------------------
/apps/web/src/components/global/toast/toast.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Transition } from '@headlessui/react'
4 | import { CheckCircleIcon, ExclamationCircleIcon, XCircleIcon } from '@heroicons/react/20/solid'
5 | import { Fragment } from 'react'
6 |
7 | import { useToastDispatch, useToastState } from './use-toast'
8 |
9 | // https://tailwindui.com/components/application-ui/overlays/notifications
10 | export function Toast() {
11 | const { show, icon, heading, description } = useToastState()
12 | const { hideToast } = useToastDispatch()
13 |
14 | return (
15 | <>
16 | {/* Global Toast live region, render this permanently at the end of the document */}
17 |
21 |
22 | {/* Toast panel, dynamically insert this into the live region when it needs to be displayed */}
23 |
33 |
34 |
35 |
36 |
37 | {icon === 'success' && }
38 | {icon === 'error' && }
39 |
40 |
41 |
{heading}
42 | {description &&
{description}
}
43 |
44 |
45 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | >
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/apps/web/src/components/layout/breadcrumb/breadcrumb-container.tsx:
--------------------------------------------------------------------------------
1 | import { BreadcrumbQueryService } from '@panda-project/use-case'
2 |
3 | import Breadcrumb from './breadcrumb'
4 |
5 | type Props = {
6 | items?: LinkItem[]
7 | current: CurrentItem
8 | }
9 |
10 | type LinkItem = {
11 | name: string
12 | path: string
13 | }
14 |
15 | type CurrentItem = {
16 | name: string
17 | }
18 |
19 | export async function BreadcrumbContainer(props: Props) {
20 | const { data } = await new BreadcrumbQueryService().exec()
21 |
22 | if (!data) {
23 | return パンくずを表示できません
24 | }
25 |
26 | const { productName, projectName } = data
27 | const linkItems: LinkItem[] = [
28 | { name: productName, path: `/${productName}` },
29 | { name: projectName, path: `/${productName}/${projectName}` },
30 | ...(props.items?.map((item) => ({
31 | name: item.name,
32 | path: `/${productName}/${projectName}${item.path}`,
33 | })) ?? []),
34 | ]
35 |
36 | return
37 | }
38 |
--------------------------------------------------------------------------------
/apps/web/src/components/layout/breadcrumb/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { ChevronRightIcon } from '@heroicons/react/20/solid'
4 | import Link from 'next/link'
5 | import { ComponentProps } from 'react'
6 |
7 | import { BreadcrumbContainer } from './breadcrumb-container'
8 |
9 | type Props = Required>
10 |
11 | export default function Breadcrumb({ items, current }: Props) {
12 | return (
13 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/apps/web/src/components/layout/breadcrumb/index.ts:
--------------------------------------------------------------------------------
1 | export * from './breadcrumb-container'
2 |
--------------------------------------------------------------------------------
/apps/web/src/components/layout/search-bar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'
4 |
5 | import { useUnimplemented } from '~/hooks'
6 |
7 | export default function SearchBar() {
8 | const onChange = useUnimplemented()
9 |
10 | return (
11 |
12 |
15 |
16 |
20 |
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/apps/web/src/components/layout/sidebar/index.ts:
--------------------------------------------------------------------------------
1 | export * from './sidebar-container'
2 |
--------------------------------------------------------------------------------
/apps/web/src/components/layout/sidebar/sidebar-container.tsx:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { SidebarQueryService } from '@panda-project/use-case'
4 |
5 | import Sidebar from './sidebar'
6 |
7 | export async function SidebarContainer() {
8 | const { data } = await new SidebarQueryService().exec()
9 |
10 | if (!data) {
11 | return sidebar を表示できません
12 | }
13 |
14 | return
15 | }
16 |
--------------------------------------------------------------------------------
/apps/web/src/components/layout/user-name.tsx:
--------------------------------------------------------------------------------
1 | import { UserIcon } from '@heroicons/react/24/outline'
2 |
3 | export default function UserName() {
4 | return (
5 |
6 | Your profile
7 |
8 | 田中 太郎
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/apps/web/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | import { useToastDispatch } from '~/components/global/toast'
2 |
3 | // TODO: ファイルに切り出す
4 | export function useUnimplemented() {
5 | const { showToast } = useToastDispatch()
6 |
7 | return () => {
8 | showToast({
9 | icon: 'error',
10 | heading: 'Unimplemented',
11 | description: 'この機能は実装されていません',
12 | })
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/apps/web/src/utils.ts:
--------------------------------------------------------------------------------
1 | export function assertDefined(value: T): asserts value is NonNullable {
2 | if (value === null) {
3 | throw new Error(`arg is null`)
4 | }
5 | }
6 |
7 | export function assertIsString(arg: T | string): asserts arg is string {
8 | if (typeof arg === 'string') {
9 | throw new Error(`arg is not string`)
10 | }
11 | }
12 |
13 | export function classNames(...classes: any[]) {
14 | return classes.filter(Boolean).join(' ')
15 | }
16 |
--------------------------------------------------------------------------------
/apps/web/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
7 | ],
8 | theme: {
9 | extend: {
10 | backgroundImage: {
11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
12 | 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
13 | },
14 | },
15 | },
16 | plugins: [require('@tailwindcss/forms')],
17 | }
18 |
--------------------------------------------------------------------------------
/apps/web/todo.md:
--------------------------------------------------------------------------------
1 | ## 目標
2 |
3 | CLI からでも Web からでも操作できるクリーンアーキテクチャの実験。余力があればさらに、デスクトップAppも作ってもいい
4 | 「スクラムを管理するWeb App」というリリースのための個人開発ではなく、クリーンアーキテクチャとDDDの学習のための個人開発。
5 | リリースのためなら、Spabase + GraphQLを使いたい
6 |
7 | ## ゴール
8 |
9 | ゴールは、ローカルの DB を操作し、CLIと同じことができるようにすること
10 |
11 | やらないこと
12 |
13 | - バックログの作成と操作。cli と同じ操作だけできるようにする
14 | - 画面も作り込まず、最低限にする
15 | - i18n
16 |
17 | -
18 |
19 | ## 方針
20 |
21 | - form の submit と data fetch するだけの web app。
22 | - API は Next.js の API(/api) をコールするだけにする
23 | - v13 での form の submit、data fetch の仕方、api の作り方を調べる
24 |
25 | ## 期間
26 |
27 | 2w くらい。1w は Next.js の学習に当てる必要があるため。ChatGPTが Next.js v13を知らないので自分の学習に時間がかかってしまう。
28 |
29 | ## TODO
30 |
31 | - [ ] DB を作成する(存在チェックはしない)
32 | - [ ] プロダクト名、プロジェクト名を入力する
33 | - [ ] 社員を登録する(複数)
34 | - [ ] 社員の名前を編集する
35 | - [ ] 社員を削除する
36 | - [ ] スクラムチームを作成する
37 | - [ ] スクラムチームのメンバーを表示する
38 | - [ ] スクラムチームのメンバーを変更する(PO, SM, 開発者)
39 | - [ ] 開発者を追加する
40 | - [ ] 開発者を除外する
41 | - [ ] スクラムチームを解散する
42 |
43 | ### todo - page
44 |
45 | product page
46 |
47 | - [x] DBを削除する(データをリセットする)ボタンを作成する
48 | - [x] 社員を追加するボタンを作成
49 |
50 | employees page
51 |
52 | - [x] 社員を追加できる
53 | - [x] 社員の名前を変更できる
54 | - [x] 社員を削除できる
55 |
56 | project page
57 |
58 | - [ ] スクラムチームを作成する
59 | - [ ] スクラムチームのメンバーを表示する
60 |
61 | ### bug fix
62 |
63 | - 社員1 を削除できない
64 | - 開発チームに登録されている社員を削除できないようにする
65 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "~/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/docs/chatgpt-model.md:
--------------------------------------------------------------------------------
1 | ```mermaid
2 | classDiagram
3 |
4 | class ScrumTeam {
5 | +ProductOwner productOwner
6 | +ScrumMaster scrumMaster
7 | +Developer[] developers
8 | +Sprint[] sprints
9 | }
10 |
11 | class Product {
12 | +ProductBacklog productBacklog
13 | +Increment[] increments
14 | +ProductGoal currentGoal
15 | }
16 |
17 | class ProductBacklog {
18 | +ProductBacklogItem[] items
19 | }
20 |
21 | class ProductBacklogItem {
22 | +description: string
23 | +status: ProductBacklogItemStatus
24 | +definitionOfDone: DefinitionOfDone
25 | }
26 |
27 | class Sprint {
28 | +SprintGoal goal
29 | +SprintBacklog sprintBacklog
30 | +Increment increment
31 | +duration: SprintDuration
32 | }
33 |
34 | class SprintBacklog {
35 | +ProductBacklogItem[] selectedItems
36 | }
37 |
38 | class Increment {
39 | +status: IncrementStatus
40 | }
41 |
42 | class ProductOwner {
43 | +establishProductGoal()
44 | +createProductBacklogItem()
45 | +sortProductBacklogItem()
46 | +adjustProductBacklog()
47 | }
48 |
49 | class ScrumMaster {
50 | +facilitateScrumEvents()
51 | +removeImpediments()
52 | }
53 |
54 | class Developer {
55 | +selectProductBacklogItemForSprint()
56 | +refineProductBacklogItem()
57 | +createIncrement()
58 | }
59 |
60 | class ProductBacklogItemStatus {
61 | <>
62 | WIP
63 | ReadyForDevelop
64 | Done
65 | }
66 |
67 | class IncrementStatus {
68 | <>
69 | Ongoing
70 | HasRegression
71 | Deployable
72 | Available
73 | }
74 |
75 | class SprintDuration {
76 | <>
77 | OneWeek
78 | TwoWeeks
79 | ThreeWeeks
80 | FourWeeks
81 | }
82 |
83 | ScrumTeam --> ProductOwner : has
84 | ScrumTeam --> ScrumMaster : has
85 | ScrumTeam --> Developer : has
86 | ScrumTeam --> Sprint : conducts
87 | Product --> ProductBacklog : has
88 | ProductBacklog --> ProductBacklogItem : contains
89 | Sprint --> SprintBacklog : has
90 | SprintBacklog --> ProductBacklogItem : selects
91 | Sprint --> Increment : produces
92 | Product --> Increment : accumulates
93 | ```
94 |
--------------------------------------------------------------------------------
/docs/data-model.md:
--------------------------------------------------------------------------------
1 | テーブル定義のためのモデリング
2 |
3 | ```mermaid
4 | %% https://qiita.com/ramuneru/items/32fbf3032b625f71b69d
5 | %% 関係性は今は適当に書いてる
6 |
7 | erDiagram
8 | products {
9 | id int
10 | name string
11 | }
12 |
13 | projects {
14 | id int
15 | name string
16 |
17 | product_id int
18 | scrum_team_id int "nullable"
19 | }
20 | projects ||--|| products: ""
21 | projects ||--|| scrum_teams: ""
22 |
23 | employees {
24 | id int
25 | first_name string
26 | family_name string
27 | }
28 |
29 | members {
30 | employee_id int
31 | %% department id
32 | }
33 | members ||--|| employees: ""
34 |
35 | scrum_member_roles {
36 | product_owner int "1"
37 | scrum_master int "2"
38 | developer int "3"
39 | }
40 |
41 | scrum_members {
42 | member_id int
43 | scrum_member_roles_id int
44 | }
45 | scrum_members ||--|| members: ""
46 | scrum_members ||--|| scrum_member_roles: ""
47 |
48 | scrum_teams {
49 | id int
50 |
51 | product_backlog_id int "nullable"
52 | }
53 |
54 | product_owners {
55 | scrum_team_id int
56 | product_owner_id int "relation -> Member"
57 | }
58 | product_owners ||--|| scrum_teams : ""
59 | product_owners ||--|| members : ""
60 |
61 | scrum_masters {
62 | scrum_team_id int
63 | scrum_master_id int "relation -> Member"
64 | }
65 | scrum_masters ||--|| scrum_teams : ""
66 | scrum_masters ||--|| members : ""
67 |
68 | developers {
69 | scrum_team_id int
70 | developer_id int "relation -> Member"
71 | }
72 | developers ||--|| scrum_teams : ""
73 | developers ||--|| members : ""
74 |
75 | ```
76 |
--------------------------------------------------------------------------------
/docs/employee-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Panda-Program-Web/scrum/338f5bf38b8964a677e5d6d310d9972201eca8c0/docs/employee-page.png
--------------------------------------------------------------------------------
/docs/todo.md:
--------------------------------------------------------------------------------
1 | ## MVP の機能開発
2 |
3 | 自分がこのプログラムをスクラム開発するつもりで実装していく。
4 |
5 | ユースケースは cli と web で共通化できるのか?
6 |
7 | - input port と output port の意義がここにある。この port で UseCase が入出力先を意識しないようにする
8 |
9 | 集約ごとにユースケース作成 -> PHP のような言語では向かない。TSだとメソッドごとにテストファイルを分けるとよさそう
10 | 1ユースケース1集約 → メリット・デメリット両方あり
11 |
12 | フロントでは、1コンポーネント 1 fetcher にする。ページ単位でデータを取得する QueryService は作らない
13 | `EmployeeListQueryService`など、コンポーネント名 + QueryService と命名する
14 | -> 全部にcontainer を作る?
15 | -> server component だけでいいものは、client component(presentation component は不要)
16 |
17 | zod のエラーにちゃんと対処する。ここはフォームごとのエラーじゃなくて、string[] に変換して、フォームの上にエラーメッセージを出すもいいと思う。
18 |
19 | ### 11.22
20 |
21 | やること
22 |
23 | - [x] cli の use case を CliCommand + use case に置き換える(上から順番に)
24 | - [x] cli の scenario から query service を抽出する
25 |
26 | ## 11.23
27 |
28 | 細かいリファクタ
29 |
30 | - [x] EmployeeId, DeveloperId 型などを作る
31 | - [x] ID を Id 型にする
32 | - [x] コマンド名を刷新する
33 | - [x] web/query-service で page 単位のものをコンポーネント単位に変更する
34 |
35 | - web の xxx page query service を廃止する
36 |
37 | - [x] lint を入れて不要なモジュールを削除する
38 | - [x] prettier を入れる
39 |
40 | - [ ] テストを書く(全部)
41 | - [ ] Output Port について考えて決める
42 | - [x] Po, Sm, Dev の getId を getEmployeeId にリファクタする
43 |
44 | ### 完了したページ
45 |
46 | 2023.11.18
47 |
48 | - [ ] チームページ(/team)
49 | - [x] チーム編集ページ(/team/edit)
50 | - [x] 社員一覧ページ(/employees)
51 |
52 | コンポーネント
53 |
54 | ---
55 |
56 | - [ ] esbuild watch のログを出す
57 |
58 | - [ ] UseCase に応じてパッケージを分ける
59 | - web -> Next.js
60 | - パワプロっぽくなるかも?「コマンドが一覧になっている。時間が経つとコマンドが変わる。コマンドを選択すると時間が経つ」
61 | - CLI -> 何かしらのツール
62 | - 保存先はローカルファイル
63 |
--------------------------------------------------------------------------------
/docs/top-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Panda-Program-Web/scrum/338f5bf38b8964a677e5d6d310d9972201eca8c0/docs/top-page.png
--------------------------------------------------------------------------------
/docs/user-story.md:
--------------------------------------------------------------------------------
1 | ユーザーストーリー
2 |
3 | ```mermaid
4 | classDiagram
5 | class BasicItem {
6 | +title : string
7 | +description : string
8 | +status : BasicItemStatusType
9 | (他には、ストーリーポイントとか担当者とかを持つ)
10 | }
11 |
12 |
13 | class Epic {
14 | +item : BasicItem
15 | +definitionOfDone : DefinitionOfDone
16 | ユーザーストーリー形式\n(誰が、何をできる。なぜならば〜からだ)
17 | (ユーザーにとっての価値)
18 | }
19 | Epic --> BasicItem
20 |
21 | class Feature {
22 | +item : BasicItem
23 | +definitionOfDone : DefinitionOfDone
24 | ユーザーストーリー形式\n(誰が、何をできる。なぜならば〜からだ)
25 | (epicよりは小さいが、実装できるほど細かくはない)
26 | }
27 | Feature --> BasicItem22
28 |
29 | class Story {
30 | +item : BasicItem
31 | +definitionOfDone : DefinitionOfDone
32 | ユーザーストーリー形式\n(誰が、何をできる。なぜならば〜からだ)
33 | (実装できるくらいに小さくする)
34 | }
35 | Story --> BasicItem
36 |
37 | class Task {
38 | +item : BasicItem
39 | 作業形式\n(〜を調査する 。〜を実装する)
40 | (実装できるくらいに小さくする)
41 | }
42 | Task --> BasicItem
43 |
44 | class ProductBacklogItem {
45 | +id : number
46 | +status : BasicItemStatusType
47 | +item : UserStory
48 | }
49 | ProductBacklogItem --> Epic
50 | ProductBacklogItem --> Feature
51 | ProductBacklogItem --> Story
52 |
53 | ProductBacklog --> ProductBacklogItem
54 | class ProductBacklog {
55 | +id : number
56 | +items : ProductBacklogItem[]
57 | (プロダクトオーナーが管理する\n並べ替えもする)
58 | }
59 |
60 | class SprintBacklogItem {
61 | +item : ImplementableItem
62 | +created : Date
63 | }
64 |
65 | SprintBacklog --> SprintBacklogItem
66 |
67 | class SprintBacklog {
68 | +id : number
69 | +items : SprintBacklogItem[]
70 | (開発者が管理する。デイリーで検査する)
71 | }
72 |
73 | SprintBacklogItem --> Story
74 | SprintBacklogItem --> Task
75 | ```
76 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scrum",
3 | "version": "0.1.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "dev": "turbo dev --parallel",
7 | "build": "turbo build",
8 | "type-check": "turbo type-check",
9 | "lint": "eslint . '*/**/*.{ts,tsx}' && turbo lint",
10 | "lint:fix": "eslint . --fix '*/**/*.{ts,tsx}'",
11 | "fmt": "prettier . --write"
12 | },
13 | "devDependencies": {
14 | "@swc/core": "^1.3.91",
15 | "@swc/jest": "^0.2.29",
16 | "@types/jest": "^29.5.5",
17 | "@typescript-eslint/eslint-plugin": "^6.12.0",
18 | "@typescript-eslint/parser": "^6.12.0",
19 | "esbuild": "^0.19.4",
20 | "eslint": "^8.54.0",
21 | "eslint-config-prettier": "^9.0.0",
22 | "eslint-plugin-import": "^2.29.0",
23 | "eslint-plugin-unused-imports": "^3.0.0",
24 | "jest": "^29.7.0",
25 | "prettier": "3.1.0",
26 | "ts-patch": "^3.0.2",
27 | "turbo": "^1.10.14",
28 | "typescript": "^5.2.2",
29 | "typescript-transform-paths": "^3.4.6"
30 | },
31 | "license": "MIT"
32 | }
33 |
--------------------------------------------------------------------------------
/packages/config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "lib": ["es2022"],
5 | "module": "ESNext",
6 | "moduleResolution": "node",
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "strict": true,
11 | "skipLibCheck": true,
12 | "noEmitOnError": true,
13 | "declaration": true,
14 | "plugins": [
15 | { "transform": "typescript-transform-paths" },
16 | { "transform": "typescript-transform-paths", "afterDeclarations": true }
17 | ]
18 | },
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/config/build.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from 'esbuild'
2 |
3 | /** @type {esbuild.BuildOptions}*/
4 | const config = {
5 | entryPoints: ['src/index.ts'],
6 | bundle: true,
7 | outfile: 'dist/index.js',
8 | platform: 'node',
9 | minify: false,
10 | logLevel: 'info',
11 | format: 'esm',
12 | }
13 |
14 | const ctx = await esbuild.context(config)
15 | await ctx.watch()
16 |
--------------------------------------------------------------------------------
/packages/config/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transform: {
3 | '^.+\\.(t|j)sx?$': '@swc/jest',
4 | },
5 | testEnvironment: 'node',
6 | extensionsToTreatAsEsm: ['.ts', '.tsx'],
7 | moduleNameMapper: {
8 | '@/(.*)$': '/src/$1',
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/packages/config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@panda-project/config",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "private": true,
6 | "publishConfig": {
7 | "access": "public"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/config/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .: {}
10 |
--------------------------------------------------------------------------------
/packages/core/index.ts:
--------------------------------------------------------------------------------
1 | export * from './src'
2 |
--------------------------------------------------------------------------------
/packages/core/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = require('@panda-project/config/jest.config.js')
2 |
3 | module.exports = config
4 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@panda-project/core",
3 | "version": "1.0.0",
4 | "main": "dist/index.js",
5 | "scripts": {
6 | "dev": "tspc --watch --emitDeclarationOnly",
7 | "build": "rm -rf dist && tspc",
8 | "type-check": "tspc --noEmit",
9 | "jest": "jest"
10 | },
11 | "dependencies": {
12 | "@panda-project/config": "workspace:*"
13 | },
14 | "private": true
15 | }
16 |
--------------------------------------------------------------------------------
/packages/core/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | dependencies:
11 | '@panda-project/config':
12 | specifier: workspace:*
13 | version: link:../config
14 |
--------------------------------------------------------------------------------
/packages/core/src/common/command.ts:
--------------------------------------------------------------------------------
1 | import { CreateProductCommand, CreateProjectCommand } from '@/scrum'
2 |
3 | export interface InitCommand {
4 | getCreateProductCommand(): CreateProductCommand
5 | getCreateProjectCommand(): CreateProjectCommand
6 | }
7 |
--------------------------------------------------------------------------------
/packages/core/src/common/id.ts:
--------------------------------------------------------------------------------
1 | export abstract class Id {
2 | constructor(public readonly value: number | null) {}
3 |
4 | toInt(): number {
5 | if (this.value === null) {
6 | throw new Error('値が null です')
7 | }
8 | return this.value
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/core/src/common/index.ts:
--------------------------------------------------------------------------------
1 | export * from './command'
2 | export * from './id'
3 | export * from './time'
4 |
--------------------------------------------------------------------------------
/packages/core/src/common/tests/id.test.ts:
--------------------------------------------------------------------------------
1 | import { Id } from '../id'
2 |
3 | class ChildClass extends Id {}
4 |
5 | describe('toInt', () => {
6 | it('should return given value', () => {
7 | const sut = new ChildClass(100)
8 | expect(sut.toInt()).toBe(100)
9 | })
10 |
11 | it('should throw an error', () => {
12 | const sut = new ChildClass(null)
13 | expect(() => {
14 | sut.toInt()
15 | }).toThrowError('値が null です')
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/packages/core/src/common/time.ts:
--------------------------------------------------------------------------------
1 | export class Duration {
2 | constructor(
3 | public readonly start: Date,
4 | public readonly end: Date
5 | ) {}
6 | }
7 |
--------------------------------------------------------------------------------
/packages/core/src/company/employee.ts:
--------------------------------------------------------------------------------
1 | import { Id } from '@/common'
2 |
3 | export class EmployeeId extends Id {
4 | constructor(public readonly value: number | null) {
5 | super(value)
6 | }
7 |
8 | static createAsNull() {
9 | return new EmployeeId(null)
10 | }
11 |
12 | equals(id: EmployeeId) {
13 | return this.value === id.value
14 | }
15 | }
16 |
17 | export class EmployeeName {
18 | constructor(
19 | public readonly firstName: string,
20 | public readonly familyName: string
21 | ) {}
22 |
23 | static createFromString(name: string): EmployeeName {
24 | if (!name.includes(' ')) {
25 | throw new Error('社員名は姓名を半角スペースで区切ってください')
26 | }
27 |
28 | const [familyName, ...rest] = name.split(' ')
29 |
30 | return new EmployeeName(rest.join(' '), familyName)
31 | }
32 |
33 | getFullName() {
34 | return `${this.familyName} ${this.firstName}`
35 | }
36 | }
37 |
38 | export class Employee {
39 | constructor(
40 | public readonly id: EmployeeId,
41 | public readonly employeeName: EmployeeName
42 | ) {}
43 |
44 | updateName(name: EmployeeName) {
45 | return new Employee(this.id, name)
46 | }
47 | }
48 |
49 | export interface EmployeeRepositoryInterface {
50 | findByIdOrFail(id: EmployeeId): Promise
51 | findAll(): Promise
52 | count(): Promise
53 | save(employee: Employee): Promise
54 | update(employee: Employee): Promise
55 | delete(employee: Employee): Promise
56 | }
57 |
58 | export class Member {
59 | constructor(
60 | public readonly employee: Employee
61 | // 部署を使うようになったら追加する
62 | // public readonly department: Department,
63 | ) {}
64 |
65 | static createFromEmployee(employee: Employee) {
66 | return new Member(employee)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/packages/core/src/company/employee/command.ts:
--------------------------------------------------------------------------------
1 | import { EmployeeName, EmployeeId } from '@panda-project/core'
2 |
3 | export interface CreateEmployeeCommand {
4 | getEmployeeName(): EmployeeName
5 | }
6 |
7 | export interface CreateMultipleEmployeeCommand {
8 | getEmployeeNames(): EmployeeName[]
9 | }
10 |
11 | export interface EditEmployeeCommand {
12 | getEmployeeId(): EmployeeId
13 | getNewEmployeeName(): EmployeeName
14 | }
15 |
16 | export interface RemoveEmployeeCommand {
17 | getEmployeeId(): EmployeeId
18 | }
19 |
--------------------------------------------------------------------------------
/packages/core/src/company/employee/index.ts:
--------------------------------------------------------------------------------
1 | export * from './command'
2 |
--------------------------------------------------------------------------------
/packages/core/src/company/index.ts:
--------------------------------------------------------------------------------
1 | export * from './employee'
2 | export * from './employee/command'
3 |
4 | export class Company {}
5 |
6 | export class Department {
7 | constructor(public readonly name: string) {}
8 | }
9 |
--------------------------------------------------------------------------------
/packages/core/src/company/tests/employee.test.ts:
--------------------------------------------------------------------------------
1 | import { EmployeeId, EmployeeName, Employee, Member } from '../employee'
2 |
3 | describe('EmployeeId', () => {
4 | describe('createAsNull', () => {
5 | it('should return self with null', () => {
6 | const actual = EmployeeId.createAsNull()
7 | expect(actual).toBeInstanceOf(EmployeeId)
8 | expect(actual.value).toBeNull()
9 | })
10 | })
11 |
12 | describe('equals', () => {
13 | it('should return true', () => {
14 | const sut = new EmployeeId(100)
15 | const target = new EmployeeId(100)
16 | expect(sut.equals(target)).toBeTruthy()
17 | })
18 |
19 | it('should return false', () => {
20 | const sut = new EmployeeId(100)
21 | const target = new EmployeeId(200)
22 | expect(sut.equals(target)).toBeFalsy()
23 | })
24 | })
25 | })
26 |
27 | describe('EmployeeName', () => {
28 | describe('createFromString', () => {
29 | it('should return EmployeeName', () => {
30 | const actual = EmployeeName.createFromString('山田 太郎')
31 | expect(actual).toBeInstanceOf(EmployeeName)
32 | expect(actual.familyName).toBe('山田')
33 | expect(actual.firstName).toBe('太郎')
34 | })
35 |
36 | it('should throw error', () => {
37 | expect(() => {
38 | EmployeeName.createFromString('山田太郎')
39 | }).toThrow('社員名は姓名を半角スペースで区切ってください')
40 | })
41 | })
42 |
43 | describe('getFullName', () => {
44 | it('should return full name', () => {
45 | const sut = new EmployeeName('太郎', '山田')
46 | expect(sut.getFullName()).toBe('山田 太郎')
47 | })
48 | })
49 | })
50 |
51 | describe('Employee', () => {
52 | describe('updateName', () => {
53 | it('should return Employee', () => {
54 | const sut = new Employee(new EmployeeId(100), new EmployeeName('太郎', '山田'))
55 | const actual = sut.updateName(new EmployeeName('花子', '鈴木'))
56 | expect(actual).toBeInstanceOf(Employee)
57 | expect(actual.id).toEqual(sut.id)
58 | expect(actual.employeeName).not.toEqual(sut.employeeName)
59 | })
60 | })
61 | })
62 |
63 | describe('Member', () => {
64 | describe('createFromEmployee', () => {
65 | it('should return Member', () => {
66 | const sut = new Employee(new EmployeeId(100), new EmployeeName('太郎', '山田'))
67 | const actual = Member.createFromEmployee(sut)
68 | expect(actual).toBeInstanceOf(Member)
69 | expect(actual.employee).toEqual(sut)
70 | })
71 | })
72 | })
73 |
--------------------------------------------------------------------------------
/packages/core/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './common'
2 | export * from './company'
3 | export * from './scrum'
4 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/artifact/artifact.ts:
--------------------------------------------------------------------------------
1 | import { Commitment } from '@/scrum/product'
2 |
3 | export interface Artifact {
4 | getCommitments(): Commitment[]
5 | }
6 |
7 | export class DefinitionOfDone implements Commitment {
8 | constructor(public readonly definition: string) {}
9 | }
10 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/artifact/increment.ts:
--------------------------------------------------------------------------------
1 | import { Artifact } from './index'
2 |
3 | import { Commitment } from '@/scrum/product'
4 |
5 | export const IncrementStatus = {
6 | Ongoing: 'ongoing',
7 | HasRegression: 'has_regression',
8 | Deployable: 'deployable',
9 | Available: 'available',
10 | } as const
11 | export type IncrementStatusType = (typeof IncrementStatus)[keyof typeof IncrementStatus]
12 |
13 | export class Increment implements Artifact {
14 | constructor(
15 | public readonly id: number,
16 | public readonly ProductBacklogId: number,
17 | public status: IncrementStatusType
18 | ) {}
19 |
20 | canDeploy(): boolean {
21 | return this.status !== IncrementStatus.HasRegression
22 | }
23 |
24 | setDeployable(): this {
25 | this.status = IncrementStatus.Deployable
26 | return this
27 | }
28 |
29 | setAvailable(): this {
30 | this.status = IncrementStatus.Available
31 | return this
32 | }
33 |
34 | setHasRegression(): this {
35 | this.status = IncrementStatus.HasRegression
36 | return this
37 | }
38 |
39 | getCommitments(): Commitment[] {
40 | return []
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/artifact/index.ts:
--------------------------------------------------------------------------------
1 | import { Commitment } from '@/scrum/product'
2 |
3 | export * from './artifact'
4 | export * from './increment'
5 | export * from './product-backlog'
6 | export * from './sprint-backlog'
7 | export * from './user-story'
8 |
9 | // TODO: scrum-event に移動させる
10 | export class SprintGoal implements Commitment {
11 | constructor(public readonly goal: string) {}
12 | }
13 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/artifact/product-backlog.ts:
--------------------------------------------------------------------------------
1 | import { Commitment, ProductGoal, ProductGoalStatus } from '../product'
2 |
3 | import { Artifact, BasicItemStatus, BasicItemStatusType, Increment, UserStory } from './index'
4 |
5 | export class ProductBacklogItem {
6 | constructor(
7 | public readonly id: number,
8 | public readonly status: BasicItemStatusType,
9 | public readonly item: UserStory
10 | ) {}
11 |
12 | canBeMovedToSprintBacklog(): boolean {
13 | return this.status === BasicItemStatus.ReadyForDevelop || this.status === BasicItemStatus.Done
14 | }
15 |
16 | hasMetDefinitionOfDone(): boolean {
17 | return this.status === BasicItemStatus.Done
18 | }
19 |
20 | meetsDefinitionOfDone(): Increment[] {
21 | // このメソッドの実装は、具体的なロジックに基づいています。
22 | return []
23 | }
24 | }
25 |
26 | export const ProductBacklogItemSortOption = {
27 | AscByPriority: 'asc_by_priority',
28 | DescByPriority: 'desc_by_priority',
29 | AscByCreated: 'asc_by_created',
30 | DescByCreated: 'desc_by_created',
31 | } as const
32 | export type ProductBacklogItemSortOptionType =
33 | (typeof ProductBacklogItemSortOption)[keyof typeof ProductBacklogItemSortOption]
34 |
35 | export class ProductBacklog implements Artifact {
36 | constructor(
37 | public readonly id: number,
38 | public readonly items: ProductBacklogItem[]
39 | ) {}
40 |
41 | add(item: ProductBacklogItem): this {
42 | this.items.push(item)
43 | return this
44 | }
45 |
46 | remove(item: ProductBacklogItem): this {
47 | const index = this.items.indexOf(item)
48 | if (index > -1) {
49 | this.items.splice(index, 1)
50 | }
51 | return this
52 | }
53 |
54 | getProductBacklogItems(): ProductBacklogItem[] {
55 | return this.items
56 | }
57 |
58 | getProductBacklogItem(id: number): ProductBacklogItem | undefined {
59 | return this.items.find((item) => item.id === id)
60 | }
61 |
62 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
63 | sort(option: ProductBacklogItemSortOptionType): this {
64 | // このメソッドの実装は、具体的なソートロジックに基づいています。
65 | return this
66 | }
67 |
68 | getCommitments(): Commitment[] {
69 | return [new ProductGoal('', ProductGoalStatus.WIP)]
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/artifact/sprint-backlog.ts:
--------------------------------------------------------------------------------
1 | import { Artifact, ImplementableItem, SprintGoal } from './index'
2 |
3 | import { Commitment } from '@/scrum/product'
4 |
5 | export class SprintBacklogItem {
6 | constructor(
7 | public readonly item: ImplementableItem,
8 | public readonly created: Date
9 | ) {}
10 | }
11 |
12 | export class SprintBacklog implements Artifact {
13 | constructor(
14 | public readonly id: number,
15 | public readonly items: SprintBacklogItem[]
16 | ) {}
17 |
18 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
19 | addItem(item: ImplementableItem): this {
20 | // このメソッドの実装は、具体的なロジックに基づいています。
21 | return this
22 | }
23 |
24 | getItems(): SprintBacklogItem[] {
25 | return this.items
26 | }
27 |
28 | getCommitments(): Commitment[] {
29 | return [new SprintGoal('')]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/artifact/user-story.ts:
--------------------------------------------------------------------------------
1 | import { DefinitionOfDone } from './index'
2 |
3 | export const BasicItemStatus = {
4 | WIP: 'wip',
5 | NeedForHelpOfProductOwner: 'need_for_help_of_product_owner',
6 | ReadyForDevelop: 'ready_for_develop',
7 | Done: 'done',
8 | } as const
9 | export type BasicItemStatusType = (typeof BasicItemStatus)[keyof typeof BasicItemStatus]
10 |
11 | export class BasicItem {
12 | constructor(
13 | public readonly title: string,
14 | public readonly description: string,
15 | public readonly status: BasicItemStatusType
16 | ) {}
17 | }
18 |
19 | export interface UserStory {}
20 | export interface ImplementableItem {}
21 |
22 | export class Epic implements UserStory {
23 | constructor(
24 | public readonly item: BasicItem,
25 | public readonly definitionOfDone: DefinitionOfDone
26 | ) {}
27 | }
28 |
29 | export class Feature implements UserStory {
30 | constructor(
31 | public readonly item: BasicItem,
32 | public readonly definitionOfDone: DefinitionOfDone
33 | ) {}
34 | }
35 |
36 | export class Story implements UserStory, ImplementableItem {
37 | constructor(
38 | public readonly item: BasicItem,
39 | public readonly definitionOfDone: DefinitionOfDone
40 | ) {}
41 | }
42 |
43 | export class Task implements ImplementableItem {
44 | constructor(public readonly item: BasicItem) {}
45 | }
46 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/index.ts:
--------------------------------------------------------------------------------
1 | export * from './artifact'
2 | export * from './product'
3 | export * from './scrum-event'
4 | export * from './team'
5 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/product/command.ts:
--------------------------------------------------------------------------------
1 | import { ProductName } from '@panda-project/core'
2 |
3 | export interface CreateProductCommand {
4 | getProductName(): ProductName
5 | }
6 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/product/index.ts:
--------------------------------------------------------------------------------
1 | export * from './command'
2 | export * from './product'
3 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/product/product.ts:
--------------------------------------------------------------------------------
1 | import { Id } from '@/common'
2 |
3 | export const ProductGoalStatus = {
4 | WIP: 'wip',
5 | Ongoing: 'ongoing',
6 | Done: 'done',
7 | Aborted: 'aborted',
8 | } as const
9 | export type ProductGoalStatusType = (typeof ProductGoalStatus)[keyof typeof ProductGoalStatus]
10 |
11 | export interface Commitment {}
12 |
13 | export class ProductGoal implements Commitment {
14 | constructor(
15 | public readonly goal: string,
16 | public readonly status: ProductGoalStatusType
17 | ) {}
18 | }
19 |
20 | export class InvalidProductNameError extends Error {
21 | constructor(message: string) {
22 | super(message)
23 | }
24 | }
25 |
26 | export class ProductId extends Id {
27 | constructor(public readonly value: number | null) {
28 | super(value)
29 | }
30 |
31 | static createAsNull() {
32 | return new ProductId(null)
33 | }
34 |
35 | equals(id: ProductId) {
36 | return this.value === id.value
37 | }
38 | }
39 |
40 | export class ProductName {
41 | constructor(public readonly value: string) {
42 | this.validate()
43 | }
44 |
45 | private validate() {
46 | if (this.value.length < 1) {
47 | throw new InvalidProductNameError('1文字以上入力してください')
48 | }
49 | }
50 | }
51 |
52 | export class Product {
53 | constructor(
54 | public readonly id: ProductId,
55 | public readonly name: ProductName
56 | ) {}
57 | }
58 |
59 | export interface ProductRepositoryInterface {
60 | fetch(): Promise
61 | findByNameOrFail(name: ProductName): Promise
62 | existsWithoutId(): Promise // CLI でしか使わないメソッドかも
63 | save(product: Product): Promise
64 | }
65 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/product/tests/product.test.ts:
--------------------------------------------------------------------------------
1 | import { ProductId, ProductName } from '../product'
2 |
3 | describe('ProductId', () => {
4 | describe('createAsNull', () => {
5 | it('should return ProductId', () => {
6 | const actual = ProductId.createAsNull()
7 | expect(actual).toBeInstanceOf(ProductId)
8 | expect(actual.value).toBeNull()
9 | })
10 | })
11 |
12 | describe('equals', () => {
13 | it('should return true', () => {
14 | const sut = new ProductId(100)
15 | const target = new ProductId(100)
16 | expect(sut.equals(target)).toBeTruthy()
17 | })
18 |
19 | it('should return false', () => {
20 | const sut = new ProductId(100)
21 | const target = new ProductId(200)
22 | expect(sut.equals(target)).toBeFalsy()
23 | })
24 | })
25 | })
26 |
27 | describe('ProductName', () => {
28 | describe('constructor', () => {
29 | it('should return ProductName', () => {
30 | const actual = new ProductName('name')
31 | expect(actual).toBeInstanceOf(ProductName)
32 | expect(actual.value).toBe('name')
33 | })
34 |
35 | it('should throw error', () => {
36 | expect(() => {
37 | new ProductName('')
38 | }).toThrow('1文字以上入力してください')
39 | })
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/scrum-event/daily-scrum.ts:
--------------------------------------------------------------------------------
1 | import { ScrumEvent, ScrumEventType, ScrumEventTypeType } from './scrum-event'
2 |
3 | import { Duration } from '@/common'
4 |
5 | export class DailyScrum implements ScrumEvent {
6 | constructor(
7 | public readonly id: number,
8 | public readonly place: string,
9 | public readonly timeBox: Duration,
10 | public readonly duration: Duration
11 | ) {}
12 |
13 | getType(): ScrumEventTypeType {
14 | return ScrumEventType.DailyScrum
15 | }
16 |
17 | getStartDate(): Date {
18 | return this.duration.start
19 | }
20 |
21 | getEndDate(): Date {
22 | return this.duration.end
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/scrum-event/index.ts:
--------------------------------------------------------------------------------
1 | export * from './daily-scrum'
2 | export * from './scrum-event'
3 | export * from './sprint-planning'
4 | export * from './sprint-retrospective'
5 | export * from './sprint-review'
6 | export * from './sprint'
7 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/scrum-event/scrum-event.ts:
--------------------------------------------------------------------------------
1 | export const ScrumEventType = {
2 | Sprint: 'sprint',
3 | SprintPlanning: 'sprint_planning',
4 | DailyScrum: 'daily_scrum',
5 | SprintReview: 'sprint_review',
6 | SprintRetrospective: 'sprint_retrospective',
7 | } as const
8 | export type ScrumEventTypeType = (typeof ScrumEventType)[keyof typeof ScrumEventType]
9 |
10 | export interface ScrumEvent {
11 | getType(): ScrumEventTypeType
12 | getStartDate(): Date
13 | getEndDate(): Date
14 | }
15 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/scrum-event/sprint-planning.ts:
--------------------------------------------------------------------------------
1 | import { ScrumEvent, ScrumEventType, ScrumEventTypeType } from './index'
2 |
3 | import { Duration } from '@/common'
4 |
5 | export class SprintPlanning implements ScrumEvent {
6 | constructor(
7 | public readonly id: number,
8 | public readonly place: string,
9 | public readonly timeBox: Duration,
10 | public readonly duration: Duration,
11 | public readonly sprintGoals: string[]
12 | ) {}
13 |
14 | getType(): ScrumEventTypeType {
15 | return ScrumEventType.SprintPlanning
16 | }
17 |
18 | getStartDate(): Date {
19 | return this.duration.start
20 | }
21 |
22 | getEndDate(): Date {
23 | return this.duration.end
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/scrum-event/sprint-retrospective.ts:
--------------------------------------------------------------------------------
1 | import { ScrumEvent, ScrumEventType, ScrumEventTypeType } from './index'
2 |
3 | import { Duration } from '@/common'
4 |
5 | export class SprintRetrospective implements ScrumEvent {
6 | constructor(
7 | public readonly id: number,
8 | public readonly place: string,
9 | public readonly timeBox: Duration,
10 | public readonly duration: Duration
11 | ) {}
12 |
13 | getType(): ScrumEventTypeType {
14 | return ScrumEventType.SprintRetrospective
15 | }
16 |
17 | getStartDate(): Date {
18 | return this.duration.start
19 | }
20 |
21 | getEndDate(): Date {
22 | return this.duration.end
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/scrum-event/sprint-review.ts:
--------------------------------------------------------------------------------
1 | import { ScrumEvent, ScrumEventType, ScrumEventTypeType } from './index'
2 |
3 | import { Duration } from '@/common'
4 | import { Member } from '@/company'
5 |
6 | export class SprintReview implements ScrumEvent {
7 | constructor(
8 | public readonly id: number,
9 | public readonly place: string,
10 | public readonly timeBox: Duration,
11 | public readonly duration: Duration,
12 | public readonly stakeholders: Member[]
13 | ) {}
14 |
15 | getType(): ScrumEventTypeType {
16 | return ScrumEventType.SprintReview
17 | }
18 |
19 | getStartDate(): Date {
20 | return this.duration.start
21 | }
22 |
23 | getEndDate(): Date {
24 | return this.duration.end
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/scrum-event/sprint.ts:
--------------------------------------------------------------------------------
1 | import { ScrumEvent, ScrumEventType, ScrumEventTypeType } from './index'
2 |
3 | import { Duration } from '@/common'
4 | import { Increment } from '@/scrum/artifact'
5 |
6 | export const SprintTimeBox = {
7 | OneWeek: 'one_week',
8 | TwoWeeks: 'two_weeks',
9 | ThreeWeeks: 'three_weeks',
10 | FourWeeks: 'four_weeks',
11 | } as const
12 |
13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
14 | class Sprint implements ScrumEvent {
15 | constructor(
16 | public readonly sprintPlanningId: number,
17 | public readonly dailyScrumIds: number[],
18 | public readonly sprintReviewId: number,
19 | public readonly sprintRetrospectiveId: number,
20 | public readonly sprintBacklogId: number,
21 | public readonly sprintGoals: string[],
22 | public readonly increments: Increment[],
23 | public readonly sprintTimeBox: (typeof SprintTimeBox)[keyof typeof SprintTimeBox],
24 | public readonly duration: Duration
25 | ) {}
26 |
27 | getType(): ScrumEventTypeType {
28 | return ScrumEventType.Sprint
29 | }
30 |
31 | getStartDate(): Date {
32 | return this.duration.start
33 | }
34 |
35 | getEndDate(): Date {
36 | return this.duration.end
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/team/command.ts:
--------------------------------------------------------------------------------
1 | import { EmployeeId, ScrumTeamId } from '@panda-project/core'
2 |
3 | export interface CreateScrumTeamCommand {
4 | getProductOwnerId(): EmployeeId
5 | getScrumMasterId(): EmployeeId
6 | getDeveloperIds(): EmployeeId[]
7 | }
8 |
9 | export interface EditScrumTeamCommand {
10 | getProductOwnerId(): EmployeeId
11 | getScrumMasterId(): EmployeeId
12 | getDeveloperIds(): EmployeeId[]
13 | }
14 |
15 | export interface AddDeveloperCommand {
16 | getDeveloperId(): EmployeeId
17 | }
18 |
19 | export interface RemoveDeveloperCommand {
20 | getDeveloperId(): EmployeeId
21 | }
22 |
23 | export interface DisbandScrumTeamCommand {
24 | getScrumTeamId(): ScrumTeamId
25 | }
26 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/team/index.ts:
--------------------------------------------------------------------------------
1 | export * from './command'
2 | export * from './project'
3 | export * from './project/command'
4 | export * from './scrum-team'
5 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/team/project.ts:
--------------------------------------------------------------------------------
1 | import { Id } from '@/common'
2 |
3 | export class InvalidProjectNameError extends Error {
4 | constructor(message: string) {
5 | super(message)
6 | }
7 | }
8 |
9 | export class ProjectId extends Id {
10 | constructor(public readonly value: number | null) {
11 | super(value)
12 | }
13 |
14 | static createAsNull() {
15 | return new ProjectId(null)
16 | }
17 |
18 | equals(id: ProjectId) {
19 | return this.value === id.value
20 | }
21 | }
22 |
23 | export class ProjectName {
24 | constructor(public readonly value: string) {
25 | this.validate()
26 | }
27 |
28 | private validate() {
29 | if (this.value.length < 1) {
30 | throw new InvalidProjectNameError('1文字以上入力してください')
31 | }
32 | }
33 | }
34 |
35 | export class Project {
36 | constructor(
37 | public readonly id: ProjectId,
38 | public readonly name: ProjectName
39 |
40 | // 必要かはわからないのでコメントアウトしておく
41 | // public readonly product: Product,
42 | // public readonly team: ScrumTeam,
43 | // public readonly sprints: Sprint[],
44 | ) {}
45 |
46 | // pickCurrentSprint(): Sprint
47 | }
48 |
49 | export interface ProjectRepositoryInterface {
50 | fetch(): Promise
51 | save(project: Project): Promise
52 | }
53 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/team/project/command.ts:
--------------------------------------------------------------------------------
1 | import { ProjectName } from '@panda-project/core'
2 |
3 | export interface CreateProjectCommand {
4 | getProjectName(): ProjectName
5 | }
6 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/team/project/index.ts:
--------------------------------------------------------------------------------
1 | export * from './command'
2 |
--------------------------------------------------------------------------------
/packages/core/src/scrum/team/tests/project.test.ts:
--------------------------------------------------------------------------------
1 | import { ProjectId, ProjectName } from '../project'
2 |
3 | describe('ProjectId', () => {
4 | describe('createAsNull', () => {
5 | it('should create as null', () => {
6 | const id = ProjectId.createAsNull()
7 | expect(id).toBeInstanceOf(ProjectId)
8 | expect(id.value).toBeNull()
9 | })
10 | })
11 |
12 | describe('equals', () => {
13 | it('should return true when the value is the same', () => {
14 | const sut = new ProjectId(1)
15 | const target = new ProjectId(1)
16 | expect(sut.equals(target)).toBeTruthy()
17 | })
18 |
19 | it('should return false when the value is different', () => {
20 | const sut = new ProjectId(1)
21 | const target = new ProjectId(2)
22 | expect(sut.equals(target)).toBeFalsy()
23 | })
24 | })
25 | })
26 |
27 | describe('ProjectName', () => {
28 | describe('constructor', () => {
29 | it('should create ProjectName', () => {
30 | const name = new ProjectName('name')
31 | expect(name).toBeInstanceOf(ProjectName)
32 | expect(name.value).toBe('name')
33 | })
34 |
35 | it('should throw error', () => {
36 | expect(() => {
37 | new ProjectName('')
38 | }).toThrow('1文字以上入力してください')
39 | })
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@panda-project/config/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["src/*"]
7 | },
8 | "outDir": "./dist"
9 | },
10 | "include": ["src"],
11 | "exclude": ["**/*.test.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/gateway/README.md:
--------------------------------------------------------------------------------
1 | // gateway パッケージの README
2 | # gateway
3 |
4 | 三層構造の Gateway 層を担うパッケージです。
5 | - Repository 実装
6 | - lowdb など外部サービスとの接続
7 |
--------------------------------------------------------------------------------
/packages/gateway/index.ts:
--------------------------------------------------------------------------------
1 | export * from './src'
2 |
--------------------------------------------------------------------------------
/packages/gateway/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | roots: ['/src'],
5 | };
6 |
--------------------------------------------------------------------------------
/packages/gateway/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@panda-project/gateway",
3 | "version": "1.0.0",
4 | "main": "src/index.ts",
5 | "types": "src/index.ts",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "tspc --emitDeclarationOnly && node ../config/build.mjs",
9 | "build": "rm -rf dist && esbuild index.ts --bundle --outfile=dist/index.js --platform=node --format=esm && tspc --emitDeclarationOnly",
10 | "type-check": "tspc --noEmit",
11 | "jest": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js"
12 | },
13 | "license": "MIT",
14 | "dependencies": {
15 | "@panda-project/config": "workspace:*",
16 | "@panda-project/core": "workspace:*",
17 | "lowdb": "6.1.1"
18 | },
19 | "devDependencies": {
20 | "@types/lowdb": "^1.0.15",
21 | "@types/node": "^20.8.2"
22 | },
23 | "private": true
24 | }
25 |
--------------------------------------------------------------------------------
/packages/gateway/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | dependencies:
11 | '@panda-project/config':
12 | specifier: workspace:*
13 | version: link:../config
14 | '@panda-project/core':
15 | specifier: workspace:*
16 | version: link:../core
17 | '@panda-project/use-case':
18 | specifier: workspace:*
19 | version: link:../use-case
20 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/.gitkeep:
--------------------------------------------------------------------------------
1 | // adapter ディレクトリ(gateway 層)
2 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/cli/.gitkeep:
--------------------------------------------------------------------------------
1 | // gateway adapter/cli ディレクトリ
2 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/cli/employee-command.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CreateEmployeeCommand,
3 | CreateMultipleEmployeeCommand,
4 | EditEmployeeCommand,
5 | EmployeeId,
6 | EmployeeName,
7 | RemoveEmployeeCommand,
8 | } from '@panda-project/core'
9 |
10 | export class CreateEmployeeCliCommand implements CreateEmployeeCommand {
11 | constructor(private readonly employeeName: string) {}
12 |
13 | getEmployeeName(): EmployeeName {
14 | return EmployeeName.createFromString(this.employeeName)
15 | }
16 | }
17 |
18 | export class CreateMultipleEmployeeCliCommand implements CreateMultipleEmployeeCommand {
19 | constructor(private readonly commaSeparatedNames: string) {}
20 |
21 | getEmployeeNames(): EmployeeName[] {
22 | return this.commaSeparatedNames
23 | .split(',')
24 | .map((name) => name.trim())
25 | .map((name) => EmployeeName.createFromString(name))
26 | }
27 | }
28 |
29 | // TODO: 後から output adapter として実装する
30 | // export class CreateMultipleEmployeeCliPresenter {
31 | // exec(dto): string {
32 | // return `社員を登録しました: ${input.count()}名`
33 | // }
34 | // }
35 |
36 | export class EditEmployeeCliCommand implements EditEmployeeCommand {
37 | constructor(
38 | private readonly employeeId: number,
39 | private readonly newEmployeeName: string
40 | ) {}
41 |
42 | getEmployeeId(): EmployeeId {
43 | return new EmployeeId(this.employeeId)
44 | }
45 |
46 | getNewEmployeeName(): EmployeeName {
47 | return EmployeeName.createFromString(this.newEmployeeName)
48 | }
49 | }
50 |
51 | export class RemoveEmployeeCliCommand implements RemoveEmployeeCommand {
52 | constructor(private readonly employeeId: number) {}
53 |
54 | getEmployeeId(): EmployeeId {
55 | return new EmployeeId(this.employeeId)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/cli/index.ts:
--------------------------------------------------------------------------------
1 | export * from './employee-command'
2 | export * from './init-scenario-command'
3 | export * from './product-command'
4 | export * from './project-command'
5 | export * from './scrum-team-command'
6 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/cli/init-scenario-command.ts:
--------------------------------------------------------------------------------
1 | import { CreateProductCliCommand } from './product-command'
2 | import { CreateProjectCliCommand } from './project-command'
3 |
4 | import { CreateProductCommand, CreateProjectCommand, InitCommand } from '@panda-project/core'
5 |
6 | export class InitCliCommand implements InitCommand {
7 | constructor(
8 | private readonly productName: string,
9 | private readonly projectName: string
10 | ) {}
11 |
12 | getCreateProductCommand(): CreateProductCommand {
13 | return new CreateProductCliCommand(this.productName)
14 | }
15 |
16 | getCreateProjectCommand(): CreateProjectCommand {
17 | return new CreateProjectCliCommand(this.projectName)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/cli/product-command.ts:
--------------------------------------------------------------------------------
1 | import { CreateProductCommand, ProductName } from '@panda-project/core'
2 |
3 | export class CreateProductCliCommand implements CreateProductCommand {
4 | constructor(private readonly productName: string) {}
5 |
6 | getProductName(): ProductName {
7 | return new ProductName(this.productName)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/cli/project-command.ts:
--------------------------------------------------------------------------------
1 | import { CreateProjectCommand, ProjectName } from '@panda-project/core'
2 |
3 | export class CreateProjectCliCommand implements CreateProjectCommand {
4 | constructor(private readonly projectName: string) {}
5 |
6 | getProjectName(): ProjectName {
7 | return new ProjectName(this.projectName)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/cli/scrum-team-command.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AddDeveloperCommand,
3 | CreateScrumTeamCommand,
4 | DisbandScrumTeamCommand,
5 | EditScrumTeamCommand,
6 | EmployeeId,
7 | RemoveDeveloperCommand,
8 | ScrumTeamId,
9 | } from '@panda-project/core'
10 | export class CreateScrumTeamCliCommand implements CreateScrumTeamCommand {
11 | constructor(
12 | private readonly productOwnerId: number,
13 | private readonly scrumMasterId: number
14 | ) {}
15 |
16 | getProductOwnerId(): EmployeeId {
17 | return new EmployeeId(this.productOwnerId)
18 | }
19 |
20 | getScrumMasterId(): EmployeeId {
21 | return new EmployeeId(this.scrumMasterId)
22 | }
23 |
24 | getDeveloperIds(): EmployeeId[] {
25 | // CLI の場合、チーム作成時に開発者は指定しない
26 | return []
27 | }
28 | }
29 |
30 | export class EditScrumTeamCliCommand implements EditScrumTeamCommand {
31 | constructor(
32 | private readonly productOwnerId: number,
33 | private readonly scrumMasterId: number,
34 | private readonly developerIds: number[]
35 | ) {}
36 |
37 | getProductOwnerId(): EmployeeId {
38 | return new EmployeeId(this.productOwnerId)
39 | }
40 |
41 | getScrumMasterId(): EmployeeId {
42 | return new EmployeeId(this.scrumMasterId)
43 | }
44 |
45 | getDeveloperIds(): EmployeeId[] {
46 | return this.developerIds.map((id) => new EmployeeId(id))
47 | }
48 | }
49 |
50 | export class AddDeveloperCliCommand implements AddDeveloperCommand {
51 | constructor(private readonly developerId: number) {}
52 |
53 | getDeveloperId(): EmployeeId {
54 | return new EmployeeId(this.developerId)
55 | }
56 | }
57 |
58 | export class RemoveDeveloperCliCommand implements RemoveDeveloperCommand {
59 | constructor(private readonly developerId: number) {}
60 |
61 | getDeveloperId(): EmployeeId {
62 | return new EmployeeId(this.developerId)
63 | }
64 | }
65 |
66 | export class DisbandScrumTeamCliCommand implements DisbandScrumTeamCommand {
67 | constructor(private readonly scrumTeamId: number) {}
68 |
69 | getScrumTeamId(): ScrumTeamId {
70 | return new ScrumTeamId(this.scrumTeamId)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/cli/tests/employee-command.test.ts:
--------------------------------------------------------------------------------
1 | import { EmployeeId } from '@panda-project/core'
2 |
3 | import {
4 | CreateEmployeeCliCommand,
5 | CreateMultipleEmployeeCliCommand,
6 | EditEmployeeCliCommand,
7 | RemoveEmployeeCliCommand,
8 | } from '../employee-command'
9 |
10 | describe('CreateEmployeeCliCommand', () => {
11 | test('getEmployeeName', () => {
12 | const sut = new CreateEmployeeCliCommand('社員 一号')
13 | expect(sut.getEmployeeName().getFullName()).toBe('社員 一号')
14 | })
15 | })
16 |
17 | describe('CreateMultipleEmployeeCliCommand', () => {
18 | test('getEmployeeNames', () => {
19 | const sut = new CreateMultipleEmployeeCliCommand('社員 一号, 社員 二号')
20 | expect(sut.getEmployeeNames().map((name) => name.getFullName())).toEqual(['社員 一号', '社員 二号'])
21 | })
22 | })
23 |
24 | describe('EditEmployeeCliCommand', () => {
25 | test('getEmployeeId', () => {
26 | const sut = new EditEmployeeCliCommand(1, '社員 一号')
27 | expect(sut.getEmployeeId()).toEqual(new EmployeeId(1))
28 | })
29 |
30 | test('getNewEmployeeName', () => {
31 | const sut = new EditEmployeeCliCommand(1, '社員 一号')
32 | expect(sut.getNewEmployeeName().getFullName()).toBe('社員 一号')
33 | })
34 | })
35 |
36 | describe('RemoveEmployeeCliCommand', () => {
37 | test('getEmployeeId', () => {
38 | const sut = new RemoveEmployeeCliCommand(1)
39 | expect(sut.getEmployeeId()).toEqual(new EmployeeId(1))
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/cli/tests/init-scenario-command.test.ts:
--------------------------------------------------------------------------------
1 | import { ProductName, ProjectName } from '@panda-project/core'
2 |
3 | import { InitCliCommand } from '../init-scenario-command'
4 |
5 | describe('InitCliCommand', () => {
6 | test('getCreateProductCommand', () => {
7 | const sut = new InitCliCommand('プロダクト', 'プロジェクト')
8 | expect(sut.getCreateProductCommand().getProductName()).toEqual(new ProductName('プロダクト'))
9 | })
10 |
11 | test('getCreateProjectCommand', () => {
12 | const sut = new InitCliCommand('プロジェクト', 'プロジェクト')
13 | expect(sut.getCreateProjectCommand().getProjectName()).toEqual(new ProjectName('プロジェクト'))
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/cli/tests/product-command.test.ts:
--------------------------------------------------------------------------------
1 | import { ProductName } from '@panda-project/core'
2 |
3 | import { CreateProductCliCommand } from '../product-command'
4 |
5 | describe('CreateProductCliCommand', () => {
6 | test('getProductName', () => {
7 | const sut = new CreateProductCliCommand('プロダクト')
8 | expect(sut.getProductName()).toEqual(new ProductName('プロダクト'))
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/cli/tests/project-command.test.ts:
--------------------------------------------------------------------------------
1 | import { ProjectName } from '@panda-project/core'
2 |
3 | import { CreateProjectCliCommand } from '../project-command'
4 |
5 | describe('CreateProjectCliCommand', () => {
6 | test('getProjectName', () => {
7 | const sut = new CreateProjectCliCommand('プロジェクト')
8 | expect(sut.getProjectName()).toEqual(new ProjectName('プロジェクト'))
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/cli/tests/scrum-team-command.test.ts:
--------------------------------------------------------------------------------
1 | import { EmployeeId, ScrumTeamId } from '@panda-project/core'
2 |
3 | import {
4 | CreateScrumTeamCliCommand,
5 | EditScrumTeamCliCommand,
6 | AddDeveloperCliCommand,
7 | RemoveDeveloperCliCommand,
8 | DisbandScrumTeamCliCommand,
9 | } from '../scrum-team-command'
10 |
11 | describe('CreateScrumTeamCliCommand', () => {
12 | const sut = new CreateScrumTeamCliCommand(1, 2)
13 |
14 | test('getProductOwnerId', () => {
15 | expect(sut.getProductOwnerId()).toEqual(new EmployeeId(1))
16 | })
17 |
18 | test('getScrumMasterId', () => {
19 | expect(sut.getScrumMasterId()).toEqual(new EmployeeId(2))
20 | })
21 |
22 | test('getDeveloperIds', () => {
23 | expect(sut.getDeveloperIds()).toEqual([])
24 | })
25 | })
26 |
27 | describe('EditScrumTeamCliCommand', () => {
28 | const sut = new EditScrumTeamCliCommand(1, 2, [3])
29 |
30 | test('getProductOwnerId', () => {
31 | expect(sut.getProductOwnerId()).toEqual(new EmployeeId(1))
32 | })
33 |
34 | test('getScrumMasterId', () => {
35 | expect(sut.getScrumMasterId()).toEqual(new EmployeeId(2))
36 | })
37 |
38 | test('getDeveloperIds', () => {
39 | expect(sut.getDeveloperIds()).toEqual([new EmployeeId(3)])
40 | })
41 | })
42 |
43 | describe('AddDeveloperCliCommand', () => {
44 | const sut = new AddDeveloperCliCommand(1)
45 |
46 | test('getDeveloperId', () => {
47 | expect(sut.getDeveloperId()).toEqual(new EmployeeId(1))
48 | })
49 | })
50 |
51 | describe('RemoveDeveloperCliCommand', () => {
52 | const sut = new RemoveDeveloperCliCommand(1)
53 |
54 | test('getDeveloperId', () => {
55 | expect(sut.getDeveloperId()).toEqual(new EmployeeId(1))
56 | })
57 | })
58 |
59 | describe('DisbandScrumTeamCliCommand', () => {
60 | const sut = new DisbandScrumTeamCliCommand(1)
61 |
62 | test('getDeveloperId', () => {
63 | expect(sut.getScrumTeamId()).toEqual(new ScrumTeamId(1))
64 | })
65 | })
66 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/web/.gitkeep:
--------------------------------------------------------------------------------
1 | // gateway adapter/web ディレクトリ
2 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/web/employee-command.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CreateEmployeeCommand,
3 | EditEmployeeCommand,
4 | EmployeeId,
5 | EmployeeName,
6 | RemoveEmployeeCommand,
7 | } from '@panda-project/core'
8 |
9 | export class CreateEmployeeWebCommand implements CreateEmployeeCommand {
10 | constructor(
11 | private readonly familyName: string,
12 | private readonly firstName: string
13 | ) {}
14 |
15 | getEmployeeName(): EmployeeName {
16 | return new EmployeeName(this.firstName, this.familyName)
17 | }
18 | }
19 |
20 | export class EditEmployeeWebCommand implements EditEmployeeCommand {
21 | constructor(
22 | private readonly employeeId: number,
23 | private readonly newFamilyName: string,
24 | private readonly newFirstName: string
25 | ) {}
26 |
27 | getEmployeeId(): EmployeeId {
28 | return new EmployeeId(this.employeeId)
29 | }
30 |
31 | getNewEmployeeName(): EmployeeName {
32 | return new EmployeeName(this.newFirstName, this.newFamilyName)
33 | }
34 | }
35 |
36 | export class RemoveEmployeeWebCommand implements RemoveEmployeeCommand {
37 | constructor(private readonly employeeId: number) {}
38 |
39 | getEmployeeId(): EmployeeId {
40 | return new EmployeeId(this.employeeId)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/web/index.ts:
--------------------------------------------------------------------------------
1 | export * from './employee-command'
2 | export * from './product-command'
3 | export * from './project-command'
4 | export * from './scrum-team-command'
5 |
6 | // scenario
7 | export * from './init-scenario-command'
8 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/web/init-scenario-command.ts:
--------------------------------------------------------------------------------
1 | import { CreateProductWebCommand } from './product-command'
2 | import { CreateProjectWebCommand } from './project-command'
3 |
4 | import { CreateProductCommand, CreateProjectCommand, InitCommand } from '@panda-project/core'
5 |
6 | export class InitWebCommand implements InitCommand {
7 | constructor(
8 | private readonly productName: string,
9 | private readonly projectName: string
10 | ) {}
11 |
12 | getCreateProductCommand(): CreateProductCommand {
13 | return new CreateProductWebCommand(this.productName)
14 | }
15 |
16 | getCreateProjectCommand(): CreateProjectCommand {
17 | return new CreateProjectWebCommand(this.projectName)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/web/product-command.ts:
--------------------------------------------------------------------------------
1 | import { ProductName } from '@panda-project/core'
2 |
3 | import { CreateProductCommand } from '@panda-project/core'
4 |
5 | export class CreateProductWebCommand implements CreateProductCommand {
6 | constructor(private readonly productName: string) {}
7 |
8 | getProductName(): ProductName {
9 | return new ProductName(this.productName)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/web/project-command.ts:
--------------------------------------------------------------------------------
1 | import { ProjectName } from '@panda-project/core'
2 |
3 | import { CreateProjectCommand } from '@panda-project/core'
4 |
5 | export class CreateProjectWebCommand implements CreateProjectCommand {
6 | constructor(private readonly projectName: string) {}
7 |
8 | getProjectName(): ProjectName {
9 | return new ProjectName(this.projectName)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/web/scrum-team-command.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CreateScrumTeamCommand,
3 | DisbandScrumTeamCommand,
4 | EditScrumTeamCommand,
5 | EmployeeId,
6 | ScrumTeamId,
7 | } from '@panda-project/core'
8 |
9 | export class CreateScrumTeamWebCommand implements CreateScrumTeamCommand {
10 | constructor(
11 | private readonly productOwnerId: string,
12 | private readonly scrumMasterId: string,
13 | private readonly developerIds: string[]
14 | ) {}
15 |
16 | getProductOwnerId(): EmployeeId {
17 | return new EmployeeId(Number.parseInt(this.productOwnerId, 10))
18 | }
19 |
20 | getScrumMasterId(): EmployeeId {
21 | return new EmployeeId(Number.parseInt(this.scrumMasterId, 10))
22 | }
23 |
24 | getDeveloperIds(): EmployeeId[] {
25 | const filteredIds = this.developerIds.filter((id) => id !== '')
26 |
27 | // 重複の有無をチェック。ID の重複を排除するために Set を使う
28 | const uniqueIds = new Set(filteredIds)
29 | if (uniqueIds.size !== filteredIds.length) {
30 | throw new Error('開発者が重複しています')
31 | }
32 |
33 | return filteredIds.map((id) => new EmployeeId(Number.parseInt(id, 10)))
34 | }
35 | }
36 |
37 | export class EditScrumTeamWebCommand implements EditScrumTeamCommand {
38 | constructor(
39 | private readonly productOwnerId: string,
40 | private readonly scrumMasterId: string,
41 | private readonly developerIds: string[]
42 | ) {}
43 |
44 | getProductOwnerId(): EmployeeId {
45 | return new EmployeeId(Number.parseInt(this.productOwnerId, 10))
46 | }
47 |
48 | getScrumMasterId(): EmployeeId {
49 | return new EmployeeId(Number.parseInt(this.scrumMasterId, 10))
50 | }
51 |
52 | getDeveloperIds(): EmployeeId[] {
53 | const filteredIds = this.developerIds.filter((id) => id !== '')
54 |
55 | // 重複の有無をチェック。ID の重複を排除するために Set を使う
56 | const uniqueIds = new Set(filteredIds)
57 | if (uniqueIds.size !== filteredIds.length) {
58 | throw new Error('開発者が重複しています')
59 | }
60 |
61 | return filteredIds.map((id) => new EmployeeId(Number.parseInt(id, 10)))
62 | }
63 | }
64 |
65 | export class DisbandScrumTeamWebCommand implements DisbandScrumTeamCommand {
66 | constructor(private readonly scrumTeamId: string) {}
67 |
68 | getScrumTeamId(): ScrumTeamId {
69 | return new ScrumTeamId(Number.parseInt(this.scrumTeamId, 10))
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/web/tests/employee-command.test.ts:
--------------------------------------------------------------------------------
1 | import { EmployeeId } from '@panda-project/core'
2 |
3 | import { CreateEmployeeWebCommand, EditEmployeeWebCommand, RemoveEmployeeWebCommand } from '../employee-command'
4 |
5 | describe('CreateEmployeeWebCommand', () => {
6 | const sut = new CreateEmployeeWebCommand('社員', '一号')
7 |
8 | test('getEmployeeName', () => {
9 | expect(sut.getEmployeeName().getFullName()).toBe('社員 一号')
10 | })
11 | })
12 |
13 | describe('EditEmployeeWebCommand', () => {
14 | const sut = new EditEmployeeWebCommand(1, '社員', '一号')
15 |
16 | test('getEmployeeId', () => {
17 | expect(sut.getEmployeeId()).toEqual(new EmployeeId(1))
18 | })
19 |
20 | test('getNewEmployeeName', () => {
21 | expect(sut.getNewEmployeeName().getFullName()).toBe('社員 一号')
22 | })
23 | })
24 |
25 | describe('RemoveEmployeeWebCommand', () => {
26 | const sut = new RemoveEmployeeWebCommand(1)
27 |
28 | test('getEmployeeId', () => {
29 | expect(sut.getEmployeeId()).toEqual(new EmployeeId(1))
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/web/tests/init-scenario-command.test.ts:
--------------------------------------------------------------------------------
1 | import { InitWebCommand } from '../init-scenario-command'
2 | import { CreateProductWebCommand } from '../product-command'
3 | import { CreateProjectWebCommand } from '../project-command'
4 |
5 | describe('InitWebCommand', () => {
6 | const sut = new InitWebCommand('プロダクト', 'プロジェクト')
7 |
8 | test('getCreateProductCommand', () => {
9 | expect(sut.getCreateProductCommand()).toEqual(new CreateProductWebCommand('プロダクト'))
10 | })
11 |
12 | test('getCreateProjectCommand', () => {
13 | expect(sut.getCreateProjectCommand()).toEqual(new CreateProjectWebCommand('プロジェクト'))
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/web/tests/product-command.test.ts:
--------------------------------------------------------------------------------
1 | import { ProductName } from '@panda-project/core'
2 |
3 | import { CreateProductWebCommand } from '../product-command'
4 |
5 | describe('CreateProductWebCommand', () => {
6 | test('getProductName', () => {
7 | const sut = new CreateProductWebCommand('プロダクト')
8 | expect(sut.getProductName()).toEqual(new ProductName('プロダクト'))
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/web/tests/project-command.test.ts:
--------------------------------------------------------------------------------
1 | import { ProjectName } from '@panda-project/core'
2 |
3 | import { CreateProjectWebCommand } from '../project-command'
4 |
5 | describe('CreateProjectWebCommand', () => {
6 | test('getProjectName', () => {
7 | const sut = new CreateProjectWebCommand('プロジェクト')
8 | expect(sut.getProjectName()).toEqual(new ProjectName('プロジェクト'))
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/packages/gateway/src/adapter/web/tests/scrum-team-command.test.ts:
--------------------------------------------------------------------------------
1 | import { EmployeeId, ScrumTeamId } from '@panda-project/core'
2 |
3 | import { CreateScrumTeamWebCommand, EditScrumTeamWebCommand, DisbandScrumTeamWebCommand } from '../scrum-team-command'
4 |
5 | describe('CreateScrumTeamWebCommand', () => {
6 | const sut = new CreateScrumTeamWebCommand('1', '2', [])
7 |
8 | test('getProductOwnerId', () => {
9 | expect(sut.getProductOwnerId()).toEqual(new EmployeeId(1))
10 | })
11 |
12 | test('getScrumMasterId', () => {
13 | expect(sut.getScrumMasterId()).toEqual(new EmployeeId(2))
14 | })
15 |
16 | describe('getDeveloperIds', () => {
17 | it('should return empty array', () => {
18 | const sut = new CreateScrumTeamWebCommand('1', '2', [])
19 | expect(sut.getDeveloperIds()).toEqual([])
20 | })
21 |
22 | it('should return array with two elements', () => {
23 | const sut = new CreateScrumTeamWebCommand('1', '2', ['3', '4'])
24 | expect(sut.getDeveloperIds()).toEqual([new EmployeeId(3), new EmployeeId(4)])
25 | })
26 |
27 | it('throws error when developer id is duplicated', () => {
28 | const sut = new CreateScrumTeamWebCommand('1', '2', ['3', '3'])
29 | expect(() => sut.getDeveloperIds()).toThrowError('開発者が重複しています')
30 | })
31 | })
32 | })
33 |
34 | describe('EditScrumTeamWebCommand', () => {
35 | const sut = new EditScrumTeamWebCommand('1', '2', ['3'])
36 |
37 | test('getProductOwnerId', () => {
38 | expect(sut.getProductOwnerId()).toEqual(new EmployeeId(1))
39 | })
40 |
41 | test('getScrumMasterId', () => {
42 | expect(sut.getScrumMasterId()).toEqual(new EmployeeId(2))
43 | })
44 |
45 | describe('getDeveloperIds', () => {
46 | it('should return empty array', () => {
47 | const sut = new EditScrumTeamWebCommand('1', '2', [])
48 | expect(sut.getDeveloperIds()).toEqual([])
49 | })
50 |
51 | it('should return array with two elements', () => {
52 | const sut = new EditScrumTeamWebCommand('1', '2', ['3', '4'])
53 | expect(sut.getDeveloperIds()).toEqual([new EmployeeId(3), new EmployeeId(4)])
54 | })
55 |
56 | it('throws error when developer id is duplicated', () => {
57 | const sut = new EditScrumTeamWebCommand('1', '2', ['3', '3'])
58 | expect(() => sut.getDeveloperIds()).toThrowError('開発者が重複しています')
59 | })
60 | })
61 | })
62 |
63 | describe('DisbandScrumTeamWebCommand', () => {
64 | const sut = new DisbandScrumTeamWebCommand('1')
65 |
66 | test('getDeveloperId', () => {
67 | expect(sut.getScrumTeamId()).toEqual(new ScrumTeamId(1))
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/packages/gateway/src/common.ts:
--------------------------------------------------------------------------------
1 | // ここに gateway 層の共通処理や型を記述できます
2 |
--------------------------------------------------------------------------------
/packages/gateway/src/external/.gitkeep:
--------------------------------------------------------------------------------
1 | // lowdb ディレクトリ(gateway 層)
2 |
--------------------------------------------------------------------------------
/packages/gateway/src/external/lowdb/.gitkeep:
--------------------------------------------------------------------------------
1 | // gateway external/lowdb ディレクトリ
2 |
--------------------------------------------------------------------------------
/packages/gateway/src/external/lowdb/database.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs'
2 | import { fileURLToPath } from 'node:url'
3 | import { dirname as pathDirname } from 'node:path'
4 |
5 | import { Adapter, Low, Memory } from 'lowdb'
6 | import { JSONFile } from 'lowdb/node'
7 |
8 | import { DataBase, createDefaultData } from './schema'
9 |
10 | const isTest = process.env.NODE_ENV === 'test'
11 |
12 | const dirname = isTest ? '/mock/path' : pathDirname(fileURLToPath(import.meta.url))
13 | const cliPathIndex = dirname.indexOf('/apps/cli')
14 | const webPathIndex = dirname.indexOf('/apps/web')
15 | const apiPathIndex = dirname.indexOf('/apps/api')
16 |
17 | if (cliPathIndex === -1 && webPathIndex === -1 && apiPathIndex === -1 && !isTest) {
18 | throw new Error('DB path not found')
19 | }
20 |
21 | const rootIndex =
22 | cliPathIndex > 0 ? cliPathIndex : webPathIndex > 0 ? webPathIndex : apiPathIndex
23 | const basePath = dirname.slice(0, rootIndex)
24 | const dbFilePath = `${basePath}/db.json`
25 |
26 | const adapter: Adapter = isTest ? new Memory() : new JSONFile(dbFilePath)
27 | const db = new Low(adapter, createDefaultData())
28 | const dbFileExists = () => fs.existsSync(dbFilePath)
29 |
30 | const createDb = async () => {
31 | await db.write()
32 | }
33 |
34 | const resetDb = async () => {
35 | const newDb = new Low(adapter, createDefaultData())
36 | await newDb.write()
37 | }
38 |
39 | export { adapter, createDb, db, dbFileExists, resetDb }
40 |
--------------------------------------------------------------------------------
/packages/gateway/src/external/lowdb/index.ts:
--------------------------------------------------------------------------------
1 | export * from './database'
2 | export * from './schema'
3 |
--------------------------------------------------------------------------------
/packages/gateway/src/external/lowdb/schema.ts:
--------------------------------------------------------------------------------
1 | export type DataBase = Documents
2 |
3 | // mermaid 記法の er 図から、schema を自動生成できるといい。
4 | export const createDefaultData = (): DataBase => ({
5 | products: [],
6 | projects: [],
7 | employees: [],
8 | members: [],
9 | scrumMemberRoles: {
10 | 1: 'product_owner',
11 | 2: 'scrum_master',
12 | 3: 'developer',
13 | },
14 | scrumTeams: [],
15 | productOwners: [],
16 | scrumMasters: [],
17 | developers: [],
18 | })
19 |
20 | export type Documents = {
21 | // company
22 | products: ProductsSchema
23 | projects: ProjectsSchema
24 | employees: EmployeesSchema
25 | members: MembersSchema
26 | // scrum
27 | scrumMemberRoles: ScrumMemberRolesSchema
28 | scrumTeams: ScrumTeamsSchema
29 | productOwners: ProductOwnersSchema
30 | scrumMasters: ScrumMastersSchema
31 | developers: DevelopersSchema
32 | }
33 |
34 | export type ProductsSchema = {
35 | id: number
36 | name: string
37 | }[]
38 |
39 | export type ProjectsSchema = {
40 | id: number
41 | name: string
42 | }[]
43 |
44 | export type EmployeesSchema = {
45 | id: number
46 | first_name: string
47 | family_name: string
48 | }[]
49 |
50 | export type MembersSchema = {
51 | employee_id: number
52 | }[]
53 |
54 | export type ScrumMemberRolesSchema = {
55 | 1: 'product_owner'
56 | 2: 'scrum_master'
57 | 3: 'developer'
58 | }
59 |
60 | export type ScrumTeamsSchema = {
61 | id: number
62 | }[]
63 | // increments, product_goals のリレーションを作成できる
64 |
65 | export type ProductOwnersSchema = {
66 | scrum_team_id: number
67 | employee_id: number // "relation -> Employee"
68 | }[]
69 |
70 | export type ScrumMastersSchema = {
71 | scrum_team_id: number
72 | employee_id: number // "relation -> Employee"
73 | }[]
74 |
75 | export type DevelopersSchema = {
76 | scrum_team_id: number
77 | employee_id: number // "relation -> Employee"
78 | }[]
79 |
--------------------------------------------------------------------------------
/packages/gateway/src/index.ts:
--------------------------------------------------------------------------------
1 | // gateway パッケージのエントリポイント
2 | export * from './adapter/cli'
3 | export * from './adapter/web'
4 | export * from './external/lowdb'
5 | export * from './repository/json'
6 |
--------------------------------------------------------------------------------
/packages/gateway/src/repository/.gitkeep:
--------------------------------------------------------------------------------
1 | // repository ディレクトリ(gateway 層)
2 |
--------------------------------------------------------------------------------
/packages/gateway/src/repository/json/.gitkeep:
--------------------------------------------------------------------------------
1 | // gateway repository/json ディレクトリ
2 |
--------------------------------------------------------------------------------
/packages/gateway/src/repository/json/employee-repository.ts:
--------------------------------------------------------------------------------
1 | import { Employee, EmployeeId, EmployeeName, EmployeeRepositoryInterface } from '@panda-project/core'
2 | import { Low } from 'lowdb'
3 |
4 | import { JsonRepository } from './json-repository'
5 |
6 | import { DataBase, db, EmployeesSchema } from '../../external/lowdb'
7 |
8 | export class EmployeeRepository extends JsonRepository implements EmployeeRepositoryInterface {
9 | constructor(private readonly lowdb: Low = db) {
10 | super()
11 | }
12 |
13 | private nextId(): EmployeeId {
14 | return new EmployeeId(this.calculateNewId(this.lowdb.data.employees))
15 | }
16 |
17 | async findByIdOrFail(id: EmployeeId): Promise {
18 | await this.lowdb.read()
19 | const { employees } = this.lowdb.data
20 | const employee = employees.find((v) => v.id === id.value)
21 | if (!employee) {
22 | throw new Error(`社員ID ${id.value} は存在しません`)
23 | }
24 | return this.mapToEmployee(employee)
25 | }
26 |
27 | async findAll(): Promise {
28 | await this.lowdb.read()
29 | const { employees } = this.lowdb.data
30 |
31 | return employees.map(this.mapToEmployee)
32 | }
33 |
34 | async count() {
35 | await this.lowdb.read()
36 | const { employees } = this.lowdb.data
37 | return employees.length
38 | }
39 |
40 | private mapToEmployee(record: EmployeesSchema[number]): Employee {
41 | return new Employee(new EmployeeId(record.id), new EmployeeName(record.first_name, record.family_name))
42 | }
43 |
44 | async save(employee: Employee) {
45 | await this.lowdb.read()
46 | const { employees } = this.lowdb.data
47 |
48 | if (employee.id.value !== null) {
49 | throw new Error('社員IDはnullである必要があります。社員の更新は update メソッドを使ってください')
50 | }
51 |
52 | const employeeId = this.nextId()
53 | employees.push({
54 | id: employeeId.toInt(),
55 | first_name: employee.employeeName.firstName,
56 | family_name: employee.employeeName.familyName,
57 | })
58 |
59 | await this.lowdb.write()
60 | return new Employee(employeeId, employee.employeeName)
61 | }
62 |
63 | async update(newEmployee: Employee) {
64 | await this.lowdb.read()
65 | const { employees } = this.lowdb.data
66 |
67 | const newEmployeeId = newEmployee.id.toInt()
68 | const index = employees.findIndex((v) => v.id === newEmployeeId)
69 | if (index === -1) {
70 | throw new Error(`社員ID ${newEmployeeId} は存在しません`)
71 | }
72 |
73 | employees[index] = {
74 | id: newEmployeeId,
75 | first_name: newEmployee.employeeName.firstName,
76 | family_name: newEmployee.employeeName.familyName,
77 | }
78 |
79 | await this.lowdb.write()
80 | return newEmployee
81 | }
82 |
83 | async delete(employee: Employee) {
84 | await this.lowdb.read()
85 | const { employees } = this.lowdb.data
86 |
87 | const index = employees.findIndex((v) => v.id === employee.id.value)
88 | if (index === -1) {
89 | throw new Error(`社員ID ${employee.id.value} は存在しません`)
90 | }
91 |
92 | employees.splice(index, 1)
93 |
94 | await this.lowdb.write()
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/packages/gateway/src/repository/json/index.ts:
--------------------------------------------------------------------------------
1 | export * from './employee-repository'
2 | export * from './product-repository'
3 | export * from './project-repository'
4 | export * from './scrum-team-repository'
5 |
--------------------------------------------------------------------------------
/packages/gateway/src/repository/json/json-repository.ts:
--------------------------------------------------------------------------------
1 | export class JsonRepository {
2 | protected calculateNewId(records: any[]): number {
3 | // 空の時
4 | if (records.length === 0) {
5 | return records.length + 1
6 | }
7 |
8 | // 欠番がある時の対応
9 | const lastRecord = records.at(records.length - 1)
10 | if (lastRecord?.id) {
11 | return lastRecord.id + 1
12 | }
13 |
14 | throw new Error('id が判定できません')
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/gateway/src/repository/json/product-repository.ts:
--------------------------------------------------------------------------------
1 | import { Product, ProductId, ProductName, ProductRepositoryInterface } from '@panda-project/core'
2 | import { Low } from 'lowdb'
3 |
4 | import { JsonRepository } from './json-repository'
5 |
6 | import { DataBase, db } from '../../external/lowdb'
7 |
8 | export class ProductRepository extends JsonRepository implements ProductRepositoryInterface {
9 | constructor(private readonly lowdb: Low = db) {
10 | super()
11 | }
12 |
13 | private nextId(): ProductId {
14 | return new ProductId(this.calculateNewId(this.lowdb.data.products))
15 | }
16 |
17 | async fetch(): Promise {
18 | await this.lowdb.read()
19 | const { products } = this.lowdb.data
20 |
21 | if (products.length === 0) {
22 | return null
23 | }
24 |
25 | const product = products[0]
26 | return new Product(new ProductId(product.id), new ProductName(product.name))
27 | }
28 |
29 | async findByNameOrFail(productName: ProductName) {
30 | await this.lowdb.read()
31 | const { products } = this.lowdb.data
32 |
33 | const product = products.find((product) => product.name === productName.value)
34 | if (!product) {
35 | throw new Error('プロダクトが存在しません')
36 | }
37 |
38 | return new Product(new ProductId(product.id), productName)
39 | }
40 |
41 | async existsWithoutId() {
42 | await this.lowdb.read()
43 | const { products } = this.lowdb.data
44 |
45 | return products.length > 0
46 | }
47 |
48 | async save(product: Product) {
49 | await this.lowdb.read()
50 | const { products } = this.lowdb.data
51 |
52 | const productId = this.nextId()
53 | products.push({
54 | id: productId.toInt(),
55 | name: product.name.value,
56 | })
57 |
58 | await this.lowdb.write()
59 | return new Product(productId, product.name)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/packages/gateway/src/repository/json/project-repository.ts:
--------------------------------------------------------------------------------
1 | import { Project, ProjectId, ProjectName, ProjectRepositoryInterface } from '@panda-project/core'
2 | import { Low } from 'lowdb'
3 |
4 | import { JsonRepository } from './json-repository'
5 |
6 | import { DataBase, db } from '../../external/lowdb'
7 |
8 | export class ProjectRepository extends JsonRepository implements ProjectRepositoryInterface {
9 | constructor(private readonly lowdb: Low = db) {
10 | super()
11 | }
12 |
13 | private nextId(): ProjectId {
14 | return new ProjectId(this.calculateNewId(this.lowdb.data.projects))
15 | }
16 |
17 | async fetch(): Promise {
18 | await this.lowdb.read()
19 | const { projects } = this.lowdb.data
20 |
21 | if (projects.length === 0) {
22 | return null
23 | }
24 |
25 | return new Project(new ProjectId(projects[0].id), new ProjectName(projects[0].name))
26 | }
27 |
28 | async save(project: Project) {
29 | await this.lowdb.read()
30 | const { projects } = this.lowdb.data
31 |
32 | const projectId = this.nextId()
33 | projects.push({
34 | id: projectId.toInt(),
35 | name: project.name.value,
36 | })
37 |
38 | await this.lowdb.write()
39 | return new Project(projectId, project.name)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/gateway/src/repository/json/tests/.gitkeep:
--------------------------------------------------------------------------------
1 | // gateway repository/json/tests ディレクトリ
2 |
--------------------------------------------------------------------------------
/packages/gateway/src/repository/json/tests/helper/database.ts:
--------------------------------------------------------------------------------
1 | import { Low } from 'lowdb'
2 |
3 | import { adapter, createDefaultData } from '@/external/lowdb'
4 |
5 | export const setupDataBase = async () => {
6 | const db = new Low(adapter, createDefaultData())
7 | await db.read()
8 | db.data = createDefaultData()
9 | await db.write()
10 | return { db }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/gateway/src/repository/json/tests/json-repository.test.ts:
--------------------------------------------------------------------------------
1 | import { JsonRepository } from '../json-repository'
2 |
3 | class ConcreteRepository extends JsonRepository {
4 | calculateNewIdForTest(records: any[]) {
5 | return super.calculateNewId(records)
6 | }
7 | }
8 |
9 | describe('JsonRepository', () => {
10 | describe('calculateNewId', () => {
11 | const concreteRepository = new ConcreteRepository()
12 |
13 | it('最初のID は 1 を返す', () => {
14 | const records = []
15 | const result = concreteRepository.calculateNewIdForTest(records)
16 | expect(result).toBe(1)
17 | })
18 |
19 | it('欠番がない時は最大値 + 1 を返す', () => {
20 | const records = [{ id: 1 }, { id: 2 }, { id: 3 }]
21 | const result = concreteRepository.calculateNewIdForTest(records)
22 | expect(result).toBe(4)
23 | })
24 |
25 | it('欠番がある時も最大値 + 1 を返す', () => {
26 | const records = [{ id: 1 }, { id: 2 }, { id: 9 }]
27 | const result = concreteRepository.calculateNewIdForTest(records)
28 | expect(result).toBe(10)
29 | })
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/packages/gateway/src/repository/json/tests/product-repository.test.ts:
--------------------------------------------------------------------------------
1 | import { ProductRepository } from '../product-repository';
2 | import {Product, ProductId, ProductName} from "@panda-project/core";
3 | import {ProductsSchema, Low} from "@/external/lowdb";
4 | import { setupDataBase } from './helper/database';
5 |
6 | let repository: ProductRepository
7 | let mockDb: Low
8 | beforeEach(async () => {
9 | const { db } = await setupDataBase()
10 | mockDb = db
11 | repository = new ProductRepository(mockDb)
12 | })
13 |
14 | // テストデータを作る
15 | const fabricate = (data: Partial = null): ProductsSchema[number] => {
16 | return {
17 | id: data?.id ?? 100,
18 | name: data?.name ?? 'テスト用プロダクト',
19 | }
20 | }
21 |
22 | // テストデータをDBに保存する
23 | const fixture = async (data: Partial = null): Promise => {
24 | const testData = fabricate(data)
25 | await mockDb.read()
26 | const { products } = mockDb.data
27 | products.push(testData)
28 | await mockDb.write()
29 | return testData
30 | }
31 |
32 | describe('fetch', () => {
33 | test('プロダクトが存在する時はプロダクトを返す', async () => {
34 | // arrange
35 | await fixture()
36 | // act
37 | const actual = await repository.fetch()
38 | // assert
39 | expect(actual).toBeInstanceOf(Product)
40 | expect(actual.id.value).toBe(100)
41 | expect(actual.name.value).toBe('テスト用プロダクト')
42 | })
43 |
44 | test('プロダクトが存在しない時はnullを返す', async () => {
45 | const actual = await repository.fetch()
46 | expect(actual).toBeNull()
47 | })
48 | })
49 |
50 | describe('findByNameOrFail', () => {
51 | test('プロダクトが存在する時はプロダクトを返す', async () => {
52 | // arrange
53 | await fixture()
54 | // act
55 | const actual = await repository.findByNameOrFail(new ProductName('テスト用プロダクト'))
56 | // assert
57 | expect(actual).toBeInstanceOf(Product)
58 | expect(actual.id.value).toBe(100)
59 | expect(actual.name.value).toBe('テスト用プロダクト')
60 | })
61 |
62 | test('プロダクトが存在しない時はエラーを返す', async () => {
63 | const name = new ProductName('存在しないプロダクト')
64 | await expect(repository.findByNameOrFail(name)).rejects.toThrowError('プロダクトが存在しません')
65 | })
66 | })
67 |
68 | describe('existsWithoutId', () => {
69 | test('プロダクトが存在する時はtrueを返す', async () => {
70 | await fixture()
71 | const actual = await repository.existsWithoutId()
72 | expect(actual).toBeTruthy()
73 | })
74 |
75 | test('プロダクトが存在しない時はfalseを返す', async () => {
76 | const actual = await repository.existsWithoutId()
77 | expect(actual).toBeFalsy()
78 | })
79 | })
80 |
81 | describe('save', () => {
82 | test('プロダクトを保存する', async () => {
83 | const product = new Product(ProductId.createAsNull(), new ProductName('新規プロダクト'))
84 | const actual = await repository.save(product)
85 | expect(actual).toBeInstanceOf(Product)
86 | expect(actual.id.value).toBe(1)
87 | expect(actual.name.value).toBe('新規プロダクト')
88 | })
89 | })
90 |
--------------------------------------------------------------------------------
/packages/gateway/src/repository/json/tests/project-repository.test.ts:
--------------------------------------------------------------------------------
1 | import { ProjectRepository } from '../project-repository';
2 | import {Project, ProjectId, ProjectName} from "@panda-project/core";
3 | import {Low, ProjectsSchema} from "@/external/lowdb";
4 | import { setupDataBase } from './helper/database';
5 |
6 | let repository: ProjectRepository
7 | let mockDb: Low
8 | beforeEach(async () => {
9 | const { db } = await setupDataBase()
10 | mockDb = db
11 | repository = new ProjectRepository(mockDb)
12 | })
13 |
14 | // テストデータを作る
15 | const fabricate = (data: Partial = null): ProjectsSchema[number] => {
16 | return {
17 | id: data?.id ?? 100,
18 | name: data?.name ?? 'テスト用プロジェクト',
19 | }
20 | }
21 |
22 | // テストデータをDBに保存する
23 | const fixture = async (data: Partial = null): Promise => {
24 | const testData = fabricate(data)
25 | await mockDb.read()
26 | const { projects } = mockDb.data
27 | projects.push(testData)
28 | await mockDb.write()
29 | return testData
30 | }
31 |
32 | describe('fetch', () => {
33 | test('プロジェクトが存在する時はプロジェクトを返す', async () => {
34 | // arrange
35 | await fixture()
36 | // act
37 | const actual = await repository.fetch()
38 | // assert
39 | expect(actual).toBeInstanceOf(Project)
40 | expect(actual?.id.value).toBe(100)
41 | expect(actual?.name.value).toBe('テスト用プロジェクト')
42 | })
43 |
44 | test('プロジェクトが存在しない時はnullを返す', async () => {
45 | const actual = await repository.fetch()
46 | expect(actual).toBeNull()
47 | })
48 | })
49 |
50 | describe('save', () => {
51 | test('プロジェクトを保存できる', async () => {
52 | const project = new Project(new ProjectId(null), new ProjectName('テスト用プロジェクト'))
53 | const actual = await repository.save(project)
54 | expect(actual).toBeInstanceOf(Project)
55 | expect(actual.id.value).toBe(1)
56 | expect(actual.name.value).toBe('テスト用プロジェクト')
57 | })
58 | })
59 |
--------------------------------------------------------------------------------
/packages/gateway/src/repository/json/tests/scrum-team-repository.test.ts:
--------------------------------------------------------------------------------
1 | import { Low, ProductOwnersSchema, ScrumTeamsSchema } from '@/external/lowdb'
2 | import { ScrumTeam, ScrumTeamId } from '@panda-project/core'
3 | import { ScrumTeamRepository } from '../scrum-team-repository'
4 | import { setupDataBase } from './helper/database'
5 |
6 | let repository: ScrumTeamRepository
7 | let mockDb: Low
8 | beforeEach(async () => {
9 | const { db } = await setupDataBase()
10 | mockDb = db
11 | repository = new ScrumTeamRepository(mockDb)
12 | })
13 |
14 | // テストデータを作る
15 | const fabricate = (data: Partial = null): ScrumTeamsSchema[number] => {
16 | return {
17 | id: data?.id ?? 100,
18 | }
19 | }
20 |
21 | // テストデータをDBに保存する
22 | const fixture = async (data: Partial = null): Promise => {
23 | const testData = fabricate(data)
24 | await mockDb.read()
25 | const { scrumTeams } = mockDb.data
26 | scrumTeams.push(testData)
27 | await mockDb.write()
28 | return testData
29 | }
30 |
31 | // テストデータを作る
32 | const fabricateProductOwner = (data: Partial = null): ProductOwnersSchema[number] => {
33 | return {
34 | scrum_team_id: data?.scrum_team_id ?? 100,
35 | employee_id: data?.employee_id ?? 100,
36 | }
37 | }
38 |
39 | // テストデータをDBに保存する
40 | describe('save', () => {
41 | // AI Generated Test
42 | it('should save new scrum team with members', async () => {
43 | const scrumTeam = new ScrumTeam(
44 | new ScrumTeamId(1),
45 | // Mock minimum required data
46 | { getEmployeeId: () => ({ toInt: () => 100 }) } as any, // ProductOwner
47 | { getEmployeeId: () => ({ toInt: () => 101 }) } as any, // ScrumMaster
48 | [{ getEmployeeId: () => ({ toInt: () => 102 }) } as any, { getEmployeeId: () => ({ toInt: () => 103 }) } as any] // Developers
49 | )
50 |
51 | await repository.save(scrumTeam)
52 |
53 | await mockDb.read()
54 | expect(mockDb.data.scrumTeams).toHaveLength(1)
55 | expect(mockDb.data.productOwners).toHaveLength(1)
56 | expect(mockDb.data.scrumMasters).toHaveLength(1)
57 | expect(mockDb.data.developers).toHaveLength(2)
58 | })
59 | })
60 |
--------------------------------------------------------------------------------
/packages/gateway/src/types.ts:
--------------------------------------------------------------------------------
1 | // gateway パッケージの型定義
2 | export {};
3 |
--------------------------------------------------------------------------------
/packages/gateway/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@panda-project/config/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["./src/*"]
7 | },
8 | "outDir": "./dist"
9 | },
10 | "include": ["src", "src/types/*.d.ts"],
11 | "exclude": ["**/*.test.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/use-case/.scaffdog/cli-scenario.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'cli-scenario'
3 | root: '.'
4 | output: 'src/cli/scenario'
5 | ignore: []
6 | questions:
7 | name: 'Scenario 名を入力してください。Scenarioという接尾辞は不要です ex. Foo'
8 | ---
9 |
10 | # `index.ts`
11 |
12 | ```ts
13 | export * from './{{ inputs.name | kebab }}-scenario'
14 | {{ read output.abs }}
15 | ```
16 |
17 | # `{{ inputs.name | kebab }}-scenario.ts`
18 |
19 | ```ts
20 | import {Logger} from "@/common";
21 |
22 | export type {{ inputs.name }}Callback = (arg: {}) => Promise<{{ inputs.name }}UserInputType>
23 |
24 | export class {{ inputs.name }}Scenario {
25 | constructor(
26 | private readonly validateUseCase: ValidateUseCase = new ValidateUseCase(),
27 | private readonly {{ inputs.name | camel }}UseCase: {{ inputs.name }}UseCase = new {{ inputs.name }}UseCase(),
28 | ) {
29 | }
30 |
31 | async exec(callback: {{ inputs.name }}Callback): Promise {
32 | await this.validateUseCase.exec()
33 | const input = await callback()
34 | await this.{{ inputs.name | camel }}UseCase.exec(new {{ inputs.name }}Input(input))
35 | }
36 | }
37 |
38 | type {{ inputs.name }}UserInputType = {
39 |
40 | }
41 |
42 | class {{ inputs.name }}Input {
43 | constructor(private readonly userInput: {{ inputs.name }}UserInputType) {}
44 |
45 |
46 | }
47 |
48 | class ValidateUseCase {
49 | constructor(
50 | private readonly
51 | ) {
52 | }
53 |
54 | async exec() {
55 |
56 | }
57 | }
58 |
59 | class {{ inputs.name }}UseCase {
60 | constructor(
61 | private readonly
62 | ) {
63 | }
64 |
65 | async exec(input: {{ inputs.name }}Input) {
66 |
67 | }
68 | }
69 | ```
70 |
--------------------------------------------------------------------------------
/packages/use-case/.scaffdog/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | files: ['*'],
3 | }
4 |
--------------------------------------------------------------------------------
/packages/use-case/.scaffdog/web-query-service.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'web-query-service'
3 | root: '.'
4 | output: 'src/web/query-service'
5 | ignore: []
6 | questions:
7 | name: 'QueryService 名を入力してください。QueryServiceという接尾辞は不要です ex. Foo'
8 | ---
9 |
10 | # `index.ts`
11 |
12 | ```ts
13 | export * from './{{ inputs.name | kebab }}-query-service'
14 | {{ read output.abs }}
15 | ```
16 |
17 | # `{{ inputs.name | kebab }}-query-service.ts`
18 |
19 | ```ts
20 | import {DefaultError, Result} from "@/web/types";
21 |
22 | type Dto = {
23 |
24 | }
25 |
26 | interface CustomError extends DefaultError {
27 |
28 | }
29 |
30 | class Query {
31 | constructor(
32 | private readonly
33 | ) {
34 | }
35 |
36 |
37 | }
38 |
39 | export class {{ inputs.name }}QueryService {
40 | constructor(
41 | private readonly
42 | ) {
43 | }
44 |
45 | async exec(input): Promise> {
46 | // validation
47 | let a = null
48 | try {
49 | const userInput = new Query(input)
50 | } catch (e: unknown) {
51 | return {
52 | data: null,
53 | error: null
54 | }
55 | }
56 |
57 | // business logic
58 |
59 |
60 | // presentation logic
61 | return {
62 | data: {},
63 | error: null,
64 | }
65 | }
66 | }
67 | ```
68 |
--------------------------------------------------------------------------------
/packages/use-case/index.ts:
--------------------------------------------------------------------------------
1 | export * from './src'
2 |
--------------------------------------------------------------------------------
/packages/use-case/jest.config.js:
--------------------------------------------------------------------------------
1 | import config from '@panda-project/config/jest.config.js'
2 |
3 | export default config
4 |
--------------------------------------------------------------------------------
/packages/use-case/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@panda-project/use-case",
3 | "version": "1.0.0",
4 | "main": "dist/index.js",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "tspc --emitDeclarationOnly && node ../config/build.mjs",
8 | "build": "rm -rf dist && esbuild index.ts --bundle --outfile=dist/index.js --platform=node --format=esm && tspc --emitDeclarationOnly",
9 | "type-check": "tspc --noEmit"
10 | },
11 | "dependencies": {
12 | "@panda-project/config": "workspace:*",
13 | "@panda-project/core": "workspace:*",
14 | "@panda-project/gateway": "workspace:*"
15 | },
16 | "devDependencies": {
17 | "@types/node": "^20.8.2"
18 | },
19 | "private": true
20 | }
21 |
--------------------------------------------------------------------------------
/packages/use-case/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | dependencies:
11 | '@panda-project/config':
12 | specifier: workspace:*
13 | version: link:../config
14 | '@panda-project/core':
15 | specifier: workspace:*
16 | version: link:../core
17 | lowdb:
18 | specifier: 6.1.1
19 | version: 6.1.1
20 | devDependencies:
21 | '@types/lowdb':
22 | specifier: ^1.0.15
23 | version: 1.0.15
24 | '@types/node':
25 | specifier: ^20.8.2
26 | version: 20.17.50
27 |
28 | packages:
29 |
30 | '@types/lodash@4.17.17':
31 | resolution: {integrity: sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==}
32 |
33 | '@types/lowdb@1.0.15':
34 | resolution: {integrity: sha512-xaMNIveDCryK4UvnUJOc2BCOH0lPivdvWHrutsLryo9r9Id3RqZq2RDmT4eddiEPYzu7nJMw6nFIcVifcqjWqg==}
35 |
36 | '@types/node@20.17.50':
37 | resolution: {integrity: sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==}
38 |
39 | lowdb@6.1.1:
40 | resolution: {integrity: sha512-HO13FCxI8SCwfj2JRXOKgXggxnmfSc+l0aJsZ5I34X3pwzG/DPBSKyKu3Zkgg/pNmx854SVgE2la0oUeh6wzNw==}
41 | engines: {node: '>=16'}
42 |
43 | steno@3.2.0:
44 | resolution: {integrity: sha512-zPKkv+LqoYffxrtD0GIVA08DvF6v1dW02qpP5XnERoobq9g3MKcTSBTi08gbGNFMNRo3TQV/6kBw811T1LUhKg==}
45 | engines: {node: '>=16'}
46 |
47 | undici-types@6.19.8:
48 | resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
49 |
50 | snapshots:
51 |
52 | '@types/lodash@4.17.17': {}
53 |
54 | '@types/lowdb@1.0.15':
55 | dependencies:
56 | '@types/lodash': 4.17.17
57 |
58 | '@types/node@20.17.50':
59 | dependencies:
60 | undici-types: 6.19.8
61 |
62 | lowdb@6.1.1:
63 | dependencies:
64 | steno: 3.2.0
65 |
66 | steno@3.2.0: {}
67 |
68 | undici-types@6.19.8: {}
69 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/cli/add-developer-query-service.ts:
--------------------------------------------------------------------------------
1 | import { EmployeeRepositoryInterface, ScrumTeamRepositoryInterface } from '@panda-project/core'
2 |
3 | import { EmployeeRepository, ScrumTeamRepository } from '@panda-project/gateway'
4 |
5 | export type AddDeveloperQueryServiceDto = {
6 | candidateEmployees: { id: number; name: string }[]
7 | }
8 |
9 | export class AddDeveloperQueryService {
10 | constructor(
11 | private readonly employeeRepository: EmployeeRepositoryInterface = new EmployeeRepository(),
12 | private readonly scrumTeamRepository: ScrumTeamRepositoryInterface = new ScrumTeamRepository()
13 | ) {}
14 |
15 | async exec(): Promise {
16 | const employees = await this.employeeRepository.findAll()
17 | const scrumTeam = await this.scrumTeamRepository.fetchOrFail()
18 | const allScrumMemberDeveloperIds = scrumTeam.getDeveloperIds()
19 |
20 | const candidateEmployees = employees
21 | .filter((employee) => !allScrumMemberDeveloperIds.includes(employee.id))
22 | .map((employee) => ({
23 | id: employee.id.toInt(),
24 | name: employee.employeeName.getFullName(),
25 | }))
26 |
27 | return {
28 | candidateEmployees,
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/cli/create-scrum-team-query-service.ts:
--------------------------------------------------------------------------------
1 | import { EmployeeRepositoryInterface, ScrumTeamRepositoryInterface } from '@panda-project/core'
2 |
3 | import { EmployeeRepository, ScrumTeamRepository } from '@panda-project/gateway'
4 |
5 | export type CreateScrumTeamQueryServiceDto = {
6 | employees: { id: number; name: string }[]
7 | }
8 |
9 | export class CreateScrumTeamQueryService {
10 | constructor(
11 | private readonly scrumTeamRepository: ScrumTeamRepositoryInterface = new ScrumTeamRepository(),
12 | private readonly employeeRepository: EmployeeRepositoryInterface = new EmployeeRepository()
13 | ) {}
14 |
15 | async exec(): Promise {
16 | const employees = await this.employeeRepository.findAll()
17 | const scrumTeam = await this.scrumTeamRepository.fetchOrFail() // TODO: findOrNull のメソッドを作る
18 |
19 | const employeeIdsWithoutPoAndSm = employees
20 | .filter((employee) => {
21 | const isPo = scrumTeam.getProductOwnerId().equals(employee.id)
22 | const isSm = scrumTeam.getScrumMasterId().equals(employee.id)
23 | return !isPo && !isSm
24 | })
25 | .map((employee) => ({
26 | id: employee.id.toInt(),
27 | name: employee.employeeName.getFullName(),
28 | }))
29 |
30 | return {
31 | employees: employeeIdsWithoutPoAndSm,
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/cli/disband-scrum-team-query-service.ts:
--------------------------------------------------------------------------------
1 | import { ScrumTeamRepositoryInterface } from '@panda-project/core'
2 |
3 | import { ScrumTeamRepository } from '@panda-project/gateway'
4 |
5 | type Dto = {
6 | scrumTeamId: number
7 | }
8 |
9 | export class DisbandScrumTeamQueryService {
10 | constructor(private readonly scrumTeamRepository: ScrumTeamRepositoryInterface = new ScrumTeamRepository()) {}
11 |
12 | async exec(): Promise {
13 | const scrumTeam = await this.scrumTeamRepository.fetchOrFail()
14 | return { scrumTeamId: scrumTeam.id.toInt() }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/cli/edit-employee-query-service.ts:
--------------------------------------------------------------------------------
1 | import { EmployeeRepositoryInterface } from '@panda-project/core'
2 |
3 | import { EmployeeRepository } from '@panda-project/gateway'
4 |
5 | export type EditEmployeeQueryServiceDto = { id: number; name: string }[]
6 |
7 | export class EditEmployeeQueryService {
8 | constructor(private readonly employeeRepository: EmployeeRepositoryInterface = new EmployeeRepository()) {}
9 |
10 | async exec(): Promise {
11 | const employees = await this.employeeRepository.findAll()
12 | return employees.map((employee) => ({
13 | id: employee.id.toInt(),
14 | name: employee.employeeName.getFullName(),
15 | }))
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/cli/edit-scrum-team-query-service.ts:
--------------------------------------------------------------------------------
1 | import { EmployeeId, EmployeeRepositoryInterface, ScrumTeamRepositoryInterface } from '@panda-project/core'
2 |
3 | import { EmployeeRepository, ScrumTeamRepository } from '@panda-project/gateway'
4 |
5 | export type EditScrumTeamQueryServiceDto = {
6 | candidateEmployees: { id: number; name: string }[]
7 | productOwnerId: number
8 | scumMasterId: number
9 | developerIds: number[]
10 | }
11 |
12 | export class EditScrumTeamQueryServiceInput {
13 | constructor(private readonly employeeIds: number[]) {}
14 |
15 | getEmployeeIds(): EmployeeId[] {
16 | return this.employeeIds.map((id) => new EmployeeId(id))
17 | }
18 | }
19 |
20 | export class EditScrumTeamQueryService {
21 | constructor(
22 | private readonly employeeRepository: EmployeeRepositoryInterface = new EmployeeRepository(),
23 | private readonly scrumTeamRepository: ScrumTeamRepositoryInterface = new ScrumTeamRepository()
24 | ) {}
25 |
26 | async exec(input: EditScrumTeamQueryServiceInput | null = null): Promise {
27 | const employees = await this.employeeRepository.findAll()
28 | const ids = input?.getEmployeeIds() ?? []
29 |
30 | const candidateEmployees = employees
31 | .filter((employee) => {
32 | for (const id of ids) {
33 | if (id.equals(employee.id)) {
34 | return false
35 | }
36 | }
37 | return true
38 | })
39 | .map((employee) => ({
40 | id: employee.id.toInt(),
41 | name: employee.employeeName.getFullName(),
42 | }))
43 |
44 | const scrumTeam = await this.scrumTeamRepository.fetchOrFail()
45 |
46 | return {
47 | candidateEmployees,
48 | productOwnerId: scrumTeam.getProductOwnerId().toInt(),
49 | scumMasterId: scrumTeam.getScrumMasterId().toInt(),
50 | developerIds: scrumTeam.getDeveloperIds().map((id) => id.toInt()),
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/cli/index.ts:
--------------------------------------------------------------------------------
1 | export * from './add-developer-query-service'
2 | export * from './create-scrum-team-query-service'
3 | export * from './disband-scrum-team-query-service'
4 | export * from './edit-employee-query-service'
5 | export * from './edit-scrum-team-query-service'
6 | export * from './list-scrum-team-query-service'
7 | export * from './remove-developer-query-service'
8 | export * from './remove-employee-query-service'
9 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/cli/list-scrum-team-query-service.ts:
--------------------------------------------------------------------------------
1 | import { ScrumTeamRepositoryInterface } from '@panda-project/core'
2 |
3 | import { ScrumTeamRepository } from '@panda-project/gateway'
4 |
5 | type Dto = {
6 | poName: string
7 | smName: string
8 | developerNames: string[]
9 | }
10 |
11 | export class ListScrumTeamQueryService {
12 | constructor(private readonly scrumTeamRepository: ScrumTeamRepositoryInterface = new ScrumTeamRepository()) {}
13 |
14 | async exec(): Promise {
15 | const { productOwner, scrumMaster, developers } = await this.scrumTeamRepository.fetchOrFail()
16 | return {
17 | poName: productOwner.getFullName(),
18 | smName: scrumMaster.getFullName(),
19 | developerNames: developers.map((developer) => developer.getFullName()),
20 | }
21 | }
22 | }
23 |
24 | export class ListScrumTeamPresenter {
25 | exec(dto: Dto) {
26 | const { poName, smName, developerNames } = dto
27 | const developerBody =
28 | developerNames.length === 0
29 | ? '開発者はいません'
30 | : `開発者(${developerNames.length}名): ${developerNames.join(', ')}`
31 | return `プロダクトオーナー: ${poName}
32 | スクラムマスター: ${smName}
33 | ${developerBody}`
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/cli/remove-developer-query-service.ts:
--------------------------------------------------------------------------------
1 | import { ScrumTeamRepositoryInterface } from '@panda-project/core'
2 |
3 | import { ScrumTeamRepository } from '@panda-project/gateway'
4 |
5 | export type RemoveDeveloperQueryServiceDto = {
6 | developers: { id: number; name: string }[]
7 | }
8 |
9 | export class RemoveDeveloperQueryService {
10 | constructor(private readonly scrumTeamRepository: ScrumTeamRepositoryInterface = new ScrumTeamRepository()) {}
11 |
12 | async exec(): Promise {
13 | const scrumTeam = await this.scrumTeamRepository.fetchOrFail()
14 | const developers = scrumTeam.developers.map((developer) => ({
15 | id: developer.getEmployeeId().toInt(),
16 | name: developer.getFullName(),
17 | }))
18 |
19 | return { developers }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/cli/remove-employee-query-service.ts:
--------------------------------------------------------------------------------
1 | import { EmployeeRepositoryInterface } from '@panda-project/core'
2 |
3 | import { EmployeeRepository } from '@panda-project/gateway'
4 |
5 | export type RemoveEmployeeQueryServiceDto = { id: number; name: string }[]
6 |
7 | export class RemoveEmployeeQueryService {
8 | constructor(private readonly employeeRepository: EmployeeRepositoryInterface = new EmployeeRepository()) {}
9 |
10 | async exec(): Promise {
11 | const employees = await this.employeeRepository.findAll()
12 | return employees.map((employee) => ({
13 | id: employee.id.toInt(),
14 | name: employee.employeeName.getFullName(),
15 | }))
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/index.ts:
--------------------------------------------------------------------------------
1 | export * from './cli'
2 | export * from './web'
3 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/web/breadcrumb-query-service.ts:
--------------------------------------------------------------------------------
1 | import { ProductRepositoryInterface, ProjectRepositoryInterface } from '@panda-project/core'
2 |
3 | import { Result } from './types'
4 |
5 | import { ProductRepository, ProjectRepository } from '@panda-project/gateway'
6 |
7 | export type BreadcrumbDto = {
8 | projectName: string
9 | productName: string
10 | }
11 |
12 | export class BreadcrumbQueryService {
13 | constructor(
14 | private readonly productRepository: ProductRepositoryInterface = new ProductRepository(),
15 | private readonly projectRepository: ProjectRepositoryInterface = new ProjectRepository()
16 | ) {}
17 |
18 | async exec(): Promise> {
19 | // business logic
20 | const project = await this.projectRepository.fetch()
21 | if (project === null) {
22 | // { data: null, error: {reason; xxx }} を返すようにする
23 | throw new Error('プロジェクトを取得できませんでした')
24 | }
25 |
26 | const product = await this.productRepository.fetch()
27 | if (product === null) {
28 | // { data: null, error: {reason; xxx }} を返すようにする
29 | throw new Error('プロダクトを取得できませんでした')
30 | }
31 |
32 | // presentation logic
33 | return {
34 | data: {
35 | projectName: project.name.value,
36 | productName: product.name.value,
37 | },
38 | error: null,
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/web/employee-list-query-service.ts:
--------------------------------------------------------------------------------
1 | import { Employee, EmployeeRepositoryInterface, ProductName, ProductRepositoryInterface } from '@panda-project/core'
2 |
3 | import { Result } from './types'
4 |
5 | import { EmployeeRepository, ProductRepository } from '@panda-project/gateway'
6 |
7 | export type EmployeeListQueryServiceDto = {
8 | employees: {
9 | id: NonNullable
10 | name: string
11 | }[]
12 | productName: ProductName['value'] | null
13 | }
14 |
15 | export class EmployeeListQueryService {
16 | constructor(
17 | private readonly employeeRepository: EmployeeRepositoryInterface = new EmployeeRepository(),
18 | private readonly productRepository: ProductRepositoryInterface = new ProductRepository()
19 | ) {}
20 |
21 | async exec(): Promise> {
22 | const employees = await this.employeeRepository.findAll()
23 | const product = await this.productRepository.fetch()
24 |
25 | if (employees.length === 0 || product === null) {
26 | return {
27 | data: {
28 | employees: [],
29 | productName: null,
30 | },
31 | error: null,
32 | }
33 | }
34 |
35 | return {
36 | data: {
37 | employees: employees.map((employee) => ({
38 | id: employee.id.toInt(),
39 | name: employee.employeeName.getFullName(),
40 | })),
41 | productName: product.name.value,
42 | },
43 | error: null,
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/web/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types'
2 |
3 | export * from './scrum-team-edit-query-service'
4 | export * from './scrum-team-query-service'
5 | export * from './employee-list-query-service'
6 | export * from './project-list-query-service'
7 | export * from './top-page-query-service'
8 | export * from './sidebar-query-service'
9 | export * from './breadcrumb-query-service'
10 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/web/project-list-query-service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Product,
3 | ProductRepositoryInterface,
4 | Project,
5 | ProjectRepositoryInterface,
6 | ScrumTeam,
7 | ScrumTeamRepositoryInterface,
8 | } from '@panda-project/core'
9 |
10 | import { DefaultError, ErrorReason, Result } from './types'
11 |
12 | import { ProductRepository, ProjectRepository, ScrumTeamRepository } from '@panda-project/gateway'
13 |
14 | export type ProjectListQueryServiceDto = {
15 | product?: {
16 | id: NonNullable
17 | name: Product['name']['value']
18 | }
19 | project?: {
20 | id: NonNullable
21 | name: Project['name']['value']
22 | }
23 | scrumTeam?: {
24 | poName: ReturnType
25 | smName: ReturnType
26 | developersCount: number
27 | } | null
28 | }
29 |
30 | interface CustomError extends DefaultError {
31 | reason: typeof ErrorReason.ProductNotExists
32 | }
33 |
34 | export class ProjectListQueryService {
35 | constructor(
36 | private readonly productRepository: ProductRepositoryInterface = new ProductRepository(),
37 | private readonly projectRepository: ProjectRepositoryInterface = new ProjectRepository(),
38 | private readonly scrumTeamRepository: ScrumTeamRepositoryInterface = new ScrumTeamRepository()
39 | ) {}
40 |
41 | async exec(): Promise> {
42 | // business logic
43 | const product = await this.productRepository.fetch()
44 | if (product === null) {
45 | return {
46 | data: null,
47 | error: {
48 | reason: ErrorReason.ProductNotExists,
49 | },
50 | }
51 | }
52 |
53 | // product と project は同時に作るのでここを通ることはない
54 | const project = (await this.projectRepository.fetch())!
55 |
56 | try {
57 | const scrumTeam = await this.scrumTeamRepository.fetchOrFail()
58 |
59 | // presentation logic
60 | return {
61 | data: {
62 | product: {
63 | id: product.id.toInt(),
64 | name: product.name.value,
65 | },
66 | project: {
67 | id: project.id.toInt(),
68 | name: project.name.value,
69 | },
70 | scrumTeam: {
71 | poName: scrumTeam.productOwner.getFullName(),
72 | smName: scrumTeam.scrumMaster.getFullName(),
73 | developersCount: scrumTeam.developers.length,
74 | },
75 | },
76 | error: null,
77 | }
78 | } catch (e) {
79 | return {
80 | data: {
81 | product: {
82 | id: product.id.toInt(),
83 | name: product.name.value,
84 | },
85 | project: {
86 | id: project.id.toInt(),
87 | name: project.name.value,
88 | },
89 | scrumTeam: null,
90 | },
91 | error: null,
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/web/scrum-team-edit-query-service.ts:
--------------------------------------------------------------------------------
1 | import { EmployeeRepositoryInterface, ScrumTeamRepositoryInterface } from '@panda-project/core'
2 |
3 | import { Result } from './types'
4 |
5 | import { EmployeeRepository, ScrumTeamRepository } from '@panda-project/gateway'
6 |
7 | export type ScrumTeamEditQueryServiceDto = {
8 | scrumTeam: {
9 | id: number
10 | scrumMaster: {
11 | employeeId: number
12 | name: string
13 | isDeveloper: boolean
14 | }
15 | productOwner: {
16 | employeeId: number
17 | name: string
18 | isDeveloper: boolean
19 | }
20 | developers: {
21 | employeeId: number
22 | name: string
23 | }[]
24 | } | null
25 | employees: {
26 | id: number
27 | fullName: string
28 | }[]
29 | }
30 |
31 | export class ScrumTeamEditQueryService {
32 | constructor(
33 | private readonly scrumTeamRepository: ScrumTeamRepositoryInterface = new ScrumTeamRepository(),
34 | private readonly employeeRepository: EmployeeRepositoryInterface = new EmployeeRepository()
35 | ) {}
36 |
37 | async exec(): Promise> {
38 | const allEmployees = await this.employeeRepository.findAll()
39 | const employees = allEmployees.map((employee) => ({
40 | id: employee.id.toInt(),
41 | fullName: employee.employeeName.getFullName(),
42 | }))
43 |
44 | try {
45 | const { id, scrumMaster, productOwner, developers } = await this.scrumTeamRepository.fetchOrFail()
46 | // presentation logic
47 | return {
48 | data: {
49 | scrumTeam: {
50 | id: id.toInt(),
51 | scrumMaster: {
52 | employeeId: scrumMaster.getEmployeeId().toInt(),
53 | name: scrumMaster.getFullName(),
54 | isDeveloper: scrumMaster.isDeveloper(),
55 | },
56 | productOwner: {
57 | employeeId: productOwner.getEmployeeId().toInt(),
58 | name: productOwner.getFullName(),
59 | isDeveloper: productOwner.isDeveloper(),
60 | },
61 | developers: developers.map((developer) => ({
62 | employeeId: developer.getEmployeeId().toInt(),
63 | name: developer.getFullName(),
64 | })),
65 | },
66 | employees,
67 | },
68 | error: null,
69 | }
70 | } catch {
71 | return {
72 | data: { scrumTeam: null, employees },
73 | error: null,
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/web/scrum-team-query-service.ts:
--------------------------------------------------------------------------------
1 | import { ScrumTeamRepositoryInterface } from '@panda-project/core'
2 |
3 | import { Result } from './types'
4 |
5 | import { ScrumTeamRepository } from '@panda-project/gateway'
6 |
7 | export type ScrumTeamQueryServiceDto = {
8 | scrumTeam: {
9 | scrumMaster: {
10 | employeeId: number
11 | name: string
12 | isDeveloper: boolean
13 | }
14 | productOwner: {
15 | employeeId: number
16 | name: string
17 | isDeveloper: boolean
18 | }
19 | developers: {
20 | employeeId: number
21 | name: string
22 | }[]
23 | } | null
24 | }
25 |
26 | export class ScrumTeamQueryService {
27 | constructor(private readonly scrumTeamRepository: ScrumTeamRepositoryInterface = new ScrumTeamRepository()) {}
28 |
29 | async exec(): Promise> {
30 | try {
31 | const { scrumMaster, productOwner, developers } = await this.scrumTeamRepository.fetchOrFail()
32 | // presentation logic
33 | return {
34 | data: {
35 | scrumTeam: {
36 | scrumMaster: {
37 | employeeId: scrumMaster.getEmployeeId().toInt(),
38 | name: scrumMaster.getFullName(),
39 | isDeveloper: scrumMaster.isDeveloper(),
40 | },
41 | productOwner: {
42 | employeeId: productOwner.getEmployeeId().toInt(),
43 | name: productOwner.getFullName(),
44 | isDeveloper: productOwner.isDeveloper(),
45 | },
46 | developers: developers.map((developer) => ({
47 | employeeId: developer.getEmployeeId().toInt(),
48 | name: developer.getFullName(),
49 | })),
50 | },
51 | },
52 | error: null,
53 | }
54 | } catch {
55 | return {
56 | data: { scrumTeam: null },
57 | error: null,
58 | }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/web/sidebar-query-service.ts:
--------------------------------------------------------------------------------
1 | import { ProductRepositoryInterface, ProjectRepositoryInterface } from '@panda-project/core'
2 |
3 | import { Result } from './types'
4 |
5 | import { ProductRepository, ProjectRepository } from '@panda-project/gateway'
6 |
7 | export type SidebarDto = {
8 | projectName: string
9 | productName: string
10 | }
11 |
12 | export class SidebarQueryService {
13 | constructor(
14 | private readonly productRepository: ProductRepositoryInterface = new ProductRepository(),
15 | private readonly projectRepository: ProjectRepositoryInterface = new ProjectRepository()
16 | ) {}
17 |
18 | async exec(): Promise> {
19 | // business logic
20 | const project = await this.projectRepository.fetch()
21 | if (project === null) {
22 | // { data: null, error: {reason; xxx }} を返すようにする
23 | throw new Error('プロジェクトを取得できませんでした')
24 | }
25 |
26 | const product = await this.productRepository.fetch()
27 | if (product === null) {
28 | // { data: null, error: {reason; xxx }} を返すようにする
29 | throw new Error('プロジェクトを取得できませんでした')
30 | }
31 |
32 | // presentation logic
33 | return {
34 | data: {
35 | projectName: project.name.value,
36 | productName: product.name.value,
37 | },
38 | error: null,
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/web/top-page-query-service.ts:
--------------------------------------------------------------------------------
1 | import { ProductRepositoryInterface } from '@panda-project/core'
2 |
3 | import { Result } from './types'
4 |
5 | import { createDb, dbFileExists, ProductRepository } from '@panda-project/gateway'
6 |
7 | type Dto = {
8 | productName: string | null
9 | }
10 |
11 | export class TopPageQueryService {
12 | constructor(private readonly productRepository: ProductRepositoryInterface = new ProductRepository()) {}
13 |
14 | async exec(): Promise> {
15 | // DB がない時は、DB + Product, Project を作成する
16 | const product = await this.productRepository.fetch()
17 |
18 | if (!dbFileExists()) {
19 | // DB を作成する
20 | // 副作用があるので本当は望ましくない
21 | // TODO: トップページの useEffect でAPI をコールして実行するようにする。
22 | await createDb()
23 | }
24 |
25 | if (product === null) {
26 | return {
27 | data: { productName: null },
28 | error: null,
29 | }
30 | }
31 |
32 | // Product, Project がある場合は、/:project に移動する
33 | return {
34 | data: { productName: product.name.value },
35 | error: null,
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/query-service/web/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface DefaultError {
2 | reason: ErrorReasonValueType
3 | }
4 |
5 | export type Result = { data: T; error: null } | { data: null; error: E }
6 |
7 | export const ErrorReason = {
8 | // common
9 | DbNotExists: 'db_not_exists',
10 | UnknownError: 'unknown_error',
11 | // product
12 | ProductNotExists: 'product_not_exists',
13 | InvalidProductName: 'invalid_product_name',
14 | // project
15 | ProjectNotExists: 'project_not_exists',
16 | InvalidProjectName: 'invalid_project_name',
17 | // scrum team
18 | ScrumTeamNotExists: 'scrum_team_not_exists',
19 | } as const
20 | export type ErrorReasonValueType = (typeof ErrorReason)[keyof typeof ErrorReason]
21 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/scenario/init/index.ts:
--------------------------------------------------------------------------------
1 | export * from './scenario'
2 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/scenario/init/scenario.ts:
--------------------------------------------------------------------------------
1 | import { ProductUseCase } from '@/application/use-case/product'
2 | import { ProjectUseCase } from '@/application/use-case/project'
3 | import { InitCommand } from '@panda-project/core'
4 |
5 | export class InitScenario {
6 | constructor(
7 | private readonly productUseCase: ProductUseCase = new ProductUseCase(),
8 | private readonly projectUseCase: ProjectUseCase = new ProjectUseCase()
9 | ) {}
10 |
11 | async exec(command: InitCommand) {
12 | // 1つしかないはずなので、存在しない場合はエラーにする
13 | // 本格的にやるなら(product の id を取得するなら)、ログインする機能が必要
14 | const existsProduct = await this.productUseCase.exists()
15 | if (existsProduct) {
16 | throw new Error('プロダクトは作成済みです')
17 | }
18 |
19 | await this.productUseCase.create(command.getCreateProductCommand())
20 | await this.projectUseCase.create(command.getCreateProjectCommand())
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/use-case/employee/index.ts:
--------------------------------------------------------------------------------
1 | export * from './use-case'
2 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/use-case/employee/use-case.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CreateEmployeeCommand,
3 | CreateMultipleEmployeeCommand,
4 | EditEmployeeCommand,
5 | Employee,
6 | EmployeeId,
7 | EmployeeRepositoryInterface,
8 | RemoveEmployeeCommand,
9 | ScrumTeamRepositoryInterface,
10 | } from '@panda-project/core'
11 |
12 | import { EmployeeRepository, ScrumTeamRepository } from '@panda-project/gateway'
13 |
14 | export class EmployeeUseCase {
15 | constructor(
16 | private readonly employeeRepository: EmployeeRepositoryInterface = new EmployeeRepository(),
17 | private readonly scrumTeamRepository: ScrumTeamRepositoryInterface = new ScrumTeamRepository()
18 | ) {}
19 |
20 | async create(command: CreateEmployeeCommand): Promise {
21 | const count = await this.employeeRepository.count()
22 | if (count > 50) {
23 | throw new Error('登録できる従業員数の上限に達しました。登録可能な人数は50名以下です')
24 | }
25 |
26 | const employeeName = command.getEmployeeName()
27 | const employee = new Employee(EmployeeId.createAsNull(), employeeName)
28 |
29 | await this.employeeRepository.save(employee)
30 | }
31 |
32 | async createMultiple(command: CreateMultipleEmployeeCommand): Promise {
33 | const names = command.getEmployeeNames()
34 |
35 | // n+1 だが、DB は json なので特に問題にしない。
36 | // RDB なら、bulk insert の処理を repository に実装する
37 | for (const name of names) {
38 | const count = await this.employeeRepository.count()
39 | if (count > 50) {
40 | throw new Error('登録できる従業員数の上限に達しました。登録可能な人数は50名以下です')
41 | }
42 |
43 | const employee = new Employee(EmployeeId.createAsNull(), name)
44 | await this.employeeRepository.save(employee)
45 | }
46 | }
47 |
48 | async edit(command: EditEmployeeCommand) {
49 | const employee = await this.employeeRepository.findByIdOrFail(command.getEmployeeId())
50 | const newName = command.getNewEmployeeName()
51 | const newEmployee = employee.updateName(newName)
52 |
53 | await this.employeeRepository.update(newEmployee)
54 | }
55 |
56 | async remove(command: RemoveEmployeeCommand) {
57 | const employee = await this.employeeRepository.findByIdOrFail(command.getEmployeeId())
58 |
59 | let scrumTeam = null
60 | try {
61 | scrumTeam = await this.scrumTeamRepository.fetchOrFail()
62 | } catch (e: unknown) {
63 | // スクラムチームが存在しない場合は、社員を削除できる
64 | await this.employeeRepository.delete(employee)
65 | return
66 | }
67 |
68 | const isBelongToScrumTeam = scrumTeam.isBelongTo(employee.id)
69 | if (isBelongToScrumTeam) {
70 | throw new Error('社員がスクラムチームに所属しているため、削除できません')
71 | }
72 |
73 | await this.employeeRepository.delete(employee)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/use-case/product/index.ts:
--------------------------------------------------------------------------------
1 | export * from './use-case'
2 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/use-case/product/use-case.ts:
--------------------------------------------------------------------------------
1 | import { CreateProductCommand, Product, ProductId, ProductRepositoryInterface } from '@panda-project/core'
2 |
3 | import { ProductRepository } from '@panda-project/gateway'
4 |
5 | export class ProductUseCase {
6 | constructor(private readonly productRepository: ProductRepositoryInterface = new ProductRepository()) {}
7 |
8 | async create(command: CreateProductCommand) {
9 | const product = new Product(ProductId.createAsNull(), command.getProductName())
10 | return await this.productRepository.save(product)
11 | }
12 |
13 | async exists(): Promise {
14 | return await this.productRepository.existsWithoutId()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/use-case/project/index.ts:
--------------------------------------------------------------------------------
1 | export * from './use-case'
2 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/use-case/project/use-case.ts:
--------------------------------------------------------------------------------
1 | import { CreateProjectCommand, ProductId, Project, ProjectRepositoryInterface } from '@panda-project/core'
2 | import { ProjectRepository } from '@panda-project/gateway'
3 |
4 | export class ProjectUseCase {
5 | constructor(private readonly productRepository: ProjectRepositoryInterface = new ProjectRepository()) {}
6 |
7 | async create(command: CreateProjectCommand) {
8 | const product = new Project(ProductId.createAsNull(), command.getProjectName())
9 | return await this.productRepository.save(product)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/use-case/src/application/use-case/scrum-team/index.ts:
--------------------------------------------------------------------------------
1 | export * from './use-case'
2 |
--------------------------------------------------------------------------------
/packages/use-case/src/index.ts:
--------------------------------------------------------------------------------
1 | // query service
2 | export * from './application/query-service'
3 |
4 | // scenario
5 | export * from './application/scenario/init'
6 |
7 | // use case
8 | export * from './application/use-case/employee'
9 | export * from './application/use-case/product'
10 | export * from './application/use-case/project'
11 | export * from './application/use-case/scrum-team'
12 |
--------------------------------------------------------------------------------
/packages/use-case/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@panda-project/config/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["./src/*"]
7 | },
8 | "outDir": "./dist"
9 | },
10 | "include": ["src"],
11 | "exclude": ["**/*.test.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'apps/*'
3 | - 'packages/*'
4 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "pipeline": {
4 | "build": {
5 | "dependsOn": ["^build"],
6 | "outputs": ["dist/**"],
7 | "env": ["NODE_ENV"],
8 | "cache": true,
9 | "persistent": false,
10 | "maxParallel": 4
11 | },
12 | "type-check": {},
13 | "lint": {},
14 | "dev": {
15 | "cache": false,
16 | "persistent": true
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------