개발 무지렁이

[Spring Boot] 카카오페이(KakaoPay) 단건 결제 기능 구현, 결제 준비 API 및 결제 승인 API 본문

Backend/스프링부트

[Spring Boot] 카카오페이(KakaoPay) 단건 결제 기능 구현, 결제 준비 API 및 결제 승인 API

Gaejirang-e 2023. 10. 3. 12:39

🪛 Application.yml

  spring:
    thymeleaf:
      cache: false
      prefix: classpath:/templates/
      suffix: .html
    devtools:
      livereload:
        enabled: true
      restart:
        enabled: true
    datasource:
      url: jdbc:h2:tcp://localhost/~/test
      username: sa
      password:
      driver-class-name: org.h2.Driver
    jpa:
      hibernate:
        ddl-auto: create

  server:
    port: [지정한 포트번호]

  my:
    admin: [kakao developers에서 발급받은 admin key]

📜 payment.html

  <!DOCTYPE html>
  <html lang="ko">
  <head>
    <meta charset="UTF-8">
    <title>카카오 결제</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <style>
      #btn-kakao {
          background-color: #FAE300;
        color: #3C1E1E;
        font-weight: 800;
        border: none;
        border-radius: 12px;
        padding: 10px 20px;
        cursor: pointer;
      }
    </style>
  </head>
  <body>
    <button id="btn-kakao">kakao pay <i class="fa-solid fa-comment"></i></button>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
    <script>
      $(function() {
          $("#btn-kakao").click(function() {
            $.ajax({
                url: '/pay/ready',
                  type: 'get',
                  data: {
                    item_name: '초코파이',
                      quantity: "4",
                      total_amount: "8800",
                      tax_free_amount: "0",
                },
                  success: function(response) {
                    alert(response.next_redirect_pc_url); //kakao payment api에서 알아서 응답해주는 url
                      location.href = response.next_redirect_pc_url;
                },
                  error: function(xhr, err, status) {
                    console.log(xhr.responseText);
                      alert(err + "이(가) 발생했습니다: " + status);
                }
            });
        });
      });
    </script>
  </body>
  </html>
🖤 카카오 페이(단건결제): 결제 준비 API -> (비즈니스 로직) -> 결제 승인 API
(준비 API승인 API 사이에 우리의 로직을 넣을 수 있다.)

𐂂 결제 준비 요청
📦 요청에 필요한 정보 (필수만 *)
- cid 가맹점코드, 10자 (String)
- partner_order_id 가맹점 주문번호, 최대 100자 (String)
- partner_user_id 가맹점 회원, 최대 100자 (String)
- item_name 상품명, 최대 100자 (String)
- quantity 상품수량 (Integer)
- total_amount 상품 총액 (Integer)
- tax_free_amount 상품 비과세 금액 (Integer)
- approval_url 결제 성공시 redirect url, 최대 255자 (String)
- cancel_url 결제 취소시 redirect url, 최대 255자 (String)
- fail_url 결제 실패시 redirect url, 최대 255자(String)

📦 KAKAO PAY가 응답해주는 정보
- tid 결제 고유 번호 (String)
- next_redirect_pc_url 요청한 클라이언트가 PC 웹일 경우, QR코드로 이동하는 url (String)
- created_at 결제 준비 요청 시간 (Date)

📦 KakaoPayReadyResponse.java

  @Getter
  @Setter
  @ToString
  public class KakaoPayReadyResponse {
      private String tid;
      private String next_redirect_pc_url;
      private Date created_at;
  }

📜 PayController.java

  @Slf4j
  @RequiredArgsConstructor
  @Controller
  @RequestMapping("/pay")
  public class PayController {
      private final PayService payService;

      @GetMapping("/ready")
      public @ResponseBody KakaoPayReadyResponse kakaoPayReady(@RequestParam Map<String, Object> params) {
          //클라이언트에서 jquery ajax로 넘긴 정보가 잘 넘어왔는지 확인
          log.info("item_name: " + params.get("item_name"));
        log.info("quantity: " + params.get("quantity"));
        log.info("total_amount: " + params.get("total_amount"));
        log.info("tax_free_amount: " + params.get("tax_free_amount"));

          KakaoPayReadyResponse readyResponse = payService.kakaoPayReady(params); //kakaoPay 요청양식에 따라 요청객체 만들어 보내는 메서드(밑에서 구현)
          log.info(readyResponse.toString()); //kakaoPay가 준비요청 후 보내준 정보 확인
          return readyResponse;
      }
  }

𖠃 결제 승인 요청
📦 요청에 필요한 정보 (필수만 *)
- cid 가맹점코드, 10자 (String)
- tid 결제 고유번호 (String)
- partner_order_id 가맹점 주문번호, 최대 100자 (String)
- partner_user_id 가맹점 회원, 최대 100자 (String)
- pg_token 결제 승인시 요청을 인증하는 토큰 (String)

📦 KAKAO PAY가 응답해주는 정보
- aid 요청 고유번호 (String)
- tid 결제 고유번호 (String)
- cid 가맹점 코드, 10자 (String)
- sid 정기 결제용 id (String)
- partner_order_id 가맹점 주문번호, 최대 100자 (String)
- partner_user_id 가맹점 회원, 최대 100자 (String)
- payment_method_type 결제수단 (CARD or MONEY / String)
- item_name 상품명, 최대 100자 (String)
- quantity 상품수량 (Integer)
- created_at 결제 준비 요청 시각 (String)
- approved_at 결제 승인 시각 (String)
- amount 전체 결제 금액, 비과세 금액, 부가세 금액, 사용한 포인트 금액, 할인금액 (Amount)

📦 Amount.java

  @Getter
  @Setter
  @ToString
  public class Amount {
      private Integer total;
      private Integer tax_free;
      private Integer vat;
      private Integer point;
      private Integer discount;
  }

📦 KakaoPayApproveResponse.java

  @Getter
  @Setter
  @ToString
  public class KaokaoPayApproveResponse {
      private String aid;
      private String tid;
      private String cid;
      private String sid;
      private String partner_order_id;
      private String partner_user_id;
      private String payment_method_type;

      private String item_name;
      private String item_code;
      private Integer quantity;
      private String created_at;
      private String approved_at;

      private Amount amount;
  }

📜 PayController.java

  @Slf4j
  @RequiredArgsConstructor
  @Controller
  @RequestMapping("/pay")
  public class PayController {
      private final PayService payService;

      @GetMapping("/ready")
      public @ResponseBody KakaoPayReadyResponse kakaoPayReady(@RequestParam Map<String, Object> params) {
        ...
      }

      @GetMapping("/success")
      public String kakaoPayApprove(@RequestParam("pg_token") String pgToken) { //pgToken 알아서 들어온다
          KakaoPayApproveResponse approveResponse = payService.kakaoPayApprove(pgToken); //kakaoPay 요청양식에 따라 요청객체 만들어 보내는 메서드(밑에서 구현)
          return "pay_completed"; //결제 승인 후 redirect 할 페이지 (알아서 구현)
      }
  }

PayService.java / kakaoPay 요청양식에 따라 요청객체 만들어 보내는 메서드 포함(준비, 승인)
  @Slf4j
  @RequiredArgsConstructor
  @Service
  public class PayService {
      @Value("${my.admin}")
      private String ADMIN_KEY;

      private KakaoPayReadyResponse readyResponse;

      public KakaoPayReadyResponse kakaoPayReady(Map<String, Object> params) {
          MultiValueMap<String,Object> payParams = new LinkedMultiValueMap<>();
          payParams.add("cid", "TC0ONETIME"); //테스트 결제는 가맹점 코드로 'TC0ONETIME'를 사용
        payParams.add("partner_order_id", "KA2020338445"); //일단 아무값이나 hard coding.
        payParams.add("partner_user_id", "kakaopayTest"); //일단 아무값이나 hard coding.
        payParams.add("item_name", params.get("item_name"));
        payParams.add("quantity", params.get("quantity"));
        payParams.add("total_amount", params.get("total_amount"));
        payParams.add("tax_free_amount", params.get("tax_free_amount"));
        payParams.add("approval_url", "http://localhost:83/pay/success"); //결제 성공시 넘어갈 url
        payParams.add("cancel_url", "http://localhost:83/pay/cancel"); //결제 취소시 넘어갈 url
        payParams.add("fail_url", "http://localhost:83/pay/fail"); //결제 실패시 넘어갈 url

          HttpEntity<Map> requestEntity = new HttpEntity<>(payParams, this.getHeaders());
          RestTemplate template = new RestTemplate();
          String url = "https://kapi.kakao.com/v1/payment/ready";

          readyResponse = template.postForObject(
              url,
              requestEntity,
              KakaoPayReadyResponse.class
          );

          return readyResponse;
      }

      @Transactional
      public KakaoPayApproveResponse kakaoPayApprove(String pgToken) {
          MultiValueMap<String, Object> payParams = new LinkedMultiValueMap<>();

          payParams.add("cid", "TC0ONETIME"); //테스트 결제는 가맹점 코드로 'TC0ONETIME'를 사용
        payParams.add("tid", kakaoPayReadyResponse.getTid());
        payParams.add("partner_order_id", "KA2020338445"); //일단 아무값이나 hard coding.
        payParams.add("partner_user_id", "kakaopayTest"); //일단 아무값이나 hard coding.
        payParams.add("pg_token", pgToken);

          HttpEntity<Map> requestEntity = new HttpEntity<>(payParams, this.getHeaders());
          RestTemplate template = new RestTemplate();
          String url = "https://kapi.kakao.com/v1/payment/approve";

          KakaoApproveResponse approveResponse = template.postForObject(
              url,
              requestEntity,
              KakaoPayApproveResponse.class
          );

          /*
          데이터베이스 저장 및 비즈니스 로직
          ...
          */

          return approveResponse;
      }

      private HttpHeaders getHeades() {
          HttpHeaders headers = new HttpHeaders();

          String auth = "KakaoAK " + ADMIN_KEY;
          headers.set("Authorization", auth);
          headers.set("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
          return headers;
      }
  }
🦉 MultiValueMap / LinkedMultiValueMap (구현객체 中 하나)
: 하나의 키여러개의 값(동일한 타입)매핑(다중 매핑)할 수 있는 Map 인터페이스확장된 구조를 말한다.
구현 객체 중의 하나인 LinkedMultiValueMap순서가 유지되는 Linked List를 사용하여 데이터를 저장한다.

🦉 HttpEntity
: Http 요청/응답의 객체를 나타내는 인터페이스다.
new HttpEntity([본문데이터], [헤더])

🦉 RestTemplate
: Rest방식으로 API호출할 수 있는 🌱 Spring Framework의 내장 클래스다.
RESTful 웹서비스를 호출하고 응답을 처리하는데 사용된다.
(HTTP 메서드 (GET, POST, PUT, DELETE 등) 지원)

ex. postForObject([요청을 보낼 url], [본문데이터], [원격서버로부터 받을 응답의 데이터타입]);
: HTTP POST 요청을 통해 데이터를 전송하고,
서버로부터 받은 응답을 지정한 객체타입으로 파싱해서 받을 수 있다.

📕 참고 자료 📕
Tistory's Card

Comments