2024.05.27 프로그래머스-북끼오 (bookkio) 좋아요 기능 구현
Bookkio 프로젝트 4일차
👍좋아요 API 구하기
likes 테이블을 위와 같이 생성하여, user_id는 users.id를 FK로 book_id는 books.id를 FK 설정하여준다.
likes 테이블의 각 컬럼은 각각 users, books 의 id에 대해 N 개를 받아들이므로
N : 1 의관계가 성립한다.
좋아요 API 설계
- 좋아요 추가 API
- Method :
POST
- URI :
/likes/{book_id}
- HTTP Status Code : 200
- Request Body
- Token > Header “Authorization”
현재 Token 을 저장해놓을 수 없으므로, Token을 저장했다는 전제하에 아래 Body 사용
1 2 3 4
{ "email" : "사용자 EMAIL", "id" : "사용자 ID" }
- Method :
- Response Body
- 좋아요 취소 API
- Method :
DELETE
- URI :
/likes/{book_id}
- HTTP Status Code : 200
- Request Body
- Response Body
- Method :
좋아요 API 코드 작성
새롭게 만드는 기능이며, URI또한 기존에 사용하던 것들과 다르므로, 새로운 Router와 Controller를 만든다.
likes.route.js
1 2 3 4 5 6 7 8 9 10 11
const express = require("express"); const router = express.Router(); const { addLike, deleteLike } = require("../controller/likes.controller.js"); router.post("/:bookId", addLike); router.delete("/:bookId", deleteLike); module.exports = router;
likes.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
/** * 좋아요 추가 API * @param {import("express").Request} req * @param {import("express").Response} res */ const addLike = (req, res) => { return res.status(200).json({ message: "좋아요 추가 API" }); }; /** * 좋아용 제거 API * @param {import("express").Request} req * @param {import("express").Response} res * @returns */ const deleteLike = (req, res) => { return res.status(200).json({ message: "좋아요 제거 API" }); }; module.exports = { addLike, deleteLike, };
app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
const createError = require("http-errors"); const express = require("express"); const path = require("path"); const cookieParser = require("cookie-parser"); const logger = require("morgan"); const usersRouter = require("./routes/users.router.js"); const booksRouter = require("./routes/books.route.js"); const cartsRouter = require("./routes/cart.route.js"); const ordersRouter = require("./routes/orders.route.js"); const likesRouter = require("./routes/likes.router.js"); const app = express(); // view engine setup app.set("views", path.join(__dirname, "views")); app.set("view engine", "ejs"); app.use(logger("dev")); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, "public"))); // Routing app.use("/users", usersRouter); app.use("/books", booksRouter); app.use("/cart", cartsRouter); app.use("/orders", ordersRouter); app.use("/likes", likesRouter);
위의 controller코드에서 좋아요 기능이 정상적으로 동작하도록 예외처리 없이 한번 함수의 코드를 작성해보자.
기능이 동작하는 likes.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
const dbConnection = require("../model/mysql.js"); const { StatusCodes } = require("http-status-codes"); /** * 좋아요 추가 API * @param {import("express").Request} req * @param {import("express").Response} res */ const addLike = (req, res) => { const { userId } = req.body; const { bookId } = req.params; let sqlQuery = ` INSERT INTO likes (user_id, book_id) VALUES (?, ?); `; dbConnection.query(sqlQuery, [+userId, +bookId], (err, result) => { if (err) { return res .status(StatusCodes.BAD_REQUEST) .json({ message: "잘못된 요청정보 입니다." }); } return res.status(StatusCodes.OK).json(result.affectedRows); }); }; /** * 좋아용 제거 API * @param {import("express").Request} req * @param {import("express").Response} res * @returns */ const deleteLike = (req, res) => { const { userId } = req.body; const { bookId } = req.params; let sqlQuery = ` DELETE FROM likes WHERE user_id=? AND book_id=?; `; dbConnection.query(sqlQuery, [+userId, +bookId], (err, result) => { if (err) { return res .status(StatusCodes.BAD_REQUEST) .json({ message: "올바르지 않은 요청 입니다." }); } return res.status(StatusCodes.OK).json(result.affectedRows); }); }; module.exports = { addLike, deleteLike, };
좋아요 수 반환을 위한 서브 쿼리 작성
1
2
3
SELECT *,
(SELECT COUNT(*) from likes WHERE book_id = books.id) AS likes
FROM books;
- like 컬럼을 새로 만들어, 해당 컬럼에 서브 쿼리를 작성하여 수행한다.
- 이때 각 books.id 에 대한 COUNT 값을 계수하기 위해, likes테이블의 book_id 는 book.id를 참조하도록 위와 같이 작성하면, 프로그래밍 언어의 반복문 처럼, books 각 행의 id를 참조하여 서브쿼리문을 실행 하게 된다.
해당 쿼리문중, 서브 쿼리에 해당하는 구문을 도서 조회 API 컨트롤러에 추가적으로 적용 시킨다.
books.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
const dbConnection = require("../model/mysql.js"); const { StatusCodes } = require("http-status-codes"); /** * 전체 도서 조회 Or 카테고리별 도서 조회 * @param {import("express").Request} req * @param {import("express").Response} res * @param {import("express").NextFunction} next */ const searchBooks = (req, res, next) => { let { category_id, limit, currentpage } = req.query; if (!limit || !currentpage) { limit = !limit ? 5 : limit; currentpage = !currentpage ? 1 : currentpage; } const offset = +limit * (+currentpage - 1); //카테고리 검색 if (category_id) { let sqlQuery = ` SELECT *, (SELECT COUNT(*) from likes WHERE book_id = books.id) AS likes FROM books LEFT JOIN category ON books.category_id = category.id WHERE books.category_id = ? LIMIT ? OFFSET ?; `; dbConnection.query( sqlQuery, [+category_id, +limit, offset], (err, results) => { if (err) { console.log(err); return res.status(StatusCodes.BAD_REQUEST); } if (results.length > 0) { return res.status(StatusCodes.OK).json(results); } else { return res.status(StatusCodes.NOT_FOUND).end(); } } ); } else { // 전체 도서 조회 let sqlQuery = ` SELECT *, (SELECT COUNT(*) from likes WHERE book_id = books.id) AS likes FROM books LIMIT ? OFFSET ? `; dbConnection.query(sqlQuery, [+limit, offset], (err, results) => { if (err) { console.log(err); return res.status(StatusCodes.BAD_REQUEST).end(); } if (results[0]) { const books = results.map((book) => { const resultBook = { id: book.id, title: book.title, summary: book.summary, author: book.author, price: book.price, pub_date: book.pub_date, likes: book.likes, }; return resultBook; }); return res.status(StatusCodes.OK).json(books); } }); } }; /** * 개별 도서 조회 로직 * @param {import("express").Request} req * @param {import("express").Response} res * @param {import("express").NextFunction} next */ const searchOneBook = (req, res, next) => { const { bookId } = req.params; let sqlQuery = ` SELECT *, (SELECT COUNT(*) from likes WHERE book_id = books.id) AS likes FROM books WHERE id=?; `; dbConnection.query(sqlQuery, [+bookId], (err, results) => { if (err) { console.log(err); return res.status(StatusCodes.BAD_REQUEST).end(); } const book = results[0]; if (book) { return res.status(StatusCodes.OK).json(book); } else { return res.status(StatusCodes.NOT_FOUND).end(); } }); }; /** * 1달 이내 출간된 신간 도서 조회 * @param {import("express").Request} req * @param {import("express").Response} res * @param {import("express").NextFunction} next */ const getNewBooks = (req, res, next) => { const { limit, currentpage } = req.query; const offset = +limit * (+currentpage - 1); let sqlQuery = ` SELECT *, (SELECT COUNT(*) from likes WHERE book_id = books.id) AS likes FROM bookkio.books WHERE pub_date BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW() LIMIT ? OFFSET ? `; dbConnection.query(sqlQuery, [+limit, offset], (err, results) => { if (err) { console.log(err); return res.status(StatusCodes.BAD_REQUEST).end(); } if (results.length > 0) { return res.status(StatusCodes.OK).json(results); } else { return res.status(StatusCodes.NOT_FOUND).end(); } }); }; module.exports = { searchBooks, searchOneBook, getNewBooks, };
사용자가 좋아요한 책인지 확인하는 쿼리
1
2
3
4
SELECT *,
(SELECT COUNT(*) FROM likes WHERE book_id =books.id) AS likes,
(SELECT EXISTS (SELECT * FROM likes WHERE user_id=1 AND book_id=1)) AS liked
FROM books;
위의 쿼리를 통하여 2번째 서브쿼리는 기존과 똑같이 각 도서가 얼만큼의 좋아요를 받았는지 확인하고,
3번째 줄의 서브쿼리는 user의 id와 book의 id를 통하여, 사용자가 해당 도서에 좋아요를 눌렀는지 여부를 확인할 수 있다.
- 위의 코드에서 EXISTS 구문은 조회된 값의 갯수를 반환 하게 되는데 무조건 1아니면 0이 발생
- WHERE 절로 이어서 사용하는 EXIST구문의 경우 TRUE, FALSE 를 반환한다.
이런 쿼리를 작성했다면, 한 가지 적용할 점이 생긴다.
개별 도서를 조회할 때, 좋아요 수와, 유저의 좋아요 여부, 카테고리 명 까지 출력해줄수는 없을까?
위에 해당하는 쿼리를 작성해았다.
1
2
3
4
5
6
7
SELECT *,
(SELECT COUNT(*) FROM likes WHERE book_id = books.id) AS likes,
(SELECT EXISTS (SELECT * FROM likes WHERE user_id=1 AND book_id=1)) AS liked
FROM books
LEFT JOIN category
ON books.category_id = category.id
WHERE books.id= 1;
해당 쿼리를 적용하여 개별 도서 조회
코드를 수정하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 개별 도서 조회 로직
* @param {import("express").Request} req
* @param {import("express").Response} res
* @param {import("express").NextFunction} next
*/
const searchOneBook = (req, res, next) => {
const { bookId } = req.params;
const { userId } = req.body;
let sqlQuery = `
SELECT *,
(SELECT COUNT(*) FROM likes WHERE book_id = books.id) AS likes,
(SELECT EXISTS (SELECT * FROM likes WHERE user_id=? AND book_id=?)) AS liked
FROM books
LEFT JOIN category
ON books.category_id = category.category_id
WHERE books.id= ?;
`;
dbConnection.query(sqlQuery, [+userId, +bookId, +bookId], (err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
const book = results[0];
if (book) {
return res.status(StatusCodes.OK).json(book);
} else {
return res.status(StatusCodes.NOT_FOUND).end();
}
});
};
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.