├── 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 | 
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 |
--------------------------------------------------------------------------------