프로그래머스 데브코스, 국비지원교육, 코딩부트캠프
테이블을 조인하여 도서를 조회할 때 카테고리 명을 반환할 수 있게끔 했다. 신간 도서를 조회할 수 있도록 했고, 또한 페이징을 구현하였다.
CategoryController.js
const conn = require('../mariadb');
const { StatusCodes } = require('http-status-codes');
const allCategory = (req, res) => {
const sql = 'select * from category';
conn.query(sql, (err, results) => {
if (err) {
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: '서버 에러',
});
}
if (results.length === 0) {
return res.status(StatusCodes.NOT_FOUND).json({
message: '카테고리가 없습니다.',
});
}
return res.status(StatusCodes.OK).json(results);
});
};
module.exports = { allCategory };
category.js
const express = require('express');
const router = express.Router();
const { allCategory } = require('../controller/CategoryController');
router.use(express.json());
// 전체 카테고리 조회
router.get('/', allCategory);
module.exports = router;
카테고리는 말 그대로 전체 카테고리만 받아오면 되기 때문에 그냥 데이터베이스에 저장 된 모든 카테고리를 반환할 수 있게 구현하였다.
먼저 books 테이블과 category 테이블의 관계는 위와 같다. 도서를 반환할 때 카테고리명도 함께 반환하게 하기 위해서 테이블을 조인한다. 기존 코드에서 크게 달라지지 않고 SQL문만 조금 수정하면 된다.
values = [];
const sql = 'select * from books left join category on books.category_id = category.id where category_id = ?';
values.push(category_id);
conn.query(sql, values, (err, results) => {
// SQL 쿼리 실행
}
조인 조건은 books 테이블의 category_id가 category 테이블의 id와 같을 때이다. 이후 쿼리로 받아온 category_id를 필터링해서 결과값을 보여준다.
도서 조회를 해보면 위처럼 name으로 카테고리명이 함께 반환되는 것을 볼 수 있다.
현재 날짜를 기준으로 최근 한 달 이내 책이 발간되었을 때 해당 책만 필터링해서 보여준다. 요청 쿼리로 news
가 true
일 때만 신간 도서가 필터링되어 보인다.
const allBooks = (req, res) => {
const { category_id, news } = req.query;
let sql = 'select * from books';
if (category_id && news) {
sql +=
' left join category on books.category_id = category.id where category_id = ? and pub_date between date_sub(now(), interval 1 month) and now()';
values.push(category_id);
} else if (category_id) {
sql += ' left join category on books.category_id = category.id where category_id = ?';
values.push(category_id);
} else if (news) {
sql += ' where pub_date between date_sub(now(), interval 1 month) and now()';
}
conn.query(sql, values, (err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: '서버 에러',
});
}
if (results.length === 0) {
const message = category_id ? '해당하는 도서가 없습니다.' : '도서가 없습니다.';
return res.status(StatusCodes.NOT_FOUND).json({
message: message,
});
}
return res.status(StatusCodes.OK).json(results);
});
}
SQL문
'select * from books where pub_date between date_sub(now(), interval 1 month) and now()'
DATE_SUB
를 이용하여 한달 이전 날짜와 현재 날짜 사이에 출간한 도서만 조회하는 SQL문이다.
select * from books
가 반복되고 있기 때문에 해당 SQL을 밖으로 빼고 필요한 SQL문은 조건에 따라 추가하였다.카테고리명
, 신간 도서 여부
를 받아오기 때문에 카테고리명만 받아올 때, 혹은 신간 여부만 받아올 때 또는 둘 다 받아올 때를 나누어서 SQL문을 더해준다.category_id
가 있다면 해당하는 도서가 없다는 메시지를, category_id
가 없다면 도서가 없다는 메시지를 출력한다.예시 사진은 내가 페이징까지 전부 구현한 후라서 limit
와 current_page
를 요청 쿼리로 함께 받아 온다. news
가 true일 때, 즉 현재 날짜에서 한 달 이내에 출간한 책만 잘 반환되는 걸 볼 수 있다.
const allBooks = (req, res) => {
const { category_id, news, limit, current_page } = req.query;
// limit : 페이지 당 도서 수
// currentPage : 현재 페이지
// offset : 페이지 당 도서 수 * (현재 페이지 - 1)
const parsedLimit = parseInt(limit);
const parsedCurrentPage = parseInt(current_page);
if (!parsedLimit || !parsedCurrentPage || parsedLimit < 1 || parsedCurrentPage < 1) {
return res.status(StatusCodes.BAD_REQUEST).json({
error: 'limit 혹은 currentPage가 잘못되었습니다.',
});
}
const offset = parsedLimit * (parsedCurrentPage - 1);
const values = [];
let sql = 'select * from books';
if (category_id && news) {
sql +=
' left join category on books.category_id = category.id where category_id = ? and pub_date between date_sub(now(), interval 1 month) and now()';
values.push(category_id);
} else if (category_id) {
sql += ' left join category on books.category_id = category.id 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 ?, ?';
values.push(offset, parsedLimit);
conn.query(sql, values, (err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: '서버 에러',
});
}
if (results.length === 0) {
const message = category_id ? '해당하는 도서가 없습니다.' : '도서가 없습니다.';
return res.status(StatusCodes.NOT_FOUND).json({
message: message,
});
}
return res.status(StatusCodes.OK).json(results);
});
};
드디어 대망의 페이징이다! 페이징을 요청할 때는 페이지 당 몇 개의 데이터를 보여줄 것인지, 그리고 현재 페이지가 어디인지, 또한 데이터베이스에서 가져올 도서 목록의 시작 위치를 나타낼 변수 또한 필요하다.
먼저 해당 코드에서 limit
요청 쿼리는 페이지 당 도서 수를 몇 개로 할지 보낸다. current_page
는 현재 페이지를 보낸다.
그리고 offset
은, 가져온 도서 목록에서 시작 위치를 나타낸다. 예를 들어서 페이지 당 도서 수를 5로 잡고, 현재 페이지가 3이라면 offset
은 15가 된다. 즉, 데이터베이스에서 15번째 도서부터 가져와야 한다는 것을 뜻한다.
따라서 offset = 페이지 당 도서 수 * (현재 페이지 -1)
으로 계산할 수 있다.
SQL문
select * from books limit ?, ?;
// offset, limit 순으로 들어간다.
select * from books limit ? offset ?;
// 위의 쿼리와 같다. 다만 이 쿼리는 limit, offset 순으로 들어간다.
limit은 반환하는 데이터의 수를 제한해준다. 건너뛸 데이터의 수, 그리고 반환할 최대 데이터 수를 차례대로 넣어준다.
1. 첫번째 에러
서버를 실행하고 postman으로 API 테스트를 하려고 했는데 아뿔싸! 갑자기 서버 에러가 발생하고 말았다.
그리고 콘솔로 에러 로그를 찍어보니 위와 같은 메시지가 나왔다. 이유는 정말 간단했다... 처음에는 내가 첫 초기화를 한 SQL문에 select * from books limit ?, ?
식으로 변수 저장을 해두었다. 문제는 이쪽이었다.
찾아보니 LIMIT 문은 SQL 문의 가장 끝에 들어가야 한다고 한다. 그래서 모든 조건문이 실행된 이후 새롭게 LIMIT가 추가될 수 있도록 수정하였다.
다시 서버를 실행해보면 에러 없이 데이터를 잘 반환하는 것을 볼 수 있다.
두 번째 에러는 정말 단순했다. 그냥 limit
와 current_page
요청 쿼리를 받아오지 않으면 서버 에러가 발생하는 거였는데, 사실 도서를 조회할 때 필수적으로 페이징이 들어가긴 하겠지만... 그래도 정말 혹시나 하는 상황이 있을 수 있으니 예외 처리를 추가해줬다. limit
와 current_page
는 무조건 1 이상이어야 하니 1보다 작은 수가 오거나 아예 limit
나 current_page
가 오지 않을 때 404 Bad Request 에러를 반환했다.