본문 바로가기
TIL with Programmers

[TIL] 10/17 도서 API, 장바구니 API 수정 (feat.JWT)

by 보먀 2024. 10. 20.
728x90
반응형

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당 도서수}&currentPage={현재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;
728x90
반응형