├── .github ├── ISSUE_TEMPLATE │ └── feature_request.md └── pull_request_template.md ├── .gitignore ├── .nvmrc ├── Contribute.md ├── LICENSE ├── README.md ├── client └── app │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ ├── src │ ├── App.css │ ├── App.tsx │ ├── apis │ │ ├── comment.ts │ │ ├── count.ts │ │ └── instance.ts │ ├── common │ │ ├── index.ts │ │ ├── instruction │ │ │ ├── CardInstruction.tsx │ │ │ ├── ChannelInstruction.tsx │ │ │ ├── ContactInstruction.tsx │ │ │ ├── DisplayPortfolioInstruction.tsx │ │ │ ├── Introduction.tsx │ │ │ ├── ProgressBarInstruction.tsx │ │ │ ├── SkillInstruction.tsx │ │ │ ├── TeckstackInputInstruction.tsx │ │ │ └── VisitorCounterInstruction.tsx │ │ └── style │ │ │ ├── styled.d.ts │ │ │ └── theme.tsx │ ├── hooks │ │ └── useComment.tsx │ ├── index.css │ └── index.tsx │ └── tsconfig.json ├── config ├── .client.env └── .server.env ├── package-lock.json ├── package.json ├── scripts ├── deploy-client.sh ├── exit-all.sh ├── exit-client.sh ├── exit-server.sh ├── install-dependencies.sh ├── start-all.sh ├── start-client.sh └── start-server.sh └── server ├── .db ├── etc │ ├── my.cnf │ └── mysqld.cnf └── initdb.d │ └── create_table.sql ├── .gitignore ├── app ├── .eslintrc.json ├── .prettierrc.json ├── jest.config.js ├── main.ts ├── package-lock.json ├── package.json ├── src │ ├── apis │ │ ├── middlewares │ │ │ └── validationCheck.ts │ │ ├── module │ │ │ ├── error.ts │ │ │ └── validator.ts │ │ └── visitor │ │ │ ├── index.ts │ │ │ ├── visitor.ctrl.ts │ │ │ └── visitor.d.ts │ ├── config │ │ └── db.ts │ ├── model │ │ └── visitorRepository.ts │ └── service │ │ ├── error.ts │ │ └── visitor.ts ├── swagger.yaml └── tsconfig.json ├── docker-compose.yml └── dockerfile /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | > Write your description 12 | 13 | ## Progress 14 | - [ ] todo1 15 | - [ ] todo2 16 | - [ ] todo3 17 | 18 | ## Reference 19 | - not required 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | > Brief description of the changes and additional descriptions 3 | 4 | ## Issue Number 5 | Write resolved issue number 6 | 7 | ## Modification History 8 | - [ ] modifications 1 9 | - [ ] modifications 2 10 | - [ ] modifications 3 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | .DS_Store/ -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.18.0 2 | -------------------------------------------------------------------------------- /Contribute.md: -------------------------------------------------------------------------------- 1 | ![Open_Source_Love](https://img.shields.io/badge/open source-❤-yellow) 2 | ![license_badge](https://img.shields.io/badge/license-MIT-green) 3 | 4 | 5 | # create-dev-portfolio Contributing Guide 6 | 7 | This document is intended for contributors and is draw to help them contribute. 8 | This document contains information about issue generation, fork and code modification, and create pull request. 9 | 10 | Thank you for contributing to our project! 👏 11 | 12 | 13 | ## How to contribute our project 14 | Now, let me explain how to contribute to our project. 15 | ## Issues 16 | If you find a problem when the using project, please create an issue. 17 | 18 | ### create New issue 19 | First, check the open and closed issues. Another contributor may be correcting the problem you found, or you may have already completed the correction. 20 | 21 | If you have a new issue, click New Issue and write script using the issue template created. 22 | ![image](https://user-images.githubusercontent.com/83394348/190107357-41ebd946-e2e9-4686-be63-3d9d890cfbff.png) 23 | 24 | 25 | ### solved the issue 26 | After you resolve the issue, close the issue you created. 27 | 28 | 29 | ## Fork the Repository 30 | Click the Fork button in the upper right corner of the create-dev-portfolio repository. A replica of our repository will be created in your github account. 31 | ![image](https://user-images.githubusercontent.com/83394348/195345715-6c5a3c27-b061-4d29-9600-9969d01c616d.png) 32 | 33 | 34 | 35 | ## Clone the Repository 36 | Clone the forked repository in local. Click the code button and click the Copy to Clipboard icon next to url. 37 | 38 | 39 | 40 | ### caution 41 | - You need to get clone url from your fork repository. 42 | 43 | 44 | ### Open the terminal and run this command 45 | ``` 46 | git clone 47 | ``` 48 | When executing this command, our repository will be replicated locally. Now solve the issue there. 49 | 50 | 51 | 52 | ## Contribution reflected 53 | ### Commit your update 54 | If you have resolving the issue, please commit your corrections. 55 | 56 | ### Create Pull Request 57 | When you're finished with the changes, create a pull request, also known as a PR. 58 | - Please create a pull request with the **develop** branch. 59 | - Create a pull request and select administrator as the reviewer. 60 | - Administrator List : [woorim960](https://github.com/woorim960), [seohyunsim](https://github.com/seohyunsim), [soonki-98](https://github.com/soonki-98), [jisu3817](https://github.com/jisu3817) 61 | - If you add it as a reviewer, the administrator will review your PR. 62 | 63 | ![image](https://user-images.githubusercontent.com/83394348/190122089-01da5226-392a-44ad-ac2a-04885f5ddbf9.png) 64 | 65 | 66 | 67 | ## Congratulations! 🎉 68 | Your PR is merged. 69 | 70 | Thank you for contributing to the improvement of create-dev-portfolio. 71 | 72 | Now you are a contributor to our project! 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 모던 애자일 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-dev-portfolio 2 | 3 | ![docker-badge](https://img.shields.io/badge/node-v16.18.0-green) 4 | ![docker-badge](https://img.shields.io/badge/npm-v8.19.2-green) 5 | ![dev-portfolio-badge](https://img.shields.io/badge/npm--dependency-dev--portfolio@v2.1.5-green) 6 | ![docker-badge](https://img.shields.io/badge/npx-orange) 7 | ![docker-badge](https://img.shields.io/badge/boiler--plate--app-black) 8 | ![docker-badge](https://img.shields.io/badge/Docker-red) 9 | ![docker-compose-badge](https://img.shields.io/badge/Docker_Compose-red) 10 | ![type-script-badge](https://img.shields.io/badge/TypeScript-blue) 11 | 12 | `create-dev-portfolio` is a boiler-plate application developed using the [dev-portfolio](https://github.com/modern-agile-team/dev-portfolio) library. 13 | It supports both clients and servers, and if you follow the recommended systems below, anyone can easily develop the web. 14 | 15 | If you want to check the dev-portfolio library, please refer to the following link: [dev-portfolio](https://github.com/modern-agile-team/dev-portfolio) 16 | 17 | ![ezgif com-gif-maker](https://user-images.githubusercontent.com/56839474/194718430-5404fa1f-c24b-48a0-8730-15db2d3bde00.gif) 18 | 19 | ## List 20 | 21 | - Recommended systems 22 | - Run 23 | - Exit 24 | - Deploy 25 | - Tip 26 | - Refers 27 | - Swagger API 28 | - Infra Structure 29 | - Database ERD 30 | - Example 31 | - License 32 | - Contributor 33 | 34 | ## Recommended systems 35 | 36 | **Operating System**: Linux & MacOS 37 | 38 | > Windows is also available, but bash terminal is recommended, and the following tools must be operated based on bash. 39 | 40 | **Tools** 41 | 42 | 1. recommended: **Bash terminal** 43 | 2. required: **Docker** & **Docker-compose** 44 | - Install [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/). 45 | > If **mac** and **windows** have **docker desktop** installed, **docker compose** is also installed, so there is no need to install it separately. 46 | 1. Click [here](https://docs.docker.com/get-docker/) to install Docker. 47 | 2. Click [here](https://docs.docker.com/compose/install/) to install Docker Compose. 48 | 3. **node** ≥ `v16.18.0` 49 | - Using nvm, you can easily change the node version. Please refer to the following: [NVM](https://www.notion.so/NVM-53c04d5c8837480e8601e6bd39abc62a#6db02c02a5c549cbaaa7884cac709a9e) 50 | 4. **npm** ≥ `v8.19.2` 51 | 52 | ## Run 53 | 54 | 1. Install this repo. 55 | 56 | ```bash 57 | # Install this repo 58 | $ npx create-dev-portfolio 59 | ``` 60 | 61 | 2. Run `dev-portfolio` by daemon. 62 | 63 | ```bash 64 | # If you want to run both the client and the server, enter the command below. 65 | $ npm run start:all 66 | 67 | # Run only the client. 68 | $ npm run start:client 69 | 70 | # Run only the server. 71 | $ npm run start:server 72 | ``` 73 | 74 | ## Exit 75 | 76 | ```bash 77 | # If you want to exit both the client and the server, enter the command below. 78 | $ npm run exit:all 79 | 80 | # Exit only the client. 81 | $ npm run exit:client 82 | 83 | # Exit only the server. 84 | $ npm run exit:server 85 | ``` 86 | 87 | ## Deploy 88 | 89 | Please note that **only [client-app](https://github.com/modern-agile-team/create-dev-portfolio/tree/master/client/app) are deployed** except for [server-app](https://github.com/modern-agile-team/create-dev-portfolio/tree/master/server) on [Vercel](https://vercel.com/). If you want to deploy the server code, you have to deploy it directly using the cloud provided by AWS, GCP, Oracle, etc. 90 | The deploying guide will be released soon, so please look forward to it. 91 | 92 | ```bash 93 | # If you enter the command below, the deploy will proceed automatically to the web page where new URL is registered on Vercel. 94 | $ npm run deploy:client 95 | ``` 96 | 97 | ## Tip 98 | 99 | 1. **If you want to customize your client. 100 | Check to README.md in [dev-portfolio](https://github.com/modern-agile-team/dev-portfolio).** 101 | please go to the link below! 102 | 103 | https://github.com/modern-agile-team/dev-portfolio/blob/master/README.md 104 | 105 | 2. **If you want to change environment variables such as PORT, DB. 106 | Customize files called `.*.env`.** 107 | By default, it works normally without modification. 108 | 109 | ````bash # Move to dev-portfolio folder. 110 | $ cd dev-portfolio 111 | 112 | # Customize the .*.env file as you. 113 | $ vi ./config/.client.env 114 | $ vi ./config/.server.env 115 | ``` 116 | ```` 117 | 118 | ## Refers 119 | 120 | ### Swagger API 121 | 122 | 스크린샷 2022-09-12 오후 9 14 15 123 | 124 | You can view server apis very easily by using the Swagger documentation. 125 | To use the swagger, the `dev-portfolio` server must be in a working state. 126 | 127 | If the server is up, go to the link below. 128 | 129 | ```bash 130 | http://localhost:/swagger 131 | ``` 132 | 133 | ### Infra Structure 134 | 135 | 스크린샷 2022-09-15 오후 9 48 37 136 | 137 | ### Database ERD 138 | 139 | 스크린샷 2022-10-12 오전 12 33 55 140 | 141 | ### Example 142 | 143 | 1. <[dev-portfolio-app](https://github.com/modern-agile-team/dev-portfolio-app)> http://52.78.64.144 144 | 145 | ![ezgif com-gif-maker](https://user-images.githubusercontent.com/56839474/194718430-5404fa1f-c24b-48a0-8730-15db2d3bde00.gif) 146 | 147 |
148 | 149 | 2. <[woorim960](https://github.com/woorim960/woorim-personal-website)> http://152.70.89.184 150 | 151 | ![ezgif com-gif-maker (1)](https://user-images.githubusercontent.com/56839474/194719475-1cc2469e-7b7b-4ef0-8f87-236fa3aefbe1.gif) 152 | 153 |
154 | 155 | 3. <[seohyunsim](https://github.com/seohyunsim/seohyunsim-portfolio)> https://seohyunsim-portfolio.vercel.app/ 156 | 157 | 스크린샷 2022-09-12 오후 8 52 52 158 | 159 |
160 | 161 | ## License 162 | 163 | [MIT](https://github.com/modern-agile-team/create-dev-portfolio/blob/master/LICENSE) 164 | 165 | ## Contributor 166 | 167 | - [seohyunsim](https://github.com/seohyunsim) 168 | - [soonki-98](https://github.com/soonki-98) 169 | - [jisu3817](https://github.com/jisu3817) 170 | - [woorim960](https://github.com/woorim960) 171 | -------------------------------------------------------------------------------- /client/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /client/app/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /client/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.3.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.5.2", 10 | "@types/node": "^16.11.43", 11 | "@types/react": "^18.0.15", 12 | "@types/react-dom": "^18.0.6", 13 | "@types/styled-components": "^5.1.25", 14 | "dev-portfolio": "^2.1.5", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-scripts": "5.0.1", 18 | "typescript": "^4.7.4", 19 | "web-vitals": "^2.1.4" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modern-agile-team/create-dev-portfolio/1d1b2c156dc3b50a0d3202d30c6afc23abc3dca9/client/app/public/favicon.ico -------------------------------------------------------------------------------- /client/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Dev-portfolio 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/app/src/App.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/earlyaccess/notosanskr.css); 2 | 3 | body { 4 | margin: 0; 5 | } 6 | 7 | * { 8 | font-family: "Noto Sans KR", sans-serif; 9 | } 10 | -------------------------------------------------------------------------------- /client/app/src/App.tsx: -------------------------------------------------------------------------------- 1 | /* This is imported components of dev-portfolio library */ 2 | import { 3 | Header, 4 | TechStackList, 5 | Contact, 6 | Intro, 7 | Gallery, 8 | Item, 9 | Masonry, 10 | Image, 11 | Carousel, 12 | Experience, 13 | TechStackInput, 14 | VisitorComment, 15 | } from "dev-portfolio"; 16 | import styled from "styled-components"; 17 | 18 | import { 19 | CardInstruction, 20 | Introduction, 21 | TeckstackInputInstruction, 22 | DisplayPortfolioInstruction, 23 | VisitorCounterInstruction, 24 | SkillInstruction, 25 | ChannelInstruction, 26 | ProgressBarInstruction, 27 | ContactInstruction, 28 | } from "./common"; 29 | import color from "./common/style/theme"; 30 | import useComment from "./hooks/useComment"; 31 | import "./App.css"; 32 | 33 | function App() { 34 | /** 35 | * If you want to view README.md of 'dev-portfolio', go to the link below. 36 | * {@link https://github.com/modern-agile-team/dev-portfolio#readme} 37 | */ 38 | 39 | /* These are variables and handler functions used in VisitorComment component. */ 40 | const { 41 | comment, 42 | commentList, 43 | password, 44 | nickname, 45 | handleChangeDescription, 46 | handleChangeNickname, 47 | handleChangePassword, 48 | handleCreateComment, 49 | } = useComment(); 50 | 51 | return ( 52 | /** 53 | * The 'className' in the
tag surrounding the components of 'dev-portfolio' must be 'App'. 54 | * Only then can the SideBar in the
component recognize id props and automatically assign all components into the SideBar. 55 | */ 56 |
57 | {/** 58 | * @component Header 59 | * {@link https://github.com/modern-agile-team/dev-portfolio#header} 60 | */} 61 |
101 | 102 | {/** 103 | * Just introduction for dev-portfolio-app. 104 | * 105 | * If you want to view internal of Introduction, 106 | * go to the './src/common/instruction/Introduction.tsx' 107 | */} 108 | 109 | 110 | {/** 111 | * @component Intro 112 | * {@link https://github.com/modern-agile-team/dev-portfolio#intro} 113 | */} 114 | 120 | 121 | {/** 122 | * Just introduction for TechStackInput component. 123 | * 124 | * If you want to view internal of TeckstackInputInstruction, 125 | * go to the './src/common/instruction/TeckstackInputInstruction.tsx' 126 | */} 127 | 128 | 129 | {/** 130 | * TechStackInput used only to find the logoName value in the TechStackList. 131 | * 132 | * @component TechStackInput 133 | * {@link none} 134 | */} 135 | 136 | 137 | {/** 138 | * @component TechStackList 139 | * {@link https://github.com/modern-agile-team/dev-portfolio#techstacklist} 140 | */} 141 | 142 | Tech Stack List 143 | 144 | 196 | 197 | {/** 198 | * @component ProgressBar 199 | * {@link https://github.com/modern-agile-team/dev-portfolio#progressbar} 200 | * 201 | * If you want to view ProgressBar component, 202 | * go to the './src/common/instruction/ProgressBarInstruction.tsx' 203 | */} 204 | 205 | 206 | {/** 207 | * @component Skill 208 | * {@link https://github.com/modern-agile-team/dev-portfolio#skill} 209 | * 210 | * If you want to view Skill component, 211 | * go to the './src/common/instruction/SkillInstruction.tsx' 212 | */} 213 | 214 | 215 | {/** 216 | * Just introduction for Carousel, Gallery and Masonry. 217 | * 218 | * If you want to view internal of DisplayPortfolioInstruction, 219 | * go to the './src/common/instruction/DisplayPortfolioInstruction.tsx' 220 | */} 221 | 222 | 223 | {/** 224 | * @component Carousel 225 | * {@link https://github.com/modern-agile-team/dev-portfolio#carousel} 226 | */} 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | {/** 236 | * @component Gallery 237 | * {@link https://github.com/modern-agile-team/dev-portfolio#gallery} 238 | */} 239 | 240 | {[1, 2, 3, 4, 5, 6, 7, 8, 9].map((idx) => ( 241 | 246 | ))} 247 | 248 | 249 | {/** 250 | * @component Masonry 251 | * {@link https://github.com/modern-agile-team/dev-portfolio#masonry} 252 | */} 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | {/** 270 | * @component VisitorCounter 271 | * {@link https://github.com/modern-agile-team/dev-portfolio#visitorcounter} 272 | * 273 | * If you want to view Card component, 274 | * go to the './src/common/instruction/VisitorCounterInstruction.tsx' 275 | */} 276 | 277 | 278 | {/** 279 | * @component VisitorComment 280 | * {@link https://github.com/modern-agile-team/dev-portfolio#visitorcomment} 281 | */} 282 | 283 | Visitor Comments 284 | 285 | 300 | 301 | {/** 302 | * @component Card 303 | * {@link https://github.com/modern-agile-team/dev-portfolio#card} 304 | * 305 | * If you want to view Card component, 306 | * go to the './src/common/instruction/CardInstruction.tsx' 307 | */} 308 | 309 | 310 | {/** 311 | * @component Experience 312 | * {@link https://github.com/modern-agile-team/dev-portfolio#experience} 313 | */} 314 | 315 | 316 | {/** 317 | * @component Channel 318 | * {@link https://github.com/modern-agile-team/dev-portfolio#channel} 319 | * 320 | * @component Channels 321 | * {@link https://github.com/modern-agile-team/dev-portfolio#channels} 322 | * 323 | * If you want to view Channel and Channels component, 324 | * go to the './src/common/instruction/ChannelInstruction.tsx' 325 | */} 326 | 327 | 328 | {/** 329 | * Just introduction for Contact. 330 | * 331 | * If you want to view internal of ContactInstruction, 332 | * go to the './src/common/instruction/ContactInstruction.tsx' 333 | */} 334 | 335 | 336 | {/** 337 | * @component Contact 338 | * {@link https://github.com/modern-agile-team/dev-portfolio#contact} 339 | */} 340 | 378 |
379 | ); 380 | } 381 | 382 | export default App; 383 | 384 | /** 385 | * Just styled component for TechStackList's title 386 | * 387 | * If you don't need this, delete both TechStackListTitle component and the style components below. 388 | */ 389 | const TechStackListTitle = styled.h1` 390 | color: ${color.mainColor}; 391 | margin: 1em 1em; 392 | padding-bottom: 15px; 393 | border-bottom: 1px solid; 394 | `; 395 | 396 | /** 397 | * Just styled component for VisitorComment's title 398 | * 399 | * If you don't need this, delete both VisitorCommentTitle component and the style components below. 400 | */ 401 | const VisitorCommentTitle = styled.h1` 402 | margin: 1em 1em 0 1em; 403 | padding-bottom: 15px; 404 | `; 405 | 406 | /** 407 | * Just styled component for Carousel's title 408 | * 409 | * If you don't need this, delete both CarouselWrap component and the style components below. 410 | */ 411 | const CarouselWrap = styled.div` 412 | background-color: ${color.mainColor}; 413 | padding: 2em 0; 414 | svg { 415 | color: white; 416 | } 417 | `; 418 | -------------------------------------------------------------------------------- /client/app/src/apis/comment.ts: -------------------------------------------------------------------------------- 1 | import instance from "./instance"; 2 | 3 | class CommentHTTP { 4 | private instance = instance; 5 | 6 | async getCommment() { 7 | return await this.instance.get("/apis/visitor/comments"); 8 | } 9 | 10 | async createComment(body: any) { 11 | return await this.instance.post("/apis/visitor/comment", body); 12 | } 13 | } 14 | 15 | const commentAPI = new CommentHTTP(); 16 | 17 | export default commentAPI; 18 | -------------------------------------------------------------------------------- /client/app/src/apis/count.ts: -------------------------------------------------------------------------------- 1 | import instance from "./instance"; 2 | 3 | class CountHTTP { 4 | private instance = instance; 5 | 6 | async getCount() { 7 | return await this.instance.patch("/apis/visitor/count"); 8 | } 9 | } 10 | 11 | const countAPI = new CountHTTP(); 12 | 13 | export default countAPI; 14 | -------------------------------------------------------------------------------- /client/app/src/apis/instance.ts: -------------------------------------------------------------------------------- 1 | const { 2 | REACT_APP_NODE_ENV, // 3 | REACT_APP_SERVER_HOST_ADDRESS, // 4 | REACT_APP_SERVER_PORT, // 5 | } = process.env; 6 | 7 | export class HTTP { 8 | private domain: string = ""; 9 | 10 | constructor(domain: string) { 11 | this.domain = domain; 12 | } 13 | 14 | async get(api: string) { 15 | return await fetch(`${this.domain}${api}`).then((res) => res.json()); 16 | } 17 | 18 | async post(api: string, data: any) { 19 | return await fetch(`${this.domain}${api}`, { 20 | method: "POST", 21 | headers: { 22 | "Content-Type": "application/json", 23 | }, 24 | body: JSON.stringify(data), 25 | }).then((res) => res.json()); 26 | } 27 | 28 | async put(api: string, data: any) { 29 | return await fetch(`${this.domain}${api}`, { 30 | method: "PUT", 31 | ...data, 32 | }).then((res) => res.json()); 33 | } 34 | 35 | async delete(api: string, data: any) { 36 | return await fetch(`${this.domain}${api}`, { 37 | method: "DELETE", 38 | ...data, 39 | }).then((res) => res.json()); 40 | } 41 | 42 | async patch(api: string) { 43 | return await fetch(`${this.domain}${api}`, { 44 | method: "PATCH", 45 | }).then((res) => res.json()); 46 | } 47 | } 48 | 49 | const instance = new HTTP( 50 | `http://${ 51 | REACT_APP_NODE_ENV === "production" 52 | ? REACT_APP_SERVER_HOST_ADDRESS 53 | : "localhost" 54 | }:${REACT_APP_SERVER_PORT}` 55 | ); 56 | 57 | export default instance; 58 | -------------------------------------------------------------------------------- /client/app/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Introduction } from "./instruction/Introduction"; 2 | export { default as CardInstruction } from "./instruction/CardInstruction"; 3 | export { default as DisplayPortfolioInstruction } from "./instruction/DisplayPortfolioInstruction"; 4 | export { default as VisitorCounterInstruction } from "./instruction/VisitorCounterInstruction"; 5 | export { default as TeckstackInputInstruction } from "./instruction/TeckstackInputInstruction"; 6 | export { default as SkillInstruction } from "./instruction/SkillInstruction"; 7 | export { default as ChannelInstruction } from "./instruction/ChannelInstruction"; 8 | export { default as ProgressBarInstruction } from "./instruction/ProgressBarInstruction"; 9 | export { default as ContactInstruction } from "./instruction/ContactInstruction"; 10 | -------------------------------------------------------------------------------- /client/app/src/common/instruction/CardInstruction.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "dev-portfolio"; 2 | import styled from "styled-components"; 3 | 4 | const CardInstruction = ({ id }: { id: string }) => { 5 | return ( 6 |
7 | Card 8 | 9 | 10 | This is Card component 11 | 12 | 13 | You can use this components anyware 14 | 15 | 16 | See official documentation for details 17 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | export default CardInstruction; 24 | 25 | const CardTitle = styled.h1` 26 | margin: 2em 1em 0 1em; 27 | padding-bottom: 15px; 28 | border-bottom: 1px solid; 29 | `; 30 | 31 | const CardWrap = styled.div` 32 | display: flex; 33 | padding: 3em 3em; 34 | justify-content: space-around; 35 | `; 36 | -------------------------------------------------------------------------------- /client/app/src/common/instruction/ChannelInstruction.tsx: -------------------------------------------------------------------------------- 1 | import { Channel, Channels } from "dev-portfolio"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | import color from "../style/theme"; 5 | 6 | /** 7 | * @component Channel 8 | * {@link https://github.com/modern-agile-team/dev-portfolio#channel} 9 | * 10 | * @component Channels 11 | * {@link https://github.com/modern-agile-team/dev-portfolio#channels} 12 | * 13 | * If you want to view Channel and Channels component, 14 | * go to the './src/common/instruction/ChannelInstruction.tsx' 15 | */ 16 | const ChannelInstruction = ({ id }: { id: string }) => { 17 | return ( 18 | 19 |

Channel & Channels

20 | This component can use anywhere. 21 | 22 | Typically, it can be added to the Header and Contact components and 23 | used. 24 | 25 | 26 | Use Channel if you want to use icons alone or Channels if you want to 27 | use multiple icons. 28 | 29 | 30 |
31 |

Channel

32 | 33 |
34 |
35 |

Channels

36 | 37 |
38 |
39 |
40 | ); 41 | }; 42 | 43 | export default ChannelInstruction; 44 | 45 | const Wrap = styled.div` 46 | padding: 1em 2.2em 0em 2.2em; 47 | display: flex; 48 | flex-direction: column; 49 | gap: 1em; 50 | h1 { 51 | margin: 0; 52 | padding-bottom: 15px; 53 | border-bottom: 1px solid; 54 | } 55 | h2 { 56 | margin: 0px 0px 10px 0px; 57 | } 58 | `; 59 | 60 | const ChannelWrap = styled.div` 61 | width: 50%; 62 | margin: 1em auto; 63 | display: flex; 64 | justify-content: space-between; 65 | flex-wrap: wrap; 66 | .Channel { 67 | margin: 1em 0; 68 | padding: 15px 30px; 69 | border: 1px solid ${color.mainColor}; 70 | border-radius: 12px; 71 | text-align: center; 72 | box-shadow: 0px 0px 0px 0.5px ${color.pointColor}; 73 | } 74 | .Channels { 75 | margin: 1em 0; 76 | padding: 15px 30px; 77 | border: 1px solid ${color.mainColor}; 78 | border-radius: 12px; 79 | text-align: center; 80 | box-shadow: 0px 0px 0px 0.5px ${color.pointColor}; 81 | } 82 | @media screen and (max-width: 750px) { 83 | justify-content: center; 84 | } 85 | `; 86 | -------------------------------------------------------------------------------- /client/app/src/common/instruction/ContactInstruction.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | /** 5 | * Just introduction for Contact. 6 | * 7 | * If you want to view internal of ContactInstruction, 8 | * go to the './src/common/instruction/ContactInstruction.tsx' 9 | */ 10 | const ContactInstruction = () => { 11 | return ( 12 | 13 |

Contact

14 | 15 | This component is freedom to fill out the necessary parts such as your 16 | information, contact network, etc. 17 | 18 | 19 | The script written below is an example, and fill it in with the 20 | information you want to provide! 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default ContactInstruction; 27 | 28 | const Wrap = styled.div` 29 | padding: 3em 2.2em 2em 2.2em; 30 | display: flex; 31 | flex-direction: column; 32 | gap: 1em; 33 | h1 { 34 | margin: 0; 35 | padding-bottom: 15px; 36 | border-bottom: 1px solid; 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /client/app/src/common/instruction/DisplayPortfolioInstruction.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const DisplayPortfolioInstruction = () => { 5 | return ( 6 | 7 |

Show your career & portfolio

8 | 9 | The three components below are created for use in various areas, 10 | including your career, experience, and portfolio. 11 | 12 | There are three components: Carousel, Gallery, and Masonry. 13 | 14 | In common, all components need images, so if you want to write only in 15 | text, use Experience components. 16 | 17 | For more information on using components, see README.md. 18 |
19 | ); 20 | }; 21 | 22 | export default DisplayPortfolioInstruction; 23 | 24 | const Wrap = styled.div` 25 | padding: 1em 2.2em 5em 2.2em; 26 | display: flex; 27 | flex-direction: column; 28 | gap: 1em; 29 | h1 { 30 | margin: 0; 31 | padding-bottom: 15px; 32 | border-bottom: 1px solid; 33 | } 34 | `; 35 | -------------------------------------------------------------------------------- /client/app/src/common/instruction/Introduction.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import color from "../style/theme"; 4 | 5 | /** 6 | * Just introduction for dev-portfolio-app. 7 | * 8 | * If you want to view internal of Introduction, 9 | * go to the './src/common/instruction/Introduction.tsx' 10 | */ 11 | const Introduction = ({ id }: { id: string }) => { 12 | return ( 13 | 14 | Hi there! 15 |
16 | This manual will help you create a better portfolio. 17 | 18 | Create a creative portfolio by referring to simple examples of the 19 | components below. 20 | 21 | 22 | This article was created for a simple introduction, and when you make it 23 | yourself, please delete it and use it. 24 | 25 |
26 | Then, shall we go see the components? Scroll down and follow! 27 |
28 | ); 29 | }; 30 | 31 | export default Introduction; 32 | 33 | const Wrap = styled.div` 34 | padding: 2em; 35 | /* height: 30vh; */ 36 | display: flex; 37 | flex-direction: column; 38 | font-size: 26px; 39 | font-weight: 400; 40 | /* text-align: center; */ 41 | /* justify-content: space-between; */ 42 | `; 43 | -------------------------------------------------------------------------------- /client/app/src/common/instruction/ProgressBarInstruction.tsx: -------------------------------------------------------------------------------- 1 | import { ProgressBar } from "dev-portfolio"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | import color from "../../common/style/theme"; 5 | 6 | /** 7 | * @component ProgressBar 8 | * {@link https://github.com/modern-agile-team/dev-portfolio#progressbar} 9 | * 10 | * If you want to view ProgressBar component, 11 | * go to the './src/common/instruction/ProgressBarInstruction.tsx' 12 | */ 13 | const ProgressBarInstruction = ({ id }: { id: string }) => { 14 | return ( 15 | 16 |

ProgressBar

17 | 18 | If you want to use the Progress bar alone on the Tech Stack List, use 19 | the ProgressBar component. 20 | 21 | For a detailed description of props, see README.md. 22 | 32 |
33 | ); 34 | }; 35 | 36 | export default ProgressBarInstruction; 37 | 38 | const Wrap = styled.div` 39 | padding: 1em 2.2em 2em 2.2em; 40 | display: flex; 41 | flex-direction: column; 42 | gap: 1em; 43 | h1 { 44 | margin: 0; 45 | padding-bottom: 15px; 46 | border-bottom: 1px solid; 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /client/app/src/common/instruction/SkillInstruction.tsx: -------------------------------------------------------------------------------- 1 | import { Skill } from "dev-portfolio"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | 5 | /** 6 | * @component Skill 7 | * {@link https://github.com/modern-agile-team/dev-portfolio#skill} 8 | * 9 | * If you want to view Skill component, 10 | * go to the './src/common/instruction/SkillInstruction.tsx' 11 | */ 12 | const SkillInstruction = ({ id }: { id: string }) => { 13 | return ( 14 | 15 |

Skill

16 | 17 | If you want to show each stack without using the Tech Stack List, you 18 | can use this component. 19 | 20 | See dev-portfolio README.md for iconName assignments. 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | ); 34 | }; 35 | 36 | export default SkillInstruction; 37 | 38 | const Wrap = styled.div` 39 | padding: 1em 2.2em 5em 2.2em; 40 | display: flex; 41 | flex-direction: column; 42 | gap: 1em; 43 | h1 { 44 | margin: 0; 45 | padding-bottom: 15px; 46 | border-bottom: 1px solid; 47 | } 48 | `; 49 | 50 | const SKillWrap = styled.div` 51 | display: flex; 52 | justify-content: space-between; 53 | flex-wrap: wrap; 54 | gap: 1em; 55 | `; 56 | -------------------------------------------------------------------------------- /client/app/src/common/instruction/TeckstackInputInstruction.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | /** 5 | * TechStackInput used only to find the logoName value in the TechStackList. 6 | * @component TechStackInput 7 | * {@link none} 8 | * 9 | * If you want to view internal of TeckstackInputInstruction, 10 | * go to the './src/common/instruction/TeckstackInputInstruction.tsx' 11 | */ 12 | const TeckstackInputInstruction = ({ id }: { id: string }) => { 13 | return ( 14 | 15 |

TechStackInput

16 | 17 | This component is designed to help you select logo when you write the 18 | TechStackList props. 19 | 20 | 21 | If you want to know the name of the logo for the technology you use when 22 | you write TechStackList props, search for your technology stack in the 23 | input window below. 24 | 25 | 26 | Name of the icon you searched is the nameOption's logoName value in 27 | techStackList props. 28 | 29 | 30 | If you find the logo name you want, you can write it on the 31 | 'TechStackList' component and delete the 'TechStackInput' component! 32 | 33 |
34 | ); 35 | }; 36 | 37 | export default TeckstackInputInstruction; 38 | 39 | const Wrap = styled.div` 40 | padding: 2.2em; 41 | padding-bottom: 0px; 42 | display: flex; 43 | flex-direction: column; 44 | gap: 1em; 45 | h1 { 46 | margin: 0; 47 | padding-bottom: 15px; 48 | border-bottom: 1px solid; 49 | } 50 | `; 51 | -------------------------------------------------------------------------------- /client/app/src/common/instruction/VisitorCounterInstruction.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import styled from "styled-components"; 3 | import { VisitorCounter } from "dev-portfolio"; 4 | import countAPI from "../../apis/count"; 5 | import color from "../style/theme"; 6 | 7 | const VisitorCounterInstruction = ({ id }: { id: string }) => { 8 | const [todayCounter, setTodayCounter] = useState(0); 9 | const [totalCounter, setTotalCounter] = useState(0); 10 | 11 | useEffect(() => { 12 | (async () => { 13 | const result = await countAPI.getCount(); 14 | setTodayCounter(result.todayCount); 15 | setTotalCounter(result.totalCount); 16 | })(); 17 | }, []); 18 | 19 | return ( 20 | 21 | 22 |

Visitor Counter

23 | 24 | This component is a component that can check the number of visitors to 25 | your portfolio. 26 | 27 | 28 | It consists of three themes and consists of default, simple, and 29 | big-size themes. 30 | 31 | 32 | A personal server can be built through a docker-compose, and detailed 33 | instructions on how to use it will be written. 34 | 35 | 36 | 42 | 49 | 50 |
51 | 62 |
63 | ); 64 | }; 65 | 66 | export default VisitorCounterInstruction; 67 | 68 | const Wrap = styled.div` 69 | padding: 3em 0em; 70 | `; 71 | 72 | const ThemeWrap = styled.div` 73 | padding: 2em; 74 | display: flex; 75 | justify-content: space-around; 76 | `; 77 | 78 | const InstructionWrap = styled.div` 79 | padding: 0em 2em; 80 | display: flex; 81 | flex-direction: column; 82 | gap: 1em; 83 | h1 { 84 | margin: 0; 85 | padding-bottom: 15px; 86 | border-bottom: 1px solid; 87 | } 88 | `; 89 | -------------------------------------------------------------------------------- /client/app/src/common/style/styled.d.ts: -------------------------------------------------------------------------------- 1 | import "styled-components"; 2 | 3 | declare module "styled-components" { 4 | export interface DefaultColor { 5 | mainColor: string; 6 | pointColor: string; 7 | lightGrey: string; 8 | } 9 | } -------------------------------------------------------------------------------- /client/app/src/common/style/theme.tsx: -------------------------------------------------------------------------------- 1 | const color: any = { 2 | mainColor: "#111829", 3 | pointColor: "#6082ef", 4 | lightGrey: "rgb(247, 247, 247)", 5 | }; 6 | 7 | export default color; 8 | -------------------------------------------------------------------------------- /client/app/src/hooks/useComment.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import commentAPI from "../apis/comment"; 3 | 4 | const useComment = () => { 5 | const [commentList, setCommentList] = useState(undefined); 6 | const [comment, setComment] = useState(""); 7 | const [nickname, setNickname] = useState(""); 8 | const [password, setPassword] = useState(""); 9 | const initializeCommentState = () => { 10 | setComment(""); 11 | setNickname(""); 12 | setPassword(""); 13 | }; 14 | const handleChangeDescription = (e?: React.ChangeEvent) => { 15 | if (!e) return; 16 | const target = e.currentTarget as HTMLTextAreaElement; 17 | setComment(target.value); 18 | }; 19 | const handleChangeNickname = (e?: React.ChangeEvent) => { 20 | if (!e) return; 21 | const target = e.currentTarget as HTMLInputElement; 22 | setNickname(target.value); 23 | }; 24 | const handleChangePassword = (e?: React.ChangeEvent) => { 25 | if (!e) return; 26 | const target = e.currentTarget as HTMLInputElement; 27 | setPassword(target.value); 28 | }; 29 | 30 | const handleCreateComment = async ( 31 | e?: React.MouseEvent 32 | ) => { 33 | const data = { nickname, password, description: comment }; 34 | const result = await commentAPI.createComment(data); 35 | if (result.statusCode === 400) { 36 | alert(result.detail[0].constraints.isLength); 37 | initializeCommentState(); 38 | return; 39 | } 40 | const date = new Date(); 41 | let year = date.getFullYear(); 42 | let month = date.getMonth() + 1; 43 | let today = date.getDate(); 44 | 45 | if (!commentList) { 46 | setCommentList([data]); 47 | } else { 48 | setCommentList([ 49 | ...commentList, 50 | { ...data, date: `${year}-${month}-${today}` }, 51 | ]); 52 | initializeCommentState(); 53 | console.log(result); 54 | } 55 | }; 56 | 57 | useEffect(() => { 58 | (async () => { 59 | const result = await commentAPI.getCommment(); 60 | console.log(result); 61 | setCommentList(result.visitorComments); 62 | })(); 63 | }, []); 64 | 65 | return { 66 | handleChangeDescription, 67 | handleChangeNickname, 68 | handleChangePassword, 69 | handleCreateComment, 70 | commentList, 71 | comment, 72 | nickname, 73 | password, 74 | }; 75 | }; 76 | 77 | export default useComment; 78 | -------------------------------------------------------------------------------- /client/app/src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modern-agile-team/create-dev-portfolio/1d1b2c156dc3b50a0d3202d30c6afc23abc3dca9/client/app/src/index.css -------------------------------------------------------------------------------- /client/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | const root = ReactDOM.createRoot( 7 | document.getElementById("root") as HTMLElement 8 | ); 9 | root.render(); 10 | 11 | // If you want to start measuring performance in your app, pass a function 12 | // to log results (for example: reportWebVitals(console.log)) 13 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 14 | -------------------------------------------------------------------------------- /client/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /config/.client.env: -------------------------------------------------------------------------------- 1 | # If you want to deploy the production, put the "production". 2 | REACT_APP_NODE_ENV="development" 3 | 4 | # Client port number to open this react-app. 5 | PORT=3000 6 | 7 | # Enter the same value as the PORT environment variable written in .server.env. 8 | REACT_APP_SERVER_PORT=8000 9 | 10 | # Put the IP or host address of the computer you are using. 11 | # The REACT_APP_SERVER_HOST_ADDRESS below only works when REACT_APP_NODE_ENV is "production", otherwise it works as "localhost". 12 | REACT_APP_SERVER_HOST_ADDRESS="localhost" 13 | -------------------------------------------------------------------------------- /config/.server.env: -------------------------------------------------------------------------------- 1 | # If you want to deploy the production, put the "production". 2 | NODE_ENV="development" 3 | 4 | # NodeJS 5 | PORT=8000 6 | 7 | # Put the IP or host address of the computer you are using. 8 | # The HOST_ADDRESS below only works when NODE_ENV is "production", otherwise it works as "localhost". 9 | HOST_ADDRESS="localhost" 10 | 11 | # For accessing from Nodejs to MySQL 12 | DB_HOST="db" 13 | DB_USER="root" 14 | DB_PSWORD="password" 15 | DB_DATABASE="dev_portfolio" 16 | DB_PORT=3306 17 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-dev-portfolio", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "create-dev-portfolio", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "env-cmd": "^10.1.0" 13 | } 14 | }, 15 | "node_modules/commander": { 16 | "version": "4.1.1", 17 | "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", 18 | "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", 19 | "engines": { 20 | "node": ">= 6" 21 | } 22 | }, 23 | "node_modules/cross-spawn": { 24 | "version": "7.0.3", 25 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 26 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 27 | "dependencies": { 28 | "path-key": "^3.1.0", 29 | "shebang-command": "^2.0.0", 30 | "which": "^2.0.1" 31 | }, 32 | "engines": { 33 | "node": ">= 8" 34 | } 35 | }, 36 | "node_modules/env-cmd": { 37 | "version": "10.1.0", 38 | "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz", 39 | "integrity": "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==", 40 | "dependencies": { 41 | "commander": "^4.0.0", 42 | "cross-spawn": "^7.0.0" 43 | }, 44 | "bin": { 45 | "env-cmd": "bin/env-cmd.js" 46 | }, 47 | "engines": { 48 | "node": ">=8.0.0" 49 | } 50 | }, 51 | "node_modules/isexe": { 52 | "version": "2.0.0", 53 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 54 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 55 | }, 56 | "node_modules/path-key": { 57 | "version": "3.1.1", 58 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 59 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 60 | "engines": { 61 | "node": ">=8" 62 | } 63 | }, 64 | "node_modules/shebang-command": { 65 | "version": "2.0.0", 66 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 67 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 68 | "dependencies": { 69 | "shebang-regex": "^3.0.0" 70 | }, 71 | "engines": { 72 | "node": ">=8" 73 | } 74 | }, 75 | "node_modules/shebang-regex": { 76 | "version": "3.0.0", 77 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 78 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 79 | "engines": { 80 | "node": ">=8" 81 | } 82 | }, 83 | "node_modules/which": { 84 | "version": "2.0.2", 85 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 86 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 87 | "dependencies": { 88 | "isexe": "^2.0.0" 89 | }, 90 | "bin": { 91 | "node-which": "bin/node-which" 92 | }, 93 | "engines": { 94 | "node": ">= 8" 95 | } 96 | } 97 | }, 98 | "dependencies": { 99 | "commander": { 100 | "version": "4.1.1", 101 | "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", 102 | "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" 103 | }, 104 | "cross-spawn": { 105 | "version": "7.0.3", 106 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 107 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 108 | "requires": { 109 | "path-key": "^3.1.0", 110 | "shebang-command": "^2.0.0", 111 | "which": "^2.0.1" 112 | } 113 | }, 114 | "env-cmd": { 115 | "version": "10.1.0", 116 | "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz", 117 | "integrity": "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==", 118 | "requires": { 119 | "commander": "^4.0.0", 120 | "cross-spawn": "^7.0.0" 121 | } 122 | }, 123 | "isexe": { 124 | "version": "2.0.0", 125 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 126 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 127 | }, 128 | "path-key": { 129 | "version": "3.1.1", 130 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 131 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" 132 | }, 133 | "shebang-command": { 134 | "version": "2.0.0", 135 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 136 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 137 | "requires": { 138 | "shebang-regex": "^3.0.0" 139 | } 140 | }, 141 | "shebang-regex": { 142 | "version": "3.0.0", 143 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 144 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" 145 | }, 146 | "which": { 147 | "version": "2.0.2", 148 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 149 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 150 | "requires": { 151 | "isexe": "^2.0.0" 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-dev-portfolio", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start:all": "bash ./scripts/start-all.sh", 8 | "start:server": "bash ./scripts/start-server.sh", 9 | "start:client": "bash ./scripts/start-client.sh", 10 | "exit:server": "bash ./scripts/exit-server.sh", 11 | "exit:client": "bash ./scripts/exit-client.sh", 12 | "exit:all": "bash ./scripts/exit-all.sh", 13 | "bootstrap": "bash ./scripts/install-dependencies.sh", 14 | "deploy:client": "bash ./scripts/deploy-client.sh", 15 | "start:windows": "cd ./server/app && tsc && copy swagger.yaml build && cd ../../ && docker-compose -f ./server/docker-compose.yml --env-file ./config/.server.env up -d && env-cmd -f ./config/.client.env npm start --prefix ./client/app" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/modern-agile-team/create-dev-portfolio.git" 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/modern-agile-team/create-dev-portfolio/issues" 26 | }, 27 | "homepage": "https://github.com/modern-agile-team/create-dev-portfolio#readme", 28 | "dependencies": { 29 | "env-cmd": "^10.1.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /scripts/deploy-client.sh: -------------------------------------------------------------------------------- 1 | npx vercel ./client/app -y -------------------------------------------------------------------------------- /scripts/exit-all.sh: -------------------------------------------------------------------------------- 1 | docker-compose -f ./server/docker-compose.yml --env-file ./config/.server.env down && npx pm2 delete all -------------------------------------------------------------------------------- /scripts/exit-client.sh: -------------------------------------------------------------------------------- 1 | npx pm2 delete all -------------------------------------------------------------------------------- /scripts/exit-server.sh: -------------------------------------------------------------------------------- 1 | docker-compose -f ./server/docker-compose.yml --env-file ./config/.server.env down 2 | -------------------------------------------------------------------------------- /scripts/install-dependencies.sh: -------------------------------------------------------------------------------- 1 | npm ci && cd client/app && npm ci && cd ../../server/app && npm ci -------------------------------------------------------------------------------- /scripts/start-all.sh: -------------------------------------------------------------------------------- 1 | # Print message on console. 2 | echo "[info] Starting server and client..." 3 | 4 | # OS variable 5 | CHECK_OS="`uname -s`" 6 | 7 | # Check OS and then RUN the services 8 | if [[ ${CHECK_OS} = "Darwin"* ]]; then 9 | npm run build --prefix ./server/app && docker-compose -f ./server/docker-compose.yml --env-file ./config/.server.env up -d && npm run start:client 10 | elif [[ "$CHECK_OS" = "Linux"* ]]; then 11 | npm run build --prefix ./server/app && docker-compose -f ./server/docker-compose.yml --env-file ./config/.server.env up -d && npm run start:client 12 | elif [[ ${CHECK_OS} = "MINGW32"* ]]; then 13 | npm run start:windows 14 | elif [[ ${CHECK_OS} = "MINGW64"* ]]; then 15 | npm run start:windows 16 | elif [[ ${CHECK_OS} = "CYGWIN"* ]]; then 17 | npm run start:windows 18 | fi 19 | 20 | # Print message on console. 21 | echo "[info] Now enjoy the your dev-portfolio web!!" -------------------------------------------------------------------------------- /scripts/start-client.sh: -------------------------------------------------------------------------------- 1 | env-cmd -f ./config/.client.env npx pm2 start --name client-dev-portfolio 'npm start --prefix ./client/app' -------------------------------------------------------------------------------- /scripts/start-server.sh: -------------------------------------------------------------------------------- 1 | npm run build --prefix ./server/app && docker-compose -f ./server/docker-compose.yml --env-file ./config/.server.env up -d -------------------------------------------------------------------------------- /server/.db/etc/my.cnf: -------------------------------------------------------------------------------- 1 | # For advice on how to change settings please see 2 | # http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html 3 | 4 | [mysqld] 5 | max_connections=350 6 | 7 | # 8 | # Remove leading # and set to the amount of RAM for the most im`port`ant data 9 | # cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%. 10 | # innodb_buffer_pool_size = 128M 11 | # 12 | # Remove leading # to turn on a very important data integrity option: logging 13 | # changes to the binary log between backups. 14 | # log_bin 15 | # 16 | # Remove leading # to set options mainly useful for reporting servers. 17 | # The server defaults are faster for transactions and fast SELECTs. 18 | # Adjust sizes as needed, experiment to find the optimal values. 19 | # join_buffer_size = 128M 20 | # sort_buffer_size = 2M 21 | # read_rnd_buffer_size = 2M 22 | skip-host-cache 23 | skip-name-resolve 24 | datadir=/var/lib/mysql 25 | socket=/var/run/mysqld/mysqld.sock 26 | secure-file-priv=/var/lib/mysql-files 27 | user=root 28 | port=3306 29 | 30 | # Disabling symbolic-links is recommended to prevent assorted security risks 31 | symbolic-links=0 32 | 33 | #log-error=/var/log/mysqld.log 34 | pid-file=/var/run/mysqld/mysqld.pid 35 | [client] 36 | socket=/var/run/mysqld/mysqld.sock 37 | 38 | !includedir /etc/mysql/conf.d/ 39 | !includedir /etc/mysql/mysql.conf.d/ 40 | -------------------------------------------------------------------------------- /server/.db/etc/mysqld.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | default-character-set = utf8mb4 3 | 4 | [mysqld] 5 | init-connect = 'SET collation_connection = utf8mb4_unicode_ci' 6 | init-connect = 'SET NAMES utf8mb4' 7 | character-set-server = utf8mb4 8 | collation-server = utf8mb4_unicode_ci 9 | default_time_zone='+09:00' 10 | 11 | [mysql] 12 | default-character-set = utf8mb4 13 | 14 | [mysqldump] 15 | default-character-set = utf8mb4 -------------------------------------------------------------------------------- /server/.db/initdb.d/create_table.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS dev_portfolio; 2 | 3 | USE dev_portfolio; 4 | 5 | CREATE TABLE IF NOT EXISTS `number_of_visitors` ( 6 | `visitor_id` int NOT NULL AUTO_INCREMENT, 7 | `total_count` int NOT NULL DEFAULT '0', 8 | `today_count` int DEFAULT '0', 9 | `today_date` datetime DEFAULT CURRENT_TIMESTAMP, 10 | PRIMARY KEY (`visitor_id`) 11 | ); 12 | 13 | CREATE TABLE IF NOT EXISTS `visitor_comments` ( 14 | `visitor_comment_id` int NOT NULL AUTO_INCREMENT, 15 | `nickname` varchar(20) NOT NULL, 16 | `password` varchar(60) NOT NULL, 17 | `description` varchar(255) NOT NULL, 18 | `create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | PRIMARY KEY (`visitor_comment_id`) 20 | ); 21 | 22 | INSERT INTO number_of_visitors (total_count, today_count) VALUES (0, 0); 23 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | .DS_Store 3 | 4 | # Build 5 | build/ 6 | 7 | # ENV 8 | .db.env 9 | 10 | # Docker - DB 11 | data/ 12 | 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | pnpm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | lerna-debug.log* 21 | 22 | # Diagnostic reports (https://nodejs.org/api/report.html) 23 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 24 | 25 | # Runtime data 26 | pids 27 | *.pid 28 | *.seed 29 | *.pid.lock 30 | 31 | # Directory for instrumented libs generated by jscoverage/JSCover 32 | lib-cov 33 | 34 | # Coverage directory used by tools like istanbul 35 | coverage 36 | *.lcov 37 | 38 | # nyc test coverage 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # Bower dependency directory (https://bower.io/) 45 | bower_components 46 | 47 | # node-waf configuration 48 | .lock-wscript 49 | 50 | # Compiled binary addons (https://nodejs.org/api/addons.html) 51 | build/Release 52 | 53 | # Dependency directories 54 | node_modules/ 55 | jspm_packages/ 56 | 57 | # TypeScript v1 declaration files 58 | typings/ 59 | 60 | # TypeScript cache 61 | *.tsbuildinfo 62 | 63 | # Optional npm cache directory 64 | .npm 65 | 66 | # Optional eslint cache 67 | .eslintcache 68 | 69 | # Microbundle cache 70 | .rpt2_cache/ 71 | .rts2_cache_cjs/ 72 | .rts2_cache_es/ 73 | .rts2_cache_umd/ 74 | 75 | # Optional REPL history 76 | .node_repl_history 77 | 78 | # Output of 'npm pack' 79 | *.tgz 80 | 81 | # Yarn Integrity file 82 | .yarn-integrity 83 | 84 | # dotenv environment variables file 85 | .env 86 | .env.test 87 | 88 | # parcel-bundler cache (https://parceljs.org/) 89 | .cache 90 | 91 | # Next.js build output 92 | .next 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # compiled output 120 | /dist 121 | /node_modules 122 | 123 | # OS 124 | .DS_Store 125 | 126 | # Tests 127 | /coverage 128 | /.nyc_output 129 | 130 | # IDEs and editors 131 | /.idea 132 | .project 133 | .classpath 134 | .c9/ 135 | *.launch 136 | .settings/ 137 | *.sublime-workspace 138 | 139 | # IDE - VSCode 140 | .vscode/* 141 | !.vscode/settings.json 142 | !.vscode/tasks.json 143 | !.vscode/launch.json 144 | !.vscode/extensions.json -------------------------------------------------------------------------------- /server/app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 11 | "plugin:prettier/recommended", 12 | "prettier/@typescript-eslint" 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "project": "./tsconfig.json", 17 | "ecmaVersion": 2018, 18 | "sourceType": "module" 19 | }, 20 | "plugins": ["@typescript-eslint"], 21 | "rules": { 22 | "prettier/prettier": "error" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /server/app/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "consistent", 8 | "trailingComma": "es5", 9 | "bracketSpacing": true, 10 | "arrowParens": "always", 11 | "endOfLine": "lf" 12 | } 13 | -------------------------------------------------------------------------------- /server/app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | testMatch: [ 4 | '**/__tests__/**/*.+(ts|tsx|js)', 5 | '**/?(*.)+(spec|test).+(ts|tsx|js)', 6 | ], 7 | transform: { 8 | '^.+\\.(ts|tsx)$': 'ts-jest', 9 | }, 10 | transformIgnorePatterns: ['/node_modules/'], 11 | coveragePathIgnorePatterns: ['/node_modules/'], 12 | }; 13 | -------------------------------------------------------------------------------- /server/app/main.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import swaggerUi from 'swagger-ui-express'; 3 | import YAML from 'yamljs'; 4 | import path from 'path'; 5 | import dotenv from 'dotenv'; 6 | import cors from 'cors'; 7 | 8 | dotenv.config({ path: '../../config/.server.env' }); 9 | 10 | const app = express(); 11 | const PORT = process.env.PORT || 8000; 12 | const HOST_ADDRESS = 13 | process.env.NODE_ENV === 'production' 14 | ? process.env.HOST_ADDRESS || 'localhost' 15 | : 'localhost'; 16 | 17 | const swaggerSpec = YAML.load(path.join(__dirname, './swagger.yaml')); 18 | const portInjectedSwaggerSpec = JSON.stringify(swaggerSpec) 19 | .replace('{PORT}', PORT.toString()) 20 | .replace('{HOST_ADDRESS}', HOST_ADDRESS); 21 | 22 | import visitor from './src/apis/visitor'; 23 | 24 | app.use(cors()); 25 | 26 | app.listen(PORT, () => { 27 | console.log(`server start at ${PORT}`); 28 | }); 29 | 30 | app.use(express.json()); 31 | app.use(express.urlencoded({ extended: true })); 32 | 33 | app.use( 34 | '/swagger', 35 | swaggerUi.serve, 36 | swaggerUi.setup(YAML.parse(portInjectedSwaggerSpec)) 37 | ); 38 | 39 | app.use('/apis/visitor', visitor); 40 | 41 | export = app; 42 | -------------------------------------------------------------------------------- /server/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev-portfolio-backend-auto-set", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc && cp ./swagger.yaml ./build", 9 | "start": "node ./build/main.js", 10 | "start:prod": "tsc && cp ./swagger.yaml ./build && node ./build/main.js", 11 | "start:dev": "nodemon --exec ts-node ./main.ts", 12 | "swagger": "swagger-cli bundle ./src/swagger/swagger.yaml --outfile build/swagger.yaml --type yaml" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/modern-agile-team/dev-portfolio-open-api.git" 17 | }, 18 | "author": "", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/modern-agile-team/dev-portfolio-open-api/issues" 22 | }, 23 | "homepage": "https://github.com/modern-agile-team/dev-portfolio-open-api#readme", 24 | "dependencies": { 25 | "@types/bcrypt": "^5.0.0", 26 | "@types/cors": "^2.8.12", 27 | "@types/crypto-js": "^4.1.1", 28 | "ajv-dist": "^8.11.0", 29 | "bcrypt": "^5.0.1", 30 | "class-validator": "^0.13.2", 31 | "cors": "^2.8.5", 32 | "crypto": "^1.0.1", 33 | "dotenv": "^16.0.1", 34 | "express": "^4.18.1", 35 | "moment": "^2.29.4", 36 | "mysql2": "^2.3.3", 37 | "swagger-cli": "^4.0.4", 38 | "swagger-ui-express": "^4.5.0", 39 | "typescript": "^4.7.4", 40 | "yamljs": "^0.3.0" 41 | }, 42 | "devDependencies": { 43 | "@types/express": "^4.17.13", 44 | "@types/jest": "^28.1.4", 45 | "@types/swagger-ui-express": "^4.1.3", 46 | "@types/yamljs": "^0.2.31", 47 | "@typescript-eslint/eslint-plugin": "^5.30.5", 48 | "@typescript-eslint/parser": "^5.30.5", 49 | "eslint": "^8.19.0", 50 | "eslint-config-airbnb-base": "^15.0.0", 51 | "eslint-config-prettier": "^8.5.0", 52 | "eslint-plugin-import": "^2.26.0", 53 | "eslint-plugin-prettier": "^4.2.1", 54 | "jest": "^28.1.2", 55 | "nodemon": "^2.0.19", 56 | "prettier": "^2.7.1", 57 | "swagger-autogen": "^2.21.5", 58 | "ts-jest": "^28.0.5", 59 | "ts-node": "^10.8.2" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/app/src/apis/middlewares/validationCheck.ts: -------------------------------------------------------------------------------- 1 | import { validate, ValidationError } from 'class-validator'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | import VisitorCommentValidator from '../module/validator'; 4 | 5 | interface ValidationErrorItem { 6 | property?: string; 7 | constraints?: object; 8 | } 9 | 10 | /** 11 | * @description Validation logic for request 12 | * @param {Request} req 13 | * @param {Response} res 14 | * @param {NextFunction} next 15 | * @returns 16 | */ 17 | async function validationCheck( 18 | req: Request, 19 | res: Response, 20 | next: NextFunction 21 | ) { 22 | const RequestVisitorComment = Object.assign(req.body); 23 | 24 | const visitorComment = new VisitorCommentValidator(); 25 | 26 | Object.assign(visitorComment, RequestVisitorComment); 27 | 28 | const validationError: ValidationError[] = await validate(visitorComment); 29 | 30 | if (validationError.length > 0) { 31 | const errorList = validationError.map((error) => { 32 | const errorObj: ValidationErrorItem = {}; 33 | 34 | errorObj.property = error.property; 35 | errorObj.constraints = error.constraints; 36 | 37 | return errorObj; 38 | }); 39 | 40 | return res.status(400).json({ 41 | statusCode: 400, 42 | message: 'The request data is malformed', 43 | detail: errorList, 44 | }); 45 | } 46 | 47 | next(); 48 | } 49 | 50 | export default validationCheck; 51 | -------------------------------------------------------------------------------- /server/app/src/apis/module/error.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestError, 3 | NotFoundError, 4 | ServerError, 5 | } from '../../service/error'; 6 | import { Response } from 'express'; 7 | 8 | /** 9 | * Branch handling of error code capabilities for error codes 10 | * @param err `{ unknown | ServerError | BadRequestError | NotFoundError }` 11 | * @param res 12 | * @returns {Response} res 13 | */ 14 | function errorResposne( 15 | err: unknown | ServerError | BadRequestError | NotFoundError, 16 | res: Response 17 | ) { 18 | if (err instanceof ServerError) { 19 | return res.status(500).json({ statusCode: err.code, msg: err.message }); 20 | } 21 | if (err instanceof BadRequestError) { 22 | return res.status(400).json({ statusCode: err.code, msg: err.message }); 23 | } 24 | if (err instanceof NotFoundError) { 25 | return res.status(404).json({ statusCode: err.code, msg: err.message }); 26 | } 27 | return res.status(500).json({ statusCode: 500, msg: 'Unknown error' }); 28 | } 29 | 30 | export default errorResposne; 31 | -------------------------------------------------------------------------------- /server/app/src/apis/module/validator.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, Length } from 'class-validator'; 2 | 3 | class VisitorCommentValidator { 4 | @IsOptional() 5 | @Length(0, 20) 6 | public nickname?: string | undefined; 7 | 8 | @Length(4, 20) 9 | public password!: string; 10 | 11 | @Length(1, 250) 12 | public description!: string; 13 | } 14 | 15 | export default VisitorCommentValidator; 16 | -------------------------------------------------------------------------------- /server/app/src/apis/visitor/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import visitorCtrl from './visitor.ctrl'; 3 | import validationCheck from '../middlewares/validationCheck'; 4 | const router: express.Router = express.Router(); 5 | 6 | router.patch('/count', visitorCtrl.updateAndGetVisitor); 7 | 8 | router.post('/comment', validationCheck, visitorCtrl.createVisitComment); 9 | router.get('/comments', visitorCtrl.getVisitorComments); 10 | router.delete('/comment/:id', visitorCtrl.deleteVisitorCommentById); 11 | router.patch( 12 | '/comment/:id', 13 | validationCheck, 14 | visitorCtrl.updateVisitCommentById 15 | ); 16 | 17 | export default router; 18 | -------------------------------------------------------------------------------- /server/app/src/apis/visitor/visitor.ctrl.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import Visitor from '../../service/visitor'; 3 | import VisitorRepository from '../../model/visitorRepository'; 4 | import { BadRequestError, ServerError } from '../../service/error'; 5 | import errorResposne from '../module/error'; 6 | import { VisitorCmtDto } from './visitor'; 7 | 8 | /** 9 | * Visitor increase and lookup API 10 | * @url /apis/visitor/count 11 | * @method patch 12 | * @resBody `{ statusCode: number, todayCount: number, totalCount: number }` 13 | * success response body 14 | * @resBody `{ statusCode: number, msg: string }` fail response body 15 | */ 16 | const updateAndGetVisitor = async (req: Request, res: Response) => { 17 | try { 18 | const visitor = new Visitor(new VisitorRepository()); 19 | 20 | const response = await visitor.updateAndGetVisitorCnt(); 21 | 22 | return res.status(200).json({ statusCode: 200, ...response }); 23 | } catch (err) { 24 | return errorResposne(err, res); 25 | } 26 | }; 27 | 28 | /** 29 | * Visit comment generation API 30 | * @url /apis/visitor/comment 31 | * @method post 32 | * @reqBody `{ nickname: string, password: string, description: string }` 33 | * @resBody `{ statusCode: number, commendId: number, msg: string }` success response body 34 | * @resBody `{ statusCode: number, msg: string }` fail response body 35 | */ 36 | const createVisitComment = async (req: Request, res: Response) => { 37 | const RequestVisitorComment: VisitorCmtDto = Object.assign(req.body); 38 | 39 | try { 40 | if (RequestVisitorComment.nickname?.length === 0) { 41 | req.body.nickname = '익명'; 42 | } 43 | 44 | const visitor = new Visitor(new VisitorRepository(), RequestVisitorComment); 45 | 46 | const response = await visitor.createComment(); 47 | 48 | if (response) { 49 | return res.status(201).json({ 50 | statusCode: 201, 51 | commentId: response, 52 | msg: 'Successful visitor comment creation', 53 | }); 54 | } 55 | throw new ServerError('Interver Server Error') 56 | } catch (err) { 57 | return errorResposne(err, res); 58 | } 59 | }; 60 | 61 | /** 62 | * API for editing visited comments 63 | * @url /apis/visitor/comment/:id 64 | * @method patch 65 | * @reqParams `{ id: number }` Unique number of edit comment 66 | * @reqBody `{ password: string, description: string }` 67 | * @resBody `{ statusCode: number, msg: string }` success or fail response body 68 | */ 69 | const updateVisitCommentById = async (req: Request, res: Response) => { 70 | const { id: visitorCommentId } = req.params; 71 | 72 | const requestVisitorComment: VisitorCmtDto = Object.assign(req.body); 73 | 74 | try { 75 | const visitor = new Visitor(new VisitorRepository(), requestVisitorComment); 76 | 77 | const response = await visitor.updateCommentById(Number(visitorCommentId)); 78 | 79 | if (!response.success) { 80 | return res.status(401).json({ statusCode: 401, msg: response.msg }); 81 | } 82 | return res.status(200).json({ statusCode: 200, msg: response.msg }); 83 | } catch (err) { 84 | return errorResposne(err, res); 85 | } 86 | }; 87 | 88 | /** 89 | * All visit comment lookup APIs 90 | * @url /apis/visitor/comments 91 | * @method get 92 | * @resBody `{ statusCode: number, visitorComments: [{id: number, nickname: string, 93 | description: string, date: string }] }` success response body 94 | * @resBody `{ statusCode: number, mesg: string }` fail response body 95 | */ 96 | const getVisitorComments = async (req: Request, res: Response) => { 97 | try { 98 | const visitor = new Visitor(new VisitorRepository()); 99 | 100 | const { visitorComments } = await visitor.getVisitorComments(); 101 | 102 | return res.status(200).json({ statusCode: 200, visitorComments }); 103 | } catch (err) { 104 | return errorResposne(err, res); 105 | } 106 | }; 107 | 108 | /** 109 | * @typedef {Object} ResBody 110 | * @property {number} statusCode 111 | * @property {string} msg 112 | */ 113 | 114 | /** 115 | * Visit comment delete API 116 | * @url /apis/visitor/comment/:id 117 | * @method delete 118 | * @reqParams `{ id: number }` Unique ID of the target to be deleted 119 | * @resBody `{ statusCode: number, msg: string }` success response body 120 | * @resBody `{ statusCode: number, msg: string }` fail response body 121 | */ 122 | const deleteVisitorCommentById = async (req: Request, res: Response) => { 123 | try { 124 | const visitorCommentId = req.params.id; 125 | 126 | if (!visitorCommentId) { 127 | throw new BadRequestError('id params is undefined'); 128 | } 129 | const visitor = new Visitor(new VisitorRepository()); 130 | 131 | const response = await visitor.deleteVisitorCommentById( 132 | Number(visitorCommentId) 133 | ); 134 | 135 | if (response) { 136 | return res.status(200).json({ 137 | statusCode: 200, 138 | msg: 'Successful deletion of visitor comment', 139 | }); 140 | } 141 | } catch (err) { 142 | return errorResposne(err, res); 143 | } 144 | }; 145 | 146 | export = { 147 | updateAndGetVisitor, 148 | createVisitComment, 149 | updateVisitCommentById, 150 | getVisitorComments, 151 | deleteVisitorCommentById, 152 | }; 153 | -------------------------------------------------------------------------------- /server/app/src/apis/visitor/visitor.d.ts: -------------------------------------------------------------------------------- 1 | import { RowDataPacket } from 'mysql2'; 2 | 3 | export interface VisitorDto extends RowDataPacket { 4 | todayCount: number; 5 | totalCount: number; 6 | todayDate?: string; 7 | } 8 | 9 | export interface VisitorCmtDto { 10 | nickname?: string; 11 | password: string; 12 | description: string; 13 | } 14 | 15 | export interface VisitorCmtEntity extends RowDataPacket { 16 | id: number; 17 | nickname: string; 18 | description: string; 19 | date: string; 20 | } 21 | 22 | export interface VisitorPasswordEntity extends RowDataPacket { 23 | password: string; 24 | } 25 | -------------------------------------------------------------------------------- /server/app/src/config/db.ts: -------------------------------------------------------------------------------- 1 | import mysql from 'mysql2'; 2 | import 'dotenv/config'; 3 | 4 | const config = { 5 | host: process.env.DB_HOST, 6 | user: process.env.DB_USER, 7 | password: process.env.DB_PSWORD, 8 | database: process.env.DB_DATABASE, 9 | port: Number(process.env.DB_PORT), 10 | connectionLimit: 300, 11 | }; 12 | 13 | const mysqlPool = mysql.createPool(config); 14 | 15 | const db = mysqlPool.promise(); 16 | 17 | export default db; 18 | -------------------------------------------------------------------------------- /server/app/src/model/visitorRepository.ts: -------------------------------------------------------------------------------- 1 | import { OkPacket, ResultSetHeader, RowDataPacket } from 'mysql2'; 2 | import { 3 | VisitorCmtDto, 4 | VisitorCmtEntity, 5 | VisitorDto, 6 | } from '../apis/visitor/visitor'; 7 | import db from '../config/db'; 8 | import { ServerError } from '../service/error'; 9 | 10 | /** 11 | * Visitor-related data processing class 12 | */ 13 | class VisitorRepository { 14 | /** 15 | * Today number of visitors and total number of visitors inquiry method 16 | * @returns `{ todayCount: number, totalCount: number }` 17 | */ 18 | async getVisitorCnt(): Promise { 19 | let conn; 20 | try { 21 | conn = await db.getConnection(); 22 | 23 | const query = ` 24 | SELECT today_count AS todayCount, total_count AS totalCount 25 | FROM number_of_visitors 26 | WHERE visitor_id = 1`; 27 | 28 | const [rows] = await conn.execute(query); 29 | 30 | return rows[0]; 31 | } catch (error) { 32 | throw new ServerError('Database Error Occurred'); 33 | } finally { 34 | conn?.release(); 35 | } 36 | } 37 | 38 | /** 39 | * Today's visits and total visits increase method 40 | * @returns `boolean` Whether to update 41 | */ 42 | async updateTodayVisitorCnt(): Promise { 43 | let conn; 44 | try { 45 | conn = await db.getConnection(); 46 | 47 | const query = ` 48 | UPDATE number_of_visitors 49 | SET today_count = today_count + 1, total_count = total_count + 1 50 | WHERE visitor_id = 1;`; 51 | 52 | const [row] = await conn.execute(query); 53 | 54 | return row.affectedRows; 55 | } catch (error) { 56 | throw new ServerError('Database Error Occurred'); 57 | } finally { 58 | conn?.release(); 59 | } 60 | } 61 | 62 | /** 63 | * Inquery today's date for visitor count table 64 | * @returns `string` today's date 65 | * @example 2022-09-12 00:00:00 66 | */ 67 | async getVisitorTodayDate() { 68 | let conn; 69 | try { 70 | conn = await db.getConnection(); 71 | 72 | const query = 73 | 'SELECT today_date AS todayDate FROM number_of_visitors WHERE visitor_id = 1;'; 74 | 75 | const [row] = await conn.execute(query); 76 | 77 | return row[0].todayDate; 78 | } catch (error) { 79 | throw new ServerError('Database Error Occurred'); 80 | } finally { 81 | conn?.release(); 82 | } 83 | } 84 | 85 | /** 86 | * Request date and number of visitors today, total number of visitors update method 87 | * @params 88 | * @todayDate `string` today date 89 | * @returns `boolean` Whether to update 90 | */ 91 | async updateTodayAndToTalVisitorCnt(todayDate: string) { 92 | let conn; 93 | try { 94 | conn = await db.getConnection(); 95 | 96 | const query = ` 97 | UPDATE number_of_visitors 98 | SET today_count = 1, total_count = total_count + 1, today_date = ? 99 | WHERE visitor_id = 1;`; 100 | 101 | const [row] = await conn.execute(query, [todayDate]); 102 | 103 | return row.affectedRows; 104 | } catch (error) { 105 | throw new ServerError('Database Error Occurred'); 106 | } finally { 107 | conn?.release(); 108 | } 109 | } 110 | 111 | /** 112 | * Visit comment generation method 113 | * @params `{ nickname: string, password: string, description: string }` 114 | * @returns `number` Unique ID of the updated visit comment 115 | */ 116 | async createComment({ 117 | nickname, 118 | password, 119 | description, 120 | }: VisitorCmtDto): Promise { 121 | let conn; 122 | try { 123 | conn = await db.getConnection(); 124 | 125 | const query = ` 126 | INSERT INTO visitor_comments (nickname, password, description) 127 | VALUES (?, ?, ?);`; 128 | 129 | const [row] = await conn.execute(query, [ 130 | nickname, 131 | password, 132 | description, 133 | ]); 134 | 135 | return row.insertId; 136 | } catch (error) { 137 | throw new ServerError('Database Error Occurred'); 138 | } finally { 139 | conn?.release(); 140 | } 141 | } 142 | 143 | /** 144 | * Visit comment inquery method for ID 145 | * @params `number` Unique ID of the inquery target 146 | * @returns `{ id: number, nickname: string, description: string, date: string }` 147 | */ 148 | async getVisitorCommentById( 149 | visitorCommentId: number 150 | ): Promise { 151 | let conn; 152 | try { 153 | conn = await db.getConnection(); 154 | 155 | const query = `SELECT * FROM visitor_comments WHERE visitor_comment_id = ?;`; 156 | 157 | const [row] = await conn.execute(query, [ 158 | visitorCommentId, 159 | ]); 160 | 161 | return row[0]; 162 | } catch (error) { 163 | throw new ServerError('Database Error Occurred'); 164 | } 165 | } 166 | 167 | /** 168 | * Save visit comment modification method 169 | * @params 170 | * @visitorCommentId `number` Unique ID to be modified 171 | * @description `string` Modified comments 172 | * @returns `boolean` Whether to update 173 | */ 174 | async updateVisitorComment( 175 | visitorCommentId: number, 176 | description: string 177 | ): Promise { 178 | let conn; 179 | try { 180 | conn = await db.getConnection(); 181 | 182 | const query = `UPDATE visitor_comments SET description = ? WHERE visitor_comment_id = ?`; 183 | 184 | const [row] = await conn.execute(query, [ 185 | description, 186 | visitorCommentId, 187 | ]); 188 | 189 | return row.affectedRows; 190 | } catch (error) { 191 | throw new ServerError('Database Error Occurred'); 192 | } 193 | } 194 | 195 | /** 196 | * All visit comment inquery method 197 | * @returns `[{ id: number, nickname: string, description: string, date: string }]` 198 | */ 199 | async getVisitorComments(): Promise { 200 | let conn; 201 | try { 202 | conn = await db.getConnection(); 203 | 204 | const query = ` 205 | SELECT visitor_comment_id AS id, nickname, description, DATE_FORMAT(create_date, '%y-%m-%d') AS date 206 | FROM visitor_comments;`; 207 | 208 | const [row] = await conn.execute(query); 209 | 210 | return row; 211 | } catch (error) { 212 | throw new ServerError('Database Error Occurred'); 213 | } 214 | } 215 | 216 | /** 217 | * Delete visiting comments with unique ID method 218 | * @params 219 | * @id `number` Unique ID for deletion 220 | * @returns `boolean` Whether to update 221 | */ 222 | async deleteVisitorCommentById(id: number): Promise { 223 | let conn; 224 | try { 225 | conn = await db.getConnection(); 226 | 227 | const query = 'DELETE FROM visitor_comments WHERE visitor_comment_id=?;'; 228 | 229 | const [row] = await conn.execute(query, [id]); 230 | 231 | return row.affectedRows; 232 | } catch (error) { 233 | throw new ServerError('Database Error Occurred'); 234 | } 235 | } 236 | } 237 | 238 | export default VisitorRepository; 239 | -------------------------------------------------------------------------------- /server/app/src/service/error.ts: -------------------------------------------------------------------------------- 1 | export class ServerError extends Error { 2 | code: number; 3 | constructor(message: string) { 4 | super(message); 5 | this.name = 'ServerError'; 6 | this.code = 500; 7 | } 8 | } 9 | 10 | export class BadRequestError extends Error { 11 | code: number; 12 | constructor(message: string) { 13 | super(message); 14 | this.name = 'BadRequestError'; 15 | this.code = 400; 16 | } 17 | } 18 | 19 | export class NotFoundError extends Error { 20 | code: number; 21 | constructor(message: string) { 22 | super(message); 23 | this.name = 'NotFoundError'; 24 | this.code = 404; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/app/src/service/visitor.ts: -------------------------------------------------------------------------------- 1 | import { VisitorCmtDto, VisitorCmtEntity, VisitorDto } from '../apis/visitor/visitor'; 2 | import VisitorRepository from '../model/visitorRepository'; 3 | import bcrypt from 'bcrypt'; 4 | import { NotFoundError, ServerError } from './error'; 5 | import moment from 'moment'; 6 | 7 | interface Response { 8 | success: boolean; 9 | msg: string; 10 | } 11 | 12 | 13 | /** 14 | * Service class for visitors 15 | */ 16 | class Visitor { 17 | private readonly visitorRepository: VisitorRepository; 18 | readonly body; 19 | constructor(visitorRepository: VisitorRepository, body?: any) { 20 | this.visitorRepository = visitorRepository; 21 | this.body = body; 22 | } 23 | 24 | /** 25 | * Method for retrieving number of visitors 26 | * @returns visitorCnt `{ todayCount: number, totalCount: number }` 27 | */ 28 | async getVisitorCnt() { 29 | const visitorCnt = await this.visitorRepository.getVisitorCnt(); 30 | return visitorCnt; 31 | } 32 | 33 | /** 34 | * Update the number of visitors every time they visit your portfolio web 35 | * @returns visitorCnt `{ todayCount: number, totalCount: number }` 36 | */ 37 | async updateAndGetVisitorCnt() { 38 | const todayDate = await this.visitorRepository.getVisitorTodayDate(); 39 | const formatTodayDate = moment(todayDate, 'YYYY-MM-DD'); 40 | const reqDate = moment().format('YYYY-MM-DD'); 41 | 42 | let isUpdate: number; 43 | 44 | if (moment(reqDate).diff(moment(formatTodayDate)) > 0) { 45 | isUpdate = await this.visitorRepository.updateTodayAndToTalVisitorCnt( 46 | reqDate 47 | ); 48 | } else { 49 | isUpdate = await this.visitorRepository.updateTodayVisitorCnt(); 50 | } 51 | 52 | if (isUpdate) { 53 | const visitorCnt = await this.getVisitorCnt(); 54 | 55 | return visitorCnt; 56 | } 57 | throw new ServerError('Interval server error'); 58 | } 59 | 60 | /** 61 | * Create visitor comment information 62 | * @returns Promise to return a unique ID for the generated visit comment of the string type 63 | */ 64 | async createComment(): Promise { 65 | const { body } = this; 66 | const encryptedPassword = await this.encryptPassword(body.password); 67 | 68 | const visitorComment: VisitorCmtDto = { 69 | nickname: body.nickname, 70 | password: encryptedPassword, 71 | description: body.description, 72 | }; 73 | 74 | const commentId = await this.visitorRepository.createComment( 75 | visitorComment 76 | ); 77 | 78 | if (commentId) { 79 | return commentId; 80 | } 81 | throw new ServerError('Interver Server Error'); 82 | } 83 | 84 | /** 85 | * Method for password encryption 86 | * @param password String type as value before encryption 87 | * @returns Encrypt password to return a promise of string type 88 | */ 89 | private async encryptPassword(password: string): Promise { 90 | const saltRounds = 10; 91 | 92 | const encryptedPassword = await bcrypt 93 | .genSalt(saltRounds) 94 | .then((salt: string) => { 95 | return bcrypt.hash(password, salt); 96 | }); 97 | 98 | return encryptedPassword; 99 | } 100 | 101 | /** 102 | * Visit comment update method 103 | * @param visitorCommentId Unique ID for modification of number type 104 | * @returns `{ success: boolean, msg: string }` 105 | * The password matches to perform the operation and returns a successful result or a failure result. 106 | * Throw error if no data exists for requested id. 107 | */ 108 | async updateCommentById(visitorCommentId: number): Promise { 109 | const { password, description }: VisitorCmtDto = this.body; 110 | 111 | const visitorComment = await this.visitorRepository.getVisitorCommentById( 112 | visitorCommentId 113 | ); 114 | 115 | if (!visitorComment) { 116 | throw new NotFoundError('No data exists'); 117 | } 118 | 119 | const isSamePassword = await this.comparePassword( 120 | password, 121 | visitorComment.password 122 | ); 123 | 124 | if (!isSamePassword) { 125 | return { success: false, msg: 'Passwords do not match' }; 126 | } 127 | 128 | await this.visitorRepository.updateVisitorComment( 129 | visitorCommentId, 130 | description 131 | ); 132 | 133 | return { success: true, msg: 'Visitor comment update complete' }; 134 | } 135 | 136 | /** 137 | * Methods to check for matching encrypted passwords 138 | * @param password Password Verification Target 139 | * @param encryptedPassword Valid password for string type 140 | * @returns Match comparison results to return success or reject due to error 141 | */ 142 | private async comparePassword(password: string, encryptedPassword: string) { 143 | return await bcrypt.compare(password, encryptedPassword); 144 | } 145 | 146 | /** 147 | * Methods for querying all visitor comments 148 | * @returns `{ visitorComments: [{ id: number, nickname: string, description: string, date: string}]}` 149 | */ 150 | async getVisitorComments(): Promise<{ visitorComments: VisitorCmtEntity[] }> { 151 | const visitorComments = await this.visitorRepository.getVisitorComments(); 152 | 153 | return { visitorComments }; 154 | } 155 | 156 | /** 157 | * Method to delete visiting comments corresponding to IDs 158 | * @param visitorCommentId Unique ID for deletion of number type 159 | * @returns Returns an error because there is no target for deletion or returns true for success 160 | */ 161 | async deleteVisitorCommentById(visitorCommentId: number): Promise { 162 | const visitorComment = await this.visitorRepository.getVisitorCommentById( 163 | visitorCommentId 164 | ); 165 | 166 | if (!visitorComment) { 167 | throw new NotFoundError('No data exists'); 168 | } 169 | 170 | const isDelete = await this.visitorRepository.deleteVisitorCommentById( 171 | visitorCommentId 172 | ); 173 | 174 | if (isDelete) { 175 | return true; 176 | } 177 | 178 | throw new ServerError('Interver Server Error'); 179 | } 180 | } 181 | export default Visitor; 182 | -------------------------------------------------------------------------------- /server/app/swagger.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.0' 2 | info: 3 | version: 1.0.0 4 | title: 'dev-portfolio API' 5 | description: 'dev-portfolio API' 6 | servers: 7 | - url: http://{HOST_ADDRESS}:{PORT}/ 8 | paths: 9 | /apis/visitor/count: 10 | patch: 11 | summary: 'Update number of visitors API' 12 | responses: 13 | '200': 14 | descriptoin: 'OK' 15 | content: 16 | application/json: 17 | schema: 18 | properties: 19 | statusCode: 20 | type: number 21 | example: 200 22 | todayCount: 23 | type: number 24 | example: 4 25 | totalCount: 26 | type: number 27 | example: 4 28 | '500': 29 | descripiton: 'Server Error' 30 | content: 31 | application/json: 32 | schema: 33 | $ref: '#/components/schemas/ServerError' 34 | 35 | /apis/visitor/comments: 36 | get: 37 | summary: 'Get comments API' 38 | responses: 39 | '200': 40 | description: 'OK' 41 | content: 42 | application/json: 43 | schema: 44 | properties: 45 | statusCode: 46 | type: number 47 | example: 200 48 | visitorComments: 49 | type: array 50 | items: 51 | type: object 52 | properties: 53 | id: 54 | type: number 55 | example: 1 56 | nickname: 57 | type: string 58 | example: 'nodeDeveloper' 59 | description: 60 | type: string 61 | example: 'the site is nice!' 62 | date: 63 | type: string 64 | example: '2022-09-12' 65 | '500': 66 | descripiton: 'Server Error' 67 | content: 68 | application/json: 69 | schema: 70 | $ref: '#/components/schemas/ServerError' 71 | /apis/visitor/comment: 72 | post: 73 | summary: 'Create comment API' 74 | requestBody: 75 | required: true 76 | content: 77 | application/json: 78 | schema: 79 | properties: 80 | nickname: 81 | type: string 82 | example: 'nodeDeveloper' 83 | password: 84 | type: string 85 | example: '1234' 86 | description: 87 | type: string 88 | example: 'hello' 89 | responses: 90 | '201': 91 | description: 'Created' 92 | content: 93 | application/json: 94 | schema: 95 | properties: 96 | statusCode: 97 | type: number 98 | example: 201 99 | commentId: 100 | type: number 101 | example: 2 102 | msg: 103 | type: string 104 | example: 'Successful visitor comment creation' 105 | '400': 106 | description: 'Bad Request' 107 | content: 108 | applciation/json: 109 | schema: 110 | $ref: '#/components/schemas/BadRequestError' 111 | '500': 112 | descripiton: 'Server Error' 113 | content: 114 | application/json: 115 | schema: 116 | $ref: '#/components/schemas/ServerError' 117 | /apis/visitor/comment/{id}: 118 | patch: 119 | summary: 'Update comment API' 120 | parameters: 121 | - in: path 122 | name: id 123 | description: 'visitor comment id' 124 | required: true 125 | schema: 126 | type: number 127 | requestBody: 128 | required: true 129 | content: 130 | application/json: 131 | schema: 132 | properties: 133 | password: 134 | type: string 135 | example: '1234' 136 | description: 137 | type: string 138 | example: 'hello!!!!' 139 | responses: 140 | '200': 141 | description: 'OK' 142 | content: 143 | applciation/json: 144 | schema: 145 | properties: 146 | statusCode: 147 | type: number 148 | example: 200 149 | msg: 150 | type: string 151 | example: 'Visitor comment update complete' 152 | '400': 153 | description: 'Bad Request' 154 | content: 155 | applciation/json: 156 | schema: 157 | $ref: '#/components/schemas/BadRequestError' 158 | '401': 159 | description: 'Unauthorized' 160 | content: 161 | applciation/json: 162 | schema: 163 | properties: 164 | statusCode: 165 | type: number 166 | example: 401 167 | msg: 168 | type: string 169 | example: 'Passwords do not match' 170 | '404': 171 | description: 'Not Found' 172 | content: 173 | applciation/json: 174 | schema: 175 | $ref: '#/components/schemas/NotFoundError' 176 | '500': 177 | description: 'Server Error' 178 | content: 179 | applciation/json: 180 | schema: 181 | $ref: '#/components/schemas/ServerError' 182 | delete: 183 | summary: 'Delete comment API' 184 | parameters: 185 | - name: id 186 | in: path 187 | description: 'visitor comment id' 188 | required: true 189 | schema: 190 | type: string 191 | responses: 192 | '200': 193 | description: 'OK' 194 | content: 195 | applciation/json: 196 | schema: 197 | properties: 198 | statusCode: 199 | type: number 200 | example: 200 201 | msg: 202 | type: string 203 | example: 'Successful deletion of visitor comment' 204 | '400': 205 | description: 'Bad Request' 206 | content: 207 | applciation/json: 208 | schema: 209 | $ref: '#/components/schemas/BadRequestError' 210 | '404': 211 | description: 'Not Found' 212 | content: 213 | applciation/json: 214 | schema: 215 | $ref: '#/components/schemas/NotFoundError' 216 | '500': 217 | description: 'Server Error' 218 | content: 219 | applciation/json: 220 | schema: 221 | $ref: '#/components/schemas/ServerError' 222 | 223 | components: 224 | schemas: 225 | ServerError: 226 | type: object 227 | properties: 228 | statusCode: 229 | type: number 230 | example: 500 231 | msg: 232 | type: string 233 | example: 'server error message' 234 | NotFoundError: 235 | type: object 236 | properties: 237 | statusCode: 238 | type: number 239 | example: 404 240 | msg: 241 | type: string 242 | example: 'No data exists' 243 | BadRequestError: 244 | type: object 245 | properties: 246 | statusCode: 247 | type: number 248 | example: 400 249 | msg: 250 | type: string 251 | example: 'error message' 252 | -------------------------------------------------------------------------------- /server/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true /* Enable constraints that allow a TypeScript project to be used with project references. */, 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | "experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */, 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | // "rootDir": "" /* Specify the root folder within your source files. */, 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [] /* Specify type package names to be included without being referenced in a source file. */, 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 43 | "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "./build" /* Specify an output folder for all emitted files. */, 53 | // "removeComments": true, 54 | "noEmitOnError": true, 55 | // "noEmit": true, /* Disable emitting files from a compilation. */ 56 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 57 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 58 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 59 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 62 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 63 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 64 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 65 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 66 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 67 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 68 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 69 | // "declarationDir": "./", /* the output directory for generated declaration files. */ 70 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 71 | 72 | /* Interop Constraints */ 73 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 74 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 75 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 76 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 77 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 78 | 79 | /* Type Checking */ 80 | "strict": true /* Enable all strict type-checking options. */, 81 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 82 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 83 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 84 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 85 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 86 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 87 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 88 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 89 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 90 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 91 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 92 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 93 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 94 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 95 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 96 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 97 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 98 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 99 | 100 | /* Completeness */ 101 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 102 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 103 | }, 104 | "exclude": ["app/src/tests/**/*", "./jest.config.js"], // files to exclude 105 | "include": ["app/src", "./main.ts"] // Build only the files you want 106 | } 107 | -------------------------------------------------------------------------------- /server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | db: 5 | image: mysql:5.7 6 | platform: linux/amd64 7 | container_name: dev-portfolio-mysql 8 | ports: 9 | - ${DB_PORT}:3306 10 | volumes: 11 | - ./.db/etc/my.cnf:/etc/my.cnf 12 | - ./.db/etc/mysqld.cnf:/etc/mysql/mysql.conf.d/mysqld.cnf 13 | - ./.db/data:/var/lib/mysql 14 | - ./.db/initdb.d:/docker-entrypoint-initdb.d 15 | environment: 16 | - MYSQL_ROOT_PASSWORD=${DB_PSWORD} 17 | - MYSQL_DATABASE=${DB_DATABASE} 18 | - TZ=Asia/Seoul 19 | restart: always 20 | 21 | nodejs: 22 | depends_on: 23 | - db 24 | build: 25 | context: . 26 | dockerfile: dockerfile 27 | image: dev-portfolio-nodejs-image 28 | container_name: dev-portfolio-nodejs-container 29 | ports: 30 | - ${PORT}:${PORT} 31 | env_file: 32 | - ../config/.server.env 33 | restart: always 34 | -------------------------------------------------------------------------------- /server/dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.18.0 2 | 3 | WORKDIR /usr/app 4 | 5 | COPY ./app/tsconfig.json ./ 6 | COPY ./app/package* ./ 7 | COPY ./app/build ./build 8 | 9 | RUN npm ci --production 10 | 11 | CMD ["npm", "start"] 12 | --------------------------------------------------------------------------------