어제에 이은 라우터 파일 수정부터 들어갔다.
logger 파일을 편하게 보기 위해서 JSON formatter을 이용하는 게 좋다.
라우터 파일 null 체크
// 입력값 null 체크
if (!params.name) {
const err = new Error('Not allowed null (name)');
logger.error(err.toString());
res.status(500).json({ err: err.toString() });
}
비즈니스 로직 호출
// 비즈니스 로직 호출
const result = await departmentService.reg(params);
logger.info(`(department.reg.result) ${JSON.stringify(result)}`);
logger.info(`(department.reg.result2) ${result}`);
리스트 조회 DAO
Op : sequelize에서 제공하는 Operators
리스트 조회(selectList) : 검색 조건을 받는다.
검색을 돌리기 위해 Department.findAndCountAll 사용
where라는 객체를 걸기 위해 setQuery라는 빈 객체를 만듦
검색 거는 법 : where = { name: params.name }
// 리스트 조회
selectList(params) {
// where 검색 조건
const setQuery = {};
if (params.name) {
setQuery.where = {
...setQuery.where,
name: { [Op.like]: `%${params.name}%` }, // like 검색
};
}
},
/*
setQuery에 새로운 key: value 쌍을 넣는 방식
{
...setQuery,
key: value
}
이런 식(리액트에서 이런 식으로 엄청 많이 함)
*/
...setQuery는 아래 코드와 같다.
where = {
name: { [Op.like]: `%${params.name}%` }
}
정렬 조건
order by name asc, code desc : 똑같은 name이 있다면 code 역순으로 정렬
이게 그대로 적용되는 것이기 때문에 setQuery.order = [['id', 'DESC', 'code', 'ASC;]]; 여기에도 이런식으로 조건을 여러개 걸 수도 있다.
// order by 정렬 조건
setQuery.order = [['id', 'DESC']];
return new Promise((resolve, reject) => {
Department.findAndCountAll({
...setQuery,
}).then((selectedList) => {
resolve(selectedList);
}).catch((err) => {
reject(err);
});
});
리스트 조회 Service
// selectList
async list(params) {
let result = null;
try {
result = await departmentDao.selectList(params);
logger.debug(`(departmentService.list) ${JSON.stringify(result)}`);
} catch (err) {
logger.error(`(departmentService.list) ${err.toString()}`);
return new Promise((resolve, reject) => {
reject(err);
});
}
return new Promise((resolve) => {
resolve(result);
});
},
};
리스트 조회 Routes
// 리스트 조회
router.get('/', async (req, res) => {
try {
const params = {
name: req.query.name,
};
logger.info(`(department.list.params) ${JSON.stringify(params)}`);
const result = await departmentService.list(params);
logger.info(`(department.list.result) ${JSON.stringify(result)}`);
// 최종 응답
res.status(200).json(result);
} catch (err) {
res.status(500).json({ err: err.toString() });
}
});
결과
count, row?
DB에서 제공하는 데이터 자르는 기술
limit : 몇개까지 보여줄거냐 / offset : 앞에 몇개를 건너뛸거냐
ex. 전체 데이터 : 100개 / 페이지당 10개 => <1페이지> limit = 10, offset = 0
count : 100개, rows : 10개 => 페이징 처리를 위한 데이터
상세정보 조회 DAO
// 상세정보 조회
selectInfo(params) {
return new Promise((resolve, reject) => {
Department.findByPk(
params.id,
).then((selectedInfo) => {
resolve(selectedInfo);
}).catch((err) => {
reject(err);
});
});
},
상세정보 조회 Service
// selectInfo
async info(params) {
let result = null;
try {
result = await departmentDao.selectInfo(params);
logger.debug(`(departmentService.info) ${JSON.stringify(result)}`);
} catch (err) {
logger.error(`(departmentService.info) ${err.toString()}`);
return new Promise((resolve, reject) => {
reject(err);
});
}
return new Promise((resolve) => {
resolve(result);
});
},
상세정보 조회 Routes
/:id = 주소/1일 경우 1을 id로 받겠다(값이 들어가고 키, 밸류는 안들어간다)
// 상세정보 조회
router.get('/:id', async (req, res) => {
try {
const params = {
id: req.params.id,
};
logger.info(`(department.info.params) ${JSON.stringify(params)}`);
const result = await departmentService.info(params);
logger.info(`(department.info.result) ${JSON.stringify(result)}`);
// 최종 응답
res.status(200).json(result);
} catch (err) {
res.status(500).json({ err: err.toString() });
}
});
수정 Routes
상세정보 조회와 비슷하나 put 방식으로 변경 id는 params에서 추출
// 수정
router.put('/:id', async (req, res) => {
try {
const params = {
id: req.params.id,
name: req.body.name,
code: req.body.code,
description: req.body.description,
};
logger.info(`(department.update.params) ${JSON.stringify(params)}`);
const result = await departmentService.edit(params);
logger.info(`(department.update.result) ${JSON.stringify(result)}`);
// 최종 응답
res.status(200).json(result);
} catch (err) {
res.status(500).json({ err: err.toString() });
}
});
updatedCount : 몇 개의 row가 업데이트 되었는 지
삭제 DAO
삭제된 날짜가 찍히고 삭제된 것으로 sequelize가 인식한다.
// 삭제
delete(params) {
return new Promise((resolve, reject) => {
Department.destroy({
where: { id: params.id },
}).then((deleted) => {
resolve({ deletedCount: deleted });
}).catch((err) => {
reject(err);
});
});
},
진짜 삭제하고 싶은 특수한 경우
Department.destroy({
force: true // 삭제된 날짜가 찍히는 게 아니라 진짜 삭제됨(특수한 경우 빼고 잘 안씀)
//...
})
삭제 Service
// delete
async delete(params) {
let result = null;
try {
result = await departmentDao.delete(params);
logger.debug(`(departmentService.delete) ${JSON.stringify(result)}`);
} catch (err) {
logger.error(`(departmentService.delete) ${err.toString()}`);
return new Promise((resolve, reject) => {
reject(err);
});
}
return new Promise((resolve) => {
resolve(result);
});
},
삭제 Routes
// 삭제
router.delete('/:id', async (req, res) => {
try {
const params = {
id: req.params.id,
};
logger.info(`(department.delete.params) ${JSON.stringify(params)}`);
const result = await departmentService.delete(params);
logger.info(`(department.delete.result) ${JSON.stringify(result)}`);
// 최종 응답
res.status(200).json(result);
} catch (err) {
res.status(500).json({ err: err.toString() });
}
});
models/department.js
paranoid : true => 진짜 삭제할 수 있는 deletedAt 모델 불러옴(위의 force: true를 쓸 수 있게 해줌)
{
sequelize,
// tableName: 'tableName', // table명을 수동으로 생성 함
// freezeTableName: true, // true: table명의 복수형 변환을 막음
underscored: true, // true: underscored, false: camelCase
timestamps: true, // createAt, updatedAt
paranoid: true, // deletedAt
}
DB에는 있는데 sequelize에서는 삭제 된 걸로 뜬다.
DB에서 복구하고 싶은 경우
update from departments
set delete_at = null
where id = 1;
사용자 관리
express에서 기존에 생성한 users 삭제
- app.js에서 users 관련 코드 삭제
- /routes/users.js 파일 삭제
/models/user.js 빈 파일 생성
테이블 빈 필드 생성
department.js에서 복붙해오고 return sper.init 안의 항목만 수정한다.
보고 만들어야 할 테이블에 department_id처럼 적혀 있어도 camelCase로 departmentId처럼 만들어준다.
확인은 개수를 세어서 하면 좋다.
const Sequelize = require('sequelize');
module.exports = class Department extends Sequelize.Model {
static init(sequelize) {
return super.init({
departmentId: {
},
name: {
},
userid: {
},
password: {
},
role: {
},
email: {
},
phone: {
},
updatedPwDate: {
},
}, {
sequelize,
// tableName: 'tableName', // table명을 수동으로 생성 함
// freezeTableName: true, // true: table명의 복수형 변환을 막음
underscored: true, // true: underscored, false: camelCase
timestamps: true, // createAt, updatedAt
paranoid: true, // deletedAt
});
}
};
타입 설정
return super.init({
departmentId: {
type: Sequelize.INTEGER,
},
name: {
type: Sequelize.STRING(100),
},
userid: {
type: Sequelize.STRING(255),
},
password: {
type: Sequelize.STRING(500),
},
role: {
type: Sequelize.STRING(20),
},
email: {
type: Sequelize.STRING(255),
},
phone: {
type: Sequelize.STRING(255),
},
updatedPwDate: {
type: Sequelize.DATE,
},
},
// ...
unique, allowNull 추가
unique: true
allowNull: false // null을 허용하지 않는다.
Table join - Associations
DB 차원에서 관계를 맺을 수 있다. 공식 문서 참고
HasOne : 사용자가 부서를 하나 갖고 있다.
BelongsTo : 부서가 어느 사용자에게 속해 있다.
HasMany : 필드가 1대 다수로 관계를 맺고 있다.
BelongsToMany : 여러 군데에 속해 있다.
belongTo
static associate(db) {
db.User.belongsTo(db.Department, { foreignKey: { name: 'departmentId', onDelete: 'SET NULL', as: 'Department' } });
}
=> 1대 1 관계(db.User가 db.Department에 속해있다.) : 사용자 한 명은 한 부서에만 들어갈 수 있기 때문
: 뒤에는 어떻게 맺는지 적혀있다. FK로 맺어져 있는데 name이 departmentId인 항목을 통해서 맺겠다. 만약 속해 있는 소속 부서가 사라진다면 (원래는 FK가 깨져서 삭제 불가) null로 처리하겠다(onDelete: 'SET NULL') as는 알리아스(안써주면 문제가 생김)인데 부서 하나에만 들어갈 수 있어 단수형이다.
hasMany
static associate(db) {
db.Department.hasMany(db.User, { foreignKey: { name: 'departmentId' }, onDelete: 'SET NULL', as: 'Users' });
}
1대 다수 관계 : 한 부서에 여러 사용자가 속해 있기 때문
as가 복수인 이유는 여러 사용자가 들어갈 수 있기 때문이다.
=> 두군데다 써주는 이유는 각각 써줘야 각각 찾아갈 수 있기 때문이다
모델 index 파일 설정
index 파일 설정할 때는 npm run dev를 끄고 하는게 좋다.
: 저장될때마다 테이블을 만들면서 자칫하면 FK가 없는 테이블이 만들어지는데 그럼 FK가 적용이 되지 않기 때문
index 파일의 나머지는 department와 똑같이 설정해주되
association 관계 생성 코드를 추가해준다.
// association(관계 생성)
Department.associate(db);
User.associate(db);
사용자 관리 기능 생성
테이블 조인을 확인하기 위해 등록과 리스트 조회만 생성해본다.
사용자 등록
먼저 departmentDao.js에서 해당 코드를 복사해온다. Department 부분만 User로 수정해본다.
const { Op } = require('sequelize');
const { User } = require('../models/index');
// index.js에서 가져옴
const dao = {
// 등록
insert(params) {
// 동기식으로 처리해야함
return new Promise((resolve, reject) => {
User.create(params).then((inserted) => {
resolve(inserted);
}).catch((err) => {
reject(err);
});
});
},
};
module.exports = dao;
inserted 안엔 아래처럼 데이터가 모두 들어있다.
User.create(params).then((inserted) => {
const inserted = {
id: 1,
name: 'aaa',
userid: 'kim',
password: 'asdf'
// 생략
};
resolve(inserted);
})
이때 password는 제외하고 출력해야하므로 inserteResult라는 임시 변수를 만들어서 inserted를 담아주고 password는 delete로 빼준다.
// password는 제외하고 리턴함
const insertedResult = { ...inserted };
delete insertedResult.dataValues.password;
이때 에러가 났었다.
[nodemon] restarting due to changes...
[nodemon] starting `node ./bin/www`
C:\Workspace\nodeproj\node_modules\express\lib\router\index.js:458
throw new TypeError('Router.use() requires a middleware function
))
^
TypeError: Router.use() requires a middleware function but got a Objec
at Function.use (C:\Workspace\nodeproj\node_modules\express\lib\ro
at Object.<anonymous> (C:\Workspace\nodeproj\routes\index.js:28:8)
at Module._compile (internal/modules/cjs/loader.js:1085:14)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1
at Module.load (internal/modules/cjs/loader.js:950:32)
at Function.Module._load (internal/modules/cjs/loader.js:790:12)
at Module.require (internal/modules/cjs/loader.js:974:19)
at require (internal/modules/cjs/helpers.js:93:18)
at Object.<anonymous> (C:\Workspace\nodeproj\app.js:12:21)
at Module._compile (internal/modules/cjs/loader.js:1085:14)
[nodemon] app crashed - waiting for file changes before starting...
Router.use() requires a middleware function but got a Objec...
에러 원인은 routes/user.js 파일에서 module.exports를 해주지 않았기 때문이었다.
사용자 조회 DAO
검색 조건 2개(userid, name)
attributes로 password 필드 제외
include로 join 관계 표시
// 리스트 조회
selectList(params) {
// where 검색 조건
const setQuery = {};
if (params.name) {
setQuery.where = {
...setQuery.where,
name: { [Op.like]: `%${params.name}%` }, // like검색
};
}
if (params.userid) {
setQuery.where = {
...setQuery.where,
userid: params.userid, // '=' 검색
};
}
// order by 정렬 조건
setQuery.order = [['id', 'DESC']];
return new Promise((resolve, reject) => {
User.findAndCountAll({
...setQuery,
attributes: { exclude: ['password'] }, // password 필드 제외
include: [ // join 관계 표시
{
model: Department, // models/index의 Department
as: 'Department',
// attributes: ['id', 'name'], // 특정 조건만 조회
},
],
}).then((selectedList) => {
resolve(selectedList);
}).catch((err) => {
reject(err);
});
});
},
include 덕분에 사용자가 속한 Department의 데이터도 불러온다.
Body 내용
{
"count": 1,
"rows": [
{
"id": 3,
"departmentId": 4,
"name": "bam",
"userid": "poo451",
"role": "member",
"email": "asd@aas",
"phone": "010-8888-8888",
"updatedPwDate": null,
"createdAt": "2022-01-26T06:45:37.460Z",
"updatedAt": "2022-01-26T06:45:37.460Z",
"deletedAt": null,
"Department": {
"id": 4,
"name": "song",
"code": "7654",
"description": "abcd",
"createdAt": "2022-01-26T02:30:36.945Z",
"updatedAt": "2022-01-26T02:30:36.945Z",
"deletedAt": null
}
}
]
}
logger.debug는 왜 backtick(`)으로 쓰는가?
console.log가 아니기 때문에 ''가 안되기 때문
나머지 수정, 삭제도 API 문서를 참고해 만들었다.
다만 몇가지 유의해야 할 항목이 있었다.
1. 상세 조회 시 Department를 include 하기(리스트 조회처럼)
// 상세정보 조회
selectInfo(params) {
return new Promise((resolve, reject) => {
User.findByPk(params.id, {
attributes: { exclude: ['password'] },
include: [
{
model: Department, // models/index의 Department
as: 'Department',
},
],
}).then((selectedInfo) => {
resolve(selectedInfo);
}).catch((err) => {
reject(err);
});
});
},
그냥 attributes와 include를 넣어주면 에러가 떴다. 알고 봤더니 {}안에 넣어줘야하는 거였다.
2. user 수정 시 userid, passoword는 수정하면 안된다.
따라서 아래와 같이 항목에서 빼주었다.
router.put('/:id', async (req, res) => {
try {
const params = {
id: req.params.id,
departmentId: req.body.departmentId,
name: req.body.name,
role: req.body.role,
email: req.body.email,
phone: req.body.phone,
};
이때 에러가 떴었다.
2022-01-26 16:47:01.017[error] (userService.edit) Error: WHERE parameter "id" has invalid "undefined" value
id: req.params.id, 항목을 같이 빼주는 바람에 발생한 에러였다. 다시 넣어줬다.
비밀번호 암호화
현재 DB에 암호가 평문으로 들어가서 그대로 노출되어 있다. 이건 불법이므로 암호화가 필요하다.
단방향 암호화(hash)
abc - 암호화 -> xyz
xyz - 복호화 -> abc => 복호화가 수학적으로 불가능한 상태
비밀번호 인증은
원문을 암호화한 값(xyz)를 비교하여 맞는지 다른지 알아낼 수 있다.
양방향(대칭키) 암호화
abc - 암호화(key) -> xyz
xyz - 복호화(key) -> abc
sja256으로 처리할 것이기 때문에 우선 라이브러리(crypto)를 설치해준다.
npm install crypto
hash 처리 함수 만들기
lib/hashUtil.js
틀부터 만들어준다.
const crypto = require('crypto');
const iterations = 1005; // 반복횟수(법적으로 1000번 이상 권장)
// 횟수는 바꾸면 큰일난다. 절대 바꾸면 안됨(당연함)
const hashUtil = {
};
module.exports = hashUtil;
Promise
반복 횟수가 많을수록 속도가 느리기 때문에 Promise로 return 및 async await를 걸어줘야 한다.
const hashUtil = {
// hash함수 생성
makePasswordHash(password) {
return new Promise((resolve, reject) => {
if (!password) {
reject(new Error('Not allowed null (password)'));
}
salt
똑같은 데이터를 받아도 결과는 달라야 한다.
abc - salt -> xxx
abc - salt -> yyy
이런 식으로!
그래서 salt 만드는 것도 비밀이다. (이 부분)
// 1. salt 생성
const salt = crypto.randomBytes(64).toString('base64');
저걸 못해서 깨진 함수로는 MD5가 있다.(DB에 해당 암호화된 구문을 치면 값이 그대로 나옴...더이상 hash 함수 취급 못받음)
sha256 / sha512
sha256(향후 n년) / sha512(향후 nn년 법적으로 가능)
512는 처리해야하는 사항이 많기 때문에 아직은 256을 써도 충분하다.
crypto.pbkdf2(password, salt, iterations, 64, 'sha256', (err, derivedKey) => {
위 부분의 'sha256'만 'sha512'로 바꿔주면 된다.
[Node.js] PBKDF2 비밀번호 암호화하기
1. 서론 사용자들의 비밀번호 정보를 DB에 저장할 때는 반드시 암호화가 필요하다. 그러기 위한 가장 일반적인 방식이 바로 PBKDF2 방식이다. 오늘은 이 방식이 무엇인지? 그리고 node.js에서는 어
surprisecomputer.tistory.com
결과(마지막 password 주목)
느낀 점
javascript가 코드의 자유도가 높다는 말을 어렴풋이 이해할 수 있었다.
sequelize에서 많이 쓰는 함수를 써봤는데 좀 낯설어서 열심히 봐야겠다...
'공부 > Digital Twin Bootcamp' 카테고리의 다른 글
TIL_220128_토큰 관리 / 검색 / 로그인 (0) | 2022.01.28 |
---|---|
TIL_220127_토큰 관리 (0) | 2022.01.27 |
TIL_220125_Backend_CRUD (0) | 2022.01.25 |
TIL_220124_Backend (0) | 2022.01.25 |
TIL_220121_IOT (0) | 2022.01.21 |