Wecode - Project 3 (부트캠프)/Project 3 과정

Project 3 - 최종 주문/결제 api 코드 설명

JBS 12 2023. 10. 28. 01:25

코드를 왜 이렇게 썼는지, 왜 여기에 썼는지 설명할 줄 아는 것이 중요하다. 

 

이전에는 블로그 기록하면서 코드를 작성했는데

계속 수정되기도 하고

바로바로 통신하고 확인해보고 이렇게 저렇게 해보는 도전정신이 줄어들어,

 

그때그때 주석으로 달아두었다.

 

그 정신없는 와중에 기록해두었던

주석 하나하나를 뜯어보며


routers

orderRouter를 Index에서 불러온다

 orderRouter.post('/', orderController.createOrders);     // 토큰 X
 orderRouter.post('/', verifyToken, orderController.createOrders);     //토큰 O
 orderRouter.post('/', verifyToken, asyncWrap(orderController.createOrders)); // async 사용 -> try, catch, next 사용할 필요 없음

 

 

controllers

orderController를 index에서 불러온다

 

 

const userId = req.userId        // header의 authorization의 token의 userId 

= const {userId} = req 


 

const createOrders = async(req, res) => {

try { 

        const { } = req.body // 필요한 값을 body에서 모두 받아옴 

 

         await orderService.createOrders ( ) ;         // 이것들을 다음 단계 (orderService)의 createOrders함수 로 보내줄거야. 

        return res.status(200).json ({message : 'Success' }); 

}       catch (error){

         res.status(error.status || 500).json ({message: error.message});

}

}; 

//throw가 없는데 catch할 수 있는 이유는, orderService.js에서 throw 한 거를 여기서 catch하기에 


 

아래 코드를 실행 시, error의 메세지가 배열에 담겨서 나온다 - > 굳이 그럴 필요가 없어서, 삭제한 코드 

const errorMessages = [];
    if (error.message === 'ordered productId is not in the carts') {
      errorMessages.push('ordered productId is not in the carts');
    }
    if (error.message === 'ordered more products than cartsQuantity') {
      errorMessages.push('ordered more products than cartsQuantity');
    }
    if (error.message === 'not enough points') {
      errorMessages.push('not enough points');
    }
    if (errorMessages.length === 0) {
      return res.status(error.status || 200).json({ message: 'Success' });
    } else {
    res.status(error.status || 500).json({ message: error.message });

  }

 

service.js 

 

구매 api를 

1) 상세페이지/목록페이지에서 '바로구매'

2) 장바구니에서 '구매' 

 

그러면, 이게 장바구니에서 넘어왔는지 아닌지를 체크 

-> from carst에서 true: 장바구니에서 구매 / false: 바로구매 

 

const createOrders = async ( ) => {

// controller.js에서 받아온 값


const isUserPoints = await orderDao.isUserPoints(userId); 

// orderDao에 userId를 보내서, orderDao에서 isUserPoints를 orderService로 가져온다. 

// 그 가져온 함수를 orderService에서 (편의상 orderDao에서 쓰는 함수랑 똑같이) const isUserPoints라고 한다. 

 


에러핸들링 1 : 포인트가 부족할 때 주문/결제 안 되게 ( 상품의 가격이 갖고 있는 point 보다 클 때이니) 

totalPrice > isUserPoint 

그럴 경우, 에러를 던져라 throwError 


1)  order 테이블에 주문 정보 저장 

 

orderDetails 테이블에 (주문/결제 후 저장되는 테이블)

orderId, productId, quantity가 저장되는데,

 

productId는 req.body에서 프론트로부터 받고,

quantity는 productId가 끌고 오는 product 테이블이 있으니 해결 

 

그러면 남은 orderId는 

order 테이블에서 가져온다.

 

스포를 위해 orderDao를 미리 가져와보면, (layered 되어 있어서, 다음 단계를 보면서 이해 가능)

orderDetails에 데이터 저장 실패했는데, 

order 테이블에는 데이터가 계속 추가 되는 경우 막기 위한 transaction (롤백)

 

if ( ) { throw new Error(orderDetails 주문정보 저장 실패, 처음으로 롤백') 

 


2) 결제 단계를 위해, 

포인트 차감 결제니까, 주문/결제와 동시에, 상품의 totalPrice만큼 user테이블의 points가 차감 되어야 한다. 

 

isUserPoints - totalPrice를

remainPoints로 변수 설정 하고 

 

orderDao에 있는 updatePoints 함수의 값을 

orderService로 가져온다, 

 

그러기 위해 orderService.js에 있는 userId, remainPoints를

orderDao.js로 보내준다

 

 

처음에는 points를 isUserPoints - totalPrice로 뺄 생각을 못하고
updatePoints로만 Update(수정)해주면 된다고 생각했다.

그런데, 만약 totalPrice가 2000원이고, IsUserPoints가 5000원 갖고 있으면, 
남은 금액은 3000원이어야 하는데, 

update 그냥 하면, IsUserPoints가 2000원이 됨. 

결제 단계에서 실패했는데, (결제이후 저장되어야 할) orderDetails 테이블에

데이터가 계속 추가되는 것을 막기 위한 transaction 

 

if( ) {  throw new Error('3단계 결제 실패, 처음으로 롤백! ') } 

 

 

 

 

여기서부터 for( ) 안에 들어가는 코드들

장바구니 -> 구매 로직에서, 

장바구니에 여러 개의 product들이 담겨서, 여러 개 구매할 상황을 고려하여, (여러 product id가 배열로 담길 것 가정)


for 문(for loop)에 넣어준다. 

형태]  for ( let i = 0; i < products.length; i++ ){. } 

 

 

 

<products 안에 들어오는 배열에서 각각의 productId, quantity 뽑아오기>
    

const productId = products[i].productId;
const quantity = products[i].quantity;

 



에러 핸들링: carts 에 담기지 않은 product를 주문할 때

-> for문 안에 있는 이유: productId 라서 for( )안에 들어가야 함

 

 

 

const isProductInCarts = await orderDao.isProductInCarts(userId, productId);

 

// await: userId, productId를  orderDao로 보내준다

orderDao에서 쓸 값 (userId, productId)  보내주기

-> cartsId 없이, carts의 productId를  req.body에서 받아온 productId와 비교

 

(orderDao : 데이터베이스에서 carts 테이블 접근/ 장바구니에 해당 productId 있는지 확인,

                       order api에서 req.body에서 받아온 userId, productId 일치하는 걸로, select) 

 


 

4) 장바구니 삭제 로직


장바구니 수량과 주문/결제한 수량을 비교해서 

 

부분 주문 시 장바구니의 수량을 업데이트 혹은 

전체 주문 시 장바구니 수량 삭제 해줘야 한다

 

그러기 위해, cart quantity를 알아야 한다. 


const cartQuantity = await orderDao.cartQuantity(userId, productId);

 

--> orderDao에 await로 order한 userId, productId를 보내준다

      orderDao에서 carts 장바구니 테이블에 접근하여, userId와 productId가 일치하는 quantity를 뽑아올 것이다 select 

 

그 해당 quantity를 우리는 cartQuantity로 불러오기로 했다. 

 

 

전체 삭제 시에는, orderDao에서 쿼리문을  delete 하면 되는데,

carts 에서 부분삭제 시에는 orderDao에서 쿼리문을 update로 한다.

 

그럴 경우에는 '장바구니에 담아 놓은 수량'에서 '이번에 산 수량'을 빼야 하니까, 

(쿼리문도 그냥 빼기로 하면 된다는)

 

    const updateQuantity = cartQuantity - quantity; 로 선택구매 시, 수정해야 할 '장바구니의 수량'을 우리는 updateQuantity로 부르기로 변수를 만듦

 마지막의 장바구니 수량변경/삭제 로직에 
들어가면 되는 것인데,

장바구니 수량 < 주문 수량 을 다루는 에러 핸들링에서 cartQuantity를 사용하고, 
위 에러 핸들링이 우선되어서 주문정보 저장 안 되게 해야 해서
위로 순서를 올렸다. 

사실, 하나의 api에서 모든 코드들이 동시다발적으로 이루어지지만.


에러 핸들링 : carts에 담은 수량  < 주문한 수량 (그러면 결제 안 되게) ---> 사실 장바구니에서 넘어오는 결제면 이럴 리가 없긴 함. 

 

 장바구니 수량  < order 수량 많은 경우 없음_ 장바구니에서 저장 후 넘어가니

 


    if (cartQuantity < quantity) throwError(400, 'ordered more products than cartsQuantity');

--> 위에서, orderDao에서 끌어왔던 cartQuantity를 여기에 사용 


 3) orderDetails table 주문 정보 저장 

결제 후 orderDetails에 (1번에서, order 테이블에 데이터 저장 후 생성된) orderId, (order테이블의) productId, (product 테이블에서 가져오는) quantity를 저장 

 

 orderDetails table 주문 정보 저장하는 함수를 우리는, newOrderDetails로 부르기로 했다. 


    const newOrderDetails = orderDao.newOrderDetails(orderId, productId, quantity );
    orderDetailsPromises.push(newOrderDetails);    --> for문 안에 await 쓰는경우, promiseall을 써야 해서 

4) 장바구니에서, 주문한 상품 수량만큼 삭제 

 

만약, cart수량 = order한 수량 -> delete 쿼리문       

                                    -> orderDao에 보내줘야 하는 필요한 데이터, : userId, productId 일치하는 거 삭제 (quantity는 필요없는 듯)

만약, cart수량,  order한 수량  같지 않으면 -> update 쿼리문 

                                     -> orderDao에 보내줘야 하는 필요한 데이터, : userId, productId , updateQuantity (위에 뺀 식 있음)


이 파일에서 쓰인 함수는 module.export로 내보내줘야 함 


Dao

 


dataSource에 있는 db를 가져오고 (require로 import) 


orderService.js에서 사용한 isUserPoints 함수를 

헷갈리지 않게 orderDao.js에도 사용 -> DB의 user테이블에 접근하여, userId가 같은 points를 뽑아온다

그런데, WHERE userId = ${userId} 가 아니라, Id = ${userId}인 이유는 pk 이기 때문에 

--> fk 이라면 userId로 씀

 

 

orderService.js로 그 값을 return 해주는데, 

isUserPoints가 아니라, points만 select해낸 userPoints 배열에 (당연히 userId가 일치하는 하나의 값만 담기기) 첫번째 값을 

꺼내서 거기의 Points 키를 가져온다 라는 뜻


에러핸들링 : 장바구니에 없는 제품 구매 시 에러

 

orderService.js에서 받아온 userId, productId와 

orderDao에서 DB의 carts  테이블에 접근해, carts 의 product Id를 비교, 하는데 carts에도 userId가 일치해야 함, productsId도 당연히 

 

일치하는 제품을  cartsProductId 라는 변수에 담았고, 

그 일치하는 개 하나라도 있으면 (cartsProductId.length > 0) service로 return해주고 null 

없으면, 

 

 

 

모든 에러 핸들링이 끝났고, 이제

주문 정보 저장 시작

1) orders 테이블에 주문정보 저장 = insert into 

 

이후 orderDetails 테이블에 저장할 orderId를 위해, 추출해야 하는데, 

저장한 값을 newOrder변수에 담았고, 

 

 

그 newOrder를 찍어보면 insertId가 나오는데,

insert 한 쿼리문의 정보가 나온다.

여기에서, 몇 번째로 insert되었는지 insertId가 있다 

(users, posts 테이블에서도 데이터가 하나씩 추가될 때마다, user_id, post_id가 auto_increment로 숫자가 올라가는 거처럼)

 

그래서 이거를 orders_id 로 임의로 쓰는 것


 

 

 

2) 결제 

 

 

2-1) 결제: 포인트 차감을 위한 user points 가져오기

포인트 차감 결제이니까, 

 

유저가 얼마 포인트를 갖고 있는지 알아야 한다. 

 

DB의 users 테이블에 접근해서, userId가 일치하는 points를 찾아내서 가져온다 select

우리는 그 points를 pointsFromUser라고 부르기로 했다. 

 

그 pointsFromUser 객체의 첫번째 배열의 points키에 해당하는 값을 return

service.js로 반환해준다

 

받아오는 값에,

quantity는 'select 문을 위해' 받아올 필요 없음 ,

select문으로 구할 값만!

 

2) 결제:  points 전체 or 부분 차감 (delete 없이)

return 필요없음 (res 보내줄 값이 없음 )};

장바구니 테이블 : 수량 변경만 하면 됨

 

3) orderDetails 테이블에 주문정보 저장 -> insert into

req -> controller -> service.js -> 에서 받아온 

orderId, productid, quantity를 변수화해서 저장 

await 앞에 굳이 const로 함수명 정의해주지 않아도 됨.
return할 때 필요한 건데, return 안하니

함수명 회색이여도 괜찮음. 여기에서는 return을 해줄 필요 없음
-> 주문 정보 req에서 받아와서 저장이니, res에 보내줄 값이 없기에. (getPost일 땐 return 하겠지만)

4) 장바구니에서 주문한만큼 삭제 

 

4-0) 장바구니 삭제를 위한 수량 가져오기

quantity는 'select 문을 위해' 받아올 필요 없음 , select문으로 구할 값!

console.log('카트에 들어있는 수량 :', cartQuantity);

4-1) carts 에서 전체 삭제

4-2) carts 에서 부분 삭제

return 필요없음 (res 보내줄 값이 없음 ) };

장바구니 테이블 : 수량 변경만 하면 됨

errorHandler.js
throwError.js 

설명

 

에러 핸들링 w/ [asyncwrap, next, ErrorHandler.js ]

동기 분께서 설명해주신 부분 1) async를 쓸 때 controller.js 에서 try, catch 지우고 errorHandler.js 사용 ( 에러 시 일로 감) 1-1) errorHandler.js 에서 next 사용 X 1-2) errorHandler.js 에서 next 사용 2) async를 안 쓸 때

pm-developer-justdoit.tistory.com

 

test.code

9000은 계속 시간초과라고 떠서 내가 추가한 것

 

Project 3 -[주문 api] test code 성공

test code는 코드를 수정할 때마다 test code 수정되어야 하고 에러 수정을 해야 해서 가장 마자막 단계에서 하는 게 좋다 1. 에러 팁 [수정] 2. 에러 팁 [수정] 3. 성공! [full test code]

pm-developer-justdoit.tistory.com

 

middleware - auths.js 
토큰 코드만 따로 뺀 거 

 

설명 

 

middlewware 공부 , errorhandler, async

에러 핸들링 2 [적용] 1. throw로 에러 던져보기 에러를 던지는 방법으로 throw 이는 개발자가 작성하는 모듈에서 발생가능한 에러 상황에서 던지게 되며 상위 계층이나 호출하는 곳에서 모듈의 에

pm-developer-justdoit.tistory.com