node.js로 결제 모듈 연동(아임포트)

GOO·2023년 4월 20일
0

아임포트 결제 시스템을 이용하여 결제 서비스 구현(node.js)
1.아임포트(https://portone.io/korea/ko) 사이트에 접속해서 회원가입

2.PG정보 설정하기(관리자 페이지->결제연동->테스트 연동 관리->PG사 선택)
1) 각 PG사별 포트원 관리자 페이지 설정 (https://portone.gitbook.io/docs/ready/2.-pg/payment-gateway)

3.연동정보 확인
1)포트원 관리자콘술 로그인->상점 계정관리->내 식별코드 API Keys선택
2)가맹점 식별코드(/REST API Key/REST API Secre)확인

4.백엔드의 사전검증과 프론트 엔트의 결제요청 코드

// 사용자에게 결제 화면을 보여주기 전에 서버 코드에 가맹점 주문번호와
// 결제 예정금액을 서버에 저장한다
<!DOCTYPE html>
<html lang="en">
<head>
    <!-- jQuery -->
    <script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.min.js" ></script>
    <!-- iamport.payment.js -->
    <script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-1.2.0.js"></script>
	//포트원 라이브러리 추가
	<script src="https://cdn.iamport.kr/v1/iamport.js"></script>
	<script>
      //포트원 객체 초기화 하기
      var IMP = window.IMP; 
      IMP.init("본인가맹점고유번호"); 


      await axios({
        url: "정보를 저장하는 백엔드 서비스 url",//(ex: https://api.iamport.kr/payments/prepare)
        method: "post",
        headers: { "Content-Type": "application/json" }, 
        data: {
          merchant_uid: "...", // 가맹점 주문번호
          amount: 420000, // 결제 예정금액
          //그외 order collection에 저장할 정보도 같이 전송
        }
      }).then((res)=>{
        //DB에 상품 재고가 있는지 파악하고 구매자 아이디, 가맹점 주문번호, 결제 예정금액등
        //필요한 정보를 저장하고 코드 작성하고... 결제 요청을 보낸다
        if(res.date === "ok"){
        	function requestPay() {
              IMP.request_pay({
                  pg : 'kcp.{상점ID}',//각 PG사마다 pay_method 파라미터 확인
                  pay_method : 'card',//실시간 계좌이체 'trans'
                  merchant_uid: "57008833-33004", 
                  name : '당근 10kg',
                  amount : 1004,
                  buyer_email : 'Iamport@chai.finance',
                  buyer_name : '포트원 기술지원팀',
                  buyer_tel : '010-1234-5678',
                  buyer_addr : '서울특별시 강남구 삼성동',
                  buyer_postcode : '123-456', 
                  notice_url : 'https://웹훅수신 URL',//웹훅수신 URL 설정(예: /portone-webhook)
              }, function (rsp) { // callback
                  if (rsp.success) {
                    //DB로 저장될 정보 전송
                    // axios로 HTTP 요청
                    axios({
                      url: "/payment/complete",
                      method: "post",
                      headers: { "Content-Type": "application/json" },
                      data: {
                        imp_uid: rsp.imp_uid,
                        merchant_uid: rsp.merchant_uid
                      }
                    }).then((data) => {
                      // 서버 결제 API 성공시 로직
                    })
                    console.log(rsp);
                  } else {
                    console.log(rsp);
                  }
              });
        	}
        }
  
    
      }).catch(error=>{

      });	
</script>
<meta charset="UTF-8">
    <title>Sample Payment</title>
</head>
<body>
    <button onclick="requestPay()">결제하기</button> <!-- 결제하기 버튼 생성 -->
</body>
</html>

5.서버 코드

app.use(bodyParser.json());
//클라이언트에서 아이엠포트와 결제가 완료되면 호출되는 서버의 결제완료 처리 서비스
app.post("/payments/complete", async (req, res) => {
  try {
    // req의 body에서 imp_uid, merchant_uid 추출
    const { imp_uid, merchant_uid } = req.body; 
    ...
    // 액세스 토큰(access token) 발급 받기
    const getToken = await axios({
      url: "https://api.iamport.kr/users/getToken",
      method: "post", // POST method
      headers: { "Content-Type": "application/json" }, 
      data: {
        imp_key: "imp_apikey", // REST API 키
        imp_secret: "ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6bkA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f" // REST API Secret
      }
    });
    const { access_token } = getToken.data; // 인증 토큰
    ...
    // imp_uid로 포트원 서버에서 결제 정보 조회
    const getPaymentData = await axios({
      // imp_uid 전달
      url: `https://api.iamport.kr/payments/${imp_uid}`, 
      // GET method
      method: "get", 
      // 인증 토큰 Authorization header에 추가
      headers: { "Authorization": access_token } 
    });
    
    const paymentData = getPaymentData.data; // 조회한 결제 정보
    // DB에서 결제되어야 하는 금액 조회
    const order = await Orders.findById(paymentData.merchant_uid);
    const amountToBePaid = order.amount; // 결제 되어야 하는 금액
    // ...
    // 결제 검증하기
    const { amount, status } = paymentData;
    // 결제금액 일치. 결제 된 금액 === 결제 되어야 하는 금액
    if (amount === amountToBePaid) { 
      await Orders.findByIdAndUpdate(merchant_uid, { $set: paymentData }); // DB에 결제 정보 저장
      // ...
      switch (status) {
        case "ready": // 가상계좌 발급
          // DB에 가상계좌 발급 정보 저장
          const { vbank_num, vbank_date, vbank_name } = paymentData;
          await Users.findByIdAndUpdate("/* 고객 id */", { $set: { vbank_num, vbank_date, vbank_name }});
          // 가상계좌 발급 안내 문자메시지 발송
          SMS.send({ text: `가상계좌 발급이 성공되었습니다. 계좌 정보 ${vbank_num} ${vbank_date} ${vbank_name}`});
          res.send({ status: "vbankIssued", message: "가상계좌 발급 성공" });
          break;
        case "paid": // 결제 완료
          res.send({ status: "success", message: "일반 결제 성공" });
          break;
      }
    } else { // 결제금액 불일치. 위/변조 된 결제
      throw { status: "forgery", message: "위조된 결제시도" };
    }
  } catch (e) {
    res.status(400).send(e);
  }
});   
    

6.백엔드 - 사후 검증(보안을 위해 webhook이벤트를 사용)

app.use(bodyParser.json());
app.post("/portone-webhook", async (req, res) => {
  try {
    // req의 body에서 imp_uid, merchant_uid 추출
    const { imp_uid, merchant_uid } = req.body; 
    
    // 액세스 토큰(access token) 발급 받기
    const getToken = await axios({
      url: "https://api.iamport.kr/users/getToken",
      method: "post", // POST method
      headers: { "Content-Type": "application/json" }, 
      data: {
        imp_key: "imp_apikey", // REST API 키
        imp_secret: "ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6bkA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f" // REST API Secret
      }
    });
    const { access_token } = getToken.data; // 인증 토큰
    // imp_uid로 포트원 서버에서 결제 정보 조회
    const getPaymentData = await axios({
      // imp_uid 전달
      url: `https://api.iamport.kr/payments/${imp_uid}`, 
      // GET method
      method: "get", 
      // 인증 토큰 Authorization header에 추가
      headers: { "Authorization": access_token } 
    });
    const paymentData = getPaymentData.data; // 조회한 결제 정보
    
    
    // DB에서 결제되어야 하는 금액 조회
        const order = await Orders.findById(paymentData.merchant_uid);
        const amountToBePaid = order.amount; // 결제 되어야 하는 금액
        
        // 결제 검증하기
        const { amount, status } = paymentData;
        // 결제금액 일치. 결제 된 금액 === 결제 되어야 하는 금액
        if (amount === amountToBePaid) { 
          // DB에 결제 정보 저장
          await Orders.findByIdAndUpdate(merchant_uid, { $set: paymentData }); 
          switch (status) {
            case "ready": // 가상계좌 발급
              // DB에 가상계좌 발급 정보 저장
              const { vbank_num, vbank_date, vbank_name } = paymentData;
              await Users.findByIdAndUpdate("/* 고객 id */", { $set: { vbank_num, vbank_date, vbank_name }});
              // 가상계좌 발급 안내 문자메시지 발송
              SMS.send({ text: \`가상계좌 발급이 성공되었습니다. 계좌 정보 \${vbank_num} \${vbank_date} \${vbank_name}\`});
              res.send({ status: "vbankIssued", message: "가상계좌 발급 성공" });
              break;
            case "paid": // 결제 완료
              res.send({ status: "success", message: "일반 결제 성공" });
              break;
          }
        } else { // 결제금액 불일치. 위/변조 된 결제
          throw { status: "forgery", message: "위조된 결제시도" };
        }
  } catch (e) {
    res.status(400).send(e);
  }
});
  1. 모바일 결제(특정 PG사에서 요구되는 사항) - 차후에 구현
  2. 가상계좌 환불 - 차후 구현

0개의 댓글