├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github ├── images │ ├── aggregate-dark.png │ ├── aggregate.png │ ├── clean-architecture-dark.png │ ├── clean-architecture.png │ ├── communication-flow-dark.png │ ├── communication-flow.png │ ├── dependency-injection-dark.png │ ├── dependency-injection.png │ ├── inversion-of-control-dark.png │ ├── inversion-of-control.png │ ├── packages-dark.png │ ├── packages.png │ ├── ubiquitous-dark.png │ └── ubiquitous.png └── workflows │ ├── client-a-ci.yml │ ├── client-b-ci.yml │ └── domains-adapters-ci.yml ├── .gitignore ├── .prettierrc ├── .yarn └── install-state.gz ├── .yarnrc.yml ├── LICENSE ├── README-ko.md ├── README.md ├── jest.config.js ├── package.json ├── packages ├── adapters │ ├── .eslintrc.js │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── __test__ │ │ │ └── dtos │ │ │ │ └── UserDTO.spec.ts │ │ ├── dtos │ │ │ ├── CommentDTO.ts │ │ │ ├── PostDTO.ts │ │ │ └── UserDTO.ts │ │ ├── infrastructures │ │ │ ├── AxiosHTTP.ts │ │ │ ├── WebStorage.ts │ │ │ └── interfaces │ │ │ │ ├── IAxiosHTTP.ts │ │ │ │ ├── IInfrastructures.ts │ │ │ │ └── IWebStorage.ts │ │ ├── presenters │ │ │ ├── PostPresenter.ts │ │ │ ├── UserPresenter.ts │ │ │ └── interfaces │ │ │ │ ├── IPostPresenter.ts │ │ │ │ ├── IPresenters.ts │ │ │ │ └── IUserPresenter.ts │ │ └── repositories │ │ │ ├── UserRepository.ts │ │ │ ├── comment │ │ │ ├── NetworkCommentRepository.ts │ │ │ └── StorageCommentRepository.ts │ │ │ └── post │ │ │ ├── NetworkPostRepository.ts │ │ │ └── StoragePostRepository.ts │ └── tsconfig.json ├── client-a │ ├── README.md │ ├── cypress.config.ts │ ├── cypress │ │ ├── e2e │ │ │ └── spec.cy.ts │ │ ├── fixtures │ │ │ └── example.json │ │ └── support │ │ │ ├── commands.ts │ │ │ └── e2e.ts │ ├── index.html │ ├── jest.config.js │ ├── jest.setup.js │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── Routes.tsx │ │ ├── __test__ │ │ │ └── components │ │ │ │ └── Button.spec.tsx │ │ ├── components │ │ │ ├── atoms │ │ │ │ ├── Button.tsx │ │ │ │ ├── CommentList.tsx │ │ │ │ ├── DimmedLoading.tsx │ │ │ │ ├── Input.tsx │ │ │ │ ├── Logo.tsx │ │ │ │ └── PostDetail.tsx │ │ │ ├── molecules │ │ │ │ ├── CommentForm.tsx │ │ │ │ ├── HeaderMenu.tsx │ │ │ │ ├── PostBox.tsx │ │ │ │ ├── PostForm.tsx │ │ │ │ ├── PostList.tsx │ │ │ │ └── SideMenu.tsx │ │ │ ├── organisms │ │ │ │ ├── contents │ │ │ │ │ ├── DashboardContent.tsx │ │ │ │ │ └── PostContent.tsx │ │ │ │ └── layouts │ │ │ │ │ ├── BaseFooter.tsx │ │ │ │ │ ├── BaseHeader.tsx │ │ │ │ │ └── BaseSidebar.tsx │ │ │ ├── pages │ │ │ │ ├── Dashboard.tsx │ │ │ │ └── Post.tsx │ │ │ └── templates │ │ │ │ ├── Content.tsx │ │ │ │ ├── Footer.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── Sidebar.tsx │ │ │ │ └── Template.tsx │ │ ├── constants │ │ │ └── index.ts │ │ ├── contexts │ │ │ └── Layout.ts │ │ ├── di │ │ │ ├── index.ts │ │ │ ├── infrastructures.ts │ │ │ ├── presenters.ts │ │ │ ├── repositories.ts │ │ │ └── useCases.ts │ │ ├── global.css │ │ ├── hooks │ │ │ └── usePosts.ts │ │ ├── main.tsx │ │ └── vms │ │ │ ├── CommentVM.ts │ │ │ ├── PostVM.ts │ │ │ └── interfaces │ │ │ ├── ICommentVM.ts │ │ │ └── IPostVM.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.mjs ├── client-b │ ├── README.md │ ├── cypress.config.ts │ ├── cypress │ │ ├── e2e │ │ │ └── spec.cy.ts │ │ ├── fixtures │ │ │ └── example.json │ │ └── support │ │ │ ├── commands.ts │ │ │ └── e2e.ts │ ├── jest.config.js │ ├── jest.setup.js │ ├── next-env.d.ts │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ └── index.html │ ├── src │ │ ├── __test__ │ │ │ └── components │ │ │ │ └── Button.spec.tsx │ │ ├── app │ │ │ ├── global.css │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── components │ │ │ ├── atoms │ │ │ │ ├── Button.tsx │ │ │ │ ├── DimmedLoading.tsx │ │ │ │ ├── Input.tsx │ │ │ │ └── Logo.tsx │ │ │ ├── molecules │ │ │ │ ├── HeaderMenu.tsx │ │ │ │ ├── PostBox.tsx │ │ │ │ ├── PostForm.tsx │ │ │ │ ├── PostList.tsx │ │ │ │ └── SideMenu.tsx │ │ │ ├── organisms │ │ │ │ ├── contents │ │ │ │ │ └── DashboardContent.tsx │ │ │ │ └── layouts │ │ │ │ │ ├── BaseFooter.tsx │ │ │ │ │ ├── BaseHeader.tsx │ │ │ │ │ └── BaseSidebar.tsx │ │ │ └── templates │ │ │ │ ├── Content.tsx │ │ │ │ ├── Footer.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── Sidebar.tsx │ │ │ │ └── Template.tsx │ │ ├── constants │ │ │ └── index.ts │ │ ├── contexts │ │ │ └── Layout.ts │ │ ├── di │ │ │ ├── index.ts │ │ │ ├── infrastructures.ts │ │ │ ├── interfaces │ │ │ │ ├── IPresenters.ts │ │ │ │ ├── IRepositories.ts │ │ │ │ └── IUseCases.ts │ │ │ ├── presenters.ts │ │ │ ├── repositories.ts │ │ │ └── useCases.ts │ │ └── hooks │ │ │ └── usePosts.ts │ ├── tailwind.config.js │ └── tsconfig.json └── domains │ ├── .eslintrc.js │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── __test__ │ │ └── vos │ │ │ └── UserInfoVo.spec.ts │ ├── aggregates │ │ ├── Post.ts │ │ └── interfaces │ │ │ └── IPost.ts │ ├── dtos │ │ └── interfaces │ │ │ ├── ICommentDTO.ts │ │ │ ├── IPostDTO.ts │ │ │ └── IUserDTO.ts │ ├── entities │ │ ├── Comment.ts │ │ ├── User.ts │ │ └── interfaces │ │ │ ├── IComment.ts │ │ │ └── IUser.ts │ ├── repositories │ │ └── interfaces │ │ │ ├── ICommentRepository.ts │ │ │ ├── IPostRepository.ts │ │ │ ├── IRepositories.ts │ │ │ └── IUserRepository.ts │ ├── useCases │ │ ├── PostUseCase.ts │ │ ├── UserUseCase.ts │ │ └── interfaces │ │ │ ├── IPostUseCase.ts │ │ │ ├── IUseCases.ts │ │ │ └── IUserUseCase.ts │ └── vos │ │ ├── UserInfoVO.ts │ │ └── interfaces │ │ └── IUserInfoVO.ts │ └── tsconfig.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | browser: true, 6 | node: true, 7 | jest: true 8 | }, 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | ecmaVersion: "latest", 12 | sourceType: "module", 13 | ecmaFeatures: { 14 | jsx: true 15 | } 16 | }, 17 | extends: [ 18 | "eslint:recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | "plugin:prettier/recommended", 21 | "plugin:react/recommended", 22 | "plugin:react-hooks/recommended" 23 | ], 24 | plugins: [], 25 | rules: { 26 | "@typescript-eslint/no-require-imports": "off", 27 | "react/react-in-jsx-scope": "off" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | /.yarn/releases/* binary 3 | /.yarn/plugins/**/* binary 4 | /.pnp.* binary linguist-generated 5 | -------------------------------------------------------------------------------- /.github/images/aggregate-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/aggregate-dark.png -------------------------------------------------------------------------------- /.github/images/aggregate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/aggregate.png -------------------------------------------------------------------------------- /.github/images/clean-architecture-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/clean-architecture-dark.png -------------------------------------------------------------------------------- /.github/images/clean-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/clean-architecture.png -------------------------------------------------------------------------------- /.github/images/communication-flow-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/communication-flow-dark.png -------------------------------------------------------------------------------- /.github/images/communication-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/communication-flow.png -------------------------------------------------------------------------------- /.github/images/dependency-injection-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/dependency-injection-dark.png -------------------------------------------------------------------------------- /.github/images/dependency-injection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/dependency-injection.png -------------------------------------------------------------------------------- /.github/images/inversion-of-control-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/inversion-of-control-dark.png -------------------------------------------------------------------------------- /.github/images/inversion-of-control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/inversion-of-control.png -------------------------------------------------------------------------------- /.github/images/packages-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/packages-dark.png -------------------------------------------------------------------------------- /.github/images/packages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/packages.png -------------------------------------------------------------------------------- /.github/images/ubiquitous-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/ubiquitous-dark.png -------------------------------------------------------------------------------- /.github/images/ubiquitous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.github/images/ubiquitous.png -------------------------------------------------------------------------------- /.github/workflows/client-a-ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - "main" 5 | paths: 6 | - "packages/client-a/**" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 18 20 | 21 | - name: Enable Corepack 22 | run: corepack enable 23 | 24 | - name: Set up Yarn 4.2.2 25 | run: corepack prepare yarn@4.2.2 --activate 26 | 27 | - name: Install dependencies 28 | run: yarn install 29 | 30 | - name: Run tests 31 | run: yarn test:a 32 | 33 | - name: Start client 34 | run: yarn start:a & 35 | 36 | - name: Wait for client 37 | run: npx wait-on http://localhost:4000 38 | 39 | - name: Run Cypress tests 40 | uses: cypress-io/github-action@v4 41 | with: 42 | project: packages/client-a 43 | browser: chrome 44 | 45 | - name: Stop all background tasks 46 | run: kill $(jobs -p) || true 47 | -------------------------------------------------------------------------------- /.github/workflows/client-b-ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - "main" 5 | paths: 6 | - "packages/client-b/**" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 18 20 | 21 | - name: Enable Corepack 22 | run: corepack enable 23 | 24 | - name: Set up Yarn 4.2.2 25 | run: corepack prepare yarn@4.2.2 --activate 26 | 27 | - name: Install dependencies 28 | run: yarn install 29 | 30 | - name: Run tests 31 | run: yarn test:b 32 | 33 | - name: Start client 34 | run: yarn start:b & 35 | 36 | - name: Wait for client 37 | run: npx wait-on http://localhost:4001 38 | 39 | - name: Run Cypress tests 40 | uses: cypress-io/github-action@v4 41 | with: 42 | project: packages/client-b 43 | browser: chrome 44 | 45 | - name: Stop all background tasks 46 | run: kill $(jobs -p) || true 47 | -------------------------------------------------------------------------------- /.github/workflows/domains-adapters-ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - "main" 5 | paths: 6 | - "packages/domains/**" 7 | - "packages/adapters/**" 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 18 21 | 22 | - name: Enable Corepack 23 | run: corepack enable 24 | 25 | - name: Set up Yarn 4.2.2 26 | run: corepack prepare yarn@4.2.2 --activate 27 | 28 | - name: Install dependencies 29 | run: yarn install 30 | 31 | - name: Run tests 32 | run: yarn test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | .DS_Store 3 | 4 | #!.yarn/cache 5 | .pnp.* 6 | node_modules 7 | 8 | # Env 9 | .env 10 | 11 | # Build 12 | dist 13 | 14 | # Next.js 15 | .next 16 | 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "none", 4 | "tabWidth": 2, 5 | "semi": false 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falsy/clean-architecture-for-frontend/c244dfd6f500d4c0d373dad84105596b305eafc7/.yarn/install-state.gz -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README-ko.md: -------------------------------------------------------------------------------- 1 | # Clean Architecture for Frontend 2 | 3 | 클린 아키텍처는 `DDD(Domain-driven Design)`와 `MSA(Micro Service Architecture)`와 함께 많은 프로젝트에서 활용되고 있습니다. 이 프로젝트에서는 TypeScript를 사용하면서 동일한 도메인을 공유하는 다양한 웹 클라이언트 서비스를 모노레포 구성과 클린 아키텍처 설계를 통해서 서비스를 효과적으로 확장하거나 유지 보수를 용이하게 하는 하나의 아이디어 프로젝트입니다. 4 | 5 | 만약, 프로젝트가 단순한 UI를 다루는 작은 규모의 프로젝트이거나 API 서버가 클라이언트와 맞춤으로 대응되는 환경이라면 클린 아키텍처 도입은 오히려 보일러 플레이트 코드로 인한 코드량 증가와 복잡성 증가로 서비스의 유지 보수성이 나빠질 수 있습니다. 6 | 7 | 샘플 프로젝트는 Yarn에서 기본으로 제공하는 `Workspace`를 사용하여 모노레포를 구성하고, 패키지로 클린 아키텍처의 Domains 레이어와 Adapters 레이어를 구성하였고 각각의 서비스 역시 패키지로 구성하며 각 서비스는 Domains 레이어와 Adapters 레이어의 요소를 그대로 사용하거나 또는 상속, 확장하여 서비스를 구성합니다. 8 | 9 | ## Languages 10 | 11 | - [English](https://github.com/falsy/clean-architecture-with-typescript) 12 | - [한글](https://github.com/falsy/clean-architecture-with-typescript/blob/main/README-ko.md) 13 | 14 | # Clean Architecture 15 | 16 | ![Alt Clean architecture](.github/images/clean-architecture.png#gh-light-mode-only) 17 | ![Alt Clean architecture](.github/images/clean-architecture-dark.png#gh-dark-mode-only) 18 | 19 | 다양한 아키텍처들이 그러하듯 클린 아키텍처가 갖는 기본 목적은 관심사를 분리하는 것입니다. 각의 관심사에 따라 계층을 나누고 세부 구현이 아닌 도메인 중심으로 설계하며, 내부 영역이 프레임워크나 데이터베이스, UI 등의 외부 요소에 의존하지 않도록 합니다. 20 | 21 | - 세부 구현 영역과 도메인 영역을 구분합니다. 22 | - 아키텍처는 프레임워크에 의존하지 않습니다. 23 | - 외부 영역은 내부 영역에 의존할 수 있지만, 내부 영역은 외부 영역에 의존할 수 없습니다. 24 | - 고수준, 저수준 모듈 모두 추상화에 의존합니다. 25 | 26 | ## Communitaction Flow 27 | 28 | ![Alt Communitaction Flow](.github/images/communication-flow.png#gh-light-mode-only) 29 | ![Alt Communitaction Flow](.github/images/communication-flow-dark.png#gh-dark-mode-only) 30 | 31 | 클린 아키텍처의 흐름을 간단하게 다이어그램으로 표현하면 위와 같습니다. 32 | 33 | # Monorepo 34 | 35 | ![Alt Monorepo](.github/images/packages.png#gh-light-mode-only) 36 | ![Alt Monorepo](.github/images/packages-dark.png#gh-dark-mode-only) 37 | 38 | 모노레포는 Domains 레이어와 Adapters 레이어 그리고 서비스 레이어를 각각 패키지로 의존성을 명확하게 구분하였습니다. 39 | 그리고 루트에서는 TypeScript, ESLint, Jest의 기본 설정으로 하위 패키지에서는 확장하여 사용할 수 있습니다. 40 | 41 | > 만약, 도메인을 공유하는 여러 서비스가 아닌 단일 서비스를 위한 구성이라면 모노레포를 사용하지 않고 Domains와 Adapters레이어를 각각 패키지에서 디렉토리로, 서비스 패키지는 Frameworks 디렉토리로 구성하여, 전체 프로젝트는 크게 Domians, Adapters, Frameworks로 나누고 이를 중심으로 설계할 수 있습니다. 42 | 43 | # Directory Structure 44 | 45 | ``` 46 | /packages 47 | ├─ domains 48 | │ └─ src 49 | │ ├─ aggregates 50 | │ ├─ entities 51 | │ ├─ useCases 52 | │ ├─ vos 53 | │ ├─ repositories 54 | │ │ └─ interface 55 | │ └─ dtos 56 | │ └─ interface 57 | ├─ adapters 58 | │ └─ src 59 | │ ├─ presenters 60 | │ ├─ repositories 61 | │ ├─ dtos 62 | │ └─ infrastructures 63 | │ └─ interface 64 | ├─ client-a(built with React) 65 | │ └─ src 66 | │ ├─ di 67 | │ └─ ... 68 | └─ client-b(built with Next.js) 69 | └─ src 70 | ├─ di 71 | └─ ... 72 | ``` 73 | 74 | ## Tree Shaking 75 | 76 | 위 샘플 프로젝트에서는, 각각의 서비스 패키지에서 공용 패키지(`Domains`, `Adapters`, `그 밖의 추가될 수 있는 패키지들..`)를 사용할 때 빌드된 결과물을 참조하지 않고 `Source-to-Source` 방식으로 사용하고 있습니다. 이는 최종적으로 서비스의 모듈 번들러가 효과적으로 사용되지 않는 코드를 제거하고 사용할 수 있어야 하기 때문에, 공용 패키지는 모두 `ES Modules`로 작성해야 합니다. 77 | 78 | > 대부분의 모듈 번들러는 ES Modules로 작성된 코드에 대해서 트리셰이킹을 기본으로 지원합니다. 79 | 80 | # Domains 81 | 82 | 도메인 레이어에서는 비즈니스 규칙과 비즈니스 로직을 정의합니다. 83 | 84 | 샘플 프로젝트의 경우에는 간단한 포럼 서비스의 일부분으로 사용자가 글 목록을 보거나 글과 댓글을 작성할 수 있는 서비스입니다. 모노레포로 구성된 하나의 패키지로 Entity와 Use Case 그리고 Value Object 등의 정의하고, 다양한 서비스 패키지는 이를 사용하여 서비스를 구성합니다. 85 | 86 | ## Entities 87 | 88 | Entity는 도메인 모델링의 핵심 개념 중 하나로, 고유한 식별자(Identity)를 통해 동일성을 유지하면서 상태와 행동을 가지는 객체입니다. Entity는 단순히 데이터를 보관하는 구조체가 아니라, 자신의 데이터를 직접 제어하고 관리하는 역할을 하며, 도메인 내에서 중요한 비즈니스 규칙과 로직을 표현합니다. 89 | 90 | 샘플 프로젝트에서는 Post, Comment, User 라는 3개의 엔티티로 구성되어 있습니다. 91 | 92 | ## Domain-driven Design(DDD) 93 | 94 | 클린 아키텍처는 DDD와 공통적으로 도메인 중심의 설계를 지향합니다. 클린 아키텍처는 소프트웨어의 구조적 유연성과 애플리케이션의 유지 보수, 그리고 기술의 독립성와 테스트 용이성에 중점을 두고 있으며 DDD는 비즈니스 문제 해결에 초점을 맞추고 있습니다. 95 | 96 | 하지만 클린 아키텍처는 DDD의 철학과 원칙을 일부 차용하고 있으며 DDD와 호환되며, DDD를 효과적으로 적용할 수 있습니다. 그리고 그 예로 클린 아키텍처는 DDD의 개념인 `Ubiquitous Language`와 `Aggregate Root`를 활용할 수 있습니다. 97 | 98 | ### Ubiquitous Language 99 | 100 | ![Ubiquitous Language](.github/images/ubiquitous.png#gh-light-mode-only) 101 | ![Ubiquitous Language](.github/images/ubiquitous-dark.png#gh-dark-mode-only) 102 | 103 | 유비쿼터스 언어는 프로젝트 전반에 걸쳐 의사소통의 일관성을 유지하기 위해 모든 팀원이 사용하는 공유 언어를 말합니다. 이 언어는 프로젝트 리더, 도메인 전문가, 개발자, UI/UX 디자이너, 비즈니스 분석가, QA 엔지니어 등을 포함한 모든 프로젝트 구성원이 공유해야 합니다. 그리고 이 언어는 협업 중 문서화나 대화에 사용될 뿐만 아니라 소프트웨어 모델과 코드에도 반영되어야 합니다. 104 | 105 | ### Aggregate Root 106 | 107 | ![Aggregate](.github/images/aggregate.png#gh-light-mode-only) 108 | ![Aggregate](.github/images/aggregate-dark.png#gh-dark-mode-only) 109 | 110 | Aggregate는 여러 엔티티와 값 객체를 포함할 수 있는 일관성 경계로, 내부 상태를 캡슐화하여 외부에서의 접근을 제어합니다. 모든 수정은 반드시 Aggregate Root를 통해서만 이루어지며, 이는 모델 내의 관계 복잡성을 관리하고, 서비스 확장 및 트랜잭션 복잡성 증가 시 일관성을 유지하는 데 도움이 됩니다. 111 | 112 | 샘플 프로젝트에서는 Post가 Aggregate로 사용되었으며 하위에는 종속적인 관계의 Comment 엔티티가 있습니다. 그렇기에 Comment를 추가 및 변경하기 위해서는 Post를 통해서 이루어집니다. 그리고 Post의 속성으로 작성자 즉, 글을 작성한 사용자의 정보가 필요하지만 User는 독립적인 엔티티이기 때문에 얕은 관계를 유지하기 위하여 User의 id 값과 name 정보만을 Value Object로 가지고 있습니다. 113 | 114 | ## Use Cases 115 | 116 | Use Case는 사용자와 서비스 간의 상호작용을 정의하며, 도메인 객체(Entity, Aggregate, Value Object)를 활용하여 서비스가 사용자에게 제공해야 하는 비즈니스 기능을 명확하게 합니다. 시스템 아키텍처 관점에서 Use Case는 애플리케이션 로직과 비즈니스 규칙을 분리하는 역할을 하며, 직접적으로 비즈니스 로직을 제어하기보다는 도메인 객체가 가진 비즈니스 규칙과 로직을 활용할 수 있도록 돕습니다. 117 | 118 | 샘플 프로젝트에서는 간단하게 전체 요약 글 리스트를 가져오거나 글과 댓글을 추가, 삭제, 변경과 같은 간단한 상호 작용으로 구성되어 있습니다. 119 | 120 | ## Inversion of Control 121 | 122 | ![Alt Inversion Of Control](.github/images/inversion-of-control.png#gh-light-mode-only) 123 | ![Alt Inversion Of Control](.github/images/inversion-of-control-dark.png#gh-dark-mode-only) 124 | 125 | Repository의 경우 Adapter 레이어에 해당하기 때문에 Use Case에서는 Repository에 대해서 알아서는 안됩니다. 그렇기 때문에 Use Case에서는 Repository를 추상화한 인터페이스를 가지고 구현하며, 이는 이후에 `의존성 주입(DI: Dependency Injection)`를 통해 동작합니다. 126 | 127 | # Adapters 128 | 129 | Domains 계층과 유사하게 Adatpers 계층도 모노레포 내에서 단일 패키지로 구성하여 사용합니다. Apapter에서는 일반적으로 Presenters, Repositories 및 Infrastructure 구성 요소가 포함됩니다. 이러한 구성 요소는 의존성 주입(DI)을 통해 서비스 패키지에서 사용되며 필요에 따라 상속하고 사용자 정의하여 확장할 수 있습니다. 130 | 131 | ## Infrastructures 132 | 133 | Infrastructure 레이어에서는 웹 서비스에서 일반적으로 많이 사용하는 HTTP를 사용한 외부 서버와의 통신이나 또는 LocalStorage와 같은 브라우저의 Web API와 같은 애플리케이션 외부와의 연결을 관리합니다. 134 | 135 | ## Repositories 136 | 137 | 일반적으로 백엔드에서 Repository 레이어는 데이터베이스와 관련된 `CRUD` 작업을 수행하며 데이터의 저장, 조회, 수정, 삭제와 같은 기본적인 데이터 조작을 처리합니다. 그리고 그러한 데이터베이스와의 상호작용을 추상화하여 비즈니스 로직에서 데이터 저장소에 대해 알 필요가 없도록 합니다. 138 | 139 | 같은 원리로 샘플 프로젝트에서 Repository 레이어는 API 서버와의 HTTP 통신에 관련된 POST, GET, PUT, DELETE 작업을 수행하며 그 상호작용을 추상화하여 비즈니스 로직에서는 데이터의 출처에 대해서 알 필요가 없도록 합니다. 그리고 외부 서버로부터 받은 데이터는 `DTO`로 캡슐화하여 이 데이터가 클라이언트 내부에서 사용될 때의 안정성을 보장합니다. 140 | 141 | ## Presenters 142 | 143 | Presenters 레이어에서는 UI에서 필요로하는 메서드를 가지고 사용자의 요청을 서버로 전달하는 역할을 하며, 요청에 따라 엔티티 데이터를 UI에서 사용되는 View Model로 값을 변환하여 응답하는 역할을 하기도 합니다. 144 | 145 | ## Dependency Injection 146 | 147 | ![Alt Dependency Injection](.github/images/dependency-injection.png#gh-light-mode-only) 148 | ![Alt Dependency Injection](.github/images/dependency-injection-dark.png#gh-dark-mode-only) 149 | 150 | 각각의 레이어는 최종적으로 의존성 주입(Dependency Injection)을 통해 동작합니다. 예로 들면, 각 레이어별로 인터페이스를 정의하고 이 인터페이스를 기반으로 다양한 구현체를 만들어 필요에 따라 주입하여 사용합니다. 151 | 152 | 샘플 프로젝트에서는 Repository 인터페이스를 정의하고, 이를 구현하는 NetworkRepository(HTTP 통신)와 StorageRepository(웹 스토리지 사용)를 구성한 뒤, 서비스에 따라 선택적으로 주입하여 사용하고 있습니다. 153 | 154 | 이처럼 의존성 주입을 통한 서비스 구성은 역할과 책임을 명확히 나누어 수정 범위를 최소화할 수 있으며, 추상화에 의존하는 설계를 통해 새로운 구현체를 추가하여 높은 확장성과 유연성을 가진 서비스를 개발할 수 있습니다. 155 | 156 | > 일반적으로 HTTP 통신과 웹 스토리지는 각기 다른 역할을 하므로, 샘플 프로젝트처럼 두 구현체를 같은 동작으로 정의하고 선택적으로 사용하는 경우는 드뭅니다. 이 예시는 단순히 다양한 구현체를 정의하고 그 차이를 보여주기 위한 목적으로 사용되었습니다. 157 | 158 | # Services 159 | 160 | 샘플 프로젝트의 클라이언트 서비스는 Client-A, Client-B 이렇게 2개의 간단한 서비스로 구성되어 있습니다. 두 서비스 모두 동일한 도메인 기반의 서비스로 UI 컴포넌트는 [Atomic Design](https://bradfrost.com/blog/post/atomic-web-design/)을 기반으로 설계하였습니다. 161 | 162 | ## Client-A 163 | 164 | ### Use Stack 165 | 166 | ``` 167 | Vite, React, Jotai, Tailwind CSS, Jest, RTL, Cypress 168 | ``` 169 | 170 | Client-A는 `Domains`와 `Adapters` 레이어의 요소들을 그대로 사용해서 최종적으로 `DI`된 상위 레이어의 객체를 React의 Hooks와 전역 상태 라이브러리인 [Jotai](https://jotai.org/)를 활용하여 각 도메인의 메서드를 구현하고 이는 Presenters 레이어의 역할을 수행합니다. 171 | 172 | > 기존에 Adapters 패키지에서 Presenters 디렉토리로 명시적으로 Presenters 레이어를 나누었지만 이는 프레임워크에 의존하지 않은 범용적인 Presenters이며, 위 샘플 프로젝트처럼 React를 사용하는 서비스에서는 그에 부합하는 구성을 위해서 최종적으로 의존성을 주입한 Presenters 객체와 React Hooks을 활용하여 Presenters 영역을 확장 구성하였습니다. 173 | 174 | ### Dependency Injection 175 | 176 | ```tsx 177 | import { API_URL } from "../constants" 178 | import infrastructuresFn from "./infrastructures" 179 | import repositoriesFn from "./repositories" 180 | import useCasesFn from "./useCases" 181 | import presentersFn from "./presenters" 182 | 183 | export default function di(apiUrl = API_URL) { 184 | const infrastructures = infrastructuresFn(apiUrl) 185 | const repositories = repositoriesFn(infrastructures) 186 | const useCases = useCasesFn(repositories) 187 | const presenters = presentersFn(useCases) 188 | 189 | return presenters 190 | } 191 | ``` 192 | 193 | ### Presenters 194 | 195 | ```tsx 196 | import { useCallback, useMemo, useOptimistic, useState, useTransition } from "react" 197 | import { atom, useAtom } from "jotai" 198 | import presenters from "../di" 199 | import PostVM from "../vms/PostVM" 200 | import IPostVM from "../vms/interfaces/IPostVM" 201 | 202 | const PostsAtoms = atom([]) 203 | 204 | export default function usePosts() { 205 | const di = useMemo(() => presenters(), []) 206 | 207 | const [post, setPost] = useState(null) 208 | const [posts, setPosts] = useAtom(PostsAtoms) 209 | const [optimisticPost, setOptimisticPost] = useOptimistic(post) 210 | const [optimisticPosts, setOptimisticPosts] = useOptimistic(posts) 211 | const [isPending, startTransition] = useTransition() 212 | 213 | const getPosts = useCallback(async () => { 214 | startTransition(async () => { 215 | const resPosts = await di.post.getPosts() 216 | const postVMs = resPosts.map((post) => new PostVM(post)) 217 | setPosts(postVMs) 218 | }) 219 | }, [di.post, setPosts]) 220 | 221 | ... 222 | } 223 | ``` 224 | 225 | ### View Models 226 | 227 | Client-A에서는 프로젝트 레이어에서 React의 UI 상태 관리에 적합하도록 View Model을 구성하여 사용하였습니다. 228 | 229 | ```ts 230 | import CryptoJS from "crypto-js" 231 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 232 | import ICommentVM, { ICommentVMParams } from "./interfaces/ICommentVM" 233 | 234 | export default class CommentVM implements ICommentVM { 235 | readonly id: string 236 | readonly postId: string 237 | readonly author: IUserInfoVO 238 | readonly createdAt: Date 239 | key: string 240 | content: string 241 | updatedAt: Date 242 | 243 | constructor(parmas: ICommentVMParams) { 244 | this.id = parmas.id 245 | this.postId = parmas.postId 246 | this.author = parmas.author 247 | this.content = parmas.content 248 | this.createdAt = parmas.createdAt 249 | this.updatedAt = parmas.updatedAt 250 | this.key = this.generateKey(this.id, this.updatedAt) 251 | } 252 | 253 | updateContent(content: string): void { 254 | this.content = content 255 | this.updatedAt = new Date() 256 | this.key = this.generateKey(this.id, this.updatedAt) 257 | } 258 | 259 | applyUpdatedAt(date: Date): void { 260 | this.updatedAt = date 261 | this.key = this.generateKey(this.id, this.updatedAt) 262 | } 263 | 264 | private generateKey(id: string, updatedAt: Date): string { 265 | const base = `${id}-${updatedAt.getTime()}` 266 | return CryptoJS.MD5(base).toString() 267 | } 268 | } 269 | ``` 270 | 271 | View Model에서는 위와 같이 값 변경에 따른 메서드를 제공하며(e.g., updateContent) 모든 변경에는 updatedAt 값이 함께 변경하고, updatedAt 값과 ID 값을 활용하여 고유한 `Key` 값을 만들어 사용함으로써 React가 View의 변경을 감지하고 리렌더링 할 수 있도록 하였습니다. 272 | 273 | ```tsx 274 | ... 275 | 276 | export default function usePosts() { 277 | ... 278 | 279 | const deleteComment = useCallback( 280 | async (commentId: string) => { 281 | startTransition(async () => { 282 | setOptimisticPost((prevPost) => { 283 | prevPost.deleteComment(commentId) 284 | return prevPost 285 | }) 286 | 287 | try { 288 | const isSucess = await di.post.deleteComment(commentId) 289 | if (isSucess) { 290 | const resPost = await di.post.getPost(optimisticPost.id) 291 | const postVM = new PostVM(resPost) 292 | setPost(postVM) 293 | } 294 | } catch (e) { 295 | console.error(e) 296 | } 297 | }) 298 | }, 299 | [di.post, optimisticPost, setOptimisticPost, setPost] 300 | ) 301 | 302 | ... 303 | } 304 | ``` 305 | 306 | Presenter 레이어의 Hooks에서도 위와 같이 Comment의 삭제 요청에 대한 간단한 예시로, VM에서 제공하는 메서드를 활용하여 낙관적 업데이트를 구현하고 요청이 성공하면 위 변경이 적용된 새로운 데이터를 요청하여 동기화 하도록 하였습니다. 307 | 308 | ## Client-B 309 | 310 | ### Use Stack 311 | 312 | ``` 313 | Next.js, Jotai, Tailwind CSS, Jest, RTL, Cypress 314 | ``` 315 | 316 | Client-B는 Client-A와 동일한 도메인을 활용한, 서비스 확장을 표현하는 서비스로 Client-A 서비스와 유사하지만 Client-A 서비스와 다르게 Next.js를 기반으로 하며 기존의 Client-A 서비스는 API 서버와의 HTTP 통신을 통해 데이터를 조작하지만 Client-B는 HTTP 통신 없이 로컬 저장소(Local Storage)를 기반으로 동작하는 차이가 있습니다. 317 | 318 | Client-A와 같은 `Domains` 레이어와 `Adpaters` 레이어에서 정의한 인터페이스와 구현체를 활용하여 높은 코드 재사용성을 가지고 구성할 수 있습니다. 또한, 의존성 주입(DI) 과정에서 HTTP 통신을 사용하는 Repository를 대신하여 웹 스토리지를 사용하는 Repository를 사용하는 것만으로 간단하게 새로운 서비스를 구현할 수 있습니다. 319 | 320 | > Client-B는 구제적인 기능 구현보다는 동일한 도메인을 활용한 다른 클라이언트 서비스 구성에 대한 간단한 예시입니다. 321 | 322 | ## Design System 323 | 324 | 샘플 서비스처럼 각 서비스가 동일한 프레임워크를 사용한다면, 모노레포 구성의 장점을 활용하여 공통으로 사용 가능한 UI 컴포넌트를 별도의 패키지로 구성함으로써 컴포넌트의 재사용성을 높여 더욱 효과적으로 서비스를 확장 및 유지 보수할 수도 있습니다. 325 | 326 | # 실행 327 | 328 | 샘플 프로젝트는 루트에 등록된 커맨드를 활용하여 각 패키지를 빌드 또는 실행할 수 있습니다. 329 | 330 | ## 설치 331 | 332 | ```sh 333 | $ yarn install 334 | ``` 335 | 336 | ## 실행 337 | 338 | ```sh 339 | # client-a 340 | $ yarn start:a 341 | 342 | # client-b 343 | $ yarn start:b 344 | ``` 345 | 346 | # Thank You! 347 | 348 | 모든 지원과 관심에 감사드립니다. 🙇‍♂️ 349 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean Architecture for Frontend 2 | 3 | Clean Architecture is widely used in many projects alongside `DDD(Domain-driven Design)` and `MSA(Microservice Architecture)`. This project is an idea-driven initiative that leverages TypeScript, a monorepo setup, and Clean Architecture to effectively scale and maintain various web client services that share the same domain. 4 | 5 | However, if the project is a small-scale application that primarily focuses on simple UI, or if the API server is tightly coupled with the client, adopting Clean Architecture might negatively impact maintainability due to increased code complexity and boilerplate code. 6 | 7 | In this sample project, a monorepo is structured using `Workspaces` provided by Yarn. The monorepo consists of the Domains and Adapters layers as separate packages, while each service is also packaged individually. These services directly utilize, extend, or inherit elements from the Domains and Adapters layers to build their respective implementations. 8 | 9 | #### Note. 10 | 11 | > \+ My English is not perfect, so please bear with me. 12 | 13 | ## Languages 14 | 15 | - [English](https://github.com/falsy/clean-architecture-with-typescript) 16 | - [한글](https://github.com/falsy/clean-architecture-with-typescript/blob/main/README-ko.md) 17 | 18 | # Clean Architecture 19 | 20 | ![Alt Clean architecture](.github/images/clean-architecture.png#gh-light-mode-only) 21 | ![Alt Clean architecture](.github/images/clean-architecture-dark.png#gh-dark-mode-only) 22 | 23 | As with many architectures, the primary goal of Clean Architecture is to separate concerns. It divides layers according to each concern, designs around the domain rather than detailed implementations, and ensures the inner layers do not depend on external elements like frameworks, databases, or UIs. 24 | 25 | - Separate the detailed implementation area and the domain area. 26 | - The architecture does not depend on the framework. 27 | - The outer layers can depend on the inner layers, but the inner layers cannot depend on the outer layers. 28 | - Both high-level and low-level modules depend on abstractions. 29 | 30 | ## Communitaction Flow 31 | 32 | ![Alt Communitaction Flow](.github/images/communication-flow.png#gh-light-mode-only) 33 | ![Alt Communitaction Flow](.github/images/communication-flow-dark.png#gh-dark-mode-only) 34 | 35 | The flow of Clean Architecture can be briefly illustrated in the diagram above. 36 | 37 | # Monorepo 38 | 39 | ![Alt Monorepo](.github/images/packages.png#gh-light-mode-only) 40 | ![Alt Monorepo](.github/images/packages-dark.png#gh-dark-mode-only) 41 | 42 | In the monorepo structure, the Domains layer, Adapters layer, and Service layer are clearly separated into individual packages with well-defined dependencies. At the root level, basic configurations for TypeScript, ESLint, and Jest are provided, which can be extended and used in the lower-level packages. 43 | 44 | > If the project is for a single service rather than multiple services sharing the same domain, a monorepo is not necessary. Instead, the Domains and Adapters layers can be structured as directories rather than separate packages, while the service package can be placed in the Frameworks directory. This allows the entire project to be organized into three main sections: Domains, Adapters, and Frameworks, forming the core of the architecture. 45 | 46 | # Directory Structure 47 | 48 | ``` 49 | /packages 50 | ├─ domains 51 | │ └─ src 52 | │ ├─ aggregates 53 | │ ├─ entities 54 | │ ├─ useCases 55 | │ ├─ vos 56 | │ ├─ repositories 57 | │ │ └─ interface 58 | │ └─ dtos 59 | │ └─ interface 60 | ├─ adapters 61 | │ └─ src 62 | │ ├─ presenters 63 | │ ├─ repositories 64 | │ ├─ dtos 65 | │ └─ infrastructures 66 | │ └─ interface 67 | ├─ client-a(built with React) 68 | │ └─ src 69 | │ ├─ di 70 | │ └─ ... 71 | └─ client-b(built with Next.js) 72 | └─ src 73 | ├─ di 74 | └─ ... 75 | ``` 76 | 77 | ## Tree Shaking 78 | 79 | In this sample project, service packages use shared packages (`Domains`, `Adapters`, `and other potential packages`) through a `Source-to-Source` approach, rather than referencing pre-built outputs. This approach ensures that the service's module bundler can effectively eliminate unused code during the final build. Therefore, all shared packages must be written using `ES Modules`. 80 | 81 | > Most module bundlers natively support tree shaking for code written in ES Modules. 82 | 83 | # Domains 84 | 85 | The domain layer defines business rules and business logic. 86 | 87 | In the sample project, it represents a simple forum service where users can view a list of posts or create posts and comments. It is structured as a single package within a monorepo, containing definitions for Entities, Use Cases, and Value Objects. Various service packages utilize these definitions to build their respective functionalities. 88 | 89 | ## Entities 90 | 91 | Entities are one of the core concepts in domain modeling, representing objects that maintain a unique identity and contain both state and behavior. An Entity is not just a data holder but is responsible for controlling and managing its data. It encapsulates important business rules and logic within the domain. 92 | 93 | In the sample project, there are three entities: Post, Comment, and User. 94 | 95 | ## Domain-driven Design(DDD) 96 | 97 | Clean Architecture shares a common goal with DDD in pursuing domain-centric design. While Clean Architecture focuses on structural flexibility, maintainability, technological independence, and testability of software, DDD emphasizes solving complex business problems. 98 | 99 | However, Clean Architecture adopts some of DDD's philosophy and principles, making it compatible with DDD and providing a framework to effectively implement DDD concepts. For example, Clean Architecture can leverage DDD concepts such as `Ubiquitous Language` and `Aggregate Root`. 100 | 101 | ### Ubiquitous Language 102 | 103 | ![Ubiquitous Language](.github/images/ubiquitous.png#gh-light-mode-only) 104 | ![Ubiquitous Language](.github/images/ubiquitous-dark.png#gh-dark-mode-only) 105 | 106 | Ubiquitous Language refers to a shared language used by all team members to maintain consistent communication throughout a project. This language should be shared by everyone involved, including project leaders, domain experts, developers, UI/UX designers, business analysts, and QA engineers. It should not only be used in documentation and discussions during collaboration but also be reflected in the software model and code. 107 | 108 | ### Aggregate Root 109 | 110 | ![Aggregate](.github/images/aggregate.png#gh-light-mode-only) 111 | ![Aggregate](.github/images/aggregate-dark.png#gh-dark-mode-only) 112 | 113 | An Aggregate is a consistency boundary that can include multiple entities and value objects. It encapsulates internal state and controls external access. All modifications must go through the Aggregate Root, which helps manage the complexity of relationships within the model and maintain consistency when services expand or transactions become more complex. 114 | 115 | In the sample project, Post serves as an Aggregate, with the Comment entity having a dependent relationship on it. Therefore, adding or modifying a comment must be done through the Post entity. Additionally, while the Post entity requires information about the author (the User who wrote the post), the User is an independent entity. To maintain a loose relationship, only the User's id and name are included as a Value Object within Post. 116 | 117 | ## Use Cases 118 | 119 | Use Cases define the interactions between users and the service, leveraging domain objects such as Entities, Aggregates, and Value Objects to deliver business functionality to users. From a system architecture perspective, Use Cases help separate application logic from business rules. Rather than directly controlling business logic, Use Cases facilitate interaction with the domain objects, allowing them to enforce business rules and logic. 120 | 121 | In the sample project, Use Cases include simple interactions such as retrieving a summarized list of posts, and adding, deleting, or modifying posts and comments. 122 | 123 | ## Inversion of Control 124 | 125 | ![Alt Inversion Of Control](.github/images/inversion-of-control.png#gh-light-mode-only) 126 | ![Alt Inversion Of Control](.github/images/inversion-of-control-dark.png#gh-dark-mode-only) 127 | 128 | Since the Repository belongs to the Adapter layer, the higher-level Use Case should not directly depend on it. Therefore, in the Use Case, an abstract interface for the Repository is implemented, which is later handled through `Dependency Injection(DI)`. 129 | 130 | # Adapters 131 | 132 | Similar to the domain layer, the adapters are also organized as a single package within the monorepo. The adapter layer typically includes Presenters, Repositories, and Infrastructure components. These are used in service packages through dependency injection (DI) and can be extended by inheriting and customizing them as needed. 133 | 134 | ## Infrastructures 135 | 136 | The Infrastructure layer manages external connections such as communication with external servers via HTTP or interactions with browser APIs like LocalStorage, which are commonly used in web services. 137 | 138 | ## Repositories 139 | 140 | In a typical backend, the Repository layer handles CRUD operations related to databases, such as storing, retrieving, modifying, and deleting data. It abstracts database interactions so that the business logic does not need to be aware of the underlying data store. 141 | 142 | Similarly, in the sample project, the Repository layer performs POST, GET, PUT, and DELETE operations for HTTP communication with the API server. It abstracts these interactions so the business logic is not concerned with where the data originates. Data retrieved from external servers is encapsulated as DTOs (Data Transfer Objects) to ensure stability when used internally within the client. 143 | 144 | ## Presenters 145 | 146 | The Presenter layer handles requests from the UI, forwarding them to the server. It also converts entity data into View Models used in the UI, responding appropriately based on user requests. 147 | 148 | ## Dependency Injection 149 | 150 | ![Alt Dependency Injection](.github/images/dependency-injection.png#gh-light-mode-only) 151 | ![Alt Dependency Injection](.github/images/dependency-injection-dark.png#gh-dark-mode-only) 152 | 153 | Each layer ultimately operates through Dependency Injection. For example, interfaces are defined for each layer, and various implementations are created based on these interfaces, which are then injected as needed. 154 | 155 | In the sample project, a Repository interface is defined, and two implementations, NetworkRepository (for HTTP communication) and StorageRepository (for web storage), are created. These are then injected into services based on the requirements. 156 | 157 | This approach to service configuration through Dependency Injection helps clearly define roles and responsibilities, minimizing the scope of changes. By relying on abstraction in the design, new implementations can be easily added, allowing for highly scalable and flexible service development. 158 | 159 | > Typically, HTTP communication and web storage serve different purposes, so it is uncommon to define and selectively use two implementations in the same way as in the sample project. This example was simply used to demonstrate the differences between various implementations. 160 | 161 | # Services 162 | 163 | The sample project's client services consist of two simple services: client-a and client-b. Both services are built based on the same domain-driven architecture, and their UI components are designed following the principles of [Atomic Design](https://bradfrost.com/blog/post/atomic-web-design/). 164 | 165 | ## Client-A 166 | 167 | ### Use Stack 168 | 169 | ``` 170 | Vite, React, Jotai, Tailwind CSS, Jest, RTL, Cypress 171 | ``` 172 | 173 | Client-A directly utilizes elements from the `Domains` and `Adapters` layers and implements methods for each domain using React hooks and the global state management library [Jotai](https://jotai.org/). These methods act as the Presenters layer in the final service. 174 | 175 | > Previously, the Adapters package explicitly included a Presenters directory to represent a framework-agnostic Presenters layer. However, in services like this sample project that use React, we extend the Presenters layer by injecting dependencies into the final Presenters objects and utilizing React hooks to achieve a composition that aligns with the framework. 176 | 177 | ### Dependency Injection 178 | 179 | ```tsx 180 | import { API_URL } from "../constants" 181 | import infrastructuresFn from "./infrastructures" 182 | import repositoriesFn from "./repositories" 183 | import useCasesFn from "./useCases" 184 | import presentersFn from "./presenters" 185 | 186 | export default function di(apiUrl = API_URL) { 187 | const infrastructures = infrastructuresFn(apiUrl) 188 | const repositories = repositoriesFn(infrastructures) 189 | const useCases = useCasesFn(repositories) 190 | const presenters = presentersFn(useCases) 191 | 192 | return presenters 193 | } 194 | ``` 195 | 196 | ### Presenters 197 | 198 | ```tsx 199 | import { useCallback, useMemo, useOptimistic, useState, useTransition } from "react" 200 | import { atom, useAtom } from "jotai" 201 | import presenters from "../di" 202 | import PostVM from "../vms/PostVM" 203 | import IPostVM from "../vms/interfaces/IPostVM" 204 | 205 | const PostsAtoms = atom([]) 206 | 207 | export default function usePosts() { 208 | const di = useMemo(() => presenters(), []) 209 | 210 | const [post, setPost] = useState(null) 211 | const [posts, setPosts] = useAtom(PostsAtoms) 212 | const [optimisticPost, setOptimisticPost] = useOptimistic(post) 213 | const [optimisticPosts, setOptimisticPosts] = useOptimistic(posts) 214 | const [isPending, startTransition] = useTransition() 215 | 216 | const getPosts = useCallback(async () => { 217 | startTransition(async () => { 218 | const resPosts = await di.post.getPosts() 219 | const postVMs = resPosts.map((post) => new PostVM(post)) 220 | setPosts(postVMs) 221 | }) 222 | }, [di.post, setPosts]) 223 | 224 | ... 225 | } 226 | ``` 227 | 228 | ### View Models 229 | 230 | In Client-A, we structured the View Model in the project layer to effectively manage UI state in React. 231 | 232 | ```ts 233 | import CryptoJS from "crypto-js" 234 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 235 | import ICommentVM, { ICommentVMParams } from "./interfaces/ICommentVM" 236 | 237 | export default class CommentVM implements ICommentVM { 238 | readonly id: string 239 | readonly postId: string 240 | readonly author: IUserInfoVO 241 | readonly createdAt: Date 242 | key: string 243 | content: string 244 | updatedAt: Date 245 | 246 | constructor(parmas: ICommentVMParams) { 247 | this.id = parmas.id 248 | this.postId = parmas.postId 249 | this.author = parmas.author 250 | this.content = parmas.content 251 | this.createdAt = parmas.createdAt 252 | this.updatedAt = parmas.updatedAt 253 | this.key = this.generateKey(this.id, this.updatedAt) 254 | } 255 | 256 | updateContent(content: string): void { 257 | this.content = content 258 | this.updatedAt = new Date() 259 | this.key = this.generateKey(this.id, this.updatedAt) 260 | } 261 | 262 | applyUpdatedAt(date: Date): void { 263 | this.updatedAt = date 264 | this.key = this.generateKey(this.id, this.updatedAt) 265 | } 266 | 267 | private generateKey(id: string, updatedAt: Date): string { 268 | const base = `${id}-${updatedAt.getTime()}` 269 | return CryptoJS.MD5(base).toString() 270 | } 271 | } 272 | ``` 273 | 274 | The View Model provides methods to handle value changes (e.g., updateContent). Whenever a value is updated, the updatedAt field is also modified. By using a combination of the updatedAt value and the ID, we generate a unique `key` that allows React to detect changes in the view and trigger re-renders as needed. 275 | 276 | ```tsx 277 | ... 278 | 279 | export default function usePosts() { 280 | ... 281 | 282 | const deleteComment = useCallback( 283 | async (commentId: string) => { 284 | startTransition(async () => { 285 | setOptimisticPost((prevPost) => { 286 | prevPost.deleteComment(commentId) 287 | return prevPost 288 | }) 289 | 290 | try { 291 | const isSucess = await di.post.deleteComment(commentId) 292 | if (isSucess) { 293 | const resPost = await di.post.getPost(optimisticPost.id) 294 | const postVM = new PostVM(resPost) 295 | setPost(postVM) 296 | } 297 | } catch (e) { 298 | console.error(e) 299 | } 300 | }) 301 | }, 302 | [di.post, optimisticPost, setOptimisticPost, setPost] 303 | ) 304 | 305 | ... 306 | } 307 | ``` 308 | 309 | In the Presenter layer's hooks, we implemented optimistic updates using the methods provided by the View Model. For instance, when sending a delete request for a comment, we immediately apply the changes locally. After the request succeeds, we fetch the updated data to synchronize the state. 310 | 311 | ## Client-B 312 | 313 | ### Use Stack 314 | 315 | ``` 316 | Next.js, Jotai, Tailwind CSS, Jest, RTL, Cypress 317 | ``` 318 | 319 | Client-B is a service that represents an extension of the service, using the same domain as Client-A. While similar to Client-A, Client-B is built on Next.js, whereas Client-A operates through HTTP communication with an API server to manipulate data. In contrast, Client-B operates based on local storage (Local Storage) without HTTP communication. 320 | 321 | By utilizing the interfaces and implementations defined in the Domains and Adapters layers, similar to Client-A, Client-B can be structured with high code reusability. Additionally, during the dependency injection (DI) process, a simple switch from using a repository that communicates via HTTP to one that uses local storage makes it easy to implement a new service. 322 | 323 | > Client-B serves as a simple example of structuring another client service using the same domain, rather than focusing on the specific functionality of the service. 324 | 325 | ## Design System 326 | 327 | When services use the same framework, as in this sample project, the advantages of a monorepo setup can be leveraged by creating a separate package for shared UI components. This increases component reusability, making it easier to expand and maintain services more efficiently. 328 | 329 | # Run 330 | 331 | You can build or run each package in the sample project using the commands registered at the root. 332 | 333 | ## Install 334 | 335 | ```sh 336 | $ yarn install 337 | ``` 338 | 339 | ## Start 340 | 341 | ```sh 342 | # client-a 343 | $ yarn start:a 344 | 345 | # client-b 346 | $ yarn start:b 347 | ``` 348 | 349 | # Thank You! 350 | 351 | I'm grateful for all the support and interest 🙇‍♂️ 352 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node", 3 | testMatch: ["**/?(*.)+(spec|test).[tj]s?(x)"], 4 | moduleFileExtensions: ["js", "json", "ts", "tsx"], 5 | transform: { 6 | "^.+\\.tsx?$": "ts-jest", 7 | "^.+\\.(ts|tsx|js|jsx)?$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clean-architecture-for-frontend", 3 | "private": true, 4 | "version": "4.5.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/falsy/clean-architecture-with-typescript.git" 8 | }, 9 | "author": "falsy", 10 | "license": "The Unlicense", 11 | "workspaces": [ 12 | "packages/*" 13 | ], 14 | "scripts": { 15 | "start:a": "yarn workspace client-a run start", 16 | "start:b": "yarn workspace client-b run start", 17 | "test": "yarn test:domains && yarn test:adapters && yarn test:a && yarn test:b", 18 | "test:domains": "yarn workspace domains run test", 19 | "test:adapters": "yarn workspace adapters run test", 20 | "test:a": "yarn workspace client-a run test", 21 | "test:b": "yarn workspace client-b run test", 22 | "cypress:a": "yarn workspace client-a run cypress", 23 | "cypress:b": "yarn workspace client-b run cypress", 24 | "lint": "eslint packages/*/src --ext .ts" 25 | }, 26 | "dependencies": { 27 | "react": "^19.0.0", 28 | "react-dom": "^19.0.0" 29 | }, 30 | "devDependencies": { 31 | "@testing-library/dom": "^10.4.0", 32 | "@testing-library/jest-dom": "^6.6.3", 33 | "@testing-library/react": "^16.1.0", 34 | "@types/jest": "^29.5.13", 35 | "@types/react": "^19.0.7", 36 | "@types/react-dom": "^19.0.3", 37 | "@typescript-eslint/eslint-plugin": "^8.10.0", 38 | "@typescript-eslint/parser": "^8.10.0", 39 | "cypress": "^13.17.0", 40 | "eslint": "8.57.0", 41 | "eslint-config-prettier": "^9.1.0", 42 | "eslint-plugin-prettier": "^5.2.1", 43 | "eslint-plugin-react": "^7.37.4", 44 | "eslint-plugin-react-hooks": "^5.1.0", 45 | "jest": "^29.7.0", 46 | "jest-environment-jsdom": "^29.7.0", 47 | "prettier": "^3.3.3", 48 | "ts-jest": "^29.2.5", 49 | "typescript": "^5.6.3" 50 | }, 51 | "resolutions": { 52 | "path-to-regexp": "0.1.12", 53 | "esbuild": "0.25.0" 54 | }, 55 | "packageManager": "yarn@4.2.2" 56 | } 57 | -------------------------------------------------------------------------------- /packages/adapters/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "../../.eslintrc.js" 3 | } 4 | -------------------------------------------------------------------------------- /packages/adapters/README.md: -------------------------------------------------------------------------------- 1 | # adapters 2 | -------------------------------------------------------------------------------- /packages/adapters/jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const baseConfig = require("../../jest.config") 3 | 4 | module.exports = { 5 | ...baseConfig, 6 | roots: [""], 7 | moduleNameMapper: { 8 | "^domains/(.*)$": path.resolve(__dirname, "../domains/src/$1"), 9 | "^adapters/(.*)$": path.resolve(__dirname, "../adapters/src/$1") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/adapters/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adapters", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "test": "jest" 7 | }, 8 | "dependencies": { 9 | "axios": "^1.8.2", 10 | "crypto-js": "^4.2.0", 11 | "domains": "workspace:*" 12 | }, 13 | "packageManager": "yarn@4.2.2", 14 | "devDependencies": { 15 | "@types/crypto-js": "^4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/adapters/src/__test__/dtos/UserDTO.spec.ts: -------------------------------------------------------------------------------- 1 | import IUserDTO from "domains/dtos/interfaces/IUserDTO" 2 | import UserDTO from "../../dtos/UserDTO" 3 | 4 | describe("UserDTO", () => { 5 | it("should set all properties correctly", () => { 6 | const params: IUserDTO = { 7 | id: "12345", 8 | name: "Falsy", 9 | email: "falsy@mail.com", 10 | createdAt: "2023-01-01T00:00:00Z", 11 | updatedAt: "2023-01-02T00:00:00Z" 12 | } 13 | 14 | const user = new UserDTO(params) 15 | 16 | expect(user.id).toBe("12345") 17 | expect(user.name).toBe("Falsy") 18 | expect(user.email).toBe("falsy@mail.com") 19 | expect(user.createdAt).toEqual("2023-01-01T00:00:00Z") 20 | expect(user.updatedAt).toEqual("2023-01-02T00:00:00Z") 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/adapters/src/dtos/CommentDTO.ts: -------------------------------------------------------------------------------- 1 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 2 | import ICommentDTO from "domains/dtos/interfaces/ICommentDTO" 3 | 4 | export default class CommentDTO implements ICommentDTO { 5 | readonly id: string 6 | readonly postId: string 7 | content: string 8 | readonly author: IUserInfoVO 9 | readonly createdAt: string 10 | updatedAt: string 11 | 12 | constructor(params: ICommentDTO) { 13 | this.id = params.id 14 | this.postId = params.postId 15 | this.content = params.content 16 | this.author = params.author 17 | this.createdAt = params.createdAt 18 | this.updatedAt = params.updatedAt 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/adapters/src/dtos/PostDTO.ts: -------------------------------------------------------------------------------- 1 | import IPostDTO from "domains/dtos/interfaces/IPostDTO" 2 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 3 | 4 | export default class PostDTO implements IPostDTO { 5 | readonly id: string 6 | title: string 7 | content: string 8 | readonly author: IUserInfoVO 9 | readonly createdAt: string 10 | updatedAt: string 11 | 12 | constructor(post: IPostDTO) { 13 | this.id = post.id 14 | this.title = post.title 15 | this.content = post.content 16 | this.author = post.author 17 | this.createdAt = post.createdAt 18 | this.updatedAt = post.createdAt 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/adapters/src/dtos/UserDTO.ts: -------------------------------------------------------------------------------- 1 | import IUserDTO from "domains/dtos/interfaces/IUserDTO" 2 | 3 | export default class UserDTO implements IUserDTO { 4 | readonly id: string 5 | readonly name: string 6 | readonly email: string 7 | readonly createdAt: string 8 | readonly updatedAt: string 9 | 10 | constructor(params: IUserDTO) { 11 | this.id = params.id 12 | this.name = params.name 13 | this.email = params.email 14 | this.createdAt = params.createdAt 15 | this.updatedAt = params.updatedAt 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/adapters/src/infrastructures/AxiosHTTP.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios" 2 | import { IAxiosHTTP } from "adapters/infrastructures/interfaces/IAxiosHTTP" 3 | 4 | export default class AxiosHTTP implements IAxiosHTTP { 5 | private axiosInstance: AxiosInstance 6 | 7 | constructor(baseURL: string, config?: AxiosRequestConfig) { 8 | this.axiosInstance = axios.create({ 9 | baseURL, 10 | ...config 11 | }) 12 | } 13 | 14 | get(url: string, config?: AxiosRequestConfig): Promise> { 15 | return this.axiosInstance.get(url, config) 16 | } 17 | 18 | post( 19 | url: string, 20 | data?: unknown, 21 | config?: AxiosRequestConfig 22 | ): Promise> { 23 | return this.axiosInstance.post(url, data, config) 24 | } 25 | 26 | put( 27 | url: string, 28 | data?: unknown, 29 | config?: AxiosRequestConfig 30 | ): Promise> { 31 | return this.axiosInstance.put(url, data, config) 32 | } 33 | 34 | delete( 35 | url: string, 36 | config?: AxiosRequestConfig 37 | ): Promise> { 38 | return this.axiosInstance.delete(url, config) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/adapters/src/infrastructures/WebStorage.ts: -------------------------------------------------------------------------------- 1 | import { IWebStorage } from "./interfaces/IWebStorage" 2 | 3 | export default class WebStorage implements IWebStorage { 4 | private storage: Storage 5 | 6 | constructor(storage: Storage) { 7 | this.storage = storage 8 | } 9 | 10 | async get(url: string): Promise { 11 | const data = this.storage.getItem(url) 12 | return data ? JSON.parse(data) : null 13 | } 14 | 15 | async post(url: string, data?: unknown): Promise { 16 | try { 17 | this.storage.setItem(url, JSON.stringify(data)) 18 | return true as T 19 | } catch { 20 | return false as T 21 | } 22 | } 23 | 24 | async put(url: string, data?: unknown): Promise { 25 | try { 26 | this.storage.setItem(url, JSON.stringify(data)) 27 | return true as T 28 | } catch { 29 | return false as T 30 | } 31 | } 32 | 33 | async delete(url: string): Promise { 34 | try { 35 | this.storage.removeItem(url) 36 | return true as T 37 | } catch { 38 | return false as T 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/adapters/src/infrastructures/interfaces/IAxiosHTTP.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig, AxiosResponse } from "axios" 2 | 3 | export interface IAxiosHTTP { 4 | get(url: string, config?: AxiosRequestConfig): Promise> 5 | post( 6 | url: string, 7 | data?: unknown, 8 | config?: AxiosRequestConfig 9 | ): Promise> 10 | put( 11 | url: string, 12 | data?: unknown, 13 | config?: AxiosRequestConfig 14 | ): Promise> 15 | delete(url: string, config?: AxiosRequestConfig): Promise> 16 | } 17 | -------------------------------------------------------------------------------- /packages/adapters/src/infrastructures/interfaces/IInfrastructures.ts: -------------------------------------------------------------------------------- 1 | import { IAxiosHTTP } from "./IAxiosHTTP" 2 | import { IWebStorage } from "./IWebStorage" 3 | 4 | export default interface IInfratructures { 5 | network?: IAxiosHTTP 6 | storage?: IWebStorage 7 | } 8 | -------------------------------------------------------------------------------- /packages/adapters/src/infrastructures/interfaces/IWebStorage.ts: -------------------------------------------------------------------------------- 1 | export interface IWebStorage { 2 | get(url: string): Promise 3 | post(url: string, data?: unknown): Promise 4 | put(url: string, data?: unknown): Promise 5 | delete(url: string): Promise 6 | } 7 | -------------------------------------------------------------------------------- /packages/adapters/src/presenters/PostPresenter.ts: -------------------------------------------------------------------------------- 1 | import IPost, { IRequestPostParams } from "domains/aggregates/interfaces/IPost" 2 | import IPostUseCase from "domains/useCases/interfaces/IPostUseCase" 3 | import IPostPresenter from "./interfaces/IPostPresenter" 4 | 5 | export default class PostPresenter implements IPostPresenter { 6 | private postUseCase: IPostUseCase 7 | 8 | constructor(postUseCase: IPostUseCase) { 9 | this.postUseCase = postUseCase 10 | } 11 | 12 | getPosts(): Promise { 13 | return this.postUseCase.getPosts() 14 | } 15 | 16 | getPost(postId: string): Promise { 17 | return this.postUseCase.getPost(postId) 18 | } 19 | 20 | createPost(params: IRequestPostParams): Promise { 21 | return this.postUseCase.createPost(params) 22 | } 23 | 24 | updatePost(postId: string, params: IRequestPostParams): Promise { 25 | return this.postUseCase.updatePost(postId, params) 26 | } 27 | 28 | deletePost(postId: string): Promise { 29 | return this.postUseCase.deletePost(postId) 30 | } 31 | 32 | createComment(postId: string, content: string): Promise { 33 | return this.postUseCase.createComment(postId, content) 34 | } 35 | 36 | updateComment(commentId: string, content: string): Promise { 37 | return this.postUseCase.updateComment(commentId, content) 38 | } 39 | 40 | deleteComment(commentId: string): Promise { 41 | return this.postUseCase.deleteComment(commentId) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/adapters/src/presenters/UserPresenter.ts: -------------------------------------------------------------------------------- 1 | import IUserPresenter from "./interfaces/IUserPresenter" 2 | import IUserUseCase from "domains/useCases/interfaces/IUserUseCase" 3 | 4 | export default class UserPresenter implements IUserPresenter { 5 | private userUseCase: IUserUseCase 6 | 7 | constructor(userUseCase: IUserUseCase) { 8 | this.userUseCase = userUseCase 9 | } 10 | 11 | getUser() { 12 | return this.userUseCase.getUser() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/adapters/src/presenters/interfaces/IPostPresenter.ts: -------------------------------------------------------------------------------- 1 | import IPost, { IRequestPostParams } from "domains/aggregates/interfaces/IPost" 2 | 3 | export default interface IPostPresenter { 4 | getPosts(): Promise 5 | getPost(postId: string): Promise 6 | createPost(params: IRequestPostParams): Promise 7 | updatePost(postId: string, params: IRequestPostParams): Promise 8 | deletePost(postId: string): Promise 9 | createComment(postId: string, content: string): Promise 10 | updateComment(commentId: string, content: string): Promise 11 | deleteComment(commentId: string): Promise 12 | } 13 | -------------------------------------------------------------------------------- /packages/adapters/src/presenters/interfaces/IPresenters.ts: -------------------------------------------------------------------------------- 1 | import IPostPresenter from "./IPostPresenter" 2 | import IUserPresenter from "./IUserPresenter" 3 | 4 | export default interface IPresenters { 5 | post: IPostPresenter 6 | user: IUserPresenter 7 | } 8 | -------------------------------------------------------------------------------- /packages/adapters/src/presenters/interfaces/IUserPresenter.ts: -------------------------------------------------------------------------------- 1 | import IUser from "domains/entities/interfaces/IUser" 2 | 3 | export default interface IUserPresenter { 4 | getUser(): Promise 5 | } 6 | -------------------------------------------------------------------------------- /packages/adapters/src/repositories/UserRepository.ts: -------------------------------------------------------------------------------- 1 | import IUserRepository from "domains/repositories/interfaces/IUserRepository" 2 | import IUserDTO from "domains/dtos/interfaces/IUserDTO" 3 | import IConnector from "../infrastructures/interfaces/IConnector" 4 | import UserDTO from "../dtos/UserDTO" 5 | 6 | export default class UserRepository implements IUserRepository { 7 | private connector: IConnector 8 | 9 | constructor(connector: IConnector) { 10 | this.connector = connector 11 | } 12 | 13 | async getUser(): Promise { 14 | try { 15 | const { data } = await this.connector.get("/api/users") 16 | 17 | if (!data) { 18 | return {} as IUserDTO 19 | } 20 | 21 | return new UserDTO(data) 22 | } catch (e) { 23 | console.error(e) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/adapters/src/repositories/comment/NetworkCommentRepository.ts: -------------------------------------------------------------------------------- 1 | import ICommentRepository from "domains/repositories/interfaces/ICommentRepository" 2 | import ICommentDTO from "domains/dtos/interfaces/ICommentDTO" 3 | import CommentDTO from "adapters/dtos/CommentDTO" 4 | import { IAxiosHTTP } from "adapters/infrastructures/interfaces/IAxiosHTTP" 5 | 6 | export default class NetworkCommentRepository implements ICommentRepository { 7 | private connector: IAxiosHTTP 8 | 9 | constructor(connector: IAxiosHTTP) { 10 | this.connector = connector 11 | } 12 | 13 | async getComments(postId: string): Promise { 14 | try { 15 | const { data } = await this.connector.get( 16 | `/api/posts/${postId}/comments` 17 | ) 18 | 19 | if (!data) { 20 | return [] 21 | } 22 | 23 | return data.map((comment) => { 24 | return new CommentDTO(comment) 25 | }) 26 | } catch (e) { 27 | console.error(e) 28 | } 29 | } 30 | 31 | async createComment(postId: string, content: string): Promise { 32 | try { 33 | const { data } = await this.connector.post( 34 | `/api/posts/${postId}/comments`, 35 | { 36 | content 37 | } 38 | ) 39 | 40 | return data 41 | } catch (e) { 42 | console.error(e) 43 | } 44 | } 45 | 46 | async updateComment(commentId: string, content: string): Promise { 47 | try { 48 | const { data } = await this.connector.put( 49 | `/api/comments/${commentId}`, 50 | { 51 | content 52 | } 53 | ) 54 | 55 | return data 56 | } catch (e) { 57 | console.error(e) 58 | } 59 | } 60 | 61 | async deleteComment(commentId: string): Promise { 62 | try { 63 | const { data } = await this.connector.delete( 64 | `/api/comments/${commentId}` 65 | ) 66 | 67 | return data 68 | } catch (e) { 69 | console.error(e) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/adapters/src/repositories/comment/StorageCommentRepository.ts: -------------------------------------------------------------------------------- 1 | import ICommentDTO from "domains/dtos/interfaces/ICommentDTO" 2 | import ICommentRepository from "domains/repositories/interfaces/ICommentRepository" 3 | import UserInfoVO from "domains/vos/UserInfoVO" 4 | import CommentDTO from "adapters/dtos/CommentDTO" 5 | import { IWebStorage } from "adapters/infrastructures/interfaces/IWebStorage" 6 | 7 | export default class StorageCommentRepository implements ICommentRepository { 8 | private connector: IWebStorage 9 | 10 | constructor(connector: IWebStorage) { 11 | this.connector = connector 12 | } 13 | 14 | async getComments(postId: string): Promise { 15 | try { 16 | const data = await this.connector.get("comments") 17 | 18 | if (!data) { 19 | return [] 20 | } 21 | 22 | const filter = data.filter((comment) => comment.postId === postId) 23 | 24 | return filter.map((comment) => { 25 | return new CommentDTO(comment) 26 | }) 27 | } catch (e) { 28 | console.error(e) 29 | } 30 | } 31 | 32 | async createComment(postId: string, content: string): Promise { 33 | try { 34 | const newComment = new CommentDTO({ 35 | id: String(Date.now()), 36 | postId, 37 | content, 38 | author: new UserInfoVO({ 39 | userId: "1", 40 | userName: "falsy" 41 | }), 42 | createdAt: new Date().toISOString(), 43 | updatedAt: new Date().toISOString() 44 | }) 45 | 46 | const comments = await this.getComments(postId) 47 | const isSucess = await this.connector.post( 48 | "comments", 49 | comments.concat(newComment) 50 | ) 51 | 52 | return isSucess 53 | } catch (e) { 54 | console.error(e) 55 | } 56 | } 57 | 58 | async updateComment(commentId: string, content: string): Promise { 59 | try { 60 | const data = await this.connector.get("comments") 61 | const findIndex = data.findIndex((comment) => comment.id === commentId) 62 | const updateAt = new Date().toISOString() 63 | 64 | data[findIndex].content = content 65 | data[findIndex].updatedAt = updateAt 66 | 67 | const isSucess = await this.connector.put("comments", data) 68 | 69 | return isSucess ? updateAt : "" 70 | } catch (e) { 71 | console.error(e) 72 | } 73 | } 74 | 75 | async deleteComment(commentId: string): Promise { 76 | try { 77 | const data = await this.connector.get("comments") 78 | const filter = data.filter((comment) => comment.id !== commentId) 79 | 80 | const isSucess = await this.connector.put("comments", filter) 81 | 82 | return isSucess 83 | } catch (e) { 84 | console.error(e) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/adapters/src/repositories/post/NetworkPostRepository.ts: -------------------------------------------------------------------------------- 1 | import { IRequestPostParams } from "domains/aggregates/interfaces/IPost" 2 | import IPostDTO from "domains/dtos/interfaces/IPostDTO" 3 | import UserInfoVO from "domains/vos/UserInfoVO" 4 | import IPostRepository from "domains/repositories/interfaces/IPostRepository" 5 | import PostDTO from "adapters/dtos/PostDTO" 6 | import { IAxiosHTTP } from "adapters/infrastructures/interfaces/IAxiosHTTP" 7 | 8 | export default class NetworkPostRepository implements IPostRepository { 9 | private connector: IAxiosHTTP 10 | 11 | constructor(connector: IAxiosHTTP) { 12 | this.connector = connector 13 | } 14 | 15 | async getPosts(): Promise { 16 | try { 17 | const { data } = await this.connector.get("/api/posts") 18 | 19 | if (!data) { 20 | return [] 21 | } 22 | 23 | return data.map((post) => { 24 | return new PostDTO({ 25 | id: post.id, 26 | title: post.title, 27 | content: post.content, 28 | author: new UserInfoVO(post.author), 29 | createdAt: post.createdAt, 30 | updatedAt: post.updatedAt 31 | }) 32 | }) 33 | } catch (e) { 34 | console.error(e) 35 | } 36 | } 37 | 38 | async getPost(postId: string): Promise { 39 | try { 40 | const { data } = await this.connector.get( 41 | `/api/posts/${postId}` 42 | ) 43 | 44 | if (!data) { 45 | return {} as IPostDTO 46 | } 47 | 48 | return new PostDTO({ 49 | id: data.id, 50 | title: data.title, 51 | content: data.content, 52 | author: new UserInfoVO(data.author), 53 | createdAt: data.createdAt, 54 | updatedAt: data.updatedAt 55 | }) 56 | } catch (e) { 57 | console.error(e) 58 | } 59 | } 60 | 61 | async createPost(params: IRequestPostParams): Promise { 62 | try { 63 | const { data } = await this.connector.post("/api/posts", params) 64 | 65 | return data 66 | } catch (e) { 67 | console.error(e) 68 | } 69 | } 70 | 71 | async updatePost( 72 | postId: string, 73 | params: IRequestPostParams 74 | ): Promise { 75 | try { 76 | const { data } = await this.connector.put( 77 | `/api/posts/${postId}`, 78 | params 79 | ) 80 | 81 | return data 82 | } catch (e) { 83 | console.error(e) 84 | } 85 | } 86 | 87 | async deletePost(postId: string): Promise { 88 | try { 89 | const { data } = await this.connector.delete( 90 | `/api/posts/${postId}` 91 | ) 92 | 93 | return data 94 | } catch (e) { 95 | console.error(e) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/adapters/src/repositories/post/StoragePostRepository.ts: -------------------------------------------------------------------------------- 1 | import { IRequestPostParams } from "domains/aggregates/interfaces/IPost" 2 | import IPostDTO from "domains/dtos/interfaces/IPostDTO" 3 | import UserInfoVO from "domains/vos/UserInfoVO" 4 | import IPostRepository from "domains/repositories/interfaces/IPostRepository" 5 | import PostDTO from "adapters/dtos/PostDTO" 6 | import { IWebStorage } from "adapters/infrastructures/interfaces/IWebStorage" 7 | 8 | export default class StoragePostRepository implements IPostRepository { 9 | private connector: IWebStorage 10 | 11 | constructor(connector: IWebStorage) { 12 | this.connector = connector 13 | } 14 | 15 | async getPosts(): Promise { 16 | try { 17 | const data = await this.connector.get("posts") 18 | 19 | if (!data) { 20 | return [] 21 | } 22 | 23 | return data.map((post) => { 24 | return new PostDTO({ 25 | id: post.id, 26 | title: post.title, 27 | content: post.content, 28 | author: new UserInfoVO(post.author), 29 | createdAt: post.createdAt, 30 | updatedAt: post.updatedAt 31 | }) 32 | }) 33 | } catch (e) { 34 | console.error(e) 35 | } 36 | } 37 | 38 | async getPost(postId: string): Promise { 39 | try { 40 | const data = await this.getPosts() 41 | const findPost = data.find((post) => post.id === postId) 42 | 43 | if (!data) { 44 | return {} as IPostDTO 45 | } 46 | 47 | return findPost 48 | } catch (e) { 49 | console.error(e) 50 | } 51 | } 52 | 53 | async createPost(params: IRequestPostParams): Promise { 54 | try { 55 | const newPost = new PostDTO({ 56 | id: String(Date.now()), 57 | title: params.title, 58 | content: params.content, 59 | author: new UserInfoVO({ 60 | userId: "1", 61 | userName: "falsy" 62 | }), 63 | createdAt: new Date().toISOString(), 64 | updatedAt: new Date().toISOString() 65 | }) 66 | const data = await this.getPosts() 67 | 68 | const isSucess = await this.connector.post( 69 | "posts", 70 | data.concat(newPost) 71 | ) 72 | 73 | return isSucess 74 | } catch (e) { 75 | console.error(e) 76 | } 77 | } 78 | 79 | async updatePost( 80 | postId: string, 81 | params: IRequestPostParams 82 | ): Promise { 83 | try { 84 | const data = await this.getPosts() 85 | const updateAt = new Date().toISOString() 86 | const newPosts = data.map((post) => { 87 | if (post.id === postId) { 88 | post.title = params.title 89 | post.content = params.content 90 | post.updatedAt = updateAt 91 | } 92 | return post 93 | }) 94 | 95 | const isSucess = await this.connector.put("posts", newPosts) 96 | 97 | return isSucess ? updateAt : "" 98 | } catch (e) { 99 | console.error(e) 100 | } 101 | } 102 | 103 | async deletePost(postId: string): Promise { 104 | try { 105 | const data = await this.getPosts() 106 | const filter = data.filter((post) => post.id !== postId) 107 | 108 | const isSucess = await this.connector.put("posts", filter) 109 | 110 | return isSucess 111 | } catch (e) { 112 | console.error(e) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /packages/adapters/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "esModuleInterop": true, 6 | "types": ["jest"] 7 | }, 8 | "include": ["src/**/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/client-a/README.md: -------------------------------------------------------------------------------- 1 | # client-a 2 | -------------------------------------------------------------------------------- /packages/client-a/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress" 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | // setupNodeEvents(on, config) { 6 | // implement node event listeners here 7 | // } 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /packages/client-a/cypress/e2e/spec.cy.ts: -------------------------------------------------------------------------------- 1 | describe("template spec", () => { 2 | it("passes", () => { 3 | cy.visit("http://localhost:4000") 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/client-a/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /packages/client-a/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /packages/client-a/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' -------------------------------------------------------------------------------- /packages/client-a/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Client A 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/client-a/jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const baseConfig = require("../../jest.config") 3 | 4 | module.exports = { 5 | ...baseConfig, 6 | setupFilesAfterEnv: [path.resolve(__dirname, "jest.setup.js")], 7 | testEnvironment: "jsdom", 8 | moduleNameMapper: { 9 | "^domains/(.*)$": path.resolve(__dirname, "../domains/src/$1"), 10 | "^adapters/(.*)$": path.resolve(__dirname, "../adapters/src/$1") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/client-a/jest.setup.js: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom" 2 | -------------------------------------------------------------------------------- /packages/client-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client-a", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "vite", 6 | "test": "jest", 7 | "cypress": "cypress open" 8 | }, 9 | "dependencies": { 10 | "adapters": "workspace:*", 11 | "clsx": "^2.1.1", 12 | "domains": "workspace:*", 13 | "jotai": "^2.11.0", 14 | "react": "^19.0.0", 15 | "react-dom": "^19.0.0", 16 | "react-router": "^7.5.2" 17 | }, 18 | "devDependencies": { 19 | "@types/express": "^5.0.0", 20 | "@types/react": "^19.0.2", 21 | "@types/react-dom": "^19.0.2", 22 | "@vitejs/plugin-react": "^4.3.4", 23 | "autoprefixer": "^10.4.20", 24 | "express": "^4.21.2", 25 | "postcss": "^8.4.49", 26 | "tailwindcss": "^3.4.17", 27 | "vite": "^6.2.7" 28 | }, 29 | "packageManager": "yarn@4.2.2" 30 | } 31 | -------------------------------------------------------------------------------- /packages/client-a/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/client-a/src/Routes.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter, RouterProvider } from "react-router" 2 | import Dashboard from "./components/pages/Dashboard" 3 | import Post from "./components/pages/Post" 4 | 5 | const router = createBrowserRouter([ 6 | { path: "/", element: }, 7 | { path: "/posts/:postId", element: } 8 | ]) 9 | 10 | export const Routes = () => { 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /packages/client-a/src/__test__/components/Button.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, fireEvent } from "@testing-library/react" 2 | import Button from "../../components/atoms/Button" 3 | 4 | describe("Button Component", () => { 5 | test("renders button with correct text", () => { 6 | render( 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/client-a/src/components/atoms/CommentList.tsx: -------------------------------------------------------------------------------- 1 | import ICommentVM from "../../vms/interfaces/ICommentVM" 2 | import Button from "./Button" 3 | 4 | export default function CommentList({ 5 | comments, 6 | deleteComment 7 | }: { 8 | comments: ICommentVM[] 9 | deleteComment: (commentId: string) => void 10 | }) { 11 | return ( 12 |
13 |

Comments

14 |
15 |
    16 | {comments.map((comment) => ( 17 |
  • 21 |
    22 |

    {comment.content}

    23 |
    28 |
  • 29 | ))} 30 |
31 |
32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /packages/client-a/src/components/atoms/DimmedLoading.tsx: -------------------------------------------------------------------------------- 1 | export default function DimmedLoading() { 2 | return ( 3 |
4 |

Loading...

5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /packages/client-a/src/components/atoms/Input.tsx: -------------------------------------------------------------------------------- 1 | export default function Input({ 2 | label, 3 | value, 4 | onChange, 5 | className = "", 6 | type = "text" 7 | }: { 8 | label: string 9 | value: string 10 | onChange: (e: React.ChangeEvent) => void 11 | className?: string 12 | type?: string 13 | }) { 14 | return ( 15 |
16 | 26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/client-a/src/components/atoms/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router" 2 | 3 | export default function Logo() { 4 | return ( 5 |
6 |

7 | Sample Project - Client A 8 |

9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/client-a/src/components/atoms/PostDetail.tsx: -------------------------------------------------------------------------------- 1 | import IPost from "domains/aggregates/interfaces/IPost" 2 | 3 | export default function PostDetail({ post }: { post: IPost }) { 4 | return ( 5 |
6 |

Post

7 |
8 |
9 |
10 | Title 11 |

{post?.title}

12 |
13 |
14 | Content 15 |

{post?.content}

16 |
17 |
18 |
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /packages/client-a/src/components/molecules/CommentForm.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, useState } from "react" 2 | import Input from "../atoms/Input" 3 | import Button from "../atoms/Button" 4 | 5 | export default function CommentForm({ 6 | createComment 7 | }: { 8 | createComment(commentContent: string): void 9 | }) { 10 | const [commentContent, setCommentContent] = useState("") 11 | 12 | const handleChangeCommentContent = (e: ChangeEvent) => { 13 | setCommentContent(e.target.value) 14 | } 15 | 16 | const handleClickCreateComment = () => { 17 | if (!commentContent) { 18 | window.alert("Please enter a content.") 19 | return 20 | } 21 | setCommentContent("") 22 | createComment(commentContent) 23 | } 24 | 25 | return ( 26 |
27 |

Create Comment

28 |
29 |
30 | 35 |
36 |
37 |
39 |
40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /packages/client-a/src/components/molecules/HeaderMenu.tsx: -------------------------------------------------------------------------------- 1 | export default function HeaderMenu() { 2 | return ( 3 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/client-a/src/components/molecules/PostBox.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router" 2 | import IPost from "domains/aggregates/interfaces/IPost" 3 | import Button from "../atoms/Button" 4 | import { ChangeEvent, useState } from "react" 5 | import Input from "../atoms/Input" 6 | 7 | export default function PostBox({ 8 | post, 9 | updatePost, 10 | deletePost 11 | }: { 12 | post: IPost 13 | updatePost(id: string, title: string, content: string): void 14 | deletePost(id: string): void 15 | }) { 16 | const { id, title: bTitle, content: bContent } = post 17 | 18 | const [isEdit, setIsEdit] = useState(false) 19 | const [title, setTitle] = useState(bTitle) 20 | const [content, setContent] = useState(bContent) 21 | 22 | const handleChangeTitle = (e: ChangeEvent) => { 23 | setTitle(e.target.value) 24 | } 25 | 26 | const handleChangeContent = (e: ChangeEvent) => { 27 | setContent(e.target.value) 28 | } 29 | 30 | const handleClickUpdatePost = () => { 31 | if (!title || !content) { 32 | window.alert("Please enter a title and content.") 33 | return 34 | } 35 | updatePost(id, title, content) 36 | setIsEdit(false) 37 | } 38 | 39 | return ( 40 |
41 |
42 | 43 |
44 | 45 | Title: 46 | {bTitle} 47 | 48 | 49 | Content: 50 | {bContent} 51 | 52 |
53 | 54 |
55 |
58 |
59 | {isEdit && ( 60 |
61 |
62 | 68 | 74 |
75 |
76 |
78 |
79 | )} 80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /packages/client-a/src/components/molecules/PostForm.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, useState } from "react" 2 | import Button from "../atoms/Button" 3 | import Input from "../atoms/Input" 4 | 5 | const PostForm = ({ 6 | createPost 7 | }: { 8 | createPost(title: string, content: string): void 9 | }) => { 10 | const [title, setTitle] = useState("") 11 | const [content, setContent] = useState("") 12 | 13 | const handleClickCreatePost = () => { 14 | if (!title || !content) { 15 | window.alert("Please enter a title and content.") 16 | return 17 | } 18 | setTitle("") 19 | setContent("") 20 | createPost(title, content) 21 | } 22 | 23 | const handleChangeTitle = (e: ChangeEvent) => { 24 | setTitle(e.target.value) 25 | } 26 | 27 | const handleChangeContent = (e: ChangeEvent) => { 28 | setContent(e.target.value) 29 | } 30 | 31 | return ( 32 |
33 |

Create Post

34 |
35 |
36 | 42 | 48 |
49 |
50 |
52 |
53 |
54 | ) 55 | } 56 | 57 | export default PostForm 58 | -------------------------------------------------------------------------------- /packages/client-a/src/components/molecules/PostList.tsx: -------------------------------------------------------------------------------- 1 | import IPost from "domains/aggregates/interfaces/IPost" 2 | import PostBox from "./PostBox" 3 | 4 | const PostList = ({ 5 | posts, 6 | updatePost, 7 | deletePost 8 | }: { 9 | posts: IPost[] 10 | updatePost(id: string, title: string, content: string): void 11 | deletePost(id: string): void 12 | }) => { 13 | return ( 14 |
15 |

Post List

16 |
    17 | {posts.map((post) => ( 18 |
  • 22 | 27 |
  • 28 | ))} 29 |
30 |
31 | ) 32 | } 33 | 34 | export default PostList 35 | -------------------------------------------------------------------------------- /packages/client-a/src/components/molecules/SideMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router" 2 | 3 | export default function SideMenu() { 4 | return ( 5 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/client-a/src/components/organisms/contents/DashboardContent.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | import usePosts from "../../../hooks/usePosts" 3 | import Content from "../../templates/Content" 4 | import PostForm from "../../molecules/PostForm" 5 | import PostList from "../../molecules/PostList" 6 | import DimmedLoading from "../../atoms/DimmedLoading" 7 | 8 | export default function DashboardContent() { 9 | const { isPending, posts, getPosts, createPost, updatePost, deletePost } = 10 | usePosts() 11 | 12 | useEffect(() => { 13 | getPosts() 14 | }, [getPosts]) 15 | 16 | const handleClickCreatePost = (title: string, content: string) => { 17 | createPost(title, content) 18 | } 19 | 20 | const handleClickDeletePost = (id: string) => { 21 | deletePost(id) 22 | } 23 | 24 | return ( 25 | 26 |

Dashboard

27 |
28 | 33 | 34 | {isPending && } 35 |
36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /packages/client-a/src/components/organisms/contents/PostContent.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | import { useParams } from "react-router" 3 | import usePosts from "../../../hooks/usePosts" 4 | import Content from "../../templates/Content" 5 | import CommentForm from "../../molecules/CommentForm" 6 | import DimmedLoading from "../../atoms/DimmedLoading" 7 | import PostDetail from "../../atoms/PostDetail" 8 | import CommentList from "../../atoms/CommentList" 9 | 10 | export default function PostContent() { 11 | const { postId } = useParams() 12 | const { isPending, post, getPost, createComment, deleteComment } = usePosts() 13 | 14 | useEffect(() => { 15 | getPost(postId) 16 | }, [postId, getPost]) 17 | 18 | const handleClickDeleteComment = (commentId: string) => { 19 | deleteComment(commentId) 20 | } 21 | 22 | const handleClickCreateComment = (commentContent: string) => { 23 | createComment(postId, commentContent) 24 | } 25 | 26 | return ( 27 | 28 |

Post

29 | {post && ( 30 |
31 | 32 | 36 | 37 | {isPending && } 38 |
39 | )} 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /packages/client-a/src/components/organisms/layouts/BaseFooter.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "../../templates/Footer" 2 | 3 | export default function BaseFooter() { 4 | return ( 5 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/client-a/src/components/organisms/layouts/BaseHeader.tsx: -------------------------------------------------------------------------------- 1 | import Header from "../../templates/Header" 2 | import HeaderMenu from "../../molecules/HeaderMenu" 3 | import Logo from "../../atoms/Logo" 4 | 5 | export default function BaseHeader() { 6 | return ( 7 |
}> 8 | 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/client-a/src/components/organisms/layouts/BaseSidebar.tsx: -------------------------------------------------------------------------------- 1 | import Sidebar from "../../templates/Sidebar" 2 | import SideMenu from "../../molecules/SideMenu" 3 | 4 | export default function BaseSidebar() { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /packages/client-a/src/components/pages/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import Template from "../templates/Template" 2 | import BaseFooter from "../organisms/layouts/BaseFooter" 3 | import BaseHeader from "../organisms/layouts/BaseHeader" 4 | import BaseSidebar from "../organisms/layouts/BaseSidebar" 5 | import DashboardContent from "../organisms/contents/DashboardContent" 6 | 7 | export default function Dashboard() { 8 | return ( 9 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /packages/client-a/src/components/pages/Post.tsx: -------------------------------------------------------------------------------- 1 | import Template from "../templates/Template" 2 | import BaseFooter from "../organisms/layouts/BaseFooter" 3 | import BaseHeader from "../organisms/layouts/BaseHeader" 4 | import BaseSidebar from "../organisms/layouts/BaseSidebar" 5 | import PostContent from "../organisms/contents/PostContent" 6 | 7 | export default function Post() { 8 | return ( 9 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /packages/client-a/src/components/templates/Content.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useContext, useLayoutEffect } from "react" 2 | import clsx from "clsx" 3 | import { BaseLayout } from "../../contexts/Layout" 4 | 5 | const ContentComponent = ({ 6 | className = "", 7 | children 8 | }: { 9 | className?: string 10 | children: ReactNode 11 | }) => { 12 | return
{children}
13 | } 14 | 15 | export default function Content({ 16 | className, 17 | children 18 | }: { 19 | className?: string 20 | children: ReactNode 21 | }) { 22 | const { setBaseContent } = useContext(BaseLayout) 23 | 24 | useLayoutEffect(() => { 25 | if (setBaseContent) { 26 | setBaseContent( 27 | {children} 28 | ) 29 | } 30 | return () => { 31 | if (setBaseContent) { 32 | setBaseContent(null) 33 | } 34 | } 35 | }, [className, children, setBaseContent]) 36 | 37 | if (setBaseContent) { 38 | return null 39 | } else { 40 | return {children} 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/client-a/src/components/templates/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useContext, useLayoutEffect } from "react" 2 | import { BaseLayout } from "../../contexts/Layout" 3 | 4 | const FooterComponent = ({ 5 | className, 6 | children 7 | }: { 8 | className?: string 9 | children: ReactNode 10 | }) => { 11 | return
{children}
12 | } 13 | 14 | export default function Footer({ 15 | className, 16 | children 17 | }: { 18 | className?: string 19 | children: ReactNode 20 | }) { 21 | const { setBaseFooter } = useContext(BaseLayout) 22 | 23 | useLayoutEffect(() => { 24 | if (setBaseFooter) { 25 | setBaseFooter( 26 | {children} 27 | ) 28 | } 29 | return () => { 30 | if (setBaseFooter) { 31 | setBaseFooter(null) 32 | } 33 | } 34 | }, [className, children, setBaseFooter]) 35 | 36 | if (setBaseFooter) { 37 | return null 38 | } else { 39 | return {children} 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/client-a/src/components/templates/Header.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useContext, useLayoutEffect } from "react" 2 | import clsx from "clsx" 3 | import { BaseLayout } from "../../contexts/Layout" 4 | 5 | const HeaderComponent = ({ 6 | logo, 7 | className = "", 8 | children 9 | }: { 10 | logo: ReactNode 11 | className?: string 12 | children: ReactNode 13 | }) => { 14 | return ( 15 |
21 |
{logo}
22 |
{children}
23 |
24 | ) 25 | } 26 | 27 | export default function Header({ 28 | logo, 29 | className, 30 | children 31 | }: { 32 | logo: ReactNode 33 | className?: string 34 | children: ReactNode 35 | }) { 36 | const { setBaseHeader } = useContext(BaseLayout) 37 | 38 | useLayoutEffect(() => { 39 | if (setBaseHeader) { 40 | setBaseHeader( 41 | 42 | {children} 43 | 44 | ) 45 | } 46 | return () => { 47 | if (setBaseHeader) { 48 | setBaseHeader(null) 49 | } 50 | } 51 | }, [logo, className, children, setBaseHeader]) 52 | 53 | if (setBaseHeader) { 54 | return null 55 | } else { 56 | return ( 57 | 58 | {children} 59 | 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/client-a/src/components/templates/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useContext, useLayoutEffect } from "react" 2 | import clsx from "clsx" 3 | import { BaseLayout } from "../../contexts/Layout" 4 | 5 | const SidebarComponent = ({ 6 | className = "", 7 | children 8 | }: { 9 | className?: string 10 | children: ReactNode 11 | }) => { 12 | return ( 13 | 16 | ) 17 | } 18 | 19 | export default function Sidebar({ 20 | className, 21 | children 22 | }: { 23 | className?: string 24 | children: ReactNode 25 | }) { 26 | const { setBaseSidebar } = useContext(BaseLayout) 27 | 28 | useLayoutEffect(() => { 29 | if (setBaseSidebar) { 30 | setBaseSidebar( 31 | {children} 32 | ) 33 | } 34 | return () => { 35 | if (setBaseSidebar) { 36 | setBaseSidebar(null) 37 | } 38 | } 39 | }, [className, children, setBaseSidebar]) 40 | 41 | if (setBaseSidebar) { 42 | return null 43 | } else { 44 | return {children} 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/client-a/src/components/templates/Template.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | cloneElement, 3 | ReactElement, 4 | ReactNode, 5 | useCallback, 6 | useState 7 | } from "react" 8 | import { BaseLayout } from "../../contexts/Layout" 9 | 10 | export default function Template({ children }: { children: ReactNode }) { 11 | const [header, setHeader] = useState(null) 12 | const [sidebar, setSidebar] = useState(null) 13 | const [content, setContent] = useState(null) 14 | const [footer, setFooter] = useState(null) 15 | 16 | const setBaseHeader = useCallback((comp: ReactNode) => { 17 | setHeader(comp) 18 | }, []) 19 | 20 | const setBaseSidebar = useCallback((comp: ReactNode) => { 21 | setSidebar(comp) 22 | }, []) 23 | 24 | const setBaseContent = useCallback((comp: ReactNode) => { 25 | setContent(comp) 26 | }, []) 27 | 28 | const setBaseFooter = useCallback((comp: ReactNode) => { 29 | setFooter(comp) 30 | }, []) 31 | 32 | return ( 33 | 36 |
37 | {header && cloneElement(header as ReactElement, {})} 38 |
39 | {sidebar && cloneElement(sidebar as ReactElement, {})} 40 |
41 | {content && cloneElement(content as ReactElement, {})} 42 | {footer && cloneElement(footer as ReactElement, {})} 43 |
44 |
45 |
46 | {children} 47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /packages/client-a/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const API_URL = "http://localhost:4000" 2 | -------------------------------------------------------------------------------- /packages/client-a/src/contexts/Layout.ts: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode } from "react" 2 | 3 | const BaseLayout = createContext<{ 4 | setBaseHeader?: (comp: ReactNode) => void 5 | setBaseSidebar?: (comp: ReactNode) => void 6 | setBaseFooter?: (comp: ReactNode) => void 7 | setBaseContent?: (comp: ReactNode) => void 8 | }>({}) 9 | 10 | export { BaseLayout } 11 | -------------------------------------------------------------------------------- /packages/client-a/src/di/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import { API_URL } from "../constants" 3 | import infrastructuresFn from "./infrastructures" 4 | import repositoriesFn from "./repositories" 5 | import useCasesFn from "./useCases" 6 | import presentersFn from "./presenters" 7 | 8 | export default function di(apiUrl = API_URL) { 9 | const infrastructures = infrastructuresFn(apiUrl) 10 | const repositories = repositoriesFn(infrastructures) 11 | const useCases = useCasesFn(repositories) 12 | const presenters = presentersFn(useCases) 13 | 14 | return presenters 15 | } 16 | -------------------------------------------------------------------------------- /packages/client-a/src/di/infrastructures.ts: -------------------------------------------------------------------------------- 1 | import IInfrastructures from "adapters/infrastructures/interfaces/IInfrastructures" 2 | import AxiosHTTP from "adapters/infrastructures/AxiosHTTP" 3 | 4 | export default (baseUrl: string): IInfrastructures => { 5 | return { 6 | network: new AxiosHTTP(baseUrl) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/client-a/src/di/presenters.ts: -------------------------------------------------------------------------------- 1 | import IUseCases from "domains/useCases/interfaces/IUseCases" 2 | import IPresenters from "adapters/presenters/interfaces/IPresenters" 3 | import PostPresenter from "adapters/presenters/PostPresenter" 4 | import UserPresenter from "adapters/presenters/UserPresenter" 5 | 6 | export default (useCases: IUseCases): IPresenters => { 7 | return { 8 | post: new PostPresenter(useCases.post), 9 | user: new UserPresenter(useCases.user) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/client-a/src/di/repositories.ts: -------------------------------------------------------------------------------- 1 | import IInfratructures from "adapters/infrastructures/interfaces/IInfrastructures" 2 | import IRepositories from "domains/repositories/interfaces/IRepositories" 3 | import NetworkPostRepository from "adapters/repositories/post/NetworkPostRepository" 4 | import NetworkCommentRepository from "adapters/repositories/comment/NetworkCommentRepository" 5 | import UserRepository from "adapters/repositories/UserRepository" 6 | 7 | export default (infrastructures: IInfratructures): IRepositories => { 8 | return { 9 | post: new NetworkPostRepository(infrastructures.network), 10 | comment: new NetworkCommentRepository(infrastructures.network), 11 | user: new UserRepository(infrastructures.network) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/client-a/src/di/useCases.ts: -------------------------------------------------------------------------------- 1 | import IUseCases from "domains/useCases/interfaces/IUseCases" 2 | import PostUseCase from "domains/useCases/PostUseCase" 3 | import UserUseCase from "domains/useCases/UserUseCase" 4 | import IRepositories from "domains/repositories/interfaces/IRepositories" 5 | 6 | export default (repository: IRepositories): IUseCases => { 7 | return { 8 | post: new PostUseCase(repository.post, repository.comment), 9 | user: new UserUseCase(repository.user) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/client-a/src/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/client-a/src/hooks/usePosts.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import { 3 | useCallback, 4 | useMemo, 5 | useOptimistic, 6 | useState, 7 | useTransition 8 | } from "react" 9 | import { atom, useAtom } from "jotai" 10 | import presenters from "../di" 11 | import PostVM from "../vms/PostVM" 12 | import IPostVM from "../vms/interfaces/IPostVM" 13 | 14 | const PostsAtoms = atom([]) 15 | 16 | export default function usePosts() { 17 | const di = useMemo(() => presenters(), []) 18 | 19 | const [post, setPost] = useState(null) 20 | const [posts, setPosts] = useAtom(PostsAtoms) 21 | const [optimisticPost, setOptimisticPost] = useOptimistic(post) 22 | const [optimisticPosts, setOptimisticPosts] = useOptimistic(posts) 23 | const [isPending, startTransition] = useTransition() 24 | 25 | const getPosts = useCallback(async () => { 26 | startTransition(async () => { 27 | const resPosts = await di.post.getPosts() 28 | const postVMs = resPosts.map((post) => new PostVM(post)) 29 | setPosts(postVMs) 30 | }) 31 | }, [di.post, setPosts]) 32 | 33 | const getPost = useCallback( 34 | async (postId: string) => { 35 | startTransition(async () => { 36 | const resPost = await di.post.getPost(postId) 37 | const postVM = new PostVM(resPost) 38 | setPost(postVM) 39 | }) 40 | }, 41 | [di.post, setPost] 42 | ) 43 | 44 | const createPost = useCallback( 45 | async (title: string, content: string) => { 46 | startTransition(async () => { 47 | const isSucess = await di.post.createPost({ title, content }) 48 | if (isSucess) { 49 | const resPosts = await di.post.getPosts() 50 | const postVMs = resPosts.map((post) => new PostVM(post)) 51 | setPosts(postVMs) 52 | } 53 | }) 54 | }, 55 | [di.post, setPosts] 56 | ) 57 | 58 | const updatePost = useCallback( 59 | async (postId: string, title: string, content: string) => { 60 | startTransition(async () => { 61 | setOptimisticPosts((prevPosts) => { 62 | return prevPosts.map((post) => { 63 | if (post.id === postId) { 64 | post.updateTitle(title) 65 | post.updateContent(content) 66 | } 67 | return post 68 | }) 69 | }) 70 | 71 | const updateAt = await di.post.updatePost(postId, { title, content }) 72 | if (updateAt !== "") { 73 | setPosts((prevPosts) => { 74 | return prevPosts.map((post) => { 75 | if (post.id === postId) { 76 | post.applyUpdatedAt(new Date(updateAt)) 77 | } 78 | return post 79 | }) 80 | }) 81 | } 82 | }) 83 | }, 84 | [di.post, setOptimisticPosts, setPosts] 85 | ) 86 | 87 | const deletePost = useCallback( 88 | async (postId: string) => { 89 | startTransition(async () => { 90 | setOptimisticPosts((prevPosts) => { 91 | return prevPosts.filter((post) => post.id !== postId) 92 | }) 93 | 94 | try { 95 | const isSucess = await di.post.deletePost(postId) 96 | if (isSucess) { 97 | const resPosts = await di.post.getPosts() 98 | const postVMs = resPosts.map((post) => new PostVM(post)) 99 | setPosts(postVMs) 100 | } 101 | } catch (e) { 102 | console.error(e) 103 | } 104 | }) 105 | }, 106 | [di.post, setOptimisticPosts, setPosts] 107 | ) 108 | 109 | const createComment = useCallback( 110 | async (postId: string, content: string) => { 111 | startTransition(async () => { 112 | const isSucess = await di.post.createComment(postId, content) 113 | if (isSucess) { 114 | const resPost = await di.post.getPost(postId) 115 | const postVM = new PostVM(resPost) 116 | setPost(postVM) 117 | } 118 | }) 119 | }, 120 | [di.post, setPost] 121 | ) 122 | 123 | const deleteComment = useCallback( 124 | async (commentId: string) => { 125 | startTransition(async () => { 126 | setOptimisticPost((prevPost) => { 127 | prevPost.deleteComment(commentId) 128 | return prevPost 129 | }) 130 | 131 | try { 132 | const isSucess = await di.post.deleteComment(commentId) 133 | if (isSucess) { 134 | const resPost = await di.post.getPost(optimisticPost.id) 135 | const postVM = new PostVM(resPost) 136 | setPost(postVM) 137 | } 138 | } catch (e) { 139 | console.error(e) 140 | } 141 | }) 142 | }, 143 | [di.post, optimisticPost, setOptimisticPost, setPost] 144 | ) 145 | 146 | return { 147 | isPending, 148 | posts: optimisticPosts, 149 | post: optimisticPost, 150 | getPosts, 151 | getPost, 152 | createPost, 153 | updatePost, 154 | deletePost, 155 | createComment, 156 | deleteComment 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /packages/client-a/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react" 2 | import ReactDOM from "react-dom/client" 3 | import { Routes } from "./Routes" 4 | import "./global.css" 5 | 6 | const container = document.getElementById("root") 7 | const root = ReactDOM.createRoot(container as HTMLElement) 8 | 9 | const App = () => { 10 | return ( 11 | 12 | 13 | 14 | ) 15 | } 16 | 17 | root.render() 18 | -------------------------------------------------------------------------------- /packages/client-a/src/vms/CommentVM.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from "crypto-js" 2 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 3 | import ICommentVM, { ICommentVMParams } from "./interfaces/ICommentVM" 4 | 5 | export default class CommentVM implements ICommentVM { 6 | readonly id: string 7 | readonly postId: string 8 | readonly author: IUserInfoVO 9 | readonly createdAt: Date 10 | key: string 11 | content: string 12 | updatedAt: Date 13 | 14 | constructor(parmas: ICommentVMParams) { 15 | this.id = parmas.id 16 | this.postId = parmas.postId 17 | this.author = parmas.author 18 | this.content = parmas.content 19 | this.createdAt = parmas.createdAt 20 | this.updatedAt = parmas.updatedAt 21 | this.key = this.generateKey(this.id, this.updatedAt) 22 | } 23 | 24 | updateContent(content: string): void { 25 | this.content = content 26 | this.updatedAt = new Date() 27 | this.key = this.generateKey(this.id, this.updatedAt) 28 | } 29 | 30 | applyUpdatedAt(date: Date): void { 31 | this.updatedAt = date 32 | this.key = this.generateKey(this.id, this.updatedAt) 33 | } 34 | 35 | private generateKey(id: string, updatedAt: Date): string { 36 | const base = `${id}-${updatedAt.getTime()}` 37 | return CryptoJS.MD5(base).toString() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/client-a/src/vms/PostVM.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from "crypto-js" 2 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 3 | import IPostVM, { IPostVMParams } from "./interfaces/IPostVM" 4 | import ICommentVM from "./interfaces/ICommentVM" 5 | import CommentVM from "./CommentVM" 6 | 7 | export default class PostVM implements IPostVM { 8 | readonly id: string 9 | readonly author: IUserInfoVO 10 | readonly createdAt: Date 11 | key: string 12 | title: string 13 | content: string 14 | comments: ICommentVM[] 15 | updatedAt: Date 16 | 17 | constructor(params: IPostVMParams) { 18 | this.id = params.id 19 | this.title = params.title 20 | this.content = params.content 21 | this.author = params.author 22 | this.comments = params.comments.map((comment) => new CommentVM(comment)) 23 | this.createdAt = params.createdAt 24 | this.updatedAt = params.updatedAt 25 | this.key = this.generateKey(this.id, this.updatedAt) 26 | } 27 | 28 | updateTitle(title: string): void { 29 | this.title = title 30 | this.updatedAt = new Date() 31 | this.key = this.generateKey(this.id, this.updatedAt) 32 | } 33 | 34 | updateContent(content: string): void { 35 | this.content = content 36 | this.updatedAt = new Date() 37 | this.key = this.generateKey(this.id, this.updatedAt) 38 | } 39 | 40 | applyUpdatedAt(date: Date): void { 41 | this.updatedAt = date 42 | this.key = this.generateKey(this.id, this.updatedAt) 43 | } 44 | 45 | deleteComment(commentId: string): void { 46 | this.comments = this.comments.filter((comment) => comment.id !== commentId) 47 | this.updatedAt = new Date() 48 | this.key = this.generateKey(this.id, this.updatedAt) 49 | } 50 | 51 | private generateKey(id: string, updatedAt: Date): string { 52 | const base = `${id}-${updatedAt.getTime()}` 53 | return CryptoJS.MD5(base).toString() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/client-a/src/vms/interfaces/ICommentVM.ts: -------------------------------------------------------------------------------- 1 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 2 | 3 | export default interface ICommentVM { 4 | readonly id: string 5 | readonly postId: string 6 | readonly author: IUserInfoVO 7 | readonly createdAt: Date 8 | key: string 9 | content: string 10 | updatedAt: Date 11 | updateContent(content: string): void 12 | applyUpdatedAt(date: Date): void 13 | } 14 | 15 | export interface ICommentVMParams { 16 | readonly id: string 17 | readonly postId: string 18 | readonly author: IUserInfoVO 19 | readonly content: string 20 | readonly createdAt: Date 21 | readonly updatedAt: Date 22 | } 23 | -------------------------------------------------------------------------------- /packages/client-a/src/vms/interfaces/IPostVM.ts: -------------------------------------------------------------------------------- 1 | import IComment from "domains/entities/interfaces/IComment" 2 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 3 | import ICommentVM from "./ICommentVM" 4 | 5 | export default interface IPostVM { 6 | readonly id: string 7 | readonly author: IUserInfoVO 8 | readonly createdAt: Date 9 | key: string 10 | title: string 11 | content: string 12 | comments: ICommentVM[] 13 | updatedAt: Date 14 | updateTitle(title: string): void 15 | updateContent(content: string): void 16 | deleteComment(commentId: string): void 17 | applyUpdatedAt(date: Date): void 18 | } 19 | 20 | export interface IPostVMParams { 21 | readonly id: string 22 | readonly title: string 23 | readonly content: string 24 | readonly author: IUserInfoVO 25 | readonly comments: IComment[] 26 | readonly createdAt: Date 27 | readonly updatedAt: Date 28 | } 29 | -------------------------------------------------------------------------------- /packages/client-a/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {} 6 | }, 7 | plugins: [] 8 | } 9 | -------------------------------------------------------------------------------- /packages/client-a/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["@testing-library/jest-dom", "jest"], 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "outDir": "./dist/", 7 | "module": "ESNext", 8 | "esModuleInterop": true, 9 | "target": "ES2017", 10 | "jsx": "react-jsx", 11 | "allowJs": true, 12 | "moduleResolution": "node" 13 | }, 14 | "include": ["src"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/client-a/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import { defineConfig } from "vite" 3 | import express from "express" 4 | import react from "@vitejs/plugin-react" 5 | 6 | export default defineConfig({ 7 | resolve: { 8 | alias: { 9 | domains: path.resolve(__dirname, "../domains/src/"), 10 | adapters: path.resolve(__dirname, "../adapters/src/") 11 | }, 12 | extensions: [".ts", ".tsx", ".js", ".mjs"] 13 | }, 14 | plugins: [ 15 | react(), 16 | { 17 | name: "vite-express-mock-api", 18 | configureServer(server) { 19 | const app = express() 20 | const posts = [] 21 | const comments = [] 22 | 23 | app.use(express.json()) 24 | 25 | app.get("/api/posts", (req, res) => { 26 | setTimeout(() => { 27 | res.json(posts) 28 | }, 200) 29 | }) 30 | 31 | app.delete("/api/posts/:postId", (req, res) => { 32 | setTimeout(() => { 33 | const index = posts.findIndex( 34 | (post) => post.id === req.params.postId 35 | ) 36 | posts.splice(index, 1) 37 | res.json(true) 38 | }, 200) 39 | }) 40 | 41 | app.post("/api/posts", (req, res) => { 42 | setTimeout(() => { 43 | posts.push({ 44 | id: String(Date.now()), 45 | title: req.body.title, 46 | content: req.body.content, 47 | author: { 48 | userId: "1", 49 | userName: "sample" 50 | }, 51 | createdAt: new Date().toISOString(), 52 | updatedAt: new Date().toISOString() 53 | }) 54 | res.json(true) 55 | }, 200) 56 | }) 57 | 58 | app.put("/api/posts/:postId", (req, res) => { 59 | setTimeout(() => { 60 | const index = posts.findIndex( 61 | (post) => post.id === req.params.postId 62 | ) 63 | const updatedAt = new Date().toISOString() 64 | posts[index] = { 65 | ...posts[index], 66 | title: req.body.title, 67 | content: req.body.content, 68 | updatedAt 69 | } 70 | res.json(updatedAt) 71 | }, 200) 72 | }) 73 | 74 | app.get("/api/posts/:postId", (req, res) => { 75 | setTimeout(() => { 76 | res.json(posts.find((post) => post.id === req.params.postId)) 77 | }, 200) 78 | }) 79 | 80 | app.get("/api/posts/:postId/comments", (req, res) => { 81 | setTimeout(() => { 82 | res.json( 83 | comments.filter((comment) => comment.postId === req.params.postId) 84 | ) 85 | }, 200) 86 | }) 87 | 88 | app.delete("/api/comments/:commentId", (req, res) => { 89 | setTimeout(() => { 90 | const index = comments.findIndex( 91 | (comment) => comment.id === req.params.commentId 92 | ) 93 | comments.splice(index, 1) 94 | res.json(true) 95 | }, 200) 96 | }) 97 | 98 | app.post("/api/posts/:postId/comments", (req, res) => { 99 | setTimeout(() => { 100 | comments.push({ 101 | id: String(Date.now()), 102 | postId: req.params.postId, 103 | content: req.body.content, 104 | author: { 105 | userId: "1", 106 | userName: "sample" 107 | }, 108 | createdAt: new Date().toISOString(), 109 | updatedAt: new Date().toISOString() 110 | }) 111 | res.json(true) 112 | }, 200) 113 | }) 114 | 115 | server.middlewares.use(app) 116 | } 117 | } 118 | ], 119 | server: { 120 | port: 4000, 121 | open: true, 122 | historyApiFallback: true 123 | } 124 | }) 125 | -------------------------------------------------------------------------------- /packages/client-b/README.md: -------------------------------------------------------------------------------- 1 | # client-b 2 | -------------------------------------------------------------------------------- /packages/client-b/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress" 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | // setupNodeEvents(on, config) { 6 | // implement node event listeners here 7 | // }, 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /packages/client-b/cypress/e2e/spec.cy.ts: -------------------------------------------------------------------------------- 1 | describe("template spec", () => { 2 | it("passes", () => { 3 | cy.visit("http://localhost:4001") 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/client-b/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /packages/client-b/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /packages/client-b/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' -------------------------------------------------------------------------------- /packages/client-b/jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const baseConfig = require("../../jest.config") 3 | 4 | module.exports = { 5 | ...baseConfig, 6 | setupFilesAfterEnv: [path.resolve(__dirname, "jest.setup.js")], 7 | testEnvironment: "jsdom", 8 | moduleNameMapper: { 9 | "^domains/(.*)$": path.resolve(__dirname, "../domains/src/$1"), 10 | "^adapters/(.*)$": path.resolve(__dirname, "../adapters/src/$1") 11 | }, 12 | transform: { 13 | "^.+\\.(ts|tsx|js|jsx)?$": [ 14 | "ts-jest", 15 | { 16 | tsconfig: { 17 | jsx: "react-jsx" 18 | } 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/client-b/jest.setup.js: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom" 2 | -------------------------------------------------------------------------------- /packages/client-b/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/client-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client-b", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "next dev -p 4001", 6 | "test": "jest", 7 | "cypress": "cypress open" 8 | }, 9 | "dependencies": { 10 | "adapters": "workspace:*", 11 | "clsx": "^2.1.1", 12 | "domains": "workspace:*", 13 | "jotai": "^2.11.0", 14 | "next": "^15.2.4", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^19.0.2", 20 | "@types/react-dom": "^19.0.2", 21 | "autoprefixer": "^10.4.20", 22 | "postcss": "^8.4.49", 23 | "tailwindcss": "^3.4.17" 24 | }, 25 | "packageManager": "yarn@4.2.2" 26 | } 27 | -------------------------------------------------------------------------------- /packages/client-b/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/client-b/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | client-b 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/client-b/src/__test__/components/Button.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, fireEvent } from "@testing-library/react" 2 | import Button from "../../components/atoms/Button" 3 | 4 | describe("Button Component", () => { 5 | test("renders button with correct text", () => { 6 | render( 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/client-b/src/components/atoms/DimmedLoading.tsx: -------------------------------------------------------------------------------- 1 | export default function DimmedLoading() { 2 | return ( 3 |
4 |

Loading...

5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /packages/client-b/src/components/atoms/Input.tsx: -------------------------------------------------------------------------------- 1 | export default function Input({ 2 | label, 3 | value, 4 | onChange, 5 | className = "", 6 | type = "text" 7 | }: { 8 | label: string 9 | value: string 10 | onChange: (e: React.ChangeEvent) => void 11 | className?: string 12 | type?: string 13 | }) { 14 | return ( 15 |
16 | 26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/client-b/src/components/atoms/Logo.tsx: -------------------------------------------------------------------------------- 1 | export default function Logo() { 2 | return ( 3 |
4 |

Sample Project - Client A

5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /packages/client-b/src/components/molecules/HeaderMenu.tsx: -------------------------------------------------------------------------------- 1 | export default function HeaderMenu() { 2 | return ( 3 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/client-b/src/components/molecules/PostBox.tsx: -------------------------------------------------------------------------------- 1 | import IPost from "domains/aggregates/interfaces/IPost" 2 | import Button from "../atoms/Button" 3 | 4 | export default function PostBox({ 5 | post, 6 | deletePost 7 | }: { 8 | post: IPost 9 | deletePost: (id: string) => void 10 | }) { 11 | const { id, title } = post 12 | 13 | return ( 14 |
15 |
16 |

17 | Title: 18 | {title} 19 |

20 |

21 |

24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /packages/client-b/src/components/molecules/PostForm.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, useState } from "react" 2 | import Button from "../atoms/Button" 3 | import Input from "../atoms/Input" 4 | 5 | const PostForm = ({ 6 | createPost 7 | }: { 8 | createPost(title: string, content: string): void 9 | }) => { 10 | const [title, setTitle] = useState("") 11 | const [content, setContent] = useState("") 12 | 13 | const handleClickCreatePost = () => { 14 | if (!title || !content) { 15 | window.alert("Please enter a title and content.") 16 | return 17 | } 18 | setTitle("") 19 | setContent("") 20 | createPost(title, content) 21 | } 22 | 23 | const handleChangeTitle = (e: ChangeEvent) => { 24 | setTitle(e.target.value) 25 | } 26 | 27 | const handleChangeContent = (e: ChangeEvent) => { 28 | setContent(e.target.value) 29 | } 30 | 31 | return ( 32 |
33 |

Create Post

34 |
35 |
36 | 42 | 48 |
49 |
50 |
52 |
53 |
54 | ) 55 | } 56 | 57 | export default PostForm 58 | -------------------------------------------------------------------------------- /packages/client-b/src/components/molecules/PostList.tsx: -------------------------------------------------------------------------------- 1 | import IPost from "domains/aggregates/interfaces/IPost" 2 | import PostBox from "./PostBox" 3 | 4 | const PostList = ({ 5 | posts, 6 | deletePost 7 | }: { 8 | posts: IPost[] 9 | deletePost(id: string): void 10 | }) => { 11 | return ( 12 |
13 |

Post List

14 |
    15 | {posts.map((post) => ( 16 |
  • 20 | 21 |
  • 22 | ))} 23 |
24 |
25 | ) 26 | } 27 | 28 | export default PostList 29 | -------------------------------------------------------------------------------- /packages/client-b/src/components/molecules/SideMenu.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | export default function SideMenu() { 4 | return ( 5 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/client-b/src/components/organisms/contents/DashboardContent.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | import usePosts from "../../../hooks/usePosts" 3 | import Content from "../../templates/Content" 4 | import PostForm from "../../molecules/PostForm" 5 | import PostList from "../../molecules/PostList" 6 | import DimmedLoading from "../../atoms/DimmedLoading" 7 | 8 | export default function DashboardContent() { 9 | const { isPending, posts, getPosts, createPost, deletePost } = usePosts() 10 | 11 | useEffect(() => { 12 | getPosts() 13 | }, [getPosts]) 14 | 15 | const handleClickCreatePost = (title: string, content: string) => { 16 | createPost(title, content) 17 | } 18 | 19 | const handleClickDeletePost = (id: string) => { 20 | deletePost(id) 21 | } 22 | 23 | return ( 24 | 25 |

Dashboard

26 |
27 | 28 | 29 | {isPending && } 30 |
31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /packages/client-b/src/components/organisms/layouts/BaseFooter.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "../../templates/Footer" 2 | 3 | export default function BaseFooter() { 4 | return ( 5 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/client-b/src/components/organisms/layouts/BaseHeader.tsx: -------------------------------------------------------------------------------- 1 | import Header from "../../templates/Header" 2 | import HeaderMenu from "../../molecules/HeaderMenu" 3 | import Logo from "../../atoms/Logo" 4 | 5 | export default function BaseHeader() { 6 | return ( 7 |
}> 8 | 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/client-b/src/components/organisms/layouts/BaseSidebar.tsx: -------------------------------------------------------------------------------- 1 | import Sidebar from "../../templates/Sidebar" 2 | import SideMenu from "../../molecules/SideMenu" 3 | 4 | export default function BaseSidebar() { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /packages/client-b/src/components/templates/Content.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useContext, useLayoutEffect } from "react" 2 | import clsx from "clsx" 3 | import { BaseLayout } from "../../contexts/Layout" 4 | 5 | const ContentComponent = ({ 6 | className = "", 7 | children 8 | }: { 9 | className?: string 10 | children: ReactNode 11 | }) => { 12 | return
{children}
13 | } 14 | 15 | export default function Content({ 16 | className, 17 | children 18 | }: { 19 | className?: string 20 | children: ReactNode 21 | }) { 22 | const { setBaseContent } = useContext(BaseLayout) 23 | 24 | useLayoutEffect(() => { 25 | if (setBaseContent) { 26 | setBaseContent( 27 | {children} 28 | ) 29 | } 30 | return () => { 31 | if (setBaseContent) { 32 | setBaseContent(null) 33 | } 34 | } 35 | }, [className, children, setBaseContent]) 36 | 37 | if (setBaseContent) { 38 | return null 39 | } else { 40 | return {children} 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/client-b/src/components/templates/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useContext, useLayoutEffect } from "react" 2 | import { BaseLayout } from "../../contexts/Layout" 3 | 4 | const FooterComponent = ({ 5 | className, 6 | children 7 | }: { 8 | className?: string 9 | children: ReactNode 10 | }) => { 11 | return
{children}
12 | } 13 | 14 | export default function Footer({ 15 | className, 16 | children 17 | }: { 18 | className?: string 19 | children: ReactNode 20 | }) { 21 | const { setBaseFooter } = useContext(BaseLayout) 22 | 23 | useLayoutEffect(() => { 24 | if (setBaseFooter) { 25 | setBaseFooter( 26 | {children} 27 | ) 28 | } 29 | return () => { 30 | if (setBaseFooter) { 31 | setBaseFooter(null) 32 | } 33 | } 34 | }, [className, children, setBaseFooter]) 35 | 36 | if (setBaseFooter) { 37 | return null 38 | } else { 39 | return {children} 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/client-b/src/components/templates/Header.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ReactNode, useContext, useLayoutEffect } from "react" 4 | import clsx from "clsx" 5 | import { BaseLayout } from "../../contexts/Layout" 6 | 7 | const HeaderComponent = ({ 8 | logo, 9 | className = "", 10 | children 11 | }: { 12 | logo: ReactNode 13 | className?: string 14 | children: ReactNode 15 | }) => { 16 | return ( 17 |
23 |
{logo}
24 |
{children}
25 |
26 | ) 27 | } 28 | 29 | export default function Header({ 30 | logo, 31 | className, 32 | children 33 | }: { 34 | logo: ReactNode 35 | className?: string 36 | children: ReactNode 37 | }) { 38 | const { setBaseHeader } = useContext(BaseLayout) 39 | 40 | useLayoutEffect(() => { 41 | if (setBaseHeader) { 42 | setBaseHeader( 43 | 44 | {children} 45 | 46 | ) 47 | } 48 | return () => { 49 | if (setBaseHeader) { 50 | setBaseHeader(null) 51 | } 52 | } 53 | }, [logo, className, children, setBaseHeader]) 54 | 55 | if (setBaseHeader) { 56 | return null 57 | } else { 58 | return ( 59 | 60 | {children} 61 | 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/client-b/src/components/templates/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ReactNode, useContext, useLayoutEffect } from "react" 4 | import clsx from "clsx" 5 | import { BaseLayout } from "../../contexts/Layout" 6 | 7 | const SidebarComponent = ({ 8 | className = "", 9 | children 10 | }: { 11 | className?: string 12 | children: ReactNode 13 | }) => { 14 | return ( 15 | 18 | ) 19 | } 20 | 21 | export default function Sidebar({ 22 | className, 23 | children 24 | }: { 25 | className?: string 26 | children: ReactNode 27 | }) { 28 | const { setBaseSidebar } = useContext(BaseLayout) 29 | 30 | useLayoutEffect(() => { 31 | if (setBaseSidebar) { 32 | setBaseSidebar( 33 | {children} 34 | ) 35 | } 36 | return () => { 37 | if (setBaseSidebar) { 38 | setBaseSidebar(null) 39 | } 40 | } 41 | }, [className, children, setBaseSidebar]) 42 | 43 | if (setBaseSidebar) { 44 | return null 45 | } else { 46 | return {children} 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/client-b/src/components/templates/Template.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | cloneElement, 5 | ReactElement, 6 | ReactNode, 7 | useCallback, 8 | useState 9 | } from "react" 10 | import { BaseLayout } from "../../contexts/Layout" 11 | 12 | export default function Template({ children }: { children: ReactNode }) { 13 | const [header, setHeader] = useState(null) 14 | const [sidebar, setSidebar] = useState(null) 15 | const [content, setContent] = useState(null) 16 | const [footer, setFooter] = useState(null) 17 | 18 | const setBaseHeader = useCallback((comp: ReactNode) => { 19 | setHeader(comp) 20 | }, []) 21 | 22 | const setBaseSidebar = useCallback((comp: ReactNode) => { 23 | setSidebar(comp) 24 | }, []) 25 | 26 | const setBaseContent = useCallback((comp: ReactNode) => { 27 | setContent(comp) 28 | }, []) 29 | 30 | const setBaseFooter = useCallback((comp: ReactNode) => { 31 | setFooter(comp) 32 | }, []) 33 | 34 | return ( 35 | 38 |
39 | {header && cloneElement(header as ReactElement, {})} 40 |
41 | {sidebar && cloneElement(sidebar as ReactElement, {})} 42 |
43 | {content && cloneElement(content as ReactElement, {})} 44 | {footer && cloneElement(footer as ReactElement, {})} 45 |
46 |
47 |
48 | {children} 49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /packages/client-b/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const API_URL = "http://localhost:4001" 2 | -------------------------------------------------------------------------------- /packages/client-b/src/contexts/Layout.ts: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode } from "react" 2 | 3 | const BaseLayout = createContext<{ 4 | setBaseHeader?: (comp: ReactNode) => void 5 | setBaseSidebar?: (comp: ReactNode) => void 6 | setBaseFooter?: (comp: ReactNode) => void 7 | setBaseContent?: (comp: ReactNode) => void 8 | }>({}) 9 | 10 | export { BaseLayout } 11 | -------------------------------------------------------------------------------- /packages/client-b/src/di/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import infrastructuresFn from "./infrastructures" 3 | import repositoriesFn from "./repositories" 4 | import useCasesFn from "./useCases" 5 | import presentersFn from "./presenters" 6 | 7 | export default function di(target: Storage) { 8 | const infrastructures = infrastructuresFn(target) 9 | const repositories = repositoriesFn(infrastructures) 10 | const useCases = useCasesFn(repositories) 11 | const presenters = presentersFn(useCases) 12 | 13 | return presenters 14 | } 15 | -------------------------------------------------------------------------------- /packages/client-b/src/di/infrastructures.ts: -------------------------------------------------------------------------------- 1 | import IInfrastructures from "adapters/infrastructures/interfaces/IInfrastructures" 2 | import WebStorage from "adapters/infrastructures/WebStorage" 3 | 4 | export default (target: Storage): IInfrastructures => { 5 | return { 6 | storage: new WebStorage(target) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/client-b/src/di/interfaces/IPresenters.ts: -------------------------------------------------------------------------------- 1 | import IPostPresenter from "adapters/presenters/interfaces/IPostPresenter" 2 | import IUserPresenter from "adapters/presenters/interfaces/IUserPresenter" 3 | 4 | export default interface IPresenters { 5 | post: IPostPresenter 6 | user: IUserPresenter 7 | } 8 | -------------------------------------------------------------------------------- /packages/client-b/src/di/interfaces/IRepositories.ts: -------------------------------------------------------------------------------- 1 | import IPostRepository from "domains/repositories/interfaces/IPostRepository" 2 | import ICommentRepository from "domains/repositories/interfaces/ICommentRepository" 3 | import IUserRepository from "domains/repositories/interfaces/IUserRepository" 4 | 5 | export default interface IRepositories { 6 | post: IPostRepository 7 | comment: ICommentRepository 8 | user: IUserRepository 9 | } 10 | -------------------------------------------------------------------------------- /packages/client-b/src/di/interfaces/IUseCases.ts: -------------------------------------------------------------------------------- 1 | import IPostUseCase from "domains/useCases/interfaces/IPostUseCase" 2 | import IUserUseCase from "domains/useCases/interfaces/IUserUseCase" 3 | 4 | export default interface IUseCases { 5 | post: IPostUseCase 6 | user: IUserUseCase 7 | } 8 | -------------------------------------------------------------------------------- /packages/client-b/src/di/presenters.ts: -------------------------------------------------------------------------------- 1 | import PostPresenter from "adapters/presenters/PostPresenter" 2 | import UserPresenter from "adapters/presenters/UserPresenter" 3 | import IPresenters from "./interfaces/IPresenters" 4 | import IUseCases from "./interfaces/IUseCases" 5 | 6 | export default (useCases: IUseCases): IPresenters => { 7 | return { 8 | post: new PostPresenter(useCases.post), 9 | user: new UserPresenter(useCases.user) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/client-b/src/di/repositories.ts: -------------------------------------------------------------------------------- 1 | import IInfratructures from "adapters/infrastructures/interfaces/IInfrastructures" 2 | import StoragePostRepository from "adapters/repositories/post/StoragePostRepository" 3 | import StorageCommentRepository from "adapters/repositories/comment/StorageCommentRepository" 4 | import UserRepository from "adapters/repositories/UserRepository" 5 | import IRepositories from "./interfaces/IRepositories" 6 | 7 | export default (infrastructures: IInfratructures): IRepositories => { 8 | return { 9 | post: new StoragePostRepository(infrastructures.storage), 10 | comment: new StorageCommentRepository(infrastructures.storage), 11 | user: new UserRepository(infrastructures.storage) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/client-b/src/di/useCases.ts: -------------------------------------------------------------------------------- 1 | import PostUseCase from "domains/useCases/PostUseCase" 2 | import UserUseCase from "domains/useCases/UserUseCase" 3 | import IRepositories from "./interfaces/IRepositories" 4 | import IUseCases from "./interfaces/IUseCases" 5 | 6 | export default (repository: IRepositories): IUseCases => { 7 | return { 8 | post: new PostUseCase(repository.post, repository.comment), 9 | user: new UserUseCase(repository.user) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/client-b/src/hooks/usePosts.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import { useCallback, useMemo, useOptimistic, useTransition } from "react" 3 | import { atom, useAtom } from "jotai" 4 | import IPost from "domains/aggregates/interfaces/IPost" 5 | import presenters from "../di" 6 | 7 | const PostsAtoms = atom([]) 8 | 9 | export default function usePosts() { 10 | const di = useMemo(() => presenters(globalThis.localStorage), []) 11 | 12 | const [posts, setPosts] = useAtom(PostsAtoms) 13 | const [optimisticPosts, setOptimisticPosts] = useOptimistic(posts) 14 | const [isPending, startTransition] = useTransition() 15 | 16 | const getPosts = useCallback(async () => { 17 | startTransition(async () => { 18 | const resPosts = await di.post.getPosts() 19 | setPosts(resPosts) 20 | }) 21 | }, [di.post, setPosts]) 22 | 23 | const createPost = useCallback( 24 | async (title: string, content: string) => { 25 | startTransition(async () => { 26 | const isSucess = await di.post.createPost({ title, content }) 27 | if (isSucess) { 28 | const resPosts = await di.post.getPosts() 29 | setPosts(resPosts) 30 | } 31 | }) 32 | }, 33 | [di.post, setPosts] 34 | ) 35 | 36 | const deletePost = useCallback( 37 | async (postId: string) => { 38 | startTransition(async () => { 39 | setOptimisticPosts((prevPosts) => { 40 | return prevPosts.filter((post) => post.id !== postId) 41 | }) 42 | 43 | try { 44 | const isSucess = await di.post.deletePost(postId) 45 | if (isSucess) { 46 | const resPosts = await di.post.getPosts() 47 | setPosts(resPosts) 48 | } 49 | } catch (e) { 50 | console.error(e) 51 | } 52 | }) 53 | }, 54 | [di.post, setOptimisticPosts, setPosts] 55 | ) 56 | 57 | return { 58 | isPending, 59 | posts: optimisticPosts, 60 | getPosts, 61 | createPost, 62 | deletePost 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/client-b/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {} 6 | }, 7 | plugins: [] 8 | } 9 | -------------------------------------------------------------------------------- /packages/client-b/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "outDir": "./dist/", 6 | "module": "ESNext", 7 | "esModuleInterop": true, 8 | "target": "ES2017", 9 | "jsx": "preserve", 10 | "allowJs": true, 11 | "moduleResolution": "node", 12 | "types": ["@testing-library/jest-dom", "jest"], 13 | "skipLibCheck": true, 14 | "strict": false, 15 | "noEmit": true, 16 | "incremental": true, 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ] 24 | }, 25 | "include": ["src", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules", "dist"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/domains/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "../../.eslintrc.js" 3 | } 4 | -------------------------------------------------------------------------------- /packages/domains/README.md: -------------------------------------------------------------------------------- 1 | # domains 2 | -------------------------------------------------------------------------------- /packages/domains/jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const baseConfig = require("../../jest.config") 3 | 4 | module.exports = { 5 | ...baseConfig, 6 | roots: [""], 7 | moduleNameMapper: { 8 | "^domains/(.*)$": path.resolve(__dirname, "../domains/src/$1"), 9 | "^adapters/(.*)$": path.resolve(__dirname, "../adapters/src/$1") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/domains/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "domains", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "test": "jest" 7 | }, 8 | "packageManager": "yarn@4.2.2" 9 | } 10 | -------------------------------------------------------------------------------- /packages/domains/src/__test__/vos/UserInfoVo.spec.ts: -------------------------------------------------------------------------------- 1 | import { IUserInfoVOParams } from "domains/vos/interfaces/IUserInfoVO" 2 | import UserInfoVO from "domains/vos/UserInfoVO" 3 | 4 | describe("UserInfoVO", () => { 5 | it("should set userId and userName correctly", () => { 6 | const params: IUserInfoVOParams = { 7 | userId: "12345", 8 | userName: "Falsy" 9 | } 10 | 11 | const userInfo = new UserInfoVO(params) 12 | 13 | expect(userInfo.userId).toBe("12345") 14 | expect(userInfo.userName).toBe("Falsy") 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/domains/src/aggregates/Post.ts: -------------------------------------------------------------------------------- 1 | import IComment from "domains/entities/interfaces/IComment" 2 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 3 | import IPost, { IPostParams } from "./interfaces/IPost" 4 | 5 | export default class Post implements IPost { 6 | readonly id: string 7 | readonly title: string 8 | readonly content: string 9 | readonly author: IUserInfoVO 10 | readonly comments: IComment[] 11 | readonly createdAt: Date 12 | readonly updatedAt: Date 13 | 14 | constructor(params: IPostParams) { 15 | this.id = params.id 16 | this.title = params.title 17 | this.content = params.content 18 | this.author = params.author 19 | this.comments = params.comments 20 | this.createdAt = new Date(params.createdAt) 21 | this.updatedAt = new Date(params.updatedAt) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/domains/src/aggregates/interfaces/IPost.ts: -------------------------------------------------------------------------------- 1 | import IComment from "domains/entities/interfaces/IComment" 2 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 3 | 4 | export default interface IPost { 5 | readonly id: string 6 | readonly title: string 7 | readonly content: string 8 | readonly author: IUserInfoVO 9 | readonly comments: IComment[] 10 | readonly createdAt: Date 11 | readonly updatedAt: Date 12 | } 13 | 14 | export interface IPostParams { 15 | readonly id: string 16 | readonly title: string 17 | readonly content: string 18 | readonly author: IUserInfoVO 19 | readonly comments: IComment[] 20 | readonly createdAt: string 21 | readonly updatedAt: string 22 | } 23 | 24 | export interface IRequestPostParams { 25 | readonly title: string 26 | readonly content: string 27 | } 28 | -------------------------------------------------------------------------------- /packages/domains/src/dtos/interfaces/ICommentDTO.ts: -------------------------------------------------------------------------------- 1 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 2 | 3 | export default interface ICommentDTO { 4 | readonly id: string 5 | readonly postId: string 6 | content: string 7 | readonly author: IUserInfoVO 8 | readonly createdAt: string 9 | updatedAt: string 10 | } 11 | -------------------------------------------------------------------------------- /packages/domains/src/dtos/interfaces/IPostDTO.ts: -------------------------------------------------------------------------------- 1 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 2 | 3 | export default interface IPostDTO { 4 | readonly id: string 5 | title: string 6 | content: string 7 | readonly author: IUserInfoVO 8 | readonly createdAt: string 9 | updatedAt: string 10 | } 11 | -------------------------------------------------------------------------------- /packages/domains/src/dtos/interfaces/IUserDTO.ts: -------------------------------------------------------------------------------- 1 | export default interface IUserDTO { 2 | readonly id: string 3 | readonly name: string 4 | readonly email: string 5 | readonly createdAt: string 6 | readonly updatedAt: string 7 | } 8 | -------------------------------------------------------------------------------- /packages/domains/src/entities/Comment.ts: -------------------------------------------------------------------------------- 1 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 2 | import IComment, { ICommentParams } from "./interfaces/IComment" 3 | 4 | export default class Comment implements IComment { 5 | readonly id: string 6 | readonly postId: string 7 | readonly author: IUserInfoVO 8 | readonly content: string 9 | readonly createdAt: Date 10 | readonly updatedAt: Date 11 | 12 | constructor(parmas: ICommentParams) { 13 | this.id = parmas.id 14 | this.postId = parmas.postId 15 | this.author = parmas.author 16 | this.content = parmas.content 17 | this.createdAt = new Date(parmas.createdAt) 18 | this.updatedAt = new Date(parmas.updatedAt) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/domains/src/entities/User.ts: -------------------------------------------------------------------------------- 1 | import IUser, { IUserParams } from "./interfaces/IUser" 2 | 3 | export default class User implements IUser { 4 | readonly id: string 5 | readonly name: string 6 | readonly email: string 7 | readonly createdAt: Date 8 | readonly updatedAt: Date 9 | 10 | constructor(params: IUserParams) { 11 | this.id = params.id 12 | this.name = params.name 13 | this.email = params.email 14 | this.createdAt = new Date(params.createdAt) 15 | this.updatedAt = new Date(params.updatedAt) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/domains/src/entities/interfaces/IComment.ts: -------------------------------------------------------------------------------- 1 | import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO" 2 | 3 | export default interface IComment { 4 | readonly id: string 5 | readonly postId: string 6 | readonly author: IUserInfoVO 7 | readonly content: string 8 | readonly createdAt: Date 9 | readonly updatedAt: Date 10 | } 11 | 12 | export interface ICommentParams { 13 | readonly id: string 14 | readonly postId: string 15 | readonly author: IUserInfoVO 16 | readonly content: string 17 | readonly createdAt: string 18 | readonly updatedAt: string 19 | } 20 | -------------------------------------------------------------------------------- /packages/domains/src/entities/interfaces/IUser.ts: -------------------------------------------------------------------------------- 1 | export default interface IUser { 2 | readonly id: string 3 | readonly name: string 4 | readonly email: string 5 | readonly createdAt: Date 6 | readonly updatedAt: Date 7 | } 8 | 9 | export interface IUserParams { 10 | readonly id: string 11 | readonly name: string 12 | readonly email: string 13 | readonly createdAt: string 14 | readonly updatedAt: string 15 | } 16 | -------------------------------------------------------------------------------- /packages/domains/src/repositories/interfaces/ICommentRepository.ts: -------------------------------------------------------------------------------- 1 | import ICommentDTO from "domains/dtos/interfaces/ICommentDTO" 2 | 3 | export default interface ICommentRepository { 4 | getComments(postId: string): Promise 5 | createComment(postId: string, content: string): Promise 6 | updateComment(commentId: string, content: string): Promise 7 | deleteComment(commentId: string): Promise 8 | } 9 | -------------------------------------------------------------------------------- /packages/domains/src/repositories/interfaces/IPostRepository.ts: -------------------------------------------------------------------------------- 1 | import { IRequestPostParams } from "domains/aggregates/interfaces/IPost" 2 | import IPostDTO from "domains/dtos/interfaces/IPostDTO" 3 | 4 | export default interface IPostRepository { 5 | getPosts(): Promise 6 | getPost(postId: string): Promise 7 | createPost(params: IRequestPostParams): Promise 8 | updatePost(postId: string, params: IRequestPostParams): Promise 9 | deletePost(postId: string): Promise 10 | } 11 | -------------------------------------------------------------------------------- /packages/domains/src/repositories/interfaces/IRepositories.ts: -------------------------------------------------------------------------------- 1 | import IPostRepository from "domains/repositories/interfaces/IPostRepository" 2 | import ICommentRepository from "domains/repositories/interfaces/ICommentRepository" 3 | import IUserRepository from "domains/repositories/interfaces/IUserRepository" 4 | 5 | export default interface IRepositories { 6 | post: IPostRepository 7 | comment: ICommentRepository 8 | user: IUserRepository 9 | } 10 | -------------------------------------------------------------------------------- /packages/domains/src/repositories/interfaces/IUserRepository.ts: -------------------------------------------------------------------------------- 1 | import IUserDTO from "domains/dtos/interfaces/IUserDTO" 2 | 3 | export default interface IUserRepository { 4 | getUser(): Promise 5 | } 6 | -------------------------------------------------------------------------------- /packages/domains/src/useCases/PostUseCase.ts: -------------------------------------------------------------------------------- 1 | import IPost, { IRequestPostParams } from "domains/aggregates/interfaces/IPost" 2 | import Post from "domains/aggregates/Post" 3 | import Comment from "domains/entities/Comment" 4 | import UserInfoVO from "domains/vos/UserInfoVO" 5 | import IPostDTO from "domains/dtos/interfaces/IPostDTO" 6 | import ICommentRepository from "../repositories/interfaces/ICommentRepository" 7 | import IPostRepository from "../repositories/interfaces/IPostRepository" 8 | import IPostUseCase from "./interfaces/IPostUseCase" 9 | 10 | export default class PostUseCase implements IPostUseCase { 11 | private postRepository: IPostRepository 12 | private commentRepository: ICommentRepository 13 | 14 | constructor( 15 | postRepository: IPostRepository, 16 | commentRepository: ICommentRepository 17 | ) { 18 | this.postRepository = postRepository 19 | this.commentRepository = commentRepository 20 | } 21 | 22 | async getPosts(): Promise { 23 | const posts = await this.postRepository.getPosts() 24 | 25 | return posts.map((post: IPostDTO) => { 26 | return new Post({ 27 | id: post.id, 28 | title: post.title, 29 | content: post.content, 30 | author: new UserInfoVO(post.author), 31 | comments: [], 32 | createdAt: post.createdAt, 33 | updatedAt: post.updatedAt 34 | }) 35 | }) 36 | } 37 | 38 | async getPost(postId: string): Promise { 39 | const [post, comments] = await Promise.all([ 40 | this.postRepository.getPost(postId), 41 | this.commentRepository.getComments(postId) 42 | ]) 43 | 44 | return new Post({ 45 | id: post.id, 46 | title: post.title, 47 | content: post.content, 48 | author: new UserInfoVO(post.author), 49 | comments: comments.map((comment) => { 50 | return new Comment({ 51 | id: comment.id, 52 | postId: comment.postId, 53 | author: new UserInfoVO(comment.author), 54 | content: comment.content, 55 | createdAt: comment.createdAt, 56 | updatedAt: comment.updatedAt 57 | }) 58 | }), 59 | createdAt: post.createdAt, 60 | updatedAt: post.updatedAt 61 | }) 62 | } 63 | 64 | createPost(params: IRequestPostParams): Promise { 65 | return this.postRepository.createPost(params) 66 | } 67 | 68 | updatePost(postId: string, params: IRequestPostParams): Promise { 69 | return this.postRepository.updatePost(postId, params) 70 | } 71 | 72 | deletePost(postId: string): Promise { 73 | return this.postRepository.deletePost(postId) 74 | } 75 | 76 | createComment(postId: string, content: string): Promise { 77 | return this.commentRepository.createComment(postId, content) 78 | } 79 | 80 | updateComment(commentId: string, content: string): Promise { 81 | return this.commentRepository.updateComment(commentId, content) 82 | } 83 | 84 | deleteComment(commentId: string): Promise { 85 | return this.commentRepository.deleteComment(commentId) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/domains/src/useCases/UserUseCase.ts: -------------------------------------------------------------------------------- 1 | import IUser from "domains/entities/interfaces/IUser" 2 | import User from "domains/entities/User" 3 | import IUserRepository from "domains/repositories/interfaces/IUserRepository" 4 | import IUserUseCase from "./interfaces/IUserUseCase" 5 | 6 | export default class UserUseCase implements IUserUseCase { 7 | private userRepository: IUserRepository 8 | 9 | constructor(userRepository: IUserRepository) { 10 | this.userRepository = userRepository 11 | } 12 | 13 | async getUser(): Promise { 14 | const userInfo = await this.userRepository.getUser() 15 | 16 | return new User({ 17 | id: userInfo.id, 18 | name: userInfo.name, 19 | email: userInfo.email, 20 | createdAt: userInfo.createdAt, 21 | updatedAt: userInfo.updatedAt 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/domains/src/useCases/interfaces/IPostUseCase.ts: -------------------------------------------------------------------------------- 1 | import IPost, { IRequestPostParams } from "domains/aggregates/interfaces/IPost" 2 | 3 | export default interface IPostUseCase { 4 | getPosts(): Promise 5 | getPost(postId: string): Promise 6 | createPost(params: IRequestPostParams): Promise 7 | updatePost(postId: string, params: IRequestPostParams): Promise 8 | deletePost(postId: string): Promise 9 | createComment(postId: string, content: string): Promise 10 | updateComment(commentId: string, content: string): Promise 11 | deleteComment(commentId: string): Promise 12 | } 13 | -------------------------------------------------------------------------------- /packages/domains/src/useCases/interfaces/IUseCases.ts: -------------------------------------------------------------------------------- 1 | import IPostUseCase from "domains/useCases/interfaces/IPostUseCase" 2 | import IUserUseCase from "domains/useCases/interfaces/IUserUseCase" 3 | 4 | export default interface IUseCases { 5 | post: IPostUseCase 6 | user: IUserUseCase 7 | } 8 | -------------------------------------------------------------------------------- /packages/domains/src/useCases/interfaces/IUserUseCase.ts: -------------------------------------------------------------------------------- 1 | import IUser from "domains/entities/interfaces/IUser" 2 | 3 | export default interface IUserUseCase { 4 | getUser(): Promise 5 | } 6 | -------------------------------------------------------------------------------- /packages/domains/src/vos/UserInfoVO.ts: -------------------------------------------------------------------------------- 1 | import IUserInfoVO, { IUserInfoVOParams } from "./interfaces/IUserInfoVO" 2 | 3 | export default class UserInfoVO implements IUserInfoVO { 4 | readonly userId: string 5 | readonly userName: string 6 | 7 | constructor(params: IUserInfoVOParams) { 8 | this.userId = params.userId 9 | this.userName = params.userName 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/domains/src/vos/interfaces/IUserInfoVO.ts: -------------------------------------------------------------------------------- 1 | export default interface IUserInfoVO { 2 | readonly userId: string 3 | readonly userName: string 4 | } 5 | 6 | export interface IUserInfoVOParams { 7 | readonly userId: string 8 | readonly userName: string 9 | } 10 | -------------------------------------------------------------------------------- /packages/domains/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": {}, 4 | "include": ["src"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "domains/*": ["packages/domains/src/*"], 6 | "adapters/*": ["packages/adapters/src/*"] 7 | } 8 | } 9 | } 10 | --------------------------------------------------------------------------------