[9주차 - DAY2] jwt 확인과 pagination
장바구니 조회
장바구니는 로그인이 필요한 기능이다. 따라서 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);
});
}
개별 도서 조회
사용자의 좋아요 여부를 판별해야 하기 때문에 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은 두 가지가 있다.
- SELECT COUNT(*) FROM books;
- 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()로 얻을 수 있다.
- 전체 도서를 조회할 때 사용자가 몇 번 페이지를 클릭했는지와 전체 페이지가 얼마인지 프론트 엔드는 알 수 없다. 이런 경우에 백엔드에서 프론트엔드로 데이터를 전달해줘야 하며, 프론트 입장에서 어떤 데이터가 필요한가에 대해 생각해 보는 것이 중요하다는 것을 깨달았다.