타입스크립트로 함께하는 웹 풀 사이클 개발(React, Node.js) 2기 - 9주차

이현철·2024년 4월 22일
post-thumbnail

💻 1일차 (4.22)

도서 정보 API 설계 및 구현 프로젝트/랜덤 데이터 생성 API설계 및 구현 프로젝트(13)

1. 회원 API 검토하면서 jwt 얘기

1.1 회원가입 API

1.2 로그인 API

1.3 비밀번호 초기화 요청 API

1.4 비밀번호 초기화 API

2. jwt api 테스트

토큰 발행 테스트

var jwt = require("jsonwebtoken");
const express = require("express");

const app = express();
const dotenv = require("dotenv");

dotenv.config();

app.listen(process.env.PORT);

app.get("/jwt", function (req, res) {
    var token = jwt.sign({ foo: "bar" }, process.env.PRIVATE_KEY);
    res.cookie("jwt", token, {
        httpOnly: true,
    });
    res.send("토큰 발행 완료!");
});

app.get("/jwt/decoded", function (req, res) {
    let receivedJwt = req.headers["authorization"];
    console.log("우리가 req로 전달받은 jwt : ", jwt);
    var decoded = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
    res.send(decoded);
});

쿠키는 직전에 요청으로 받았던 것을 가지고 다닌다.

토큰 발행

토큰 decode
위에 토큰 발행할 떄 나온 token을 가지고 header에 Authorization값으로 넘겨주면 된다.

3. 좋아요 추가 API에 jwt 구현

const addLike = (req, res) => {
    const { id } = req.params; //book_id

    let receivedJwt = req.headers["authorization"];
    console.log("received JWT : ", receivedJwt);

    let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
    console.log(decodedJwt);
    let sql = `INSERT INTO likes (user_id, liked_book_id) VALUES (?, ?)`;
    let values = [decodedJwt.id, id]; //decodedJwt.id는 유저의 id이다.
    conn.query(sql, values, (err, results) => {
        if (err) {
            //서버측 에러
            console.log(err);
            return res.status(StatusCodes.BAD_REQUEST).end();
        }
        res.status(StatusCodes.OK).json(results);
    });
};

4. 좋아요 취소 API에 jwt 구현, user_id 꺼내기 모듈화

const conn = require("../mariadb"); //db모듈
const { StatusCodes } = require("http-status-codes");
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");

dotenv.config();

function ensureAuthorization(req) {
    let receivedJwt = req.headers["authorization"];
    console.log("received JWT : ", receivedJwt);

    let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
    return decodedJwt;
}

const addLike = (req, res) => {
    const book_id = req.params.id; //book_id

    let authorization = ensureAuthorization(req);
    let sql = `INSERT INTO likes (user_id, liked_book_id) VALUES (?, ?)`;
    let values = [authorization.id, book_id]; //decodedJwt.id는 유저의 id이다.
    conn.query(sql, values, (err, results) => {
        if (err) {
            //서버측 에러
            console.log(err);
            return res.status(StatusCodes.BAD_REQUEST).end();
        }
        res.status(StatusCodes.OK).json(results);
    });
};

const removeLike = (req, res) => {
    //좋아요 취소
    const book_id = req.params.id; //book_id

    let authorization = ensureAuthorization(req);

    let sql = `DELETE FROM likes WHERE user_id = ? AND liked_book_id = ?`;
    let values = [authorization.id, book_id];
    conn.query(sql, values, (err, results) => {
        if (err) {
            //서버측 에러
            console.log(err);
            return res.status(StatusCodes.BAD_REQUEST).end();
        }
        res.status(StatusCodes.OK).json(results);
    });
};

module.exports = { addLike, removeLike };

5. 장바구니 담기, 조회 API jwt 구현


// [2기] 이현철
const conn = require("../mariadb"); //db모듈
const { StatusCodes } = require("http-status-codes");
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");

dotenv.config();

function ensureAuthorization(req) {
    let receivedJwt = req.headers["authorization"];
    console.log("received JWT : ", receivedJwt);

    let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
    return decodedJwt;
}

//장바구니 담기
const addToCart = (req, res) => {
    const { book_id, quantity } = req.body;
    let authorization = ensureAuthorization(req);

    let sql = `INSERT INTO cartItems (book_id, quantity, user_id) VALUES (?, ?, ?)`;
    let values = [book_id, quantity, authorization.id];
    conn.query(sql, values, (err, results) => {
        if (err) {
            //서버측 에러
            console.log(err);
            return res.status(StatusCodes.BAD_REQUEST).end();
        }
        res.status(StatusCodes.OK).json(results);
    });
};

//장바구니 아이템 목록 조회
const getCartItem = (req, res) => {
    const { selected } = req.body; //selected는 장바구니에서 내가 선택한 도서들 배열
    let authorization = ensureAuthorization(req);

    const sql = `SELECT cartItems.id, book_id, title, summary, quantity, price  
                    FROM cartItems LEFT JOIN books 
                    ON books.id = cartItems.book_id  
                    WHERE user_id = ? AND cartItems.id IN (?)`;
    let values = [authorization.id, selected];
    conn.query(sql, values, (err, results) => {
        if (err) {
            console.log(err);
            return res.status(StatusCodes.BAD_REQUEST).end();
        }
        return res.status(StatusCodes.OK).json(results);
    });
};

//[2기] 이현철
const removeCartItem = (req, res) => {
    const cartItemId = req.params.id; //cartItemId
    let sql = `DELETE FROM cartItems WHERE id = ?`;
    conn.query(sql, cartItemId, (err, results) => {
        if (err) {
            //서버측 에러
            console.log(err);
            return res.status(StatusCodes.BAD_REQUEST).end();
        }
        return res.status(StatusCodes.OK).json(results);
    });
};

module.exports = { addToCart, getCartItem, removeCartItem };

6. jwt expired?

jwt의 유효기간이 끝나게 되면 원래는 500에러가 나게 되는데, 이 에러는 매우 치명적인 것이라 이 500에러를 낼 것이 아니라 우리가 예외처리를 하여 유효기간이 지난 토큰이 들어오게 되면 "로그인 세션이 만료되었습니다"라는 문장으로 사용자에게 알려주면 된다.

7. try catch 구문 동작 원리 제대로 파헤치기

8. throw 연산자

throw는 에러를 발생시키는 연산자이다.
js에서는 오류가 아니지만 우리가 의도하지 않은 코드일 때 에러를 발생시키기에 매우 유용하다

let string = '{ "num" : 1}';
try {
  let json = JSON.parse(string);
  
  if(!json.name){
    throw new SyntaxError("입력 값에 이름이 없습니다.");
  }
  else{
    console.log(json.name);
  }
} catch(err){
  console.log(err.name);
  console.log(err.message);
}

9. TokenExpiredError

// [2기] 이현철
const conn = require("../mariadb"); //db모듈
const { StatusCodes } = require("http-status-codes");
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");

dotenv.config();

function ensureAuthorization(req, res) {
   try {
       let receivedJwt = req.headers["authorization"];
       console.log("received JWT : ", receivedJwt);

       let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
       return decodedJwt;
   } catch (err) {
       console.log(err.name);
       console.log(err.message);
       return res.status(StatusCodes.UNAUTHORIZED).json({
           message: "로그인 세션이 만료되었습니다. 다시 로그인 하세요.",
       });
   }
}

//장바구니 담기
const addToCart = (req, res) => {
   const { book_id, quantity } = req.body;
   let authorization = ensureAuthorization(req, res);

   let sql = `INSERT INTO cartItems (book_id, quantity, user_id) VALUES (?, ?, ?)`;
   let values = [book_id, quantity, authorization.id];
   conn.query(sql, values, (err, results) => {
       if (err) {
           //서버측 에러
           console.log(err);
           return res.status(StatusCodes.BAD_REQUEST).end();
       }
       res.status(StatusCodes.OK).json(results);
   });
};

//장바구니 아이템 목록 조회
const getCartItem = (req, res) => {
   const { selected } = req.body; //selected는 장바구니에서 내가 선택한 도서들 배열
   let authorization = ensureAuthorization(req, res);

   const sql = `SELECT cartItems.id, book_id, title, summary, quantity, price  
                   FROM cartItems LEFT JOIN books 
                   ON books.id = cartItems.book_id  
                   WHERE user_id = ? AND cartItems.id IN (?)`;
   let values = [authorization.id, selected];
   conn.query(sql, values, (err, results) => {
       if (err) {
           console.log(err);
           return res.status(StatusCodes.BAD_REQUEST).end();
       }
       return res.status(StatusCodes.OK).json(results);
   });
};

//[2기] 이현철
const removeCartItem = (req, res) => {
   const cartItemId = req.params.id; //cartItemId
   let sql = `DELETE FROM cartItems WHERE id = ?`;
   conn.query(sql, cartItemId, (err, results) => {
       if (err) {
           //서버측 에러
           console.log(err);
           return res.status(StatusCodes.BAD_REQUEST).end();
       }
       return res.status(StatusCodes.OK).json(results);
   });
};

module.exports = { addToCart, getCartItem, removeCartItem };

10. res를 두번 보내면 생기는 일 - instanceOf 등장

위에 코드에서 보면 ensureAuthorization코드에서 response 한번, 그 다음 장바구니 담기 코드에서 response 한번 총 두번의 response가 발생하기 때문에 발생하는 오류가 있다.

function ensureAuthorization(req, res) {
    try {
        let receivedJwt = req.headers["authorization"];
        console.log("received JWT : ", receivedJwt);

        let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
        return decodedJwt;
    } catch (err) {
        console.log(err.name);
        console.log(err.message);
        return err;
    }
}

//장바구니 아이템 목록 조회
const getCartItem = (req, res) => {
    const { selected } = req.body; //selected는 장바구니에서 내가 선택한 도서들 배열
    let authorization = ensureAuthorization(req, res);

    if (authorization instanceof jwt.TokenExpiredError) {
        return res.status(StatusCodes.UNAUTHORIZED).json({
            message: "로그인 세션이 만료되었습니다. 다시 로그인 하세요.",
        });
    } else {
        const sql = `SELECT cartItems.id, book_id, title, summary, quantity, price  
        FROM cartItems LEFT JOIN books 
        ON books.id = cartItems.book_id  
        WHERE user_id = ? AND cartItems.id IN (?)`;
        let values = [authorization.id, selected];
        conn.query(sql, values, (err, results) => {
            if (err) {
                console.log(err);
                return res.status(StatusCodes.BAD_REQUEST).end();
            }
            return res.status(StatusCodes.OK).json(results);
        });
    }
};

그래서 이런 식으로 ensureAuthorization 코드에서는 에러를 리턴해주고 만약 authorization 변수가 TokenExpiredError이면 if문에 들어가고 아니면 else문에 들어가는 방식으로 해결하면 된다.

11. JsonWebTokenError도 컨트롤해보자!

JsonWebTokenError를 처리하는 방법은 위에 했던 것과 마찬가지고 if 문에서 else if문으로 처리해주면 된다.

// [2기] 이현철
const conn = require("../mariadb"); //db모듈
const { StatusCodes } = require("http-status-codes");
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");

dotenv.config();

function ensureAuthorization(req, res) {
    try {
        let receivedJwt = req.headers["authorization"];
        console.log("received JWT : ", receivedJwt);

        let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
        return decodedJwt;
    } catch (err) {
        console.log(err.name);
        console.log(err.message);
        return err;
    }
}

//장바구니 담기
const addToCart = (req, res) => {
    const { book_id, quantity } = req.body;
    let authorization = ensureAuthorization(req, res);

    if (authorization instanceof jwt.TokenExpiredError) {
        return res.status(StatusCodes.UNAUTHORIZED).json({
            message: "로그인 세션이 만료되었습니다. 다시 로그인 하세요.",
        });
    } else if (authorization instanceof jwt.JsonWebTokenError) {
        return res.status(StatusCodes.BAD_REQUEST).json({
            message: "잘못된 토큰입니다.",
        });
    } else {
        let sql = `INSERT INTO cartItems (book_id, quantity, user_id) VALUES (?, ?, ?)`;
        let values = [book_id, quantity, authorization.id];
        conn.query(sql, values, (err, results) => {
            if (err) {
                //서버측 에러
                console.log(err);
                return res.status(StatusCodes.BAD_REQUEST).end();
            }
            res.status(StatusCodes.OK).json(results);
        });
    }
};

//장바구니 아이템 목록 조회
const getCartItem = (req, res) => {
    const { selected } = req.body; //selected는 장바구니에서 내가 선택한 도서들 배열
    let authorization = ensureAuthorization(req, res);

    if (authorization instanceof jwt.TokenExpiredError) {
        return res.status(StatusCodes.UNAUTHORIZED).json({
            message: "로그인 세션이 만료되었습니다. 다시 로그인 하세요.",
        });
    } else if (authorization instanceof jwt.JsonWebTokenError) {
        return res.status(StatusCodes.BAD_REQUEST).json({
            message: "잘못된 토큰입니다.",
        });
    } else {
        const sql = `SELECT cartItems.id, book_id, title, summary, quantity, price  
        FROM cartItems LEFT JOIN books 
        ON books.id = cartItems.book_id  
        WHERE user_id = ? AND cartItems.id IN (?)`;
        let values = [authorization.id, selected];
        conn.query(sql, values, (err, results) => {
            if (err) {
                console.log(err);
                return res.status(StatusCodes.BAD_REQUEST).end();
            }
            return res.status(StatusCodes.OK).json(results);
        });
    }
};

//[2기] 이현철
const removeCartItem = (req, res) => {
    const cartItemId = req.params.id; //cartItemId
    let sql = `DELETE FROM cartItems WHERE id = ?`;
    conn.query(sql, cartItemId, (err, results) => {
        if (err) {
            //서버측 에러
            console.log(err);
            return res.status(StatusCodes.BAD_REQUEST).end();
        }
        return res.status(StatusCodes.OK).json(results);
    });
};

module.exports = { addToCart, getCartItem, removeCartItem };

이 과정을 Cartcontroller에서만 하는 것이 아니므로 지금까지 짯던 LikeController에도 적용을 해주었다.

const conn = require("../mariadb"); //db모듈
const { StatusCodes } = require("http-status-codes");
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");

dotenv.config();

function ensureAuthorization(req, res) {
    try {
        let receivedJwt = req.headers["authorization"];
        console.log("received JWT : ", receivedJwt);

        let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
        return decodedJwt;
    } catch (err) {
        console.log(err.name);
        console.log(err.message);
        return err;
    }
}

const addLike = (req, res) => {
    const book_id = req.params.id; //book_id

    let authorization = ensureAuthorization(req, res);

    if (authorization instanceof jwt.TokenExpiredError) {
        return res.status(StatusCodes.UNAUTHORIZED).json({
            message: "로그인 세션이 만료되었습니다. 다시 로그인 하세요.",
        });
    } else if (authorization instanceof jwt.JsonWebTokenError) {
        return res.status(StatusCodes.BAD_REQUEST).json({
            message: "잘못된 토큰입니다.",
        });
    } else {
        let sql = `INSERT INTO likes (user_id, liked_book_id) VALUES (?, ?)`;
        let values = [authorization.id, book_id]; //decodedJwt.id는 유저의 id이다.
        conn.query(sql, values, (err, results) => {
            if (err) {
                //서버측 에러
                console.log(err);
                return res.status(StatusCodes.BAD_REQUEST).end();
            }
            res.status(StatusCodes.OK).json(results);
        });
    }
};

const removeLike = (req, res) => {
    //좋아요 취소
    const book_id = req.params.id; //book_id

    let authorization = ensureAuthorization(req, res);

    if (authorization instanceof jwt.TokenExpiredError) {
        return res.status(StatusCodes.UNAUTHORIZED).json({
            message: "로그인 세션이 만료되었습니다. 다시 로그인 하세요.",
        });
    } else if (authorization instanceof jwt.JsonWebTokenError) {
        return res.status(StatusCodes.BAD_REQUEST).json({
            message: "잘못된 토큰입니다.",
        });
    } else {
        let sql = `DELETE FROM likes WHERE user_id = ? AND liked_book_id = ?`;
        let values = [authorization.id, book_id];
        conn.query(sql, values, (err, results) => {
            if (err) {
                //서버측 에러
                console.log(err);
                return res.status(StatusCodes.BAD_REQUEST).end();
            }
            res.status(StatusCodes.OK).json(results);
        });
    }
};

module.exports = { addLike, removeLike };

💻 2일차 (4.23)

도서 정보 API 설계 및 구현 프로젝트/랜덤 데이터 생성 API설계 및 구현 프로젝트(14)

1. 회원 인증 모듈화

우리가 지금까지 작성한 코드를 보면 인증하는 함수를 모든 controller마다 모두 작성해주고 있었다. 이것을 빼서 모듈화 시키는 작업을 해야한다.

  • auth.js
const dotenv = require("dotenv");
const jwt = require("jsonwebtoken");
dotenv.config();

const ensureAuthorization = (req, res) => {
    try {
        let receivedJwt = req.headers["authorization"];
        console.log("received JWT : ", receivedJwt);

        let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
        return decodedJwt;
    } catch (err) {
        console.log(err.name);
        console.log(err.message);
        return err;
    }
};

module.exports = ensureAuthorization;

위 코드처럼 새로운 파일을 작성해서 export 해주고 사용해야하는 controller에서 require해서 불러오고 난 후에 사용하면 된다.

const conn = require("../mariadb"); //db모듈
const { StatusCodes } = require("http-status-codes");
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");
const ensureAuthorization = require("../auth"); //인증 모듈

dotenv.config();

// function ensureAuthorization(req, res) {
//     try {
//         let receivedJwt = req.headers["authorization"];
//         console.log("received JWT : ", receivedJwt);

//         let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
//         return decodedJwt;
//     } catch (err) {
//         console.log(err.name);
//         console.log(err.message);
//         return err;
//     }
// }

const addLike = (req, res) => {
    const book_id = req.params.id; //book_id

    let authorization = ensureAuthorization(req, res);

    if (authorization instanceof jwt.TokenExpiredError) {
        return res.status(StatusCodes.UNAUTHORIZED).json({
            message: "로그인 세션이 만료되었습니다. 다시 로그인 하세요.",
        });
    } else if (authorization instanceof jwt.JsonWebTokenError) {
        return res.status(StatusCodes.BAD_REQUEST).json({
            message: "잘못된 토큰입니다.",
        });
    } else {
        let sql = `INSERT INTO likes (user_id, liked_book_id) VALUES (?, ?)`;
        let values = [authorization.id, book_id]; //decodedJwt.id는 유저의 id이다.
        conn.query(sql, values, (err, results) => {
            if (err) {
                //서버측 에러
                console.log(err);
                return res.status(StatusCodes.BAD_REQUEST).end();
            }
            res.status(StatusCodes.OK).json(results);
        });
    }
};

const removeLike = (req, res) => {
    //좋아요 취소
    const book_id = req.params.id; //book_id

    let authorization = ensureAuthorization(req, res);

    if (authorization instanceof jwt.TokenExpiredError) {
        return res.status(StatusCodes.UNAUTHORIZED).json({
            message: "로그인 세션이 만료되었습니다. 다시 로그인 하세요.",
        });
    } else if (authorization instanceof jwt.JsonWebTokenError) {
        return res.status(StatusCodes.BAD_REQUEST).json({
            message: "잘못된 토큰입니다.",
        });
    } else {
        let sql = `DELETE FROM likes WHERE user_id = ? AND liked_book_id = ?`;
        let values = [authorization.id, book_id];
        conn.query(sql, values, (err, results) => {
            if (err) {
                //서버측 에러
                console.log(err);
                return res.status(StatusCodes.BAD_REQUEST).end();
            }
            res.status(StatusCodes.OK).json(results);
        });
    }
};

module.exports = { addLike, removeLike };

2. 내 장바구니 조회

우리가 기존에 작성했던 장바구니 조회 코드를 보면 조회하고 싶은 상품을 selected로 넘겨주는 방식을 사용하고 있었는데.. 이게 말이 안된다. 내 장바구니에 들은 상품이 뭔지 조회를 하는 API인데 selected를 넘겨준다는 것은 이미 내가 어떤 것을 담았는지 모두 알고있다는 것이다. 우리가 전에 selected를 사용한 이유는 장바구니에서 체크한 도서들만 가지고 주문하기 페이지로 넘어갔을 때 내가 어떤 도서를 선택했는지를 넘겨주려고 작성했던 것이다. 지금부터는 이 부분을 수정하려고 한다.

CartControlle.js에 있는 getCartItem 코드

//장바구니 아이템 목록 조회
const getCartItem = (req, res) => {
    const { selected } = req.body; //selected는 장바구니에서 내가 선택한 도서들 배열
    let authorization = ensureAuthorization(req, res);

    if (authorization instanceof jwt.TokenExpiredError) {
        return res.status(StatusCodes.UNAUTHORIZED).json({
            message: "로그인 세션이 만료되었습니다. 다시 로그인 하세요.",
        });
    } else if (authorization instanceof jwt.JsonWebTokenError) {
        return res.status(StatusCodes.BAD_REQUEST).json({
            message: "잘못된 토큰입니다.",
        });
    } else {
        let sql = `SELECT cartItems.id, book_id, title, summary, quantity, price  
        FROM cartItems LEFT JOIN books 
        ON books.id = cartItems.book_id  
        WHERE user_id = ? `;

        let values = [authorization.id];
        if (selected) {
            //주문서 작성시 선택한 장바구니 조회하는 용도
            sql += `AND cartItems.id IN (?)`;
            values.push(selected);
        }

        conn.query(sql, values, (err, results) => {
            if (err) {
                console.log(err);
                return res.status(StatusCodes.BAD_REQUEST).end();
            }
            return res.status(StatusCodes.OK).json(results);
        });
    }
};

위 코드를 보면 selected객체가 들어왔을 때는 우리가 장바구니에서 체크한 도서들만 가지고 주문하기 페이지로 넘어갔을 때 사용할 수 있고, selected가 없을 때는 장바구니에 담은 모든 도서를 반환하게 작성했다.

3. 장바구니 API 설계 정리 및 코드 정리

3.1 장바구니 담기 API

3.2 장바구니 아이템 조회 API

3.3 장바구니 도서 삭제 API

4. 구매 API 회원 인증 추가

//주문하기 API
const order = async (req, res) => {
    const conn = await mariadb.createConnection({
        host: "localhost",
        user: "root",
        database: "Bookshop",
        password: "root1234",
        dateStrings: true,
    });

    let authorization = ensureAuthorization(req, res);

    if (authorization instanceof jwt.TokenExpiredError) {
        return res.status(StatusCodes.UNAUTHORIZED).json({
            message: "로그인 세션이 만료되었습니다. 다시 로그인 하세요.",
        });
    } else if (authorization instanceof jwt.JsonWebTokenError) {
        return res.status(StatusCodes.BAD_REQUEST).json({
            message: "잘못된 토큰입니다.",
        });
    } else {
        const { items, delivery, totalQuantity, totalPrice, firstBookTitle } =
            req.body;

        // delievery 테이블 삽입
        let sql = `INSERT INTO delivery (address, receiver, contact) VALUES (?, ?, ?)`;
        let values = [delivery.address, delivery.receiver, delivery.contact];
        let [results] = await conn.execute(sql, values); //반환을 배열형태로 한다.
        let delivery_id = results.insertId;

        //orders 테이블에 삽입하는 코드
        sql = `INSERT INTO orders (book_title, total_quantity, total_price, user_id, delivery_id)
    VALUES (?,?,?,?,?)`;
        values = [
            firstBookTitle,
            totalQuantity,
            totalPrice,
            authorization.id,
            delivery_id,
        ];
        // firstBookTitle : 대표책제목
        [results] = await conn.execute(sql, values); //이 코드가 끝날때까지 밑의 코드가 진행되면 안되기 때문에 await
        let order_id = results.insertId;

        //items를 가지고 장바구니에서 book_id와 quantity를 조회
        sql = `SELECT book_id, quantity FROM cartItems WHERE id IN (?)`;
        let [orderItems, fields] = await conn.query(sql, [items]);

        //orderedBook 삽입하는 코드
        sql = `INSERT INTO orderedBook (order_id, book_id, quantity) VALUES ?;`;
        //items.. 는 배열이고 이걸 하나씩 꺼내서 (forEach문을 돌려야 한다.)
        values = [];
        orderItems.forEach((item) => {
            values.push([order_id, item.book_id, item.quantity]);
        });
        results = await conn.query(sql, [values]);
        let result = await deleteCartItems(conn, items);
        return res.status(StatusCodes.OK).json(result);
    }
};

5. 개별 도서 조회 좋아요 여부에 jwt 확인

개별 도서 조회할 때 왜 jwt가 필요하냐면, 우리가 그 책을 좋아요를 눌렀는지 안눌렀는지 알기 위해 꼭 필요하다. 아래부터는 그 부분을 추가해줄 예정이다.

auth.js

const dotenv = require("dotenv");
const jwt = require("jsonwebtoken");
dotenv.config();

const ensureAuthorization = (req, res) => {
    try {
        let receivedJwt = req.headers["authorization"];
        console.log("received JWT : ", receivedJwt);

        if (receivedJwt) {
            let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
            return decodedJwt;
        } else {
            throw new ReferenceError("jwt must be provided");
        }
    } catch (err) {
        console.log(err.name);
        console.log(err.message);
        return err;
    }
};

module.exports = ensureAuthorization;

위와 같이 수정한 이유는 우리가 토큰을 보내주지 않았을 때 원래는 JsonWebTokenError가 발생하게 되는데 이 에러는 토큰이 잘못되었을때만 호출되게 하고, 로그인한 상태가 아니라서 아예 헤더로 jwt가 넘어오지 않는 경우에는 ReferenceError라는 오류를 throw하는 방식을 사용했다.

BookController.js에 있는 도서 상세 조회 코드

const bookDetail = (req, res) => {
    //로그인 상태가 아니면 => liked 빼고 보내주면 되고
    //로그인 상태라면 => ㅣiked 추가해서 보내주면 된다.
    const book_id = req.params.id;
    const authorization = ensureAuthorization(req, res);

    if (authorization instanceof jwt.TokenExpiredError) {
        return res.status(StatusCodes.UNAUTHORIZED).json({
            message: "로그인 세션이 만료되었습니다. 다시 로그인 하세요.",
        });
    } else if (authorization instanceof jwt.JsonWebTokenError) {
        return res.status(StatusCodes.BAD_REQUEST).json({
            message: "잘못된 토큰입니다.",
        });
    }

    let sql = `SELECT *, (SELECT count(*) FROM likes WHERE liked_book_id=books.id) AS likes`;
    const values = [book_id];

    if (authorization) {
        sql += `, (SELECT EXISTS (SELECT * FROM likes WHERE user_id=? AND liked_book_id=?)) AS liked`;
        values.push(authorization.id, book_id);
    }

    sql += ` FROM books 
            LEFT JOIN category ON books.category_id = category.category_id 
            WHERE books.id=?`;

    conn.query(sql, values, (err, results) => {
        if (err) {
            console.error(err);
            return res.status(StatusCodes.BAD_REQUEST).end();
        }

        if (results.length) {
            return res.status(StatusCodes.OK).json(results[0]);
        } else {
            return res.status(StatusCodes.NOT_FOUND).end();
        }
    });
};

위 코드를 보면 로그인을 한 상태여서 토큰이 잘 넘어온 경우에는 liked테이블도 같이 반환해주고 로그인을 한 상태가 아니라서 토큰이 없는 경우에는 좋아요를 눌렀는지 안눌렀는지 여부는 필요없기 때문에 liked테이블은 빼고 그냥 도서 정보만 반환해주는 방식을 사용했다.

6. 전체 도서 조회 pagination 추가

프론트엔드에서는 자기가 지금 몇페이지인지, 총 몇 페이지인지에 대한 데이터를 가지고 있지 않다.
그래서 response로 필요한 정보가 추가된다.

따라서 우리는 currentPage 값과 totalCount값이 필요한데, 이 값들을 가져오는 방식에는 아래에 나와있는 두 방식이 가장 많이 사용된다.

  • SQL_CALC_FOUND_ROWS
    SQL_CALC_FOUND_ROWS는 SELECT 쿼리에 사용할 수 있는 MySQL 힌트로 쿼리 결과의 전체 row 수를 임시로 저장하게 합니다. 여기서 중요한 점은 row 수가 반환되는 row 수가 아니라 조건에 해당하는 전체 row 수를 가진다는 것이다. 따라서 LIMIT 0, 10을 지정하여 10개의 row만 가져온다고 하더라도 LIMIT이 없을 때의 쿼리 결과와 같은 row 수를 계산한다. 이 부분이 상당히 중요한 부분이다. 나중에 다시 설명하겠지만 LIMIT에 영향을 받지 않으므로 쿼리 성능을 떨어뜨릴 수 있다.

  • FOUND_ROWS
    FOUND_ROWS는 직전 쿼리에서 검색된 결과 row 수를 반환한다.

const allBooks = (req, res) => {
    let { category_id, news, limit, currentPage } = req.query;
    let offset = limit * (currentPage - 1);
    let allBooksRes = {};
    let sql = `SELECT SQL_CALC_FOUND_ROWS *, (SELECT count(*) FROM likes WHERE books.id=liked_book_id) AS likes FROM books`; //좋아요 개수가 포함된 books테이블 조회

    let values = [];

    if (category_id && news) {
        // news가 true이면서 category_id가 있는 경우
        sql += ` WHERE category_id=? AND pub_date BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW()`; // 조건문 수정
        values = [parseInt(category_id)]; // 값 추가
    } else if (category_id) {
        // category_id만 있는 경우
        sql += ` WHERE category_id=?`;
        values = [parseInt(category_id)]; // 값 추가
    } else if (news) {
        // news만 있는 경우
        sql += ` WHERE pub_date BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW()`;
    }

    sql += ` LIMIT ? OFFSET ?`; // LIMIT과 OFFSET 추가
    values.push(parseInt(limit), parseInt(offset)); // 값 추가

    conn.query(sql, values, (err, results) => {
        // 쿼리 실행
        if (err) {
            console.log(err);
            // return res.status(StatusCodes.BAD_REQUEST).end();
        }
        console.log(results);
        if (results.length) {
            allBooksRes.books = results;
        } else {
            return res.status(StatusCodes.NOT_FOUND).end();
        }
    });

    sql = `SELECT found_rows()`; // LIMIT과 OFFSET 추가

    conn.query(sql, (err, results) => {
        // 쿼리 실행
        if (err) {
            console.log(err);
            return res.status(StatusCodes.BAD_REQUEST).end();
        }
        let pagination = {};
        pagination.currentPage = parseInt(currentPage);
        console.log(results[0]);
        pagination.totalCount = results[0]["found_rows()"];

        allBooksRes.pagination = pagination;
        return res.status(StatusCodes.OK).json(allBooksRes);
    });
};

결과 예시

💻 3일차 (4.24)

도서 정보 API 설계 및 구현 프로젝트/랜덤 데이터 생성 API설계 및 구현 프로젝트(15)

1. 랜덤데이터 프로젝트 시작 : 랜덤 데이터 API 알아보기

faker js란?
faker란 간단히 설명하잠녀 가짜 더미 데이터를 반환해주는 API이다.

faker js가 왜 필요할까?
시제품을 개발하거나, 테스트를 작성하다보면 가짜 데이터가 필요할 때가 있다. 소규모 프로젝트라면 직접 가짜 데이터를 하드코딩 할 수도 있곘지만, 프로젝트의 규모가 커지게 되면 가짜 데이터를 생성하는데 들어가는 노력이 너무 클 수도 있다.
그래서 faker와 같은 가짜 데이터를 생성해주는 라이브러리를 사용하면 이러한 가짜 데이터 생성에 들어가는 비용을 획기적으로 줄일 수 있다. 함수 호출 한 번만으로 그럴듯한 가짜 데이터를 생성할 수 있기 때문에 개발 생산성과 개발자 경험을 크게 개선할 수 있다.
뿐만 아니라 데이터의 진위 여부가 중요하지 않은 개인 프로젝트나 코딩 연습을 할 때도 이러한 라이브러리를 정말 유용하게 사용할 수 있다.

faker를 사용한 간단한 예시 (원하는 사용자 수만큼 가짜 데이터 만들어주는 API)

const { faker } = require("@faker-js/faker");
const express = require("express");

const app = express();

app.listen(6666);

app.get("/fake/users/:id", (req, res) => {
    const num = parseInt(req.params.id);
    let users = [];
    let index = 1;
    while (index <= num) {
        users.push({
            email: faker.internet.email(),
            password: faker.internet.password(),
            fullname: faker.person.fullName(),
            contact: faker.phone.number(),
        });
        index++;
    }
    res.status(200).json(users);
});
profile
이현철

0개의 댓글