장바구니 조회
장바구니는 로그인이 필요한 기능이다. 따라서 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()로 얻을 수 있다.
- 전체 도서를 조회할 때 사용자가 몇 번 페이지를 클릭했는지와 전체 페이지가 얼마인지 프론트 엔드는 알 수 없다. 이런 경우에 백엔드에서 프론트엔드로 데이터를 전달해줘야 하며, 프론트 입장에서 어떤 데이터가 필요한가에 대해 생각해 보는 것이 중요하다는 것을 깨달았다.
'데브코스' 카테고리의 다른 글
[9주차 - DAY5] 프로그래밍과 C언어 (0) | 2024.04.26 |
---|---|
[9주차 - DAY3] 코드 퀄리티 & 랜덤 데이터 API (0) | 2024.04.24 |
제발 정렬 좀 시켜달라고 (0) | 2024.04.22 |
[9주차 - DAY1] jwt와 에러 처리 (0) | 2024.04.22 |
[8주차 - DAY5] 주문 API 구현 (0) | 2024.04.19 |