공부/Digital Twin Bootcamp

TIL_220126_Backend_CRUD

Ail_ 2022. 1. 26. 19:16

어제에 이은 라우터 파일 수정부터 들어갔다.

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}`);

로그 파일
DB에도 데이터 들어감

 

리스트 조회 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 삭제

  1. app.js에서 users 관련 코드 삭제
  2. /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가 복수인 이유는 여러 사용자가 들어갈 수 있기 때문이다.

 

=> 두군데다 써주는 이유는 각각 써줘야 각각 찾아갈 수 있기 때문이다

현재 department에서 property의 Foreign Keys가 아무것도 없는 걸 확인할 수 있다.

 

모델 index 파일 설정

index 파일 설정할 때는 npm run dev를 끄고 하는게 좋다.

: 저장될때마다 테이블을 만들면서 자칫하면 FK가 없는 테이블이 만들어지는데 그럼 FK가 적용이 되지 않기 때문

 

index 파일의 나머지는 department와 똑같이 설정해주되

association 관계 생성 코드를 추가해준다.

// association(관계 생성)
Department.associate(db);
User.associate(db);

생성된 테이블 및 FK키 확인
express가 users_pkey 처럼 이름도 자동으로 설정해준다.

 

사용자 관리 기능 생성

테이블 조인을 확인하기 위해 등록과 리스트 조회만 생성해본다.

 

사용자 등록

먼저 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);
      });
    });
  },
=> 시퀄라이즈의 문법에 맞춰 코딩을 하면 시퀄라이즈는 그에 따라 쿼리를 자동으로 만들어주고 실제 DB처리는 그 쿼리에 의해 이루어진다.
==> model 파일에서 정의를 내린 것은 실제 테이블의 필드로 제작이 된다.
따라서 model 파일과 실제 필드는 동일하기 때문에 DB의 필드를 저렇게 where로 조건을 걸 수 있다.

 

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'로 바꿔주면 된다.

 

PBKDF2 설명 참고

 

[Node.js] PBKDF2 비밀번호 암호화하기

1. 서론  사용자들의 비밀번호 정보를 DB에 저장할 때는 반드시 암호화가 필요하다. 그러기 위한 가장 일반적인 방식이 바로 PBKDF2 방식이다. 오늘은 이 방식이 무엇인지? 그리고 node.js에서는 어

surprisecomputer.tistory.com

결과(마지막 password 주목)

 


느낀 점

javascript가 코드의 자유도가 높다는 말을 어렴풋이 이해할 수 있었다.

sequelize에서 많이 쓰는 함수를 써봤는데 좀 낯설어서 열심히 봐야겠다...