프로젝트/도서구매사이트

1030 회원 인증 모듈화 적용, 전체 도서 pagination json 추가, 리팩토링 항목

thinktank911 2025. 10. 30. 00:16

회원인증 모듈화

  • ensureAuth가 파일마다 반복된다. ➡️ 회원 인증 기능을 모듈화할 필요성을 느꼈다.
  • auth.js 파일을 따로 만들어 모듈 exports 해준 뒤 모듈이 필요한 곳에서 호출해 쓴다.
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config();

const ensureAuth = (req, res)=>{
    try {
        let receivedJwt= req.headers["authorization"];
        console.log("received jwt: ", receivedJwt);

        if(receivedJwt){
            let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
            console.log(decodedJwt);
            return decodedJwt;
        }else{
            // return receivedJwt; // undefined
            throw new ReferenceError("jwt must be provided");
        }

       
    } catch (err) {
        console.log(err.name);
        console.log(err.message);

        return err;
    }
};

module.exports = ensureAuth;

auth.js

const ensureAuth = require('../auth');  // 인증 모듈
const jwt = require('jsonwebtoken');

// 주문하기
const order = async (req, res)=>{
    const conn = await mysql.createConnection({
        host: '127.0.0.1',
        port: '3307',
        user: 'root',
        password: 'root',
        database: 'Bookshop',
        dateStrings: true,    // 날짜 형식대로 표기
      });

    // 인증 복호화
    let authorization = ensureAuth(req, res);

    // instanceof : 객체가 어떤 클래스의 인스턴스인지 알아내기 위해 사용
    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" : "잘못된 토큰입니다."
        });
    }  

사용하는 곳 - OrderController

내 장바구니 조회

  • 현재는 장바구니에서 선택한 주문 예상 상품 조회만 구현되어 있다.
  • user_id가 있을 때 내 장바구니 목록 전체를 조회하는 기능이 추가로 필요하다.
  • 선택한 장바구니 조회에서 분기 처리로 구현 가능
    ➡️ selected 유무로 분기 처리
        // 장바구니 보기
        let sql = `SELECT cartItems.id, book_id, title, summary, quantity, price
                          FROM cartItems
                  LEFT JOIN books ON books.id = cartItems.book_id
                       WHERE user_id = ?`;
        let values = [authorization.id];

        if(selected){   // 주문서 작성 시 선택한 장바구니 목록 조회
            sql += ` AND cartItems.id IN (?)`;
            values.push(selected);
        }
       
        // SELECT 쿼리문
        conn.query(sql, values,


※ req.body 안보내주면 에러 남 ➡️ 빈 객체를 대체해서 구조분해 안전하게 작동하도록 한다.

const {selected} = req.body || {};

개별 도서 조회 좋아요 여부에 jwt 확인

  • 분기처리필요
    • 로그인 상태가 아니면 ➡️ sql문에서 liked 빼고 보내줌
      로그인 상태면 ➡️ sql문에 liked 추가
  • 로그인 상태가 아닐 시 JsonWebTokenError 에러를 반환해주고 있었다.
  • authorization이 없으니 토큰을 비교해서 잘못된 토큰이라고 말해주고 있는 것
    ➡️authorization이 없는 것, 즉 로그인 상태가 아닌 것과 잘못된 토큰을 보내주고 있는 것은 엄연히 다르므로
    다른 에러 처리를 통해 구분 필요
  • auth.js에서 received jwt: undefined 일 때 ReferenceError 던져주기
const ensureAuth = (req, res)=>{
    try {
        let receivedJwt= req.headers["authorization"];
        console.log("received jwt: ", receivedJwt);

        if(receivedJwt){
            let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
            console.log(decodedJwt);
            return decodedJwt;
        }else{
            // return receivedJwt; // undefined
            throw new ReferenceError("jwt must be provided");
        }

       
    } catch (err) {
        console.log(err.name);
        console.log(err.message);

        return err;
    }
};

장바구니 도서 삭제, 주문 상세 내역 조회 jwt 인증 구현

  • 로그인 상태여야 하는 기능들에 jwt 인증 기능을 추가한다.
    ➡️주문, 장바구니, 도서, 좋아요 등

전체 도서 조회 pagination 추가

  • pagination을 계속 유지하려면 서버에서 클라이언트로 현재페이지와 총 도서 수를 보내줘야 한다.
    ➡️json에 넣어 보내준다.

  • 총 도서 수 즉, 총 데이터(row) 수를 sql문을 통해 저장 및 조회할 수 있다.
    • sql_calc_found_rows : 총 row 수 저장
    • SELECT found_rows() : 저장한 총 row 수 가져오기
-- 총 row 수 저장 : sql_calc_found_rows
SELECT sql_calc_found_rows *
 FROM books LIMIT 4 OFFSET 8;

-- 저장한 총 row 수 가져오기
 SELECT found_rows();
  • 코드에 sql문 수정
// SQL_CALC_FOUND_ROWS : 총 ROW 수 저장
    let sql = `SELECT SQL_CALC_FOUND_ROWS *, 
                    (SELECT count(1) 
                    FROM likes 
                    WHERE liked_book_id = books.id) AS likes 
                FROM books`;
    let values = [];
    
//..(중략)..

// SELECT found_rows() : 저장된 총 ROW수 조회
    sql = ` SELECT found_rows()`;
    // sql = ` SELECT found_rows() AS foundRows`;
    conn.query(sql,
        (err, results) => {
            if(err){
                console.log(err)
                return res.status(StatusCodes.BAD_REQUEST).end()
            }
  • allbooksRes 객체 생성해서 pagination에 필요한 currentPage와 totalBooks를 구해서 넣기
    • currentPage : url에서 넘겨준 값 parseInt()해서 넣어줌
    • totalBooks : found_row() 값
// SELECT found_rows() : 저장된 총 ROW수 조회
    sql = ` SELECT found_rows()`;
    // sql = ` SELECT found_rows() AS foundRows`;
    conn.query(sql,
        (err, results) => {
            if(err){
                console.log(err)
                return res.status(StatusCodes.BAD_REQUEST).end()
            }

            // pagination 객체 생성해 넣기
            let pagination = {};
            pagination.currentPage = parseInt(currentPage);
            pagination.totalCount = results[0]["found_rows()"];

            allBooksRes.pagination = pagination;

            return res.status(StatusCodes.OK).json(allBooksRes);
        }

response 포맷 통일 (카멜 vs 스네이크)

  • 전체 도서 조회에서 pub_date ➡️pubDate 변경
if(results.length){
    // pub_date => pubDate 변경
    results.map((result)=> {
        result.pubDate = result.pub_date;
        delete result.pub_date;
    });
    allBooksRes.books = results;
} else{
    return res.status(StatusCodes.NOT_FOUND).end();
}

 

주문목록조회

  • 회원id별로 조회 필요
else{
        // 주문내역 페이지에서 보여줄 컬럼 셋팅 위해 orders와 delivery 조인
        let sql = `SELECT orders.id, created_at, address, receiver, contact,
                        rep_book_title, total_quantity, total_price
                    FROM orders LEFT JOIN delivery
                    ON orders.delivery_id = delivery.id
                   WHERE user_id = ?`;

        let [rows, fields] = await conn.query(sql, authorization.id);
        return res.status(StatusCodes.OK).json(rows);
    }

코드 퀄리티 높이기 위해 보완할 항목

  • response 포맷 통일(snake>camel), status code...
  • 데이터베이스 중복 코드 -> 모듈화
    ex. UserController => User (데이터 모듈=Model) - CRUD
    cf. DB 모듈 : mysql (자바로 치면 JDBC를 직접 해본 것) => 몽구스, 시퀄라이즈
  • 패키지 구조
    1) Router : 경로(URI, URL)와 HTTP method로 요청에 따른 경로를 찾아주는 역할
    2) Controller : 길 매니저 - 요청을 환영!, 직접 일을 하진 않아요. ex. 어떤 서비스를 부를지
    3) Service : 직접 일을 한다. ex. 어떤 쿼리를 부를지
    "비즈니스 로직"
    4) Model : 데이터베이스와 소통 -> 쿼리 집합
  • 예외처리 (try/catch) 더 해줄 곳 없는지
  • 유효성 검사 추가
  • jwt(심화) : access token이 만료되면, '로그인 연장?'
    • 로그인 시 access token(30m), refresh token(24h)
      1) access token : 로그인 인증
      2) refresh token : 로그인 연장하는 용도
  • 랜덤 데이터 (외부) API 활용해서 isbn 샘플 데이터들을 채워볼 수 있다.
  • 좋아요 수 카운팅

패키지 구조를 나눠보고 model로 쿼리 집합을 빼주는 작업을 해보고 싶다. 더불어 typeORM을 적용해보고, 몽구스, 시퀄라이즈에 대해서도 따로 공부해봐야겠다. 유효성검사 까먹기 전에 express-validator 적용해보면서 복습해봐야겠다.