$ npm i -g express-generatorhbs(Handlebars)를 HTML Eninge 기술로 사용하도록 설정합니다. 하지만, 프론트 엔드 UI는 앵귤러를 사용하여 구축합니다. Express로 구축하는 노드 서버 프로젝트는 API로써의 역할을 수행합니다.
$ express my-express --view=hbs --css=sass
create : my-express\
create : my-express\public\
create : my-express\public\javascripts\
create : my-express\public\images\
create : my-express\public\stylesheets\
create : my-express\public\stylesheets\style.sass
create : my-express\routes\
create : my-express\routes\index.js
create : my-express\routes\users.js
create : my-express\views\
create : my-express\views\error.hbs
create : my-express\views\index.hbs
create : my-express\views\layout.hbs
create : my-express\app.js
create : my-express\package.json
create : my-express\bin\
create : my-express\bin\www
change directory:
> cd my-express
install dependencies:
> npm install
run the app:
> SET DEBUG=my-express:* & npm startpackage.json
{
"name": "my-express",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.3",
"debug": "~2.6.9",
"express": "~4.16.0",
"hbs": "~4.0.1",
"http-errors": "~1.6.2",
"morgan": "~1.9.0",
"node-sass-middleware": "0.11.0"
}
}$ npm i express-sessionapp.js
const session = require('express-session');
app.use(session({
resave: false,
saveUninitialized: false,
secret: 'secret code',
cookie: {
httpOnly: true,
secure: false,
},
}));connect-flash 모듈은 POST-REDIRECT-GET 패턴을 적용할 때 사용합니다. 다음 링크에서 자세한 정보를 확인하세요.
https://en.wikipedia.org/wiki/Post/Redirect/Get
connect-flash
$ npm i connect-flashapp.js
const flash = require('connect-flash');
app.use(flash());routes\users.js
// TRY: http://localhost:3000/users/flash
router.get('/flash', function (req, res) {
req.session.message = 'Session Message';
req.flash('message', 'Flash Message');
res.redirect('/users/flash/result');
});
router.get('/flash/result', function (req, res) {
res.send(`req.session.message=${req.session.message}, req.flash('message')=${req.flash('message')}`);
});ORM 기술입니다. JS 코드로 디비, 테이블을 제어할 수 있습니다. 가장 강력한 기능은 개발자가 직접 쿼리작성을 하는 대신 엔티티의 관계를 설정하면 Sequelize가 SQL 쿼리를 자동으로 생성하기 때문에 매우 편리합니다.
$ npm i sequelize mysql2sequelize-cli
$ npm i -g sequelize-cli
$ sequelize init
Sequelize CLI [Node: 10.14.1, CLI: 5.4.0, ORM: 4.42.0]
Created "config\config.json"
Successfully created models folder at "C:\...\my-express\models".
Successfully created migrations folder at "C:\...\my-express\migrations".
Successfully created seeders folder at "C:\...\my-express\seeders".sequelize-cli가 생성한 파일의 내용을 다음처럼 변경합니다.
models\index.js
// 'use strict';
// const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
// const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
let sequelize;
// if (config.use_env_variable) {
// sequelize = new Sequelize(process.env[config.use_env_variable], config);
// } else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
// }
// fs
// .readdirSync(__dirname)
// .filter(file => {
// return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
// })
// .forEach(file => {
// const model = sequelize['import'](path.join(__dirname, file));
// db[model.name] = model;
// });
// Object.keys(db).forEach(modelName => {
// if (db[modelName].associate) {
// db[modelName].associate(db);
// }
// });
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;준비된 MySQL 또는 MariaDB의 연결정보를 config.json 파일에 설정합니다.
config\config.json
{
"development": {
"username": "root",
"password": "1111",
"database": "database_development",
"host": "127.0.0.1",
"dialect": "mysql"
},
"test": {
"username": "root",
"password": null,
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": {
"username": "root",
"password": null,
"database": "database_production",
"host": "127.0.0.1",
"dialect": "mysql"
}
}sql\dummy.js
const { User, Todo } = require('../models');
// Insert Dummy Data for Test
module.exports = async () => {
try {
await User.create({ name: 'oliver', age: 20, married: false, comment: 'programmer' });
await User.create({ name: 'jack', age: 21, married: false, comment: 'designer' });
await User.create({ name: 'harry', age: 30, married: true, comment: 'programmer' });
await User.create({ name: 'jacob', age: 25, married: false, comment: 'designer' });
await User.create({ name: 'charlie', age: 34, married: false, comment: 'programmer' });
await User.create({ name: 'thomas', age: 33, married: false, comment: 'designer' });
await User.create({ name: 'george', age: 40, married: true, comment: 'teacher' });
await User.create({ name: 'oscar', age: 49, married: true, comment: 'salesman' });
await User.create({ name: 'james', age: 55, married: false, comment: 'salesman' });
await User.create({ name: 'william', age: 66, married: true, comment: 'none' });
await User.create({ name: 'jake', age: 18, married: false, comment: 'teacher' });
console.log('▶ 11 users inserted.');
await Todo.create({ owner: 1, task: 'to learn node 1', done: false });
await Todo.create({ owner: 1, task: 'to learn node 2', done: true });
await Todo.create({ owner: 1, task: 'to learn node 3', done: false });
await Todo.create({ owner: 1, task: 'to learn node 4', done: true });
await Todo.create({ owner: 1, task: 'to learn node 5', done: false });
await Todo.create({ owner: 1, task: 'to learn node 6', done: true });
await Todo.create({ owner: 1, task: 'to learn node 7', done: false });
await Todo.create({ owner: 1, task: 'to learn node 8', done: true });
await Todo.create({ owner: 1, task: 'to learn node 9', done: false });
await Todo.create({ owner: 1, task: 'to learn node 10', done: true });
await Todo.create({ owner: 1, task: 'to learn node 11', done: false });
console.log('▶ 11 todos inserted.');
} catch (err) {
console.error(err);
throw err;
}
}app.js
const { sequelize } = require('./models');
// app.get('env') returns 'development'
// if process.env.NODE_ENV is not defined.
console.log('----------------------');
console.log('MODE: ' + app.get('env'));
console.log('----------------------');
/*
* connect to DB
*/
if (app.get('env') === 'development') {
// If force is true, each Model will run DROP TABLE IF EXISTS,
// before it tries to create its own table
sequelize.sync({
force: true,
}).then(() => {
// Insert Dummy Data for Test
require('./sql/dummy')();
});
} else {
sequelize.sync({
// default value
force: false,
});
}models\user.js
module.exports = (sequelize, DataTypes) => {
return sequelize.define('user', {
name: {
type: DataTypes.STRING(20),
allowNull: false,
unique: true,
},
age: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
},
married: {
type: DataTypes.BOOLEAN,
allowNull: false,
},
comment: {
type: DataTypes.TEXT,
allowNull: true,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: sequelize.literal('now()'),
},
}, {
timestamps: false,
});
};models\todo.js
module.exports = (sequelize, Sequelize) => {
return sequelize.define('todo', {
task: {
type: Sequelize.STRING(100),
allowNull: false,
unique: false,
},
done: {
type: Sequelize.BOOLEAN,
allowNull: false,
unique: false,
defaultValue: false,
}
}, {
timestamps: true,
paranoid: true,
underscored: true,
});
}index.js 파일의 내용을 다음처럼 수정합니다.
models\index.js
const path = require('path');
const Sequelize = require('sequelize');
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
const sequelize = new Sequelize(config.database, config.username, config.password, config);
db.sequelize = sequelize;
db.Sequelize = Sequelize;
db.User = require('./user')(sequelize, Sequelize);
db.Todo = require('./todo')(sequelize, Sequelize);
// One User has Many Todos.
// Users 테이블의 id를 Todos 테이블의 owner 칼럼에 넣는다.
db.User.hasMany(db.Todo, {
foreignKey: 'owner',
sourceKey: 'id',
});
// One Todo belongs to One User.
// Users 테이블의 id를 Todos 테이블의 owner 칼럼에 넣는다.
db.Todo.belongsTo(db.User, {
foreignKey: 'owner',
targetKey: 'id',
});
module.exports = db;$ sequelize db:create
Sequelize CLI [Node: 10.14.1, CLI: 5.4.0, ORM: 4.42.0]
Loaded configuration file "config\config.json".
Using environment "development".
sequelize deprecated String based operators are now deprecated.
Please use Symbol based operators for better security,
read more at http://docs.sequelizejs.com/manual/tutorial/querying.html#operators
node_modules\sequelize\lib\sequelize.js:242:13
Database database_development created.$ npm start
> my-express@0.0.0 start C:\...\my-express
> node ./bin/www
sequelize deprecated String based operators are now deprecated.
Please use Symbol based operators for better security,
read more at http://docs.sequelizejs.com/manual/tutorial/querying.html#operators
node_modules\sequelize\lib\sequelize.js:242:13
Server listening on port 3000
Executing (default):
CREATE TABLE IF NOT EXISTS `users` (
`id` INTEGER NOT NULL auto_increment ,
`name` VARCHAR(20) NOT NULL UNIQUE,
`age` INTEGER UNSIGNED NOT NULL,
`married` TINYINT(1) NOT NULL,
`comment` TEXT,
`created_at` DATETIME NOT NULL DEFAULT now(),
PRIMARY KEY (`id`)) ENGINE=InnoDB;
Executing (default):
SHOW INDEX FROM `users` FROM `database_development`
Executing (default):
CREATE TABLE IF NOT EXISTS `todos` (
`id` INTEGER NOT NULL auto_increment ,
`task` VARCHAR(100) NOT NULL,
`done` TINYINT(1) NOT NULL DEFAULT false,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
`deleted_at` DATETIME,
`owner` INTEGER,
PRIMARY KEY (`id`),
FOREIGN KEY (`owner`) REFERENCES `users` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB;
Executing (default):
SHOW INDEX FROM `todos` FROM `database_development`IF NOT EXISTS 옵션을 사용하므로 없는 경우에만 테이블을 생성합니다.
routes\users.js
var express = require('express');
var router = express.Router();
const { User, Todo } = require('../models');
// ---------------------------------------
// TEST
// TRY: http://localhost:3000/users/flash
router.get('/flash', function(req, res) {
req.session.message = 'Session Message';
req.flash('message', 'Flash Message');
res.redirect('/users/flash/result');
});
router.get('/flash/result', function(req, res) {
res.send(`req.session.message=${req.session.message}, req.flash('message')=${req.flash('message')}`);
});
// ---------------------------------------
// req.params : Path Variable(URL Segment)값을 갖고 있다.
// req.body : Body 영역에 할당되어 전달되는 값을 갖고 있다.
// req.query : 물음표(?) 뒤에 설정되는 Search(Query Params) 값을 갖고 있다.
router.get('/', function(req, res, next) {
User.findAll({}).then((users) => {
// 반환결과는 다음과 같다.
// [
// {
// "id": 1,
// "name": "john",
// "age": 24,
// "married": false,
// "comment": "programmer",
// "created_at": "2019-01-25T18:10:32.000Z"
// },
// {
// "id": 2,
// "name": "tom",
// "age": 32,
// "married": true,
// "comment": "actor",
// "created_at": "2019-01-25T18:10:32.000Z"
// }
// ]
res.json(users);
}).catch((err) => {
console.error(err);
next(err);
});
});
router.get('/:id', function(req, res, next) {
User.findOne({
where: {
id: req.params.id
}
}).then((user) => {
// 반환결과는 다음과 같다.
// {
// "id": 1,
// "name": "john",
// "age": 24,
// "married": false,
// "comment": "programmer",
// "created_at": "2019-01-25T18:10:32.000Z"
// }
res.json(user);
}).catch((err) => {
console.error(err);
next(err);
});
});
router.post('/', function(req, res, next) {
// 예상쿼리는 다음과 같다.
// INSERT INTO users(name, age, married, comment) VALUES('john', 24, 0, 'programmer');
User.create({
name: req.body.name,
age: req.body.age,
married: req.body.married,
comment: req.body.comment,
}).then((user) => {
console.log(user);
// 반환결과는 다음과 같다.
// {
// created_at: '2019-01-26T08:53:56.491Z',
// id: 7,
// name: '11',
// age: 22,
// married: false,
// comment: '33'
// }
res.json(user);
}).catch((err) => {
console.error(err);
next(err);
})
// 실제로 사용된 쿼리는 다음과 같다.
// INSERT INTO `users` (`id`,`name`,`age`,`married`,`comment`,`created_at`)
// VALUES (DEFAULT,'11',22,false,'33',now());
});
router.put('/:id', function(req, res, next) {
User.update({
name: req.body.name,
age: req.body.age,
married: req.body.married,
comment: req.body.comment,
}, {
where: {
id: req.params.id,
}
}).then((affectedRowsArray) => {
console.log(affectedRowsArray);
// [ 1 ]
res.json(affectedRowsArray);
}).catch((err) => {
console.error(err);
next(err);
});
});
router.delete('/:id', function(req, res, next) {
User.destroy({
where: {
id: req.params.id
}
}).then((affectedRows) => {
console.log(affectedRows);
// 1
res.json(affectedRows);
}).catch((err) => {
console.error(err);
next(err);
})
});
module.exports = router;routes\todos.js
var express = require('express');
var router = express.Router();
const { User, Todo, Sequelize: { Op, where, fn, col } } = require('../models');
// 주목: fn('date', col('created_at')) 함수로 처리하면 아래 둘다 가능하다.
// http://localhost:3000/todos?created_at=20190126
// http://localhost:3000/todos?created_at=2019-01-26
router.get('/', async function(req, res, next) {
const filter = {};
if (req.query.created_at) {
console.log(req.query.created_at);
filter.created_at = where(fn('date', col('created_at')), '=', req.query.created_at);
}
// 명시하지 않으면 [Op.and] 연산이 적용된다. and 연산일 때는 굳이 아래처럼 할 필요가 없다.
// const filter = {
// [Op.and]: [
// {
// id: req.params.id,
// },
// ]
// };
//
// if (req.query.created_at) {
// filter[Op.and].push(where(fn('date', col('created_at')), '=', req.query.created_at));
// }
try {
const todos = await Todo.findAll({
where: filter
});
console.log(todos);
// 반환결과는 다음과 같다.
// [
// {
// id: 1,
// task: "to learn node 1",
// done: false,
// created_at: "2019-01-26T12:16:18.000Z",
// updated_at: "2019-01-26T12:16:18.000Z",
// deleted_at: null,
// owner: 1
// },
// ...
// {
// id: 11,
// task: "to learn node 11",
// done: false,
// created_at: "2019-01-26T12:16:18.000Z",
// updated_at: "2019-01-26T12:16:18.000Z",
// deleted_at: null,
// owner: 1
// }
// ]
res.json(todos);
} catch (e) {
console.error(err);
next(err);
}
// 실제로 사용된 쿼리는 다음과 같다.
// SELECT
// `id`, `task`, `done`, `created_at`, `updated_at`, `deleted_at`, `owner`
// FROM `todos` AS `todo`
// WHERE (
// (`todo`.`deleted_at` > '2019-01-26 12:23:12' OR `todo`.`deleted_at` IS NULL)
// AND
// date(`created_at`) = '20190126'
// );
});
router.get('/:id', async function(req, res, next) {
try {
const todo = await Todo.findOne({
where: {
id: req.params.id,
},
});
console.log(todo);
// 반환결과는 다음과 같다.
// {
// "id": 1,
// "task": "go shpping",
// "done": true,
// "created_at": "2019-01-25T17:52:51.000Z",
// "updated_at": "2019-01-25T17:52:51.000Z",
// "deleted_at": null,
// "owner": 1
// }
res.json(todo);
} catch (e) {
console.error(err);
next(err);
}
// 실제로 사용된 쿼리는 다음과 같다.
// SELECT
// `id`, `task`, `done`, `created_at`, `updated_at`, `deleted_at`, `owner`
// FROM `todos` AS `todo`
// WHERE (
// (`todo`.`deleted_at` > '2019-01-26 12:16:25' OR `todo`.`deleted_at` IS NULL)
// AND
// `todo`.`id` = '1'
// );
});
router.post('/', async function(req, res, next) {
// 예상쿼리는 다음과 같다.
// INSERT INTO todos(owner, task, done) VALUES(1, 'go shpping', 1);
try {
const todo = await Todo.create({
owner: req.body.owner,
task: req.body.task,
done: req.body.done,
});
console.log(todo);
// 반환결과는 다음과 같다.
// {
// "id": 3,
// "owner": 1,
// "task": "to study",
// "done": false,
// "updated_at": "2019-01-26T07:54:47.651Z",
// "created_at": "2019-01-26T07:54:47.651Z"
// }
res.json(todo);
} catch (e) {
console.error(err);
next(err);
}
// 실제로 사용된 쿼리는 다음과 같다.
// INSERT INTO `todos` (`id`,`task`,`done`,`created_at`,`updated_at`,`owner`)
// VALUES (DEFAULT,'to study',false,'2019-01-26 07:54:47','2019-01-26 07:54:47',1);
});
router.put('/:id', async function(req, res, next) {
try {
const affectedRowsArray = await Todo.update({
owner: req.body.owner,
task: req.body.task,
done: req.body.done,
}, {
where: {
id: req.params.id,
}
});
console.log(affectedRowsArray);
// 반환결과는 다음과 같다.
// [ 1 ]
res.json(affectedRowsArray);
} catch (e) {
console.error(err);
next(err);
}
// 주목: updated_at 칼럼의 정보는 자동으로 갱신된다.
});
router.delete('/:id', async function(req, res, next) {
try {
const affectedRows = await Todo.destroy({
where: {
id: req.params.id,
}
});
console.log(affectedRows);
// 반환결과는 다음과 같다.
// 1
res.json(affectedRows);
} catch (e) {
console.error(err);
next(err);
}
// 주목: 로우가 삭제되는 대신 deleted_at 칼럼에 삭제시각 정보가 입력된다.
});
module.exports = router;routes\users.todos.js
var express = require('express');
var router = express.Router();
const { User, Todo, Sequelize: { Op, where, fn, col } } = require('../models');
function seekOffset(page, size) {
if (page > 0) {
return (page - 1) * size;
}
return 0;
}
// http://localhost:3000/deep/users?page=1&size=3
router.get('/', function(req, res, next) {
let page = 1;
let size = 10;
if (req.query.page) {
page = +req.query.page;
}
if (req.query.size) {
size = +req.query.size;
}
const limit = size;
const offset = seekOffset(page, size);
User.findAll({
limit,
offset,
include: {
model: Todo,
},
}).then((users) => {
// 반환결과는 다음과 같다.
// [
// {
// id: 1,
// name: "chris",
// age: 20,
// married: false,
// comment: "programmer",
// created_at: "2019-01-26T10:34:13.000Z",
// todos: [
// {
// id: 1,
// task: "to learn node",
// done: false,
// created_at: "2019-01-26T10:34:13.000Z",
// updated_at: "2019-01-26T10:34:13.000Z",
// deleted_at: null,
// owner: 1
// }
// ...
// ]
// }
// ...
// ]
res.json(users);
}).catch((err) => {
console.error(err);
next(err);
});
// 실제로 사용된 쿼리는 다음과 같다.
// SELECT
// `user`.`id`, `user`.`name`, `user`.`age`,
// `user`.`married`, `user`.`comment`, `user`.`created_at`,
// `todos`.`id` AS `todos.id`, `todos`.`task` AS `todos.task`,
// `todos`.`done` AS `todos.done`, `todos`.`created_at` AS `todos.created_at`,
// `todos`.`updated_at` AS `todos.updated_at`, `todos`.`deleted_at` AS `todos.deleted_at`,
// `todos`.`owner` AS `todos.owner`
// FROM `users` AS `user` LEFT OUTER JOIN `todos` AS `todos`
// ON `user`.`id` = `todos`.`owner`
// AND (`todos`.`deleted_at` > '2019-01-26 10:34:28' OR `todos`.`deleted_at` IS NULL);
// limit, offset을 추가로 설정하면 실제로 사용된 쿼리는 다음과 같다.
// SELECT
// `user`.*,
// `todos`.`id` AS `todos.id`, `todos`.`task` AS `todos.task`,
// `todos`.`done` AS `todos.done`, `todos`.`created_at` AS `todos.created_at`,
// `todos`.`updated_at` AS `todos.updated_at`, `todos`.`deleted_at` AS `todos.deleted_at`,
// `todos`.`owner` AS `todos.owner`
// FROM (
// SELECT `user`.`id`, `user`.`name`, `user`.`age`, `user`.`married`, `user`.`comment`, `user`.`created_at`
// FROM `users` AS `user` LIMIT 0, 3
// ) AS `user` LEFT OUTER JOIN `todos` AS `todos`
// ON `user`.`id` = `todos`.`owner`
// AND (`todos`.`deleted_at` > '2019-01-26 12:44:12' OR `todos`.`deleted_at` IS NULL);
});
// http://localhost:3000/deep/users/1?page=1&size=3&sort=id,desc
// http://localhost:3000/deep/users/1?page=1&size=3&sort=id,asc
router.get('/:id', function(req, res, next) {
let page = 1;
let size = 10;
if (req.query.page) {
page = +req.query.page;
}
if (req.query.size) {
size = +req.query.size;
}
const limit = size;
const offset = seekOffset(page, size);
const order = [];
if (req.query.sort) {
order.push(req.query.sort.split(','));
console.log(order);
// [ [ 'id', 'asc' ] ]
}
User.findOne({
where: {
id: req.params.id
},
include: {
model: Todo,
limit,
offset,
order,
},
}).then((user) => {
// 반환결과는 다음과 같다.
// {
// id: 1,
// name: "chris",
// age: 20,
// married: false,
// comment: "programmer",
// created_at: "2019-01-26T10:34:13.000Z",
// todos: [
// {
// id: 1,
// task: "to learn node",
// done: false,
// created_at: "2019-01-26T10:34:13.000Z",
// updated_at: "2019-01-26T10:34:13.000Z",
// deleted_at: null,
// owner: 1
// }
// ]
// }
res.json(user);
}).catch((err) => {
console.error(err);
next(err);
});
// 실제로 사용된 쿼리는 다음과 같다.
// SELECT
// `user`.`id`, `user`.`name`, `user`.`age`,
// `user`.`married`, `user`.`comment`, `user`.`created_at`,
// `todos`.`id` AS `todos.id`, `todos`.`task` AS `todos.task`,
// `todos`.`done` AS `todos.done`, `todos`.`created_at` AS `todos.created_at`,
// `todos`.`updated_at` AS `todos.updated_at`, `todos`.`deleted_at` AS `todos.deleted_at`,
// `todos`.`owner` AS `todos.owner`
// FROM `users` AS `user` LEFT OUTER JOIN `todos` AS `todos`
// ON `user`.`id` = `todos`.`owner`
// AND (`todos`.`deleted_at` > '2019-01-26 10:41:21' OR `todos`.`deleted_at` IS NULL)
// WHERE `user`.`id` = '1';
// limit, offset을 추가로 설정하면 실제로 사용된 쿼리는 다음과 같다.
// SELECT
// `user`.`id`, `user`.`name`, `user`.`age`, `user`.`married`, `user`.`comment`, `user`.`created_at`
// FROM `users` AS `user`
// WHERE `user`.`id` = '1';
//
// SELECT
// `id`, `task`, `done`, `created_at`, `updated_at`, `deleted_at`, `owner`
// FROM `todos` AS `todo`
// WHERE (
// (`todo`.`deleted_at` > '2019-01-28 00:40:24' OR `todo`.`deleted_at` IS NULL)
// AND
// (`todo`.`owner` IN (1) AND (`todo`.`deleted_at` > '2019-01-28 00:40:24' OR `todo`.`deleted_at` IS NULL))
// )
// ORDER BY `todo`.`id` ASC
// LIMIT 0, 3;
});
module.exports = router;클라이언트에게 데이터와 더불어서 페이징 처리를 위한 부가정보를 구해서 전달하는 방법을 살펴봅니다.
routes\pager.users.js
const { Router } = require('express');
const { User, Todo, Sequelize: { Op, where, fn, col }, sequelize } = require('../models');
const url = require('url');
const router = Router();
function seekOffset(page, size) {
if (page > 0) {
return (page - 1) * size;
}
return 0;
}
class Query {
constructor(page, size, bsize) {
this.page = page;
this.size = size;
this.bsize = bsize;
}
}
class Path {
constructor(active, link, query, text) {
this.active = active;
this.link = link;
this.query = query;
this.text = text;
}
}
class Pager {
constructor(page=1, size=10, bsize=5, rows=0, url){
if (!url) {
throw Error('url is required');
}
this.currentPage; // 현재 페이지(사용자가 보고싶은 페이지)
this.elementsPerPage; // 페이지당 표시하는 로우의 개수
this.pagesPerBlock; // 하단에 표시하는 페이징 넘버의 개수
this.totalElements; // 테이블이 갖고 있는 모든 로우의 개수
this.totalPages; // 전체 페이지 수
this.totalBlocks; // 전체 블럭 수
this.currentBlock; // 현재 블럭
this.currentBlockStartPage; // 현재 블럭의 시작 페이지 번호
this.currentBlockEndPage; // 현재 블럭의 끝 페이지 번호
this.beforePage; // 이전 페이지
this.nextPage; // 다음 페이지
this.paths = []; // Path's Array for Pagination
this.currentPage = page;
if (this.currentPage <= 0) {
this.currentPage = 1;
}
this.elementsPerPage = size;
if (this.elementsPerPage <= 0) {
this.elementsPerPage = 1;
}
this.pagesPerBlock = bsize;
if (this.pagesPerBlock <= 0) {
this.pagesPerBlock = 1;
}
this.totalElements = rows;
this.totalPages = Math.ceil(this.totalElements / this.elementsPerPage);
this.totalBlocks = Math.ceil(this.totalPages / this.pagesPerBlock);
this.currentBlock = Math.ceil(this.currentPage / this.pagesPerBlock);
this.currentBlockEndPage = this.currentBlock * this.pagesPerBlock;
this.currentBlockStartPage = this.currentBlockEndPage - this.pagesPerBlock + 1;
/*
* For Simple Paging with 2 Buttons (Before, Next)
*/
this.beforePage = this.currentPage - 1;
if (this.beforePage < 0) {
this.beforePage = 0;
}
this.nextPage = this.currentPage + 1;
if (this.nextPage > this.totalPages) {
this.nextPage = 0;
}
this.proceedPath(url);
}
proceedPath(url) {
if (this.totalElements > 0) { // 로우가 있을 때
if (this.currentBlockStartPage > this.pagesPerBlock) {
// Home 버튼
this.addPath(new Path('', url,
new Query(1, this.elementsPerPage, this.pagesPerBlock), 'Home'));
// 이전 블럭
this.addPath(new Path('', url,
new Query(this.currentBlockStartPage-1, this.elementsPerPage, this.pagesPerBlock), '<<'));
}
// Paging 버튼
for (let pno = this.currentBlockStartPage; pno <= this.currentBlockEndPage; pno++) {
if (pno == this.currentPage) {
this.addPath(new Path('active', url,
new Query(pno, this.elementsPerPage, this.pagesPerBlock), pno.toString(10)));
} else {
if (pno <= this.totalPages) {
this.addPath(new Path('', url,
new Query(pno, this.elementsPerPage, this.pagesPerBlock), pno.toString(10)));
}
}
}
if (this.currentBlockEndPage < this.totalPages) {
// 이후 블럭
this.addPath(new Path('', url,
new Query(this.currentBlockEndPage+1, this.elementsPerPage, this.pagesPerBlock), '>>'));
// Last 버튼
this.addPath(new Path('', url,
new Query(this.totalPages, this.elementsPerPage, this.pagesPerBlock), 'Last'));
}
} else { // 로우가 없을 때
this.addPath(new Path('', url,
new Query(1, this.elementsPerPage, this.pagesPerBlock), '1'));
}
}
addPath(path) {
this.paths.push(path);
}
}
// http://localhost:3000/pager/users?page=1&size=3&bsize=2
router.get('/pager/users', function(req, res, next) {
let page = 1;
let size = 10;
let bsize = 5;
if (req.query.page) {
page = +req.query.page;
}
if (req.query.size) {
size = +req.query.size;
}
if (req.query.bsize) {
bsize = +req.query.bsize;
}
const limit = size;
const offset = seekOffset(page, size);
// query() 함수를 이용하여 로우 쿼리를 사용할 수 있다.
// 주목: { type: sequelize.QueryTypes.SELECT} 옵션을 설정하지 않으면
// 메타정보를 위한 쿼리가 추가로 수행된다.
sequelize.query('SELECT COUNT(*) as count FROM users', { type: sequelize.QueryTypes.SELECT})
.then((result) => {
console.log(url.parse(req.url));
// 주목: pathname 값은 router.get() 함수에 설정한 문자열을 가져온다.
// {
// ...
// search: '?page=1&size=3&bsize=2',
// query: 'page=1&size=3&bsize=2',
// pathname: '/pager/users',
// path: '/pager/users?page=1&size=3&bsize=2',
// href: '/pager/users?page=1&size=3&bsize=2'
// }
console.log('result: ', result);
// 주목: 결과가 배열에 담겨있다.
// result: [ { count: 11 } ]
const rows = result[0].count;
const pager = new Pager(page, size, bsize, rows, url.parse(req.url).pathname);
console.log(pager);
// {
// paths: [
// {
// active: "active",
// link: "/pager/users",
// query: {
// page: 1,
// size: 3,
// bsize: 2
// },
// text: "1"
// },
// {
// active: "",
// link: "/pager/users",
// query: {
// page: 2,
// size: 3,
// bsize: 2
// },
// text: "2"
// },
// {
// active: "",
// link: "/pager/users",
// query: {
// page: 3,
// size: 3,
// bsize: 2
// },
// text: ">>"
// },
// {
// active: "",
// link: "/pager/users",
// query: {
// page: 4,
// size: 3,
// bsize: 2
// },
// text: "Last"
// }
// ],
// currentPage: 1,
// elementsPerPage: 3,
// pagesPerBlock: 2,
// totalElements: 11,
// totalPages: 4,
// totalBlocks: 2,
// currentBlock: 1,
// currentBlockEndPage: 2,
// currentBlockStartPage: 1,
// beforePage: 0,
// nextPage: 2
// }
return pager;
}).then((pager) => {
// 주목: Promise 체이닝을 위해서 return 해야 한다.
return User.findAll({
limit,
offset,
}).then((users) => {
// 모두 정상이라면 여기서 작업이 완료된다.
res.json({users, pager});
});
}).catch((err) => {
console.error(err);
next(err);
});
});
module.exports = router;app.js
var pagerUsersRouter = require('./routes/pager.users');
app.use('/', pagerUsersRouter);다음 주소로 접속하여 결과를 확인합니다.
http://localhost:3000/pager/users?page=1&size=3&bsize=2
{
"users": [
{
"id": 1,
"name": "oliver",
"age": 20,
"married": false,
"comment": "programmer",
"created_at": "2019-01-28T03:38:14.000Z"
},
{
"id": 2,
"name": "jack",
"age": 21,
"married": false,
"comment": "designer",
"created_at": "2019-01-28T03:38:14.000Z"
},
{
"id": 3,
"name": "harry",
"age": 30,
"married": true,
"comment": "programmer",
"created_at": "2019-01-28T03:38:14.000Z"
}
],
"pager": {
"paths": [
{
"active": "active",
"link": "/pager/users",
"query": {
"page": 1,
"size": 3,
"bsize": 2
},
"text": "1"
},
{
"active": "",
"link": "/pager/users",
"query": {
"page": 2,
"size": 3,
"bsize": 2
},
"text": "2"
},
{
"active": "",
"link": "/pager/users",
"query": {
"page": 3,
"size": 3,
"bsize": 2
},
"text": ">>"
},
{
"active": "",
"link": "/pager/users",
"query": {
"page": 4,
"size": 3,
"bsize": 2
},
"text": "Last"
}
],
"currentPage": 1,
"elementsPerPage": 3,
"pagesPerBlock": 2,
"totalElements": 11,
"totalPages": 4,
"totalBlocks": 2,
"currentBlock": 1,
"currentBlockEndPage": 2,
"currentBlockStartPage": 1,
"beforePage": 0,
"nextPage": 2
}
}콘텐츠 배포자의 권리를 보호하기 위해서 도메인이 다르면 브라우저가 자바스크립트 코드의 요청을 거부할 수 있습니다. 자세한 사항은 다음 사이트를 참고하세요.
https://developer.mozilla.org/ko/docs/Web/HTTP/Access_control_CORS
$ npm i corsroutes\index.js
var express = require('express');
var router = express.Router();
const cors = require('cors');
router.use(cors());모든 URL은 index.js 파일의 router로 전달되므로 간단하게 router.use(cors())라는 코드로 CORS를 적용할 수 있습니다. 아무러 설정정보 없이 사용하므로 모든 요청에 대해서 응답하는 것이 됩니다. 요청자를 필터링하기 위해서는 추가적으로 설정을 해야한다는 뜻 입니다.
다음 사이트를 참고하세요.
https://www.npmjs.com/package/cors
앞서서 작업한 Node Express 프로젝트를 기동시켜 놓은 상태에서 추가적으로 앵귤러 프로젝트를 작업하겠습니다.
$ ng new my-ng-pagination
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS$ cd my-ng-pagination
$ npm i jquery popper.js bootstrap
$ npm i @types/jquery --save-dev
$ npm i font-awesomeangular.json
"styles": [
"./node_modules/font-awesome/css/font-awesome.css",
"./node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.scss"
],
"scripts": [
"./node_modules/jquery/dist/jquery.slim.min.js",
"./node_modules/popper.js/dist/umd/popper.min.js",
"./node_modules/bootstrap/dist/js/bootstrap.min.js"
]src\styles.scss
body,
html {
width: 100%;
height: 100%;
}
.container {
margin-top: 2em;
}$ ng g c layout/header
$ ng g c page/home
$ ng g s http/userHttp
$ ng g c page/usersrc\app\app.component.html
<app-header></app-header>
<div class="container">
<router-outlet></router-outlet>
</div>src\app\app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './page/home/home.component';
import { UserComponent } from './page/user/user.component';
const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{ path: 'user', component: UserComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }src\app\layout\header\header.component.html
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" routerLink="/">Angular Love</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/home">
Home <span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/user">User</a>
</li>
</ul>
</div>
</nav>$ npm i axiossrc\app\http\user-http.service.ts
import { Injectable } from '@angular/core';
import axios from 'axios';
export class User {
id: number;
name: string;
age: number;
married: boolean;
comment: string;
created_at: string;
}
@Injectable({
providedIn: 'root'
})
export class UserHttpService {
URL: string = 'http://localhost:3000/pager/users';
constructor() { }
findAll(page, size, bsize) {
return axios.get(this.URL + '?page=' + page + '&size=' + size + '&bsize=' + bsize)
.then(function(response) {
// console.log(response);
return response.data;
});
}
findOne(id: number) {
return axios.get(this.URL + '/' + id)
.then(function(response) {
// console.log(response);
return response.data;
});
}
addOne(user: User) {
return axios.post(this.URL, user)
.then(function(response) {
// console.log(response);
return response.data;
});
}
updateOne(user: User) {
return axios.put(this.URL + '/' + user.id, user)
.then(function(response) {
// console.log(response);
return response.data;
});
}
deleteOne(id: number) {
return axios.delete(this.URL + '/' + id)
.then(function(response) {
// console.log(response);
return true;
});
}
}src\app\page\user\user.component.ts
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { UserHttpService, User } from 'src/app/http/user-http.service';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.scss']
})
export class UserComponent implements OnInit {
users: User[];
errorMessage = undefined;
paths: Array<{ active, link, query, text }>;
selectedRow: number = 3;
rows: number[] = [3, 5, 10, 20, 30];
constructor(
private userHttpService: UserHttpService,
private route: ActivatedRoute,
private router: Router) { }
ngOnInit() {
console.log(this.router.url.split('?')[0])
/**
* 초기값
*/
let page = 1;
let size = this.selectedRow;
let bsize = 2;
if (this.route.snapshot.queryParams.page) {
page = +this.route.snapshot.queryParams.page;
}
if (this.route.snapshot.queryParamMap.get('size')) {
size = +this.route.snapshot.queryParamMap.get('size');
}
if (this.route.snapshot.queryParamMap.get('bsize')) {
bsize = +this.route.snapshot.queryParamMap.get('bsize');
}
this.findAll(page, size, bsize);
}
findAll(page, size, bsize) {
this.userHttpService.findAll(page, size, bsize)
.then(data => {
this.users = data.users;
this.paths = data.pager.paths;
console.log(this.paths);
}).catch(error => {
console.log(error);
this.errorMessage = error;
});
}
changePage(query) {
if (this.selectedRow !== query.size) {
query.size = this.selectedRow;
}
this.findAll(query.page, query.size, query.bsize);
return false;
}
changeSize() {
const path = this.paths.find((item) => item.active ? true : false);
this.findAll(path.query.page, this.selectedRow, path.query.bsize);
return false;
}
}src\app\page\user\user.component.html
<div class="container">
<div class="alert alert-danger" role="alert" *ngIf="errorMessage">
{{errorMessage}}
</div>
<h3>Users</h3>
<div class="row my-2">
<div class="col-2 ml-auto">
<select class="custom-select custom-select-sm" [(ngModel)]="selectedRow" (change)="changeSize()">
<option *ngFor="let row of rows" [value]="row">{{row}}</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead class="thead-dark">
<tr>
<th>ID</th>
<th>Name</th>
<th>Age</th>
<th>Married</th>
<th>Comment</th>
<th>Created</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of users; let i=index">
<td>{{user.id}}</td>
<td>{{user.name}}</td>
<td>{{user.age}}</td>
<td>{{user.married}}</td>
<td>{{user.comment}}</td>
<td>{{user.created_at | ymd}}</td>
</tr>
</tbody>
</table>
</div>
<div class="row">
<div class="col-10">
<ul class="pagination">
<li class="page-item" *ngFor="let path of paths" [ngClass]="path.active">
<a class="page-link" (click)="changePage(path.query)">{{path.text}}</a>
</li>
</ul>
</div>
<div class="col-2">
<span class="float-right">
<button type="button" class="btn btn-primary">Write</button>
</span>
</div>
</div>
</div>
<!-- <pre>{{paths | json}}</pre> -->$ ng g p pipe/ymdsrc\app\pipe\ymd.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'ymd'
})
export class YmdPipe implements PipeTransform {
transform(date: string, delim?: string): string {
if (!delim) {
delim = '.';
}
let ymd = date.substring(0, 10);
ymd = ymd.replace(/-/g, '');
if (ymd.length !== 8) {
return date;
}
return ymd.substring(0, 4) + delim + ymd.substring(4, 6) + delim + ymd.substring(6);
}
}