1. 도서 API 수정
1.1. 로그인 유무에 따른 개별 조회 기능 구현
도서를 조회하는 기능은 로그인해야만 볼 수 있는 기능은 아님
- 로그인하고 도서 개별 조회 페이지를 보는 경우 -> 유저가 좋아요를 눌렀는지 안 눌렀는지(liked) 여부 알 수 있음
- 로그인하지 않고 도서 개별 조회 페이지를 보는 경우 -> 유저가 좋아요를 눌렀는지 안 눌렀는지(liked) 여부를 알 수 없음
=> 로그인해서 토큰이 있는 경우 liked 컬럼을 함께 주고, 로그인하지 않은 경우 liked 컬럼없이 줌
로그인을 하지 않아 아예 토큰이 없는 경우 auth 모듈에서 1차로 처리
-> receivedJwt 가 없는 경우 throw 로 ReferenceError 객체를 던짐
// auth.js
function ensureAuthorization(req, res) {
try {
let receivedJwt = req.headers["authorization"];
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;
}
};
BookController 에 ReferenceError 가 날라오면, 에러를 발생시키는 것이 아니라 liked 칼럼을 빼고 조회하도록 처리
// BookController.js
// 개별 도서 조회
const bookDetail = (req, res) => {
// 로그인 상태가 아니면 => liked 빼고 보냄
// 로그인 상태이면 => liked 추가해서
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 if (authorization instanceof ReferenceError) {
const book_id = req.params.id;
let sql = `SELECT *,
(SELECT count(*) FROM likes WHERE liked_book_id=books.id) AS likes
FROM books
LEFT JOIN category
ON books.category_id = category.category_id
WHERE books.id=?;`;
response(sql, book_id, res);
}
else {
const book_id = req.params.id;
let sql = `SELECT *,
(SELECT count(*) FROM likes WHERE liked_book_id=books.id) AS likes,
(SELECT EXISTS (SELECT * FROM likes WHERE user_id=? AND liked_book_id=?)) AS liked
FROM books
LEFT JOIN category
ON books.category_id = category.category_id
WHERE books.id=?;`;
let values = [authorization.id, book_id, book_id];
response(sql, values, res);
}
}
// 공통 부분 빼줌 (모듈화시켜줌)
function response(sql, values, res) {
conn.query(sql, values,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
if (results[0])
return res.status(StatusCodes.OK).json(results[0]);
else
return res.status(StatusCodes.NOT_FOUND).end();
}
)
}
1.2. pagination 구현
- 도서 API
Method | GET |
URI | /books?limit={page당 도서수}¤tPage={현재page} |
HTTP status code | 성공 200 |
Request Body | |
Response Body | // 전체 도서 목록에는 도서의 상세 정보 포함 { books: [ { id: 도서 id, img: 이미지 id, summary: "요약 설명", author: "도서 작가", price: 가격, likes: 좋아요 수, pubDate: "출간일" }, { id: 도서 id, img: 이미지 id, summary: "요약 설명", author: "도서 작가", price: 가격, likes: 좋아요 수, pubDate: "출간일" }, ... ], pagination: { totalPage: 총 페이지 수, currentPage: 현재 페이지, totalBooks: 총 도서 수 } } |
- SQL_CALC_FOUND_ROWS
1번과 2번 모두 페이지네이션을 구현할 때 사용되며, 출력결과가 동일하다.
SQL_CALC_FOUND_ROWS 를 사용한 2번에서는 페이지네이션된 결과를 가져오면서 동시에 전체 레코드 수를 계산하고, 두번째 쿼리에서 그 결과를 가져온다. 이 방식은 작은 테이블에서는 더 효율적일 수 있지만, 대규모 테이블에서는 성능에 상당한 영향을 미칠 수 있다. (인덱스를 사용할 수 없는 경우 테이블을 전체 스캔해야 하기 때문, 대규모 테이블이나 인덱스가 설정된 경우는 1번이 더 효율적)
(추가로 SQL_CALC_FOUND_ROWS 는 MySql 5.7 이하에서는 지원되지만, 8.0 에서는 더 이상 권장되지 않는다고 하니 참고)
# 1
SELECT * FROM books LIMIT 4 OFFSET 0;
SELECT count(*) FROM books;
# 2
SELECT SQL_CALC_FOUND_ROWS * FROM books LIMIT 4 OFFSET 0;
SELECT found_rows();
API 명세에 따라 수정한 코드
const bookDetail = (req, res) => {
// 로그인 상태가 아니면 => liked 빼고 보냄
// 로그인 상태이면 => liked 추가해서
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 if (authorization instanceof ReferenceError) {
const book_id = req.params.id;
let sql = `SELECT *,
(SELECT count(*) FROM likes WHERE liked_book_id=books.id) AS likes
FROM books
LEFT JOIN category
ON books.category_id = category.category_id
WHERE books.id=?;`;
response(sql, book_id, res);
}
else {
const book_id = req.params.id;
let sql = `SELECT *,
(SELECT count(*) FROM likes WHERE liked_book_id=books.id) AS likes,
(SELECT EXISTS (SELECT * FROM likes WHERE user_id=? AND liked_book_id=?)) AS liked
FROM books
LEFT JOIN category
ON books.category_id = category.category_id
WHERE books.id=?;`;
let values = [authorization.id, book_id, book_id];
response(sql, values, res);
}
}
// (카테고리 별, 신간 여부) 전체 도서 목록 조회
// 전체 도서 목록에는 도서의 상세 정보를 포함합니다. 필요한 데이터만 선별하여 구현 부탁드립니다.
const allBooks = (req, res) => {
let allBooksRes = {};
const {category_id, news, limit, currentPage} = req.query;
let offset = limit * (currentPage-1);
let sql = `SELECT SQL_CALC_FOUND_ROWS *, (SELECT count(*) FROM likes WHERE liked_book_id=books.id) AS likes FROM books`;
let values = [];
if (category_id && news) {
sql += ` WHERE category_id=? AND pub_date BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW()`;
values.push(category_id);
}
else if (category_id) {
sql += ` WHERE category_id=?`;
values.push(category_id);
}
else if (news) {
sql += ` WHERE pub_date BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW()`;
}
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();
}
console.log(results);
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(currentPage);
pagination.totalCount = results[0]['found_rows()'];
allBooksRes.pagination = pagination;
return res.status(StatusCodes.OK).json(allBooksRes);
}
)
}
2. 장바구니 API 수정
장바구니 API 에서 장바구니에 담긴 아이템을 삭제하는 기능도 로그인한 상태에서 가능하다.
-> 삭제도 토큰을 받은 상태에서 진행할 수 있도록 수정
const removeCartItem = (req, res) => {
const cartItemId = req.params.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 cartItems WHERE id = ?`;
conn.query(sql, cartItemId,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
res.status(StatusCodes.OK).json(results);
}
)
}
};
3. 랜덤 데이터 API 사용해보기
랜덤 데이터를 생성해주는 API를 기반으로 가짜 사용자 정보를 생성하는 API 를 만들어보았다.
일단 랜덤 데이터를 생성해주는 API 로는 Faker js, mokaroo 가 있는데, npm 에서 이미 제공해서 사용하기 쉬운 Faker js 를 사용해서 만들어보도록 하자.
// app.js
const express = require('express');
const { faker } = require('@faker-js/faker');
const createRandomUser = require('./make-random-user');
const {StatusCodes} = require('http-status-codes');
const dotenv = require('dotenv');
dotenv.config();
const app = express();
app.listen(process.env.PORT);
const makeFakeUser = require('./make-random-user');
app.get('/fake/users/', (req, res) => {
const { count } = req.query;
let fakeUsers = [];
for (let i=0; i<count; i++) {
fakeUsers.push(createRandomUser());
}
return res.status(StatusCodes.OK).json({
users: fakeUsers
})
});
// make-random-user.js
const express = require('express');
const { faker } = require('@faker-js/faker');
function createRandomUser() {
return {
email: faker.internet.email(),
username: faker.person.fullName(),
password: faker.internet.password(),
contact: faker.phone.number({ style: 'human' })
};
}
module.exports = createRandomUser;
'TIL with Programmers' 카테고리의 다른 글
[TIL] 10/22 JS 기초2 (2) | 2024.10.22 |
---|---|
[TIL] 10/21 JS 기초 (2) | 2024.10.22 |
[TIL] 10/16 JWT - TokenExpiredError, JsonWebTokenError, authorization, ERR_HTTP_HEADERS_SENT (0) | 2024.10.16 |
[TIL] 10/15 MySQL 데이터 삭제-DELETE/DROP/TRUNCATE, 주문하기 API (0) | 2024.10.15 |
[TIL] 10/14 Node.js 비동기 처리 - Promise, async, await, then, query (0) | 2024.10.14 |