본문 바로가기

끝판왕 도전기

끝판왕의 프로젝트_Node.js CRUD 실습(5)_상품 등록, 조회, 수정, 삭제

미들웨어를 구축하여 로그인이 유지된 상태에서 상품 등록, 조회, 수정, 삭제 즉 CRUD 기능을 구현해야했다. 

 

위 내용이 무슨 의미인고 하니

기존(로그인 및 미들웨어 구축 전)에는 상품 등록 시 비밀번호를 함께 입력하게끔하여 추후 수정이나 삭제 시 입력했던 비밀번호가 필요했다.

하지만 로그인 및 미들웨어를 구축함으로써 비밀번호 입력이 필요없이 본인이 작성한 글을 수정하거나 삭제할 수 있다 !

 

이번 단계에서는 기존 작성한 과제 코드를 활용하지만 몽고DB가 아닌 Sequelize를 사용하기 때문에 몇 가지 수정사항이 있는점을 유의하자!

#1. 상품 등록

* 상품 등록 시 필요한 사항
1) 입력이 필요한 내용 : productName, contents, (userId)
2) 유효성 검사
- 입력되는 데이터 형식이 올바르지 않을 경우 (400)
- 데이터가 모두 입력되지 않았을 경우 (400)

 

앞서 구축한 미들웨어를 사용하기 위해 라우터의 경로(path)뒤에 만들어 준 미들웨어를 추가하였다. 

// 구축한 미들웨어 사용하기
import authMiddleware from '../middlwares/need-signin.middlware.js';

req.post("/products", authMiddleware, async(req,res) => {...};

 

미들웨어에서 성공적으로 인증이 된 사용자의 정보를 res.locals.user에 담았는데, 여기에서 userId를 가져오도록 한다. 

나머지 정보는 body에서 입력되는 정보를 가져온다.

const {id} = res.locals.user;
const {productName, contents} = req.body;

 

유효성 검사는 아래와 같다. 

// 데이터 형식이 바르지 않을 경우
if (!req.body) { return res.status(400).json({ errorMessage: " 데이터 형식이 올바르지 않습니다. " }) };

// 데이터가 모두 입력되지 않았을 경우
if (!productName || !contents) {
  return res.status(400).json({ errorMessage: "데이터를 모두 입력해주세요." });
};

응답 코드는 모두 400을 던지고, 각 경우에 맞게 에러 메시지를 전달하고 있다. 

 

유효성 검사를 모두 통과할 경우 create 메서드를 통해 MySQL에 저장하게 된다.

// 상품 생성 (생성 시 userId는 res.locals.user의 id값을 가져온다.)
await Products.create({ userId: id, productName, contents })

res.status(201).json({ message: "상품등록에 성공하였습니다." });

 

#2. 상품 조회

* 상품 조회 시 필요사항
1) 상품 조회 시 생성 날짜를 기준으로 최신순 (DESC / 내림차순) 
2) query String 에서 정렬값이 ASC일 경우 오름차순 / DESC일 경우 내림차순
-> 대소문자 구분 없음

 

조회 시에 정렬값이 필요하기 때문에 sortValue라는 변수에 query string에서 받아온 값을 할당하고, sort라는 변수를 선언하여 조건에 맞게 다시 할당하였다. 

const sortValue = req.query.sort; // query string에서 정렬값 받아오기
let sort

// 정렬값 결정하기
if (sortValue && (sortValue.toUpperCase() === 'ASC' || sortValue.toUpperCase() === 'DESC')) { // sortValue가 있고, 정렬값이 정해진 경우
  sort = sortValue.toUpperCase(); // 정해진 정렬값을 사용하고 
} else { // 아니면 내림차순(최신순)으로 정렬한다.
  sort = 'DESC';
};

 

조회하는 메서드는 findAll 을 활용하였고, attributes 로 보여줄 요소들을 정리하였다. 

또한, include를 통해 Users 모델에 있는 userName 값을 가져올 수 있었다. (JOIN 가능 !)

추가로 order를 통해 생성일(createdAt)을 기준으로 앞서 정의한 정렬값으로 데이터를 불러오기가 가능했다. 

const products = await Products.findAll({
  attributes: { exclude: ["updatedAt"] },
  include: [{
    model: Users,
    attributes: ["userName"]
  }],
  order: [['createdAt', sort]]
});
res.status(200).json([{ products }]);

 

 

#3. 상품 상세 조회하기

* 상세 조회 시 필요한 사항
1) 각 상품의 id 값을 req.params에서 가져와 사용한다.
2) findOne 메서드를 사용하여 상품 검색 시 where을 통해 id 값이 일치하는 상품을 찾는다.
3) 유효성 검사
- 상품을 찾을 수 없을 경우 (404)

 

특정 상품을 조회 시 req.params에 id값을 받아 일치하는 상품을 보여주고자 한다.

 

이 때는 findOne 메서드를 활용했는데 받아온 productId와 Products 모델 내 id값이 일치하는 상품을 찾는다 !

const { productId } = req.params;

const detailProduct = await Products.findOne({
  where: { id: productId },
  include: [{
    model: Users,
    attributes: ["userName"]
  }]
});

마찬가지로 include를 활용하여 Users 모델에서 userName을 가지고 온다. 

 

하지만 해당하는 상품을 찾지 못할 경우 !

if (!detailProduct) {
  return res.status(404).json({ message: "상품을 찾을 수 없습니다." });
};

위와 같이 유효성 검사를 실시한다. 

 

#4. 상품 수정하기

* 상품 수정 시 필요사항
1) 내가 작성한 상품만 수정이 가능하다.
2) 수정은 productName, contents, status 를 수정할 수 있다.
3) 유효성 검사
- 본인이 작성한 상품이 아닐 경우 (403)
- 기존 작성한 상품이 없을 경우 (404)
- status 가 지정된 FOR_SALE, SOLD_OUT이 아닐 경우 (400)

 

특정한 상품을 수정해야하기에 productId를 req.params에서 받아온다.

추가로 ! 내가 등록한 상품만 수정이 가능해야하기에 res.locals.user에서 id값을 userId라는 변수로 받는다. 

req.body에서는 수정할 내용들을 입력 받는다.

const { productId } = req.params;
const { id: userId } = res.locals.user; // 로컬 유저에 들어있는 id 키값을 userId 변수로 받는다.
const { productName, contents, status } = req.body;

const existsProduct = await Products.findOne({ where: { id: productId } });

수정할 상품을 찾을 때에는 findOne 메서드를 사용하여 productId값을 비교하여 existsProduct 변수에 할당한다. 

 

이후 유효성 검사를 실시하게 되는데 여기서 한가지 애먹은게 있었다. 

우선 아래 2가지 경우(수정하고자 하는 상품이 존재하지 않을 경우, 기존 작성한 상품이 본인이 작성한게 아닐 경우)는 큰 문제 없이 처리가 가능했다. 

    // 수정할 상품이 존재하지 않을 경우 에러메시지 변환
    if (!existsProduct) {
      return res.status(404).json({ errorMessage: "상품을 찾을 수 없습니다." });
    };

    // 기존 작성한 상품이 본인 것이 아닐 경우
    if (existsProduct.userId !== userId) {
      return res.status(403).json({ errorMessage: "상품을 수정할 권한이 없습니다." });
    };

 

Products 모델에서 enum으로 status값이 설정한 2개 (FOR_SALE, SOLD_OUT) 외의 값이 들어갈 경우 validate를 설정하여 활용해보고자 했다.

문제점 
status 값에 대한 에러 핸들링 시 모델에서 설정한 유효성 검사를 활용하고자 할 때 원하는 응답코드 전송이 어려웠음

해결
앞서 JWT토큰 만료시간에 대한 에러 핸들링을 기억하는가? 
해당 내용도 비슷하게 처리가 가능할 듯 싶어 catch 부분에서 error.name으로 조건문을 걸어 원하는 응답코드와 설정한 메시지를 송출했다. 

하지만 모델에서 다른 유효성 검사 요소도 넣은 경우에도 SequelizeValidationError로 뜨는 것 같아 각 에러마다 원하는 메세지 송출하는 방법은 조금 더 연구해봐야 한다...ㅠㅠ 

그래서 임시적으로 앞선 2개의 경우는 try문에서 에러가 발생하는 경우를 직접 조건문에 삽입하여 구현했고, status관련 에러만 우선 catch에서 잡고 있다. 

 

유효성 검사를 모두 통과할 경우 update라는 메서드를 사용하여 상품을 수정할 수 있다. 

import { Op } from 'sequelize';

// 상품 수정하기
await Products.update(
  { productName, contents, status },
  {
    where: {
      [Op.and]: [{ id: productId }, { userId }]
    }
  }
 );
res.status(200).json({ message: '상품 수정이 완료되었습니다.' });

여기서는 OP라는 녀석은 option의 줄임말로 보이는데, 뒤에 and를 사용하여 두 개의 조건을 모두 만족할 경우를 두는 것으로 보인다 !

#4. 상품 삭제

* 상품 삭제 시 요구 사항
1) 내가 작성한 상품만 삭제가 가능하다.
2) 유효성 검사
- 상품을 찾을 수 없을 경우 (404)
- 내가 작성하지 않은 상품을 삭제할 경우 (403)

상품 수정과 마찬가지로 req.params에서 productId를 가져오고 res.locals.user에서 id를 가져와 userId에 할당한다. 

    const { productId } = req.params;
    const { id: userId } = res.locals.user; // 로컬 유저에 들어있는 id 키값을 userId 변수로 받는다.

    const existsProduct = await Products.findOne({ where: { id: productId } });

 

이후에 유효성 검사를 실행하는데 아래와 같다. 

    // 해당 상품을 찾을 수 없을 경우
    if (!existsProduct) {
      return res.status(404).json({ message: "상품 조회에 실패하였습니다." })
    }

    // 기존 작성한 상품이 본인 것이 아닐 경우
    if (existsProduct.userId !== userId) {
      return res.status(403).json({ errorMessage: "상품을 삭제할 권한이 없습니다." });
    };

 

유효성 검사를 모두 통과할 경우 destroy라는 메서드를 가지고 삭제할 수 있다.

await Products.destroy({
  where: {
    [Op.and]: [{ id: productId }, { userId }]
  }
});
res.status(200).json({ message: "상품이 삭제되었습니다." })

삭제 시에도 마찬가지로 OP.and를 사용하여 두 가지 경우가 모두 만족되었을 때 상품이 삭제가 될 수 있도록 하고 있다.

 

이제 과제에서 요구하는 모든 기능들이 구현되었다. 

하다보며 느낀 것은 라우터에서는 특정 코드들이 반복되고 있어 몇 번 더 진행해보면 이해가 확실히 빠를 것 같다. 

 

추가로 모델에서 유효성 검사를 해버리는 것을 공부하고 있는데

가능하다면 관련 내용도 정리하도록 하겠다. 

'끝판왕 도전기' 카테고리의 다른 글

끝판왕의 프로젝트_Node.js 펫시터 프로젝트  (0) 2023.12.13