데브코스

[9주차 - DAY2] jwt 확인과 pagination

미안하다 강림이 좀 늦었다 2024. 4. 23. 14:24

 

 

장바구니 조회

장바구니는 로그인이 필요한 기능이다. 따라서 jwt를 확인하고, 해당 사용자의 장바구니에 있는 도서들을 응답으로 보내준다. jwt를 확인하는 부분은 carts.js에 미들웨어로 추가하였다.

validateToken 함수

function validateToken(req, res, next) {
    const authorization = ensureAuthorization(req);
    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 if (authorization instanceof ReferenceError) {
        return res.status(StatusCodes.UNAUTHORIZED).json({
            message: '로그인이 필요한 기능입니다.'
        });
    }

    return next();
}

carts.js

// ...
const { validateToken } = require('../jwtAuthorization');

// 장바구니 담기
router.post(
    '/',
    [validateToken, bookIdValidate, quantityValidate, validate],
    addToCart
);

// 장바구니 아이템 목록 조회
// 선택한 장바구니 상품 목록 조회
router.get(
    '/',
    [validateToken, validate],
    getCartItems
);

// 장바구니 도서 삭제
router.delete(
    '/:id',
    [validateToken, cartIdValidate, validate],
    removeCartItem
);

 

selected는 선택한 상품을 조회할 때 사용된다. selected가 있으면 로그인한 사용자의 해당 상품들을 보내주고, 없으면 로그인한 사용자의 전체 상품을 보내준다.

CartController.js

const getCartItems = (req, res) => {
    const { selected } = req.body;

    const authorization = ensureAuthorization(req);

    let sql = `SELECT cartItems.id, book_id, title, summary, quantity, price 
                    FROM cartItems LEFT JOIN books 
                    ON cartItems.book_id = books.id
                    WHERE user_id = ?`;
    const values = [authorization.id];

    if (selected && selected.length) {
        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 없이 요청을 보냈을 때
선택한 상품이 있을 경우

 

 

개별 도서 조회

사용자의 좋아요 여부를 판별해야 하기 때문에 jwt를 확인한다. 하지만 로그인하지 않아도 개별 도서는 조회할 수 있어야 하기 때문에 jwt 확인 미들웨어를 넣지 않고 아래 로직에 구현했다.

로그인된 상태라면 'liked' 컬럼에 좋아요 여부를 표시해 준다.

const bookDetail = (req, res) => {
    let bookId = parseInt(req.params.id);
    let sql = `SELECT *,
                (SELECT COUNT(*) FROM likes WHERE liked_book_id = books.id)  AS likes `;
    let values = [];

    const authorization = ensureAuthorization(req);
    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 if (authorization instanceof ReferenceError) {
        values = [bookId];
    } else {
        sql += `, (SELECT COUNT(*) FROM likes WHERE user_id = ? AND liked_book_id = ?) AS liked `;
        values = [authorization.id, bookId, bookId];
    }

    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.log(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();
            }
        })
};

 

 

주문 API

주문과 관련된 모든 기능들은 로그인이 필요하다. 따라서 모든 주문 API에 jwt를 확인하는 미들웨어를 추가한다.

validateToken이 jwt를 확인하는 미들웨어다.

// ...
const { validateToken } = require('../jwtAuthorization');

// 주문 하기
router.post(
    '/',
    [
        validateToken,
        itemsValidate, deliveryValidate,
        quantityValidate, priceValidate,
        bookTitleValidate, validate
    ],
    order
);

// 주문 목록 조회
router.get(
    '/',
    [validateToken],
    getOrders
);

// 주문 상세 조회
router.get(
    '/:id',
    [validateToken, orderIdValidate, validate],
    getOrderDetail
);

 

 

전체 도서 조회 pagination

페이지 부분을 클릭하면 백엔드 측으로 요청이 들어오기 때문에 백엔드에서는 현재 페이지가 무엇인지 알 수 있지만 프론트 엔드는 알 수 없다. 또한, 총 몇 페이지가 존재하는지를 프론트엔드가 화면에 표시해 주려면 총 도서의 수도 알고 있어야 한다. 프론트엔드는 한 페이지에 몇 개의 도서씩 보여줘야 하는지 알고 있기 때문에 총 도서 수를 프론트엔드에게 전달해주면 전체 페이지 수를 알 수 있다.

따라서 응답으로 책들, 현재 페이지 숫자, DB에 있는 총 도서의 수를 보내줘야 한다.

 

총 도서 수를 구하는 SQL은 두 가지가 있다.

  1. SELECT COUNT(*) FROM books;
  2. SELECT SQL_CALC_FOUND_ROWS * FROM books;

SQL_CALC_FOUND_ROWS는 SELECT 쿼리에 사용할 수 있는 MySQL 힌트로, 쿼리 결과의 전체 row 수를 임시로 저장할 수 있게 해 준다. 1번을 사용하면 별도의 SELECT를 두 번 실행해야 하지만, 2번을 사용하면 실질적으로는 SELECT를 한 번만 수행하면 되기 때문에 2번 방법으로 진행한다. 2번 쿼리를 실행한 후 SELECT found_rows(); 명령으로 총 개수를 구할 수 있다.

const allBooks = (req, res) => {
    let allBooksRes = {};
    let { category_id, news, limit, current_page, sort } = req.query;
    let offset = limit * (current_page - 1);

    let sql = `SELECT SQL_CALC_FOUND_ROWS all_books.*
                    FROM (SELECT *,
                            (SELECT COUNT(*) FROM likes WHERE liked_book_id = books.id) AS likes,
                            (SELECT COUNT(*) FROM orderedBook WHERE orderedBook.book_id = books.id) AS orders 
                            FROM books) all_books `;
    let values = [];

    // ...

    sql += `LIMIT ? OFFSET ?`;
    values.push(parseInt(limit), offset);
    conn.query(sql, values,
        (err, results) => {
            if (err) {
                console.log(err);
                return res.status(StatusCodes.BAD_REQUEST).end();
            }

            if (results.length) {
                allBooksRes['books'] = results;
            } else {
                return res.status(StatusCodes.NOT_FOUND).end();
            }
        });

    sql = `SELECT found_rows()`;
    conn.query(sql,
        (err, results) => {
            if (err) {
                console.log(err);
                return res.status(StatusCodes.BAD_REQUEST).end();
            }

            let pagination = {};
            pagination['currentPage'] = parseInt(current_page);
            pagination['totalCount'] = results[0]['found_rows()'];

            allBooksRes['pagination'] = pagination;

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

 

 

배운 점

  • SQL_CALC_FOUND_ROWS를 이용해 쿼리 결과의 전체 row 수를 임시로 저장할 수 있다. 해당 값은 SELECT found_rows()로 얻을 수 있다.
  • 전체 도서를 조회할 때 사용자가 몇 번 페이지를 클릭했는지와 전체 페이지가 얼마인지 프론트 엔드는 알 수 없다. 이런 경우에 백엔드에서 프론트엔드로 데이터를 전달해줘야 하며, 프론트 입장에서 어떤 데이터가 필요한가에 대해 생각해 보는 것이 중요하다는 것을 깨달았다.