1. 계속 반복되고 있는 코드인 회원 인증부분을 모듈화를 해봅시다.
auth.js
const jwt = require('jsonwebtoken'); //jwt 모듈
const dotenv = require('dotenv'); //dotenv 모듈
dotenv.config(); //dotenv 설정
const ensureAuthorization = (req, res) => {
try {
let receivedJwt = req.headers["authorization"];
console.log(receivedJwt);
let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
console.log(decodedJwt);
return decodedJwt;
} catch (err) {
console.log(err.name);
console.log(err.message);
return err;
}
}
module.exports = ensureAuthorization;
2. 장바구니 아이템 목록 조회 -> 내 장바구니 보기
현재의 코드에서는 무조건 req.body에서 날라오는 selected가 있어야지 답이 들어갑니다.
selected에 값이 없으면, 장바구니 아이템목록이 아예 안나옵니다.

// 장바구니 아이템 목록 조회
const getCartItems = (req, res) => {
const {selected} = req.body; // selected = [1,3]
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 = `SELECT cartItems.id, book_id, title, summary, quantity, price
FROM cartItems LEFT JOIN books
ON cartItems.book_id = books.id
WHERE user_id = ? AND cartItems.id IN (?);`;
let values = [authorization.id, 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);
})
}
}
selected 없이 자신의 장바구니 목록을 전부 보여줘야합니다.
// 장바구니 아이템 목록 조회
const getCartItems = (req, res) => {
const {selected} = req.body; // selected = [1,3]
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 = `SELECT cartItems.id, book_id, title, summary, quantity, price
FROM cartItems LEFT JOIN books
ON cartItems.book_id = books.id
WHERE user_id = ?`;
let values = [authorization.id, selected];
if(selected) { // 주문서 작성 시 '선택한 장바구니 목록 조회'
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);
})
}
}
query문에서 selected내용이 있을 경우에만 `AND cartItems.id IN (?)`를 추가하는 것으로 수정하였습니다.
3. 주문 API JWT 추가
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 {
이전에 모듈화를 이미 해주었기 때문에, 이 코드만 추가하면 됩니다.
const ensureAuthorization = require('../auth');
const jwt = require('jsonwebtoken'); //jwt 모듈
const mariadb = require('mysql2/promise'); // mysql 모듈 (promise 버전)
const {StatusCodes} = require('http-status-codes'); //status code 모듈
const order = async (req, res) => {
const conn = await mariadb.createConnection({
host: '127.0.0.1',
user : 'root',
password : 'root',
database : 'BookShop',
dateStrings : true
});
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 {
const {items, delivery, totalQuantity, totalPrice, firstBookTitle} = req.body;
// delivery 테이블 삽입
let sql = "INSERT INTO delivery (address, receiver, contact) VALUES (?, ?, ?)";
let values = [delivery.address, delivery.receiver, delivery.contact];
let [results] = await conn.execute(sql, values);
let delivery_id = results.insertId;
//orders 테이블 삽입
sql = `INSERT INTO orders (book_title, total_quantity, total_price, user_id, delivery_id)
VALUES (?, ?, ?, ?, ?);`;
values = [firstBookTitle, totalQuantity, totalPrice, authorization.id, delivery_id];
[results] = await conn.execute(sql, values);
let order_id = results.insertId;
// items를 가지고, 장바구니에서 book_id, quantity 조회
sql = `SELECT book_id, quantity FROM cartItems WHERE id IN (?);`;
let [orderItems, fields] = await conn.query(sql, [items]);
//orderedBook 테이블 삽입
sql = `INSERT INTO orderedBook(order_id, book_id, quantity) VALUES ?;`;
//items.. 배열 : 요소들을 하나씩 꺼내서 (foreach문 돌려서)
values = [];
orderItems.forEach((item) => {
values.push([order_id, item.book_id, item.quantity]);
})
results = await conn.query(sql, [values]);
let result = await deleteCartItems(conn, items);
return res.status(StatusCodes.OK).json(result);
}
}
module.exports = {
order,
getOrders,
getOrderDetail
}
4. 로그인이 된 상태에서만 접근가능한 것을 고려
1) 개별 도서 조회
도서별로 user가 liked했는지 확인해줘야합니다.
로그인이 된 상태에서 조회하면, liked를 포함해서 보내줘야하고,
로그인이 되지 않은 상태에서 조회하면 liked를 포함하지 않아야합니다.
여기서 '로그인이 되지 않은 상태'는 어떻게 조건을 줘야할까요?
const bookDetail = (req, res)=> {
// 로그인 상태가 아니면 => liked 빼고 보내줌
// 로그인 상태면 => liked 포함해서 보내줌 (user_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 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];
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();
}
})
}
};
이 코드를 실행하고, Authorization은 넣어주지 않았을 때의 오류 메시지를 확인해보겠습니다.


postman의 response를 봐서는
} else if(authorization instanceof jwt.JsonWebTokenError) {
이 조건에 걸리는 것을 알 수 있고,
receivedJwt 가 undefined라는 것을 터미널에서 알 수 있습니다.
그래서 Auth.js에서 애초에 receivedJwt가 undefined일 경우의 예외처리를 해주었습니다.
const jwt = require('jsonwebtoken'); //jwt 모듈
const dotenv = require('dotenv'); //dotenv 모듈
dotenv.config(); //dotenv 설정
const ensureAuthorization = (req, res) => {
try {
let receivedJwt = req.headers["authorization"];
console.log("receivedJwt:", receivedJwt);
if(receivedJwt){
let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
console.log("decodedJwt:", decodedJwt);
return decodedJwt;
} else {
throw new ReferenceError("jwt must be provided");
}
} catch (err) {
console.log(err.name);
console.log(err.message);
return err;
}
}
module.exports = ensureAuthorization;
이럴 경우, receivedJwt가 없을 때 ReferenceError가 발생하고, 최종적으로 catch문에서 이 err를 반환합니다.
const bookDetail = (req, res)=> {
// 로그인 상태가 아니면 => liked 빼고 보내줌
// 로그인 상태면 => liked 포함해서 보내줌 (user_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 if(authorization instanceof ReferenceError) {
let 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 = ?;`;
let values = [book_id];
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();
}
})
}
else {
그러면 이렇게 ReferenceError일 경우 분기를 하나 더 만들어 따로 관리해줄 수 있게됩니다.

로그인없는 상태에서 조회하면 liked 없이 response가 날라옵니다!
5. Authorization 필요한 API에 추가
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 {
6. 전체 도서 조회 pagination 조회
SELECT * FROM BookShop.books LIMIT 4 OFFSET 0;
SELECT count(*) FROM BookShop.books;
이는 이전에 배운 것을 적용하여 페이지네이션을 위해 데이터 조회와 전체 개수 조회를 각각 수행하는 방식입니다.
SELECT SQL_CALC_FOUND_ROWS * FROM BookShop.books LIMIT 4 OFFSET 0;
SELECT found_rows();
이 두가지는 결과는 똑같지만,
첫번째의 코드를 사용할 때에는 books를 2번 셀렉트한 거에 반해,
두 번째 방식은 첫 번째 SELECT 실행 시 전체 row 수까지 함께 계산하고, 이후 FOUND_ROWS()를 통해 이미 계산된 값을 가져오는 구조입니다.
성능 속도를 위해 두 번째를 사용하는 것이 좋겠습니다.



SELECT found_rows() 의 res.json(results) 결과

// (카테고리 별, 신간 여부) 전체 도서 목록 조회
const allBooks = (req, res)=> {
let allBooksRes = {};
let {category_id, news, limit, currentPage} = req.query;
// limit : page 당 도서 수 ex. 3
// currentPage : 현재 몇 페이지 인지. ex. 1, 2, 3...
// offset : ex. 0, 3, 6, 9, 12...
// limit * (currentPage - 1)
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 = [category_id];
}
else if(category_id) {
sql += " WHERE category_id = ?";
values = [category_id];
}
else if(news) {
sql += " WHERE pub_date BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW()"
}
sql += " LIMIT ? OFFSET ?"; //WHERE절 뒤에 있어야함
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);
});
};
7. response 포맷 통일하기 -> 카멜로!
sql에서 별칭을 하나하나 지정해주어도 되지만,
results.map(function(result){
result.pubDate = result.pub_date;
delete result.pub_date;
});
이렇게 pubDate(바꾸고싶은 이름)에 복사해 넣고, 이전의 데이터를 지워서 쉽게 할 수 있습니다.
8. StatusCode 확인
const {email, password} = req.body;
let sql = 'UPDATE users SET password=?, salt=? WHERE email=?';
// 암호화된 비밀번호와 salt 값을 같이 DB에 저장
const salt = crypto.randomBytes(10).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 1000, 10, 'sha512').toString('base64');
let values = [hashPassword, salt, email];
conn.query(sql, values,
(err, results) => {
if(err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
else {
return res.status(StatusCodes.OK).json(results);
}
}
)
현재 코드에서는 sqlerr만 안나오면 무조건 200OK를 보내주지만, 여러 에러가 있을 수 있습니다.
if(err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
if(results.affectedRows == 0) {
return res.status(StatusCodes.BAD_REQUEST).end();
}
else {
return res.status(StatusCodes.OK).json(results);
}
이렇게 하면 괜찮겠습니다! 이 코드는 반복이 될테니 모듈화하면 좋을 것같습니다. 앞으로 고도화할 만한 것들을 정리해보겠습니다.
[프로젝트 과제] 🔥 코드 퀄리티를 올려보자
- response 포맷 통일(snake>camel), status code 나머지 다 바꾸기
- 데이터베이스 중복 코드 -> 모듈화
- ex. UserController => User(데이터 모듈=Model) - CRUD
- cf. DB 모듈 : mysql => 몽구스, 시퀄라이즈
- 패키지 구조
- 1) Router : 경로(URI, URL) 와 HTTP method로 요청에 따른 경로를 찾아주는 역할
- 2) Controller : 길 매니저 - 요청을 환영, 직접 일하지는 않음
- 3) Service : 직접 일을 하고 (ex. 어떤 쿼리를 부를지)
- 4) Model : 데이터 베이스와 소통 (query 집합)
- 예외처리(try/catch) 더 해줄 곳 없는지
- 유효성 검사 추가
- jwt : access token이 만료되면, 로그인 연장?
- 로그인 시 access token(ex 30m), refresh token(ex 24h) 발급. -> 30분 지나도 refesh 물어봐서 refresh token 발급해줄 수 있음
- 1) access token: 로그인 인증
- 2) refresh token: 로그인 유효기간 연장 토큰
- 랜덤 데이터 (외부)API 활용해서 isbn 샘플 데이터 채워보기
- nodemon 모듈 ...
'Devcourse > BookShop 사이드 프로젝트' 카테고리의 다른 글
| [BookShop-FE] 본격적인 UI 작업 시작 - 헤더와 푸터 (0) | 2026.04.17 |
|---|---|
| [BookShop- FE] React 스타일링 셋업 & 테마 시스템 (0) | 2026.04.14 |
| [BookShop 프로젝트] jwt 토큰 추가, jwt 예외처리, try...catch, throw (0) | 2026.03.04 |
| [BookShop 프로젝트] 주문 API - LAST_INSERT_ID(), MAX()로 최신 데이터 가져오기, insertId, 벌크 INSERT (0) | 2026.02.20 |
| [BookShop 프로젝트] 장바구니 API - fk제약조건, in(~, ~) (0) | 2026.02.19 |