├── redux-sample ├── src │ ├── redux │ │ ├── rootReducer.js │ │ └── count │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ └── reducer.js │ ├── components │ │ └── Counter │ │ │ ├── container.jsx │ │ │ └── presentation.jsx │ └── index.js ├── public │ └── index.html ├── webpack.config.js └── package.json ├── .gitignore ├── nginx ├── Dockerfile └── conf.d │ └── default.conf ├── server ├── env │ └── env-local ├── Dockerfile ├── src │ ├── entity │ │ └── schedule.ts │ ├── models │ │ ├── base.ts │ │ └── schedule.ts │ ├── infrastructure │ │ ├── routers │ │ │ └── schedule.ts │ │ └── db │ │ │ └── handler.ts │ ├── __tests__ │ │ ├── inflastructure │ │ │ └── db │ │ │ │ └── handler.test.ts │ │ └── models │ │ │ └── schedule.ts │ └── controllers │ │ └── schedule.ts ├── jest.config.js ├── package.json ├── index.ts ├── README.md └── tsconfig.json ├── .DS_Store ├── front ├── .DS_Store ├── public │ ├── .DS_Store │ ├── images │ │ └── calendar_icon.png │ └── index.html ├── src │ └── index.jsx ├── package.json └── webpack.config.js ├── db ├── conf.d │ └── my.cnf └── initdb.d │ └── 1_schema.sql ├── Makefile ├── README.md └── docker-compose.yml /redux-sample/src/redux/rootReducer.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /redux-sample/src/redux/count/actions.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /redux-sample/src/redux/count/constants.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /redux-sample/src/redux/count/reducer.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bundle.js 3 | db/volume 4 | .env -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | COPY ./conf.d/ /etc/nginx/conf.d/ -------------------------------------------------------------------------------- /server/env/env-local: -------------------------------------------------------------------------------- 1 | DB_USER=user 2 | DB_NAME=calender 3 | DB_PASS=pass -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon-taro/calender-app/HEAD/.DS_Store -------------------------------------------------------------------------------- /front/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon-taro/calender-app/HEAD/front/.DS_Store -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | WORKDIR /var/www/server 4 | 5 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /front/public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon-taro/calender-app/HEAD/front/public/.DS_Store -------------------------------------------------------------------------------- /redux-sample/src/components/Counter/container.jsx: -------------------------------------------------------------------------------- 1 | import Counter from "./presentation"; 2 | 3 | export default Counter; 4 | -------------------------------------------------------------------------------- /front/public/images/calendar_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon-taro/calender-app/HEAD/front/public/images/calendar_icon.png -------------------------------------------------------------------------------- /db/conf.d/my.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | character-set-server=utf8 3 | collation-server=utf8_general_ci 4 | 5 | [client] 6 | default-character-set=utf8 -------------------------------------------------------------------------------- /redux-sample/src/components/Counter/presentation.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Counter = () =>
counter
; 4 | 5 | export default Counter; 6 | -------------------------------------------------------------------------------- /server/src/entity/schedule.ts: -------------------------------------------------------------------------------- 1 | export interface Schedule { 2 | id?: number; 3 | title: string; 4 | description: string; 5 | date: Date; 6 | location: string; 7 | } 8 | -------------------------------------------------------------------------------- /front/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | const App = () => hello react!!; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | -------------------------------------------------------------------------------- /db/initdb.d/1_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE schedules ( 2 | id INT NOT NULL AUTO_INCREMENT, 3 | date DATETIME NOT NULL, 4 | title TEXT NOT NULL, 5 | description TEXT, 6 | location TEXT, 7 | PRIMARY KEY (id) 8 | ); -------------------------------------------------------------------------------- /redux-sample/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import Counter from "./components/Counter/container"; 5 | 6 | const App = () => ; 7 | 8 | ReactDOM.render(, document.getElementById("root")); 9 | -------------------------------------------------------------------------------- /server/src/models/base.ts: -------------------------------------------------------------------------------- 1 | import DB from "../infrastructure/db/handler"; 2 | 3 | /** 4 | * modelのベースとなるクラス 5 | * DBインスタンスをセットする 6 | */ 7 | export default class BaseModel { 8 | protected db: DB; 9 | 10 | constructor(db: DB) { 11 | this.db = db; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /nginx/conf.d/default.conf: -------------------------------------------------------------------------------- 1 | server{ 2 | charset utf-8; 3 | 4 | location /api { 5 | proxy_pass http://api:8000; 6 | } 7 | 8 | location / { 9 | add_header Cache-Control no-cache; 10 | index index.html; 11 | root /var/www/html; 12 | } 13 | } -------------------------------------------------------------------------------- /server/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["./src"], 3 | moduleFileExtensions: ["ts", "js", "json"], 4 | transform: { 5 | "^.+\\.ts$": "ts-jest" 6 | }, 7 | globals: { 8 | "ts-jest": { 9 | tsConfigFile: "tsconfig.json" 10 | } 11 | }, 12 | testMatch: ["**/__tests__/**/*.+(ts|js)"] 13 | }; 14 | -------------------------------------------------------------------------------- /redux-sample/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | make front/install 3 | make redux-sample/install 4 | make server/install 5 | 6 | front/install: 7 | npm --prefix ./front install ./front 8 | npm --prefix ./front run build 9 | 10 | redux-sample/install: 11 | npm --prefix ./redux-sample install ./redux-sample 12 | npm --prefix ./redux-sample run build 13 | 14 | server/install: 15 | npm --prefix ./server install ./server 16 | cp ./server/env/env-local ./server/.env -------------------------------------------------------------------------------- /redux-sample/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: "./src/index.js", 5 | output: { 6 | path: path.join(__dirname, "public/js"), 7 | filename: "bundle.js" 8 | }, 9 | resolve: { 10 | modules: ["node_modules"], 11 | extensions: [".js", ".jsx"] 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js|\.jsz$/, 17 | exclude: /node_modules/, 18 | use: { 19 | loader: "babel-loader", 20 | options: { 21 | presets: ["@babel/preset-env", "@babel/preset-react"] 22 | } 23 | } 24 | } 25 | ] 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /server/src/infrastructure/routers/schedule.ts: -------------------------------------------------------------------------------- 1 | import Router from "express-promise-router"; 2 | import ScheduleController from "../../controllers/schedule"; 3 | import DB from "../db/handler"; 4 | 5 | const scheduleRouter = (db: DB) => { 6 | const router = Router(); 7 | const scheduleController = new ScheduleController(db); 8 | 9 | router.get("/", scheduleController.index); 10 | router.post("/", scheduleController.create); 11 | router.get("/:id", scheduleController.show); 12 | router.delete("/:id", scheduleController.delete); 13 | router.post("/create-test-data", scheduleController.createTestData); 14 | 15 | return router; 16 | }; 17 | 18 | export default scheduleRouter; 19 | -------------------------------------------------------------------------------- /front/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calender", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "watch": "webpack --mode development --watch", 7 | "build": "webpack --mode production" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "react": "^16.8.6", 14 | "react-dom": "^16.8.6", 15 | "react-redux": "^7.1.3", 16 | "redux": "^4.0.4" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.4.5", 20 | "@babel/preset-env": "^7.4.5", 21 | "@babel/preset-react": "^7.0.0", 22 | "babel-loader": "^8.0.6", 23 | "babel-plugin-import": "^1.13.0", 24 | "css-loader": "^3.2.0", 25 | "style-loader": "^1.0.0", 26 | "webpack": "^4.34.0", 27 | "webpack-cli": "^3.3.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /redux-sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-sample", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "watch": "webpack --mode development --watch", 8 | "build": "webpack --mode production", 9 | "start": "http-server -c-1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "http-server": "^0.11.1", 16 | "react": "^16.11.0", 17 | "react-dom": "^16.11.0", 18 | "react-redux": "^7.1.3", 19 | "redux": "^4.0.4" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.7.2", 23 | "@babel/preset-env": "^7.7.1", 24 | "@babel/preset-react": "^7.7.0", 25 | "babel-loader": "^8.0.6", 26 | "webpack": "^4.41.2", 27 | "webpack-cli": "^3.3.10" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node index.ts", 8 | "test": "jest" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/dotenv": "^8.2.0", 15 | "@types/node": "^12.11.7", 16 | "dayjs": "^1.8.16", 17 | "dotenv": "^8.2.0", 18 | "express": "^4.17.1", 19 | "express-promise-router": "^3.0.3", 20 | "mysql": "^2.17.1" 21 | }, 22 | "devDependencies": { 23 | "@types/express": "^4.17.1", 24 | "@types/express-promise-router": "^3.0.0", 25 | "@types/jest": "^24.0.20", 26 | "@types/mysql": "^2.15.7", 27 | "jest": "^24.9.0", 28 | "ts-jest": "^24.1.0", 29 | "ts-node": "^8.4.1", 30 | "typescript": "^3.6.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/src/__tests__/inflastructure/db/handler.test.ts: -------------------------------------------------------------------------------- 1 | import DB from "../../../infrastructure/db/handler"; 2 | import { Schedule } from "../../../entity/schedule"; 3 | 4 | process.env.DB_USER = "user"; 5 | process.env.DB_NAME = "calender"; 6 | process.env.DB_PASS = "pass"; 7 | 8 | const db = DB.instance; 9 | 10 | beforeAll(async () => { 11 | await db.connect(); 12 | }); 13 | 14 | afterAll(async () => { 15 | db.con!.destroy(); 16 | }); 17 | 18 | describe("DBのテスト", () => { 19 | it("selectのテスト", async () => { 20 | const result = await db.query("select * from schedules"); 21 | console.log(result); 22 | }); 23 | 24 | it("insertのテスト", async () => { 25 | const result = await db.query( 26 | "insert schedules (date, title, description) values (?, ?, ?)", 27 | [new Date(), "テスト", "文字化け"] 28 | ); 29 | console.log(result); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # カレンダーの環境構築 2 | 3 | ## セットアップ 4 | 5 | docker をインストールしたのちに、 6 | 7 | ```shell 8 | $ make 9 | ``` 10 | 11 | これでもろもろインストールやビルドが終わります。`make`コマンドがない場合は、 12 | 13 | ```shell 14 | $ npm --prefix ./front install ./front 15 | $ npm --cwd ./front run build 16 | $ npm --prefix ./server install ./server 17 | $ cp ./server/env/env-local ./server/.env 18 | ``` 19 | 20 | の 4 つのコマンドを実行してください。 21 | 22 | ## サーバーの起動と停止 23 | 24 | ```shell 25 | $ docker-compose up -d 26 | ``` 27 | 28 | を実行すると[localhost:8080]()にサーバーが立ち上がります。最初はいろいろインストールするので時間がかかると思います。 29 | 30 | サーバーを止めたいときは、 31 | 32 | ```shell 33 | $ docker-compose down 34 | ``` 35 | 36 | と実行すれば ok です。 37 | 38 | API ドキュメントは[こちら](./server/README.md) 39 | 40 | ## フロントの開発をするとき 41 | 42 | フロントは`webpack`でビルドしてやる必要があります。 43 | 44 | ```shell 45 | $ cd front 46 | $ npm run watch 47 | ``` 48 | 49 | とすると、ファイルの変更を検知して自動で差分のビルドが走るようになります。自動でビルドされたものはすぐに docker 内の nginx が配信してくれるので、変更のたびに何かコマンドを打つ必要はありません。 50 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import dotenv from "dotenv"; 3 | 4 | import scheduleRouter from "./src/infrastructure/routers/schedule"; 5 | import DB from "./src/infrastructure/db/handler"; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | const port = 8000; 11 | 12 | app.use(express.json()); 13 | app.use(function(_req, res, next) { 14 | res.header("Access-Control-Allow-Origin", "*"); 15 | res.header( 16 | "Access-Control-Allow-Headers", 17 | "Origin, X-Requested-With, Content-Type, Accept" 18 | ); 19 | res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); 20 | next(); 21 | }); 22 | 23 | const db = DB.instance; 24 | 25 | db.connect().then(() => { 26 | // health check 27 | app.get("/api/hc", (_req, res) => res.send("ok!")); 28 | 29 | // routing 30 | app.use("/api/schedules", scheduleRouter(db)); 31 | 32 | // 起動 33 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 34 | }); 35 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | nginx: 5 | build: ./nginx/ 6 | ports: 7 | - "8080:80" 8 | volumes: 9 | - "./front/public:/var/www/html" 10 | networks: 11 | - private-subnet 12 | api: 13 | build: ./server/ 14 | volumes: 15 | - "./server:/var/www/server" 16 | networks: 17 | - private-subnet 18 | environment: 19 | - DB_USER=user 20 | - DB_NAME=calender 21 | - DB_PASS=pass 22 | - DB_HOST=db 23 | db: 24 | image: mysql:5.7 25 | command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci 26 | environment: 27 | MYSQL_ROOT_PASSWORD: pass 28 | MYSQL_DATABASE: calender 29 | MYSQL_USER: user 30 | MYSQL_PASSWORD: pass 31 | TZ: Asia/Tokyo 32 | volumes: 33 | - ./db/initdb.d:/docker-entrypoint-initdb.d 34 | - ./db/conf.d/my.cnf:/etc/mysql/conf.d/my.cnf 35 | networks: 36 | - private-subnet 37 | 38 | networks: 39 | private-subnet: {} 40 | -------------------------------------------------------------------------------- /front/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: "./src/index.jsx", 5 | output: { 6 | path: path.join(__dirname, "public/js"), 7 | filename: "bundle.js" 8 | }, 9 | resolve: { 10 | modules: ["node_modules"], 11 | extensions: [".js", ".jsx"] 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /(\.js|\.jsx)$/, 17 | exclude: /node_modules/, 18 | use: { 19 | loader: "babel-loader", 20 | options: { 21 | presets: [ 22 | [ 23 | "@babel/preset-env", 24 | { 25 | targets: { 26 | node: "current" 27 | } 28 | } 29 | ], 30 | "@babel/preset-react" 31 | ], 32 | plugins: [ 33 | [ 34 | "babel-plugin-import", 35 | { 36 | libraryName: "@material-ui/icons", 37 | libraryDirectory: "", 38 | camel2DashComponentName: false 39 | } 40 | ] 41 | ] 42 | } 43 | } 44 | }, 45 | { 46 | test: /\.css$/, 47 | exclude: /node_modules/, 48 | use: [ 49 | { 50 | loader: "style-loader" 51 | }, 52 | { 53 | loader: "css-loader", 54 | options: { 55 | localsConvention: "camelCase", 56 | modules: { 57 | localIdentName: "[path][name]__[local]--[hash:base64:5]" 58 | } 59 | } 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /server/src/__tests__/models/schedule.ts: -------------------------------------------------------------------------------- 1 | import DB from "../../infrastructure/db/handler"; 2 | import ScheduleModel from "../../models/schedule"; 3 | import { Schedule } from "../../entity/schedule"; 4 | 5 | process.env.DB_USER = "user"; 6 | process.env.DB_NAME = "calender"; 7 | process.env.DB_PASS = "pass"; 8 | 9 | const db = DB.instance; 10 | 11 | beforeAll(async () => { 12 | await db.connect(); 13 | }); 14 | 15 | afterAll(async () => { 16 | db.con!.destroy(); 17 | }); 18 | 19 | describe("schedule modelのテスト", () => { 20 | it("store", async () => { 21 | const scheduleModel = new ScheduleModel(db); 22 | const month = 11; 23 | const year = 2019; 24 | const result = await scheduleModel.findAll(month, year); 25 | 26 | console.log(result); 27 | }); 28 | 29 | it("store", async () => { 30 | const scheduleModel = new ScheduleModel(db); 31 | const schedule: Schedule = { 32 | title: "テスト", 33 | description: "テスト用のデータです", 34 | date: new Date(), 35 | location: "会議室" 36 | }; 37 | 38 | const result = await scheduleModel.store(schedule); 39 | console.log(result); 40 | }); 41 | 42 | it("find", async () => { 43 | const scheduleModel = new ScheduleModel(db); 44 | const result = await scheduleModel.find(1); 45 | 46 | console.log(result); 47 | }); 48 | 49 | it("delete", async () => { 50 | const scheduleModel = new ScheduleModel(db); 51 | const month = 11; 52 | const year = 2019; 53 | const schedules = await scheduleModel.findAll(month, year); 54 | 55 | const id = schedules.reverse()[0].id!; 56 | const result = await scheduleModel.delete(id); 57 | 58 | console.log(result); 59 | }); 60 | 61 | it("create test data", async () => { 62 | const scheduleModel = new ScheduleModel(db); 63 | const schedules = await scheduleModel.createTestData(); 64 | 65 | console.log(schedules); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /server/src/infrastructure/db/handler.ts: -------------------------------------------------------------------------------- 1 | import { Connection, createConnection, QueryOptions } from "mysql"; 2 | 3 | export default class DB { 4 | public con?: Connection; 5 | private retryCount: number = 0; 6 | 7 | /** 8 | * singleton(=一回だけしかインスタンスを生成できない) 9 | */ 10 | private static db?: DB; 11 | private constructor() {} 12 | 13 | static get instance() { 14 | if (!this.db) { 15 | this.db = new DB(); 16 | } 17 | 18 | return this.db; 19 | } 20 | 21 | /** 22 | * DBへの接続を行う 23 | */ 24 | async connect() { 25 | this.retryCount++; 26 | 27 | try { 28 | this.con = await this.createConnection(); 29 | } catch (err) { 30 | // コネクションに失敗したら1sごとに最大10回retry 31 | if (this.retryCount < 10) { 32 | await new Promise(resolve => setTimeout(() => resolve(), 1000)); 33 | await this.connect(); 34 | } else { 35 | throw new Error(err); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * `mysql.query`をpromiseで扱えるようにラップしたもの 42 | * @param query SQLクエリ 43 | * @param values SQLのplaceholderを与える配列 44 | */ 45 | query(query: string, values?: any): Promise { 46 | return new Promise((resolve, reject) => { 47 | this.con!.query(query, values, (err, result) => { 48 | if (err) { 49 | reject(err); 50 | } 51 | 52 | const _result = result as T; 53 | 54 | resolve(_result); 55 | }); 56 | }); 57 | } 58 | 59 | /** 60 | * コネクション作成のための関数 61 | * Promiseでコネクションを作成 62 | */ 63 | private createConnection(): Promise { 64 | return new Promise((resolve, reject) => { 65 | const connection = createConnection({ 66 | host: process.env.DB_HOST, 67 | user: process.env.DB_USER, 68 | password: process.env.DB_PASS, 69 | database: process.env.DB_NAME, 70 | charset: "utf8" 71 | }); 72 | 73 | connection.connect(err => { 74 | if (err) { 75 | reject(err); 76 | } 77 | 78 | resolve(connection); 79 | }); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /server/src/controllers/schedule.ts: -------------------------------------------------------------------------------- 1 | import ScheduleModel from "../models/schedule"; 2 | import DB from "../infrastructure/db/handler"; 3 | import { 4 | Dictionary, 5 | Request as _Request, 6 | Response 7 | } from "express-serve-static-core"; 8 | import { Schedule } from "../entity/schedule"; 9 | 10 | type Request = _Request>; 11 | 12 | export default class ScheduleController { 13 | private scheduleModel: ScheduleModel; 14 | 15 | constructor(db: DB) { 16 | this.scheduleModel = new ScheduleModel(db); 17 | } 18 | 19 | /** 20 | * 予定を取得するためのコントローラー 21 | * `month`と`year`の指定がマスト 22 | */ 23 | index = async (req: Request, res: Response) => { 24 | const year = Number(req.query.year as string); 25 | const month = Number(req.query.month as string); 26 | 27 | // queryの指定がなかったら400 Bad Request 28 | const isValid = year > 0 && month > 0 && month <= 12; 29 | if (!isValid) { 30 | res.sendStatus(400); 31 | return; 32 | } 33 | 34 | const schedules = await this.scheduleModel.findAll(month, year); 35 | 36 | res.json(schedules); 37 | }; 38 | 39 | /** 40 | * 新しい予定を作成するコントローラー 41 | */ 42 | create = async (req: Request, res: Response) => { 43 | const schedule = req.body as Schedule; 44 | const newSchedule = await this.scheduleModel.store(schedule); 45 | 46 | res.json(newSchedule); 47 | }; 48 | 49 | /** 50 | * 5つのテストデータを追加するためのコントローラー 51 | */ 52 | createTestData = async (_req: Request, res: Response) => { 53 | const schedules = await this.scheduleModel.createTestData(); 54 | 55 | res.json(schedules); 56 | }; 57 | 58 | /** 59 | * 予定を一件だけ返すコントローラー 60 | */ 61 | show = async (req: Request, res: Response) => { 62 | const id = Number(req.params.id); 63 | if (!id) { 64 | res.sendStatus(400); 65 | return; 66 | } 67 | 68 | const schedule = await this.scheduleModel.find(id); 69 | 70 | res.json(schedule); 71 | }; 72 | 73 | /** 74 | * 予定を一件だけ消すコントローラー 75 | */ 76 | delete = async (req: Request, res: Response) => { 77 | const id = Number(req.params.id); 78 | if (!id) { 79 | res.sendStatus(400); 80 | return; 81 | } 82 | 83 | await this.scheduleModel.delete(id); 84 | 85 | res.sendStatus(204); 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /server/src/models/schedule.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import "dayjs/locale/ja"; 3 | 4 | dayjs.locale("ja"); 5 | 6 | import BaseModel from "./base"; 7 | import DB from "../infrastructure/db/handler"; 8 | import { Schedule } from "../entity/schedule"; 9 | 10 | export default class ScheduleModel extends BaseModel { 11 | constructor(db: DB) { 12 | super(db); 13 | } 14 | 15 | /** 16 | * `year`年`month`月の予定を取得 17 | * @param month 月の指定 18 | * @param year 年の指定 19 | */ 20 | async findAll(month: number, year: number) { 21 | const targetMonth = dayjs(`${year}-${month}-1`); 22 | const firstDay = targetMonth.startOf("month").toISOString(); 23 | const lastDay = targetMonth.endOf("month").toISOString(); 24 | 25 | return await this.db.query( 26 | "select * from schedules where date between ? and ?;", 27 | [firstDay, lastDay] 28 | ); 29 | } 30 | 31 | /** 32 | * 指定されたidの予定を取得 33 | * @param id 追加したいデータのid 34 | */ 35 | async find(id: number) { 36 | const schedules = await this.db.query( 37 | "select * from schedules where id = ?", 38 | [id] 39 | ); 40 | return schedules[0]; 41 | } 42 | 43 | /** 44 | * 予定の追加 45 | * @param schedule 追加したい予定のデータ 46 | */ 47 | async store(schedule: Schedule) { 48 | const result = await this.db.query<{ insertId: number }>( 49 | "insert into schedules (title, description, date, location) values (?, ?, ?, ?);", 50 | [ 51 | schedule.title, 52 | schedule.description, 53 | new Date(schedule.date), 54 | schedule.location 55 | ] 56 | ); 57 | 58 | const newSchedule = await this.find(result.insertId); 59 | 60 | return newSchedule; 61 | } 62 | 63 | /** 64 | * 指定されたidの予定を削除 65 | * @param id 削除したいデータのid 66 | */ 67 | async delete(id: number) { 68 | await this.db.query("delete from schedules where id = ?", [id]); 69 | 70 | return; 71 | } 72 | 73 | /** 74 | * テストデータを5つ追加 75 | * Promise.allで全部終わるのを待ってそれの配列を返す 76 | */ 77 | async createTestData() { 78 | const testData = this.testData(); 79 | const newData = await Promise.all( 80 | testData.map(async d => await this.store(d)) 81 | ); 82 | 83 | return newData; 84 | } 85 | 86 | /** 87 | * 当日・前後1日・前後1ヶ月の同じ日付の日の予定を返す 88 | */ 89 | private testData() { 90 | const testData: Schedule[] = [ 91 | { 92 | title: "会議", 93 | description: "経営戦略会議", 94 | location: "会議室A", 95 | date: dayjs().toDate() 96 | }, 97 | { 98 | title: "会議", 99 | description: "経営戦略会議", 100 | location: "会議室A", 101 | date: dayjs() 102 | .add(1, "day") 103 | .toDate() 104 | }, 105 | { 106 | title: "会議", 107 | description: "経営戦略会議", 108 | location: "会議室A", 109 | date: dayjs() 110 | .add(-1, "day") 111 | .toDate() 112 | }, 113 | { 114 | title: "会議", 115 | description: "経営戦略会議", 116 | location: "会議室A", 117 | date: dayjs() 118 | .add(1, "month") 119 | .toDate() 120 | }, 121 | { 122 | title: "会議", 123 | description: "経営戦略会議", 124 | location: "会議室A", 125 | date: dayjs() 126 | .add(-1, "month") 127 | .toDate() 128 | } 129 | ]; 130 | 131 | return testData; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | ## 機能 2 | 3 | この API サーバーではカレンダーにおける予定の保存や取得ができるようになっています。api の path は、[localhost:8080/api](localhost:8080/api)に続く形です。(ex. path が`schedules`ならフルパスは[localhost:8080/api/schedules](localhost:8080/api/schedules)です) 4 | 5 | この API にリクエストを送ることで、データを取得したりデータの保存をしたりできるようになります。以下のドキュメントには`curl`のサンプルリクエストも書いてあるのでぜひ試してみてください。 6 | 7 | たまにサーバー側のエラーで以下のような画面が表示されることがあります。 8 | 9 | ![](error.png) 10 | 11 | curl の場合は以下のようになります。 12 | 13 | ```bash 14 | $ curl "localhost:8080/api/schedules?month=11&year=2019" 15 | 16 | 502 Bad Gateway 17 | 18 |

502 Bad Gateway

19 |
nginx/1.17.5
20 | 21 | 22 | ``` 23 | 24 | この場合は docker を再起動することで再び開発ができるようになるので試してください。 25 | 26 | ```bash 27 | docker-compose down 28 | docker-compose up -d 29 | ``` 30 | 31 | 上記のコマンドを実行するとサーバーを再起動することができます。これは、プロジェクトの一番上のディレクトリ(docker-comopose.yml があるディレクトリ)で実行してください。 32 | 33 | ## 仕様 34 | 35 | ### 特定の月の予定の全件取得 36 | 37 | 指定した`年/月`の予定を全て取得します。どちらも必須パラメーターでこれがない場合は`400 Bad Request`が返ってきます。 38 | 39 | ``` 40 | GET /schedules 41 | ``` 42 | 43 | #### パラメーター 44 | 45 | | Name | Type | description | 46 | | ------- | -------- | ----------------------------------------- | 47 | | `month` | `number` | **必須** どの月の予定を取得するのかを指定 | 48 | | `year` | `number` | **必須** どの年の予定を取得するのかを指定 | 49 | 50 | #### 例 51 | 52 | ```bash 53 | $ curl "localhost:8080/api/schedules?month=11&year=2019" 54 | ``` 55 | 56 | ```json 57 | Status: 200 OK 58 | 59 | ---- 60 | 61 | [ 62 | { 63 | "id": 1, 64 | "date": "2019-11-11T15:54:14.000Z", 65 | "title": "会議", 66 | "description": "経営戦略について", 67 | "location": "会議室A" 68 | }, 69 | { 70 | "id": 2, 71 | "date": "2019-11-21T15:00:00.000Z", 72 | "title": "ランチ", 73 | "description": "お寿司が食べたい", 74 | "location": "未定" 75 | } 76 | ] 77 | ``` 78 | 79 | ※ 別途フォーマットしているので実際は改行されずに表示されると思います。コマンドラインでフォーマットしたいときは[jq](https://stedolan.github.io/jq/)というツールを使うと綺麗にフォーマットできます。気になる方は調べてみてください。 80 | 81 | ### 一つの予定の取得 82 | 83 | この教材では必要ありませんが、CRUD には必要なので実装しました。 84 | 85 | ``` 86 | GET /schedules/:id 87 | ``` 88 | 89 | ```bash 90 | $ curl "localhost:8080/api/schedules/1" 91 | ``` 92 | 93 | ```json 94 | Status: 200 OK 95 | 96 | ---- 97 | 98 | { 99 | "id": 1, 100 | "date": "2019-11-11T15:54:14.000Z", 101 | "title": "会議", 102 | "description": "経営戦略について", 103 | "location": "会議室A" 104 | } 105 | ``` 106 | 107 | ### 予定の追加 108 | 109 | 予定の各項目を json で送ることで予定が登録できます。日付の項目は少しややこしいですが、それほど気にしなくても大丈夫です。 110 | 111 | ``` 112 | GET /schedules 113 | ``` 114 | 115 | #### パラメーター 116 | 117 | | Name | Type | description | 118 | | ------------- | ----------- | -------------------------------------------------------------------------------------------- | 119 | | `title` | `string` | **必須** 予定のタイトル | 120 | | `date` | `ISOString` | **必須** 予定の日付(日付のデータフォーマットの一つで`JSON.Stringfy()`で勝手にこの形になる) | 121 | | `description` | `string` | 予定の説明 | 122 | | `location` | `string` | 場所の指定 | 123 | 124 | #### 例 125 | 126 | ```bash 127 | $ curl -X POST \ 128 | -H "Content-Type: application/json" \ 129 | -d '{"title": "会議", "description": "経営戦略について", "date": "2019-11-11T15:54:14.000Z", "location": "会議室A"}' \ 130 | "localhost:8080/api/schedules" 131 | ``` 132 | 133 | ```json 134 | Status: 200 OK 135 | 136 | ---- 137 | 138 | { 139 | "id": 15, 140 | "date": "2019-11-11T15:54:14.000Z", 141 | "title": "会議", 142 | "description": "経営戦略について", 143 | "location": "会議室A" 144 | } 145 | ``` 146 | 147 | ### 予定の削除 148 | 149 | ``` 150 | DELETE /schedules/:id 151 | ``` 152 | 153 | 指定した id のレコードを削除することができます。 154 | 155 | #### 例 156 | 157 | ```bash 158 | $ curl "localhost:8080/api/schedules/1 159 | ``` 160 | 161 | ```json 162 | Status: 204 No Content 163 | ``` 164 | 165 | ### サンプルデータの追加 166 | 167 | データの取得から実装した方が簡単なのでテストデータを 5 件ほど追加する API を実装しました。日付データもあって POST で対応するのも面倒なのでぜひこちらを使ってください!その日の日付と前後 1 日そして前後 1 ヶ月の合計 5 日に予定が追加されます。 168 | 169 | ``` 170 | POST /schedules/create-test-data 171 | ``` 172 | 173 | ```bash 174 | $ curl -X POST "localhost:8080/api/schedules/create-test-data" 175 | ``` 176 | 177 | ```json 178 | Status: 200 OK 179 | 180 | ---- 181 | 182 | [ 183 | { 184 | "id": 1, 185 | "date": "2019-11-23T11:42:05.000Z", 186 | "title": "会議", 187 | "description": "経営戦略会議", 188 | "location": "会議室A" 189 | }, 190 | { 191 | "id": 2, 192 | "date": "2019-11-24T11:42:05.000Z", 193 | "title": "会議", 194 | "description": "経営戦略会議", 195 | "location": "会議室A" 196 | }, 197 | { 198 | "id": 3, 199 | "date": "2019-11-22T11:42:05.000Z", 200 | "title": "会議", 201 | "description": "経営戦略会議", 202 | "location": "会議室A" 203 | }, 204 | { 205 | "id": 4, 206 | "date": "2019-12-23T11:42:05.000Z", 207 | "title": "会議", 208 | "description": "経営戦略会議", 209 | "location": "会議室A" 210 | }, 211 | { 212 | "id": 5, 213 | "date": "2019-10-23T11:42:05.000Z", 214 | "title": "会議", 215 | "description": "経営戦略会議", 216 | "location": "会議室A" 217 | } 218 | ] 219 | ``` 220 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | --------------------------------------------------------------------------------