코드스테이츠 12주차 / Cmarket Database

support·2022년 2월 17일
1
post-thumbnail

들어가기 전에 역대급 어려운 스프린트인 만큼 글이 길다...🙈🙉🙊

이번 스프린트 쇼핑몰 애플리케이션 Cmarket은 배웠던 MVC 패턴에 맞게 만들어진 앱이다
각각의 패턴이 무슨 역할을 하는지를 생각하면서 스프린트를 진행하면 더 쉽게 풀어 나갈 수 있다
그리고 구현한 Cmarket이 계속적인 데이터를 가질 수 있게 배열, 객체의 인메모리 형태가 아닌 데이터 베이스를 사용, 구축해주면 된다!

이번 스프린트에서 풀어야 하는 문제
1. 데이터베이스에서 아이템 가져오기
--> 얘는 작성이 되어있어서 보고 예제로 사용하면 된다
2. 데이터베이스에 주문내역 저장하기
3. 데이터베이스에서 주문내역 가져오기

✅ 간단한 앱 구동 방식 요약

  1. 클라이언트에서 주문내역(orderlist) 페이지에 접속하면 밑의 코드가 실행된다
  useEffect(() => {
    dispatch(
      fetchData(`http://localhost:4000/users/${userId}/orders`, setOrders)
    );
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  1. http://localhost:4000/users/${userId}/orders이렇게 요청이 들어온다면 서버로 요청이 넘어간다

  2. server > app.js 로 넘어가고 app.js에서 밑의 코드를 통해 일단 /요청은 routes로 분기를 시켜준다

const indexRouter = require('./routes');
app.use('/', indexRouter);
  1. server > routers > index.js 로 먼저 분기가 되고
    여기 안에서 users로 다시 분기된다

  2. user.js에서 이 응답들은 해당 컨트롤러에서 응답 처리 하면 된다고 알려주고 있다

const controller = require('./../controllers');
router.get('/:userId/orders',controller.orders.get);
router.post('/:userId/orders',controller.orders.post);
  1. 컨트롤러로 가서 orders에 get을 찾아서 해당하는 함수를 실행한다

1. 초기설정

1) mysql 세팅 (스키마, 시드 파일 확인)

# mysql 접속 후 cmarket 데이터베이스 생성
mysql> create database cmarket;

미리 구성되어 있는 Cmarket 스키마를 기반으로 MySQL에 cmarket 데이터베이스의 테이블을 생성
다음 명령어로 cmarket 데이터베이스에 테이블을 생성

# CLI(터미널) 환경에서 (mysql 접속 종료 한 뒤) 레포지토리에 진입하여 커맨드를 입력
# cmarket 데이터베이스에 구성되어 있는 스키마 복사, -D는 데이터베이스 지정 옵션
mysql -u root -p < server/schema.sql -Dcmarket;
# 복사된 테이블에 데이터 복사
mysql -u root -p < server/seed.sql -Dcmarket;

2) .env파일 설정

.env.example파일 -> .env파일로 이름 변경 / mysql 비밀번호 입력

2. server

여기서 중요한 것
npm test 입력 시 자동으로 서버가 실행되고 테스트가 진행됩니다.
라고 유어클래스에서 미리 알려주고 있다
서버를 끄고 테스트를 돌려야 한다

1) users router 파일이 존재해야 합니다 라는 테스트 케이스는 어떤 것을 원하는지 제대로 알 수 없다
이럴때는 테스트 케이스(server/spec/server-spec.js)를 살펴보자

  it('users router 파일이 존재해야 합니다', () => {
      let hasUsersRouter = fs.existsSync('./routes/users.js');
      expect(hasUsersRouter).to.be.true;
    });

routes폴더 안에 user.js 파일을 하나 생성해주면 되는 간단한 문제다

2 GET /users는 orders controller의 get 메소드를 실행합니다
3 POST /users는 orders controller의 post 메소드를 실행합니다

server -> app.js
서버에서 클라이언트에 요청이 들어왔다고 가정하면 밑의 코드에 걸려서
indexRouter로 이동한다

const indexRouter = require('./routes');
// routes 라는 폴더를 지정했지만 기본으로 그 안의 index.js 폴더로 이동하게 된다 
app.use('/', indexRouter);

indexRouter는 routers -> index.js로 이동하게 되고
여기서 각자 라우터로 연결 해주면 된다
users에 대한 라우터가 없기 때문에 연결을 해주고
새로 생성한 users.js에는 아무런 내용이 없기 때문에 items.js를 참고해서 작성 해주면 된다

routers -> index.js


const express = require('express');
const router = express.Router();
const itemsRouter = require('./items');
const usersRouter = require('./users');


// TODO: Endpoint에 따라 적절한 Router로 연결해야 합니다.
router.use('/items', itemsRouter);
router.use('/users', usersRouter);

module.exports = router;

routers -> users.js


const router = require('express').Router();
const controller = require('./../controllers');

// POST /users/1/orders 
// GET /users/1/orders
// 테스트 케이스를 보면 users엔드포인트 뒤에 들어오는 형식이
// 있기 때문에 똑같이 맞춰줘야 컨트롤러로 분기 할 수 있다
router.get('/:userId/orders', controller.orders.get);
router.post('/:userId/orders', controller.orders.post);

module.exports = router;

이렇게 작성을 해 준뒤 실행 후 응답이 와야 테스트 케이스가 통과하기 때문에 일단 컨트롤러에서 실행후 임시 응답을 보내주자

server -> controllers -> index.js
res.end()를 작성해서 응답을 임시로 보내주게 되면 테스트 케이스가 통과한다

const models = require('../models');

module.exports = {
  items: {
    get: (req, res) => {
      models.items.get((error, result) => {
        if (error) {
          res.status(500).send('Internal Server Error');
        } else {
          res.status(200).json(result);
        }
      });
    },
  },
  orders: {
    get: (req, res) => {
      const userId = req.params.userId;
      // TODO: 요청에 따른 적절한 응답을 돌려주는 컨트롤러를 작성하세요.
      res.end()
    },
    post: (req, res) => {
      const userId = req.params.userId;
      const { orders, totalPrice } = req.body;
      // TODO: 요청에 따른 적절한 응답을 돌려주는 컨트롤러를 작성하세요.
      res.end()
    },
  },
};

1) 데이터베이스에 주문내역 저장하기

1) POST /users/:userId/orders 요청에서 클라이언트가 잘못된 요청을 했을 경우 상태코드 400을 보내야합니다.
2) POST /users/:userId/orders 요청에 성공했을 경우 상태코드 201을 보내야합니다.

spec 폴더의 테스트케이스를 보면 클라이언트가 잘못된 요청을 했을 경우를 post의 바디에 해당하는 데이터를 전부 다 보내지 않고 totalPrice만 전송을 했을 때를 말하고 있다

   it('POST /users/:userId/orders 요청에서 클라이언트가 잘못된 요청을 했을 경우 상태코드 400을 보내야합니다.', (done) => {
      chai
        .request(app)
        .post('/users/1/orders')
        .send({ totalPrice: 79800 })
        .end((err, res) => {
          expect(res).to.have.status(400);
          done();
        });
    });

    it('POST /users/:userId/orders 요청에 성공했을 경우 상태코드 201을 보내야합니다.', (done) => {
      chai
        .request(app)
        .post('/users/1/orders')
        .send({
          orders: [
            { itemId: 1, quantity: 2 },
            { itemId: 2, quantity: 5 },
          ],
          totalPrice: 79800,
        })
        .end((err, res) => {
          expect(res).to.have.status(201);
          done();
        });
    });
  });

server -> controllers -> index.js

post를 먼저 작성 해보자
orders 가 없거나 totalPrice가 없는 경우에는 클라이언트 잘못이기 때문에 400을 보내주고
나머지 경우에는 데이터 베이스에 주문 내역을 저장해야 한다
모델함수를 실행시켜주고
서버에서 데이터 베이스에 요청을 보낼 때는 어떤 응답이 올 지 모르기 때문에 비동기로 처리해야 한다

server -> models -> index.js에 가면 해당 요청의 결과를 받고 callback을 통해서 보내주고 있기 때문에 콜백을 여기서 정의해주면 된다

const models = require('../models');

module.exports = {
    post: (req, res) => {
      const userId = req.params.userId;
      const { orders, totalPrice } = req.body;
      if(!orders || !totalPrice){
        return res.status(400).send('Bad Request');
      }else{
        models.orders.post(userId, orders, totalPrice, (err, result) =>{
          if(err){
            res.status(500).send('Internal Server Error');
          } else {
            res.status(201).send('Order has been placed.');
          }
        })
        res.status(201);
      }
      res.end()
    },
  },
};

그리고 모델에서 해당 유저의 주문 요청을 데이터베이스에 생성, 저장하는 쿼리를 작성해주면 된다

post요청이 들어왔을때 주문요청을 데이터베이스에 저장하려면 어떤
테이블에 레코드를 저장해야 하는지 생각 해봐야 한다
orders 는 주문결과 , order_items는 장바구니다

파라미터를 보면 post: (userId, orders, totalPrice, callback) 유저아이디, 주문결과, 총금액을 받아오고 있으므로 데이터베이스에 각각 추가해줘야 한다
그리고 위의 테스트 케이스를 보면 아래 코드처럼 적혀있으므로
orders.itemId, orders.quantity 로 쿼리를 보내야 한다는 것을 알 수 있다

orders: [
            { itemId: 1, quantity: 2 },
            { itemId: 2, quantity: 5 },
          ]

데이터베이스를 확인하면서 생각해보자

  1. orders 테이블에 레코드를 저장
    orders -> user_id, total_price를 추가
    orders의 id(pk)를 받아오기
  2. order_items 테이블에 레코드를 저장
    order_items -> order_id, item_id, order_quantity 를 추가
    orders의 id(pk)를 order_items의 order_id(FK)로 넣어주기
    까지 해야 연결이 완성되고 이 순서로 쿼리 함수를 작성하면 된다

server -> models -> index.js

const db = require('../db');

module.exports = {
    post: (userId, orders, totalPrice, callback) => {
      const queryString = `INSERT INTO orders (user_id, total_price) VALUES (?, ?)`;
      const params = [userId, totalPrice];
      // 데이터 베이스에 쿼리를 보내서 저장해야 하기 때문에 콜백함수를 사용해서 비동기로 작성 한다 
      db.query(queryString, params, (err,result) =>{
        if(err){
          return callback(err);
        }
        // 여기서 방금 넣어준 레코드의 id를 가져오기 위해 
        // result를 콘솔에 찍어보자 ! 
        if(result){
          console.log('result',result)
        }
        const orderId = result.insertId;
        // 유저가 주문하는 갯수가 지정되어 있지 않기 때문에 한번의 쿼리로 여러개의 레코드를 생성 할 때 ? 하나를 이용한다 
        const queryString = `INSERT INTO order_items (order_id, item_id, order_quantity) VALUES ?`;
        //const params = [[orderId, itemId, order_quantity], [], []] 
        const params = [orders.map((order) => {
          return [orderId, order.itemId, order.quantity];
        })];
        db.query(queryString, params, (err, result) => {
          if(err){
            return callback(err)
          }
          return callback(null, result);
        })
      })
    }
  },
};

이 쿼리를 작성 할 때 유어클래스의 힌트를 참고해야 한다

Insert Into가 리턴하는 객체를 통해서 id(pk)값을 받아 올 수 있다
위의 링크로 가면 insert Into를 사용했을때 리턴하는 객체에 대해서 정의해 주고 있고 여기서 insertId를 사용하면 id 값을 받아 올 수 있다

if(result){
  console.log('result',result)
}
--> 결과 
/*result OkPacket {
  fieldCount: 0,
  affectedRows: 1,
  insertId: 2,
  serverStatus: 2,
  warningCount: 0,
  message: '',
  protocol41: true,
  changedRows: 0
}*/

유저가 주문하는 갯수가 지정되어 있지 않기 때문에 한번의 쿼리로 여러개의 레코드를 생성 할 때 ? 하나를 이용한다
(하나만 살 수도 있고 여러개를 살 수 있기 때문에 요청들어온 아이템 내역을 하나하나 insert into를 실행해도 되겠지만 배열로 묶은 벌크 단위로 데이터를 보내서 한번의 쿼리로 실행할 수 있도록 하면 더 좋다)

const db = require('../db');

module.exports = {
  orders: {
    post: (userId, orders, totalPrice, callback) => {
      const queryString = `INSERT INTO orders (user_id, total_price) VALUES (?, ?)`;
      const params = [userId, totalPrice];
      db.query(queryString, params, (err,result) =>{
        if(err){
          return callback(err);
        }
        console.log('orders =====', orders);
        
        const orderId = result.insertId;
        // 유저가 주문하는 갯수가 저장되어 있지 않기 때문에 한번의 쿼리로 여러개의 레코드를 생성 할 때 ? 하나를 이용한다 
        const queryString = `INSERT INTO order_items (order_id, item_id, order_quantity) VALUES ?`;
        //const params = [[orderId, itemId, order_quantity], [], []] 
        const params = [orders.map((order) => {
          const result = [orderId, order.itemId, order.quantity];
          console.log('order =====',order)
          console.log('result =====',result)
          return result
        })];
        db.query(queryString, params, (err, result) => {
          if(err){
            return callback(err)
          }
          return callback(null, result);
        })
      })
    }
  },
};

orders(주문목록)와 개별 order(주문목록)을 콘솔로 찍어보자
콘솔로 order를 찍어보게되면 객체의 형태로 들어오는데 우리는 밑의 자료의 형식과 같은 배열로 넣어줘야 하기 때문에 map을 사용해서 배열 형태로 만들어 줘야 한다 그리고 위에서 받아온 orderId도 포함되어야 하기 때문에 첫번째 인자로 넣어주면 된다

orders ===== [ { itemId: 5, quantity: 1 }, { itemId: 6, quantity: 2 } ]
order ===== { itemId: 5, quantity: 1 }
result ===== [ 2, 5, 1 ]

여기까지 작성하면 주문내역을 데이터 베이스에 저장이 되고
이제 주문내역을 가져오기만 하면 된다

2) 데이터베이스에서 주문내역 가져오기


유어클래스에서 보면 주문내역을 가져올때 위의 필드들이 필요하다고 말해주고 있다
그에 맞춰서 필요한 필드들을 써주고 합쳐주면 된다

const db = require('../db');

module.exports = {
  orders: {
    get: (userId, callback) => {
      // TODO: 해당 유저가 작성한 모든 주문을 가져오는 함수를 작성하세요
      const queryString = `SELECT orders.id, orders.created_at, orders.total_price, items.name, items.price, items.image, order_items.order_quantity FROM items
      INNER JOIN order_items ON (order_items.item_id = items.id)
      INNER JOIN orders ON (orders.id = order_items.order_id)
      WHERE (orders.user_id = ?)`;

      // 현업에서는 긴 쿼리문을 쓰는 경우가 많기 때문에 AS를 이용해서 축약해서 쓰는 연습이 필요하다 
      const queryString = `
        SELECT o.id, o.created_at, o.total_price, i.name, i.price, i.image, oi.order_quantity FROM items AS i
        INNER JOIN order_items AS oi ON (oi.item_id = i.id)
        INNER JOIN orders AS o ON (o.id = oi.order_id)
        WHERE (o.user_id = ?)`;

      db.query(queryString, [userId], (err,result) =>{
        if(err){
          return callback(err)
        } else {
          return callback(null, result)
        }
      })
    }
};

마지막으로 컨트롤러에 가서 orders의 get을 채워주게 되면 스프린트가 끝난다

const models = require('../models');

module.exports = {
  orders: {
    get: (req, res) => {
      const userId = req.params.userId;
      models.orders.get(userId, (err, result) =>{
        if(err){
          res.status(500).send('Internal Server Error');
        } else {
          res.json(result);
        }
      })
    },
};

서버를 껐다 켜도 주문 목록이 계속 유지되는 앱을 볼 수 있다 !!!!

다른 동기분들도 많이 어려우셨는지 스프린트 리뷰시간이 끝나고 질문 시간만 거의 30분이 넘었다 그만큼 이해하기 힘들었고 다섯번은 더 풀어봐야 할 것 같다

0개의 댓글