[React-SpringBoot]온라인 간편 결제(포트원) - 카카오페이, 토스페이 연동 및 API

2024. 1. 11. 09:07· 프로젝트
목차
  1. SpringBoot에서 사업자 번호 없이 프로젝트 내에서 토스, 카카오페이 결제를 만들어보자.
  2. 이 글에서는 테스트 결제 부분에 대해서 설명합니다.
  3. Front-end(React) 구성
  4. Back-end(SpringBoot)
  5. SpringBoot 패키지 내 build.gradle에 해당 내용 추가
  6. PaymentController
  7. PaymentDto
  8. PaymentEntity
  9. PaymentRepository
  10. PaymentService
  11. 결제 완료
  12. 결제 검증 실패
  13. DB에 반영이 되었을 때
  14. 프론트앤드(React)에서 결제 완료 후 백앤드로 결제 검증 후 DB 저장
  15. 주의
  16. Docs
반응형

SpringBoot에서 사업자 번호 없이 프로젝트 내에서 토스, 카카오페이 결제를 만들어보자.

💡 포트원(PortOne) 사용 이유 : 팀 프로젝트 과정에서 결제 서비스를 추가 할 상황이 있었는데 카카오페이 테스트결제를 넣어도 되지만, 클라이언트 단에서 실제로 결제를 하는 것 같은 효과를 내고 싶었고, 카카오 페이 테스트 결제에서는 Back-end단에서 결제가 정확히 완료 되었는지 취소 되었는지 불투명하여서 결제 관련 API를 찾다가 포트원 API를 찾음.

포트원 지원 결제수단 목록
  • card (신용카드)
  • trans(실시간계좌이체)
  • vbank(가상계좌)
  • phone(휴대폰소액결제)
  • paypal (페이팔 SPB 일반결제)
  • applepay (애플페이)
  • naverpay(네이버페이)
  • samsungpay(삼성페이)
  • kpay(KPay앱 )
  • kakaopay(카카오페이)
  • payco(페이코)
  • lpay(LPAY)
  • ssgpay(SSG페이)
  • tosspay(토스간편결제)
  • cultureland(컬쳐랜드)
  • smartculture(스마트문상)
  • culturegift(문화상품권)
  • happymoney(해피머니)
  • booknlife(도서문화상품권)
  • point(베네피아 포인트 등 포인트 결제 )
  • wechat(위쳇페이)
  • alipay(알리페이)
  • unionpay(유니온페이)
  • tenpay(텐페이)
  • pinpay(핀페이)
  • ssgpay_bank(SSG 은행계좌)
  • skpay(11Pay (구.SKPay))
  • naverpay_card(네이버페이 - 카드)
  • naverpay_point(네이버페이 - 포인트)
  • paypal(페이팔)
  • toss_brandpay(토스페이먼츠 브랜드페이)
  • 포트원 API 장점 : Back-end(Spring)에서 하나의 구현으로 여러 결제수단을 동시에 사용 가능

이 글에서는 테스트 결제 부분에 대해서 설명합니다.

  • 실 결제는 사업자 번호와 가맹점 신청이 있어야 가능합니다.
  1. 우선 포트원(PortOne)에 회원가입
    https://portone.io/
  1. 결제 수단 추가 및 API Key 발급
    https://admin.portone.io/integration

Untitled.png

  • “결제 연동” 메뉴로 이동

Untitled.png

  • “내 식별코드 - API Keys” 클릭

API Key가 없으면 “발급”

Untitled.png

  1. 결제 수단 추가

Untitled.png

  • 필자는 “카카오페이” 추가

Front-end(React) 구성

  • React상에서 구현

public - index.html의태그에 포트원 SDK 추가

<script src="https://cdn.iamport.kr/v1/iamport.js"></script> 
  • Payment.js
import React, {useState} from 'react';

function App() {
  const [amount, setAmount] = useState(0);

  const handleChange = (e) => {
    setAmount(e.target.value);
  }

  const ClickChargeBtn = (pg_method, amount, nickname, redirect_url) => {
    const { IMP } = window;
    IMP.init('imp.........'); // 가맹점 번호 지정
    IMP.request_pay({
      pg : `${pg_method}`, // 결제 방식 지정
      pay_method : 'card',
      merchant_uid: `mid_${new Date().getTime()}`, // 현재 시간
      name : '결제 품목 및 제목 지정',
      amount : `${amount}`, // 충전할 금액
      buyer_email : '구매자 이메일',
      buyer_name : `${nickname}`, // 충전 요청한 유저의 닉네임
      buyer_tel : '010-1222-2222',
      buyer_addr : '서울특별시 강남구 삼성동',
      buyer_postcode : '123-456',
      m_redirect_url: `${redirect_url}` // 만약 새창에서 열린다면 결제 완료 후 리다이렉션할 주소
  }, function (rsp) { // callback
      if (rsp.success) { // 만약 결제가 성공적으로 이루어졌다면
        alert("결제 성공");
      } else {
        alert("결제 실패");
      }
  }
  );
}
  return (
    <div className="App">
      <h1>Test</h1>
      <p>금액<input type="number" className='amount' onChange={handleChange}></input></p>
      <button onClick={() => ClickChargeBtn('kakaopay', amount, 'nickname', 'http://localhost:3000/redirect')}>카카오페이</button>
      <button onClick={() => ClickChargeBtn('tosspay', amount, 'nickname', 'http://localhost:3000/redirect')}>토스페이</button>
    </div>
  );
}

export default App;

Untitled.pngUntitled.png

  • 결제에 성공을 하면 해당 JSON을 반환해준다.

Untitled.png

  • 여기에서 백엔드를 활용해 이 결제에 대하여 정확히 잘 결제가 되었는지, 충전 금액에 대한 변조는 없는지에 대하여 2차로 검증을 하고, DB에 저장을 하는 과정을 작성해보겠습니다.

Back-end(SpringBoot)

SpringBoot 패키지 내 build.gradle에 해당 내용 추가

dependencies {
	implementation 'com.github.iamport:iamport-rest-client-java:0.2.21'
}

repositories {
	allprojects {
			repositories {
				maven { url 'https://jitpack.io' }
			}
		}
}

PaymentController

package com.test.springtest.payment.controller;

import com.siot.IamportRestClient.exception.IamportResponseException;
import com.test.springtest.payment.dto.PaymentDto;
import com.test.springtest.payment.service.PaymentService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.IOException;

@Controller
public class PaymentController {
    private final PaymentService paymentService;

    public PaymentController(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    @ResponseBody
    @RequestMapping("/verify/{imp_uid}") // https://URL/verify/{거래고유번호}
    public PaymentDto paymentByImpUid(@PathVariable("imp_uid") String imp_uid)
            throws IamportResponseException, IOException {
                return paymentService.verifyPayment(imp_uid); // 결제 검증 및 DB 값 삽입
    }
}

PaymentDto

package com.test.springtest.payment.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@AllArgsConstructor
@Getter
@Builder
@Setter
public class PaymentDto {
    private String impuid; // 거래 고유 번호
    private String name; // 상품명
    private String status; // 결제여부 paid : 1 , 그 외 실패
    private Long amount; // 결제금액

}

PaymentEntity

package com.test.springtest.payment.entity;

import com.test.springtest.payment.dto.PaymentDto;
import jakarta.persistence.*;
import lombok.*;

@Getter
@Setter
@Entity
@Table(name="payment_list", indexes={@Index(name = "impuid_index", columnList = "impuid", unique = true)}) // impuid 인덱스 생성
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PaymentEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long payment_id;
    // DB 내부 결제 고유 번호

    @Column(name="impuid")
    private String impuid;
    // 결제 고유 번호

    @Column(length = 255)
    private String name;
    // 상품명

    @Column
    private String status;
    // 결제 여부

    @Column
    private Long amount;
    // 금액

    // Dto -> Entity 변환
    public PaymentEntity(PaymentDto dto) {
        this.impuid = dto.getImpuid();
        this.amount = dto.getAmount();
        this.name = dto.getName();
        this.status = dto.getStatus();
    }
}

PaymentRepository

package com.test.springtest.payment.repository;

import com.test.springtest.payment.entity.PaymentEntity;
import org.springframework.data.jpa.repository.JpaRepository;


public interface PaymentRepository extends JpaRepository<PaymentEntity, String> {
    public long countByImpuidContainsIgnoreCase(String impuid); // 결제 고유 번호 중복 확인
}

PaymentService

package com.test.springtest.payment.service;

import com.siot.IamportRestClient.IamportClient;
import com.siot.IamportRestClient.exception.IamportResponseException;
import com.siot.IamportRestClient.response.IamportResponse;
import com.siot.IamportRestClient.response.Payment;
import com.test.springtest.payment.dto.PaymentDto;
import com.test.springtest.payment.entity.PaymentEntity;
import com.test.springtest.payment.repository.PaymentRepository;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service
public class PaymentService {

    private final IamportClient iamportClient;
    private final PaymentRepository paymentRepository;

    public PaymentService(PaymentRepository paymentRepository) {
        // 포트원 토큰 발급을 위해 API 키 입력
       this.iamportClient = new IamportClient("REST_API_KEY",
                "REST_API_SECRET");
        this.paymentRepository = paymentRepository;
    }

    public PaymentDto verifyPayment(String imp_uid) throws IamportResponseException, IOException {
        IamportResponse<Payment> iamportResponse = iamportClient.paymentByImpUid(imp_uid); // 결제 검증 시작
        Long amount = (iamportResponse.getResponse().getAmount()).longValue(); // 결제 금액
        String name = iamportResponse.getResponse().getName(); // 상품명
        String status = iamportResponse.getResponse().getStatus(); // paid 이면 1

        PaymentDto paymentDto = PaymentDto.builder() // Dto 변환
                .impuid(imp_uid)
                .amount(amount)
                .status(status)
                .name(name)
                .build();

        if (paymentRepository.countByImpuidContainsIgnoreCase(imp_uid) == 0) { // 중복하는 값이 없으면
            if (iamportResponse.getResponse().getStatus().equals("paid")) { // 결제가 정상적으로 이루어졌으면
                PaymentEntity paymentEntity = new PaymentEntity(paymentDto); // Dto -> Entity 변환
                paymentRepository.save(paymentEntity); // 변환된 Entity DB 저장
                return paymentDto; // 클라이언트에게 Dto 값 JSON 형태 반환
            } else {
                paymentDto.setStatus("결제 오류입니다. 다시 시도해주세요."); // 클라이언트에게 Status 값 오류 코드 보냄
                return paymentDto;
            }
        } else {
            paymentDto.setStatus("이미 결제 되었습니다."); // 클라이언트에게 Status 값 오류 코드 보냄
            return paymentDto;
        }
    }
}

결제 완료

Untitled.png

결제 검증 실패

Untitled.png

DB에 반영이 되었을 때

Untitled.png

프론트앤드(React)에서 결제 완료 후 백앤드로 결제 검증 후 DB 저장

import React, {useState} from 'react';
import axios from 'axios';

function App() {
  const [amount, setAmount] = useState(0);

  const handleChange = (e) => {
    setAmount(e.target.value);
  }

  const ClickChargeBtn = (pg_method, amount, nickname, redirect_url) => {
    const { IMP } = window;
    IMP.init('imp가맹점번호'); // 가맹점 번호 지정
    IMP.request_pay({
      pg : `${pg_method}`, // 결제 방식 지정
      pay_method : 'card',
      merchant_uid: `mid_${new Date().getTime()}`, // 현재 시간
      name : '포인트 충전',
      amount : `${amount}`, // 충전할 금액
      buyer_email : 'Iamport@chai.finance',
      buyer_name : `${nickname}`, // 충전 요청한 유저의 닉네임
      buyer_tel : '010-1234-5678',
      buyer_addr : '서울특별시 강남구 삼성동',
      buyer_postcode : '123-456',
      m_redirect_url: `${redirect_url}` // 만약 새창에서 열린다면 결제 완료 후 리다이렉션할 주소
  }, function (rsp) { // callback
      if (rsp.success) { // 만약 결제가 성공적으로 이루어졌다면
        axios.get(`http://localhost:8080/verify/` + rsp.imp_uid)
        .then((response) => {
          console.log(response);
        })
        .catch((error) => {
          console.error(error);
        });
        alert("결제 성공");
        console.log(rsp);
      } else {
        alert("결제 실패");
        console.log(rsp);
      }
  }
  );
}
  return (
    <div className="App">
      <h1>Test</h1>
      <p>금액<input type="number" className='amount' onChange={handleChange}></input></p>
      <button onClick={() => ClickChargeBtn('kakaopay', amount, 'nickname', 'http://localhost:3000/redirect')}>카카오페이</button>
      <button onClick={() => ClickChargeBtn('tosspay', amount, 'nickname', 'http://localhost:3000/redirect')}>토스페이</button>
    </div>
  );
}

export default App;

주의

  • CORS 문제 발생할 수도 있습니다.
  • 예제 및 테스트 이기에 Spring, React 코드 예외처리 미흡

Docs

https://developers.portone.io/api/rest-v1?v=v1

반응형
  1. SpringBoot에서 사업자 번호 없이 프로젝트 내에서 토스, 카카오페이 결제를 만들어보자.
  2. 이 글에서는 테스트 결제 부분에 대해서 설명합니다.
  3. Front-end(React) 구성
  4. Back-end(SpringBoot)
  5. SpringBoot 패키지 내 build.gradle에 해당 내용 추가
  6. PaymentController
  7. PaymentDto
  8. PaymentEntity
  9. PaymentRepository
  10. PaymentService
  11. 결제 완료
  12. 결제 검증 실패
  13. DB에 반영이 되었을 때
  14. 프론트앤드(React)에서 결제 완료 후 백앤드로 결제 검증 후 DB 저장
  15. 주의
  16. Docs
'프로젝트' 카테고리의 다른 글
  • 안드로이드 스마트폰으로 무료 SMS 발송 서비스 구현
  • [Android]안드로이드 카카오 로그인 디버깅 키 해시 발급
  • [React - Supabase]생일 축하 카드 서비스
Future0_
Future0_
rm -rf /
Future0_
Luna Developer Blog
Future0_
전체
오늘
어제
  • 분류 전체보기 (112)
    • 프로그래밍 (4)
      • 알고리즘 (4)
    • 보안 (14)
      • Dreamhack (4)
      • Hackthebox (1)
      • Webhacking (9)
    • 프로젝트 (4)
    • 공부 (80)
      • Database (2)
      • Python (11)
      • System (4)
      • Java (13)
      • JSP (13)
      • Spring (11)
      • Kotlin (16)
      • 자료구조 (10)
      • 기계학습 (0)
    • Docker (4)
    • Github (2)
    • Tip (1)
    • 잡담 (2)

블로그 메뉴

  • 홈
  • 태그

공지사항

인기 글

태그

  • 1.9.22
  • Java
  • 디버깅키해시
  • Computer science
  • Kotlin
  • React
  • Database
  • api 통신
  • shared preference
  • 보안
  • spring
  • ViewModel
  • 자바빈즈
  • webhacking
  • Python
  • android studio 삭제
  • 프로그래밍
  • docker
  • SpringBoot
  • native app
  • Android Studio
  • 알고리즘
  • 자료구조
  • 상속
  • 키 해시
  • 코틀린기본문법
  • 컴퓨터
  • dreamhack
  • cs
  • jsp

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.0
Future0_
[React-SpringBoot]온라인 간편 결제(포트원) - 카카오페이, 토스페이 연동 및 API
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.