├── .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 | ![image](./docs/top-page.png) 129 | 130 | プロジェクト名とプロダクト名を送信すると以下の画面に遷移します。 131 | 132 | ![image](./docs/employee-page.png) 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 |
48 | 49 | 55 |
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 |
18 | 19 |
20 | 21 |
22 |
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 | 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 |
15 |
16 | 17 |
18 | 19 |
20 |
21 |
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 |
6 | 7 |
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 |
46 |

社員を登録する

47 |
48 |
49 |
50 |
51 | 63 | 64 | 65 |
66 | 67 |
68 | 80 | 81 | 82 |
83 | 84 |
85 | 86 |
87 |
88 |
89 |
90 |
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 |
12 | 13 | 22 |
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 |
51 |
52 | 53 | {employeeId}: 54 | 55 |
56 |
57 |
58 | 69 | 70 | 71 |
72 | 73 |
74 | 85 | 86 | 87 |
88 |
89 | 90 |
91 | 92 | 93 |
94 |
95 |
96 |
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 |
12 |
13 |
14 | 15 |
16 |
17 |
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 |
39 | 40 | 41 |
42 | 51 | 52 | 53 |
54 | 55 |
56 | 65 | 66 | 67 |
68 | 69 |
70 | 71 |
72 | 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 |
  1. 14 | {message} 15 |
  2. 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 |
42 | 43 |
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' &&
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 |
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 | --------------------------------------------------------------------------------