[SpringBoot] 포트원 API 이용한 결제 기능 추가

결제 연동을 하기 위해서는 가장 간단한 방법으로 포트원 API를 이용하는 것이다. 각 PG사에서 제공하는 API를 이용하여 개발해야 하고 각 PG사마다 제공하는 API에 맞게 개발을 해야 하는데 포트원 API를 사용하면 모든 PG사와 결제수단을 쉽게 이용할 수 있다. 그리고 테스트 연동도 제공한다. 테스트 연동을 하면 자정 전에 결제 취소가 자동으로 된다.

 

PG는 Payment Gateway 약자로 거래 및 결제를 도와주는 결제 대행사이다. 포트원은 모든 PG 모아서 한 번에 제공해 준다.

 

 

포트원 API 결제 프로세스

결제 프로세스는 간단하다. 구매 페이지에서 결제창을 호출하고 PG사에 결제 인증을 요청한 다음 PG사에서 받은 결제 키 전달받는다. 그리고 PG사에 결제 요청을 하고 카드사에 결제 요청을 한 다음 고객은 결제 결과를 받는다.

 

결제 연동

포트원 관리자 콘솔에서 채널추가

결제 연동을 하기 위해서 먼저 포트원 관리자 콘솔에서 채널을 만들어야 한다.

왼쪽 탭 결제 연동 > 연동 정보에서 아래 정보대로 채널을 추가한다. 

결제대행사 칸에서 KG이니시스를 선택하고, 결제 모듈은 일반/정기결제 선택 후 다음 누르고 채널 추가 화면에서 채널이름을 기입하고 PG 상점아이디를 INIpayTest를 선택하면 된다.

 

의존성 추가

프로젝트 build.gradle.kts 파일에 아래 의존성을 추가한다.

repositories {
    mavenCentral()
    maven(url = "https://jitpack.io")
}

dependencies {
	implementation("com.github.iamport:iamport-rest-client-java:0.2.23")
}

repositores에 maven(url = "https://jitpack.io")을 추가하여 GitHub 저장소에 있는 프로젝트를 의존성으로 추가할 수 있다.

 

결제창 호출

위에서 봤드시 결제를 하려면 결제창을 먼저 호출해야 한다. 결제창 호출 가이드 문서를 보면 SDK를 추가하기 위해 script를 추가하고

<script src="https://cdn.iamport.kr/v1/iamport.js"></script>

 

그다음엔 포트원 SDK 초기화를 한다. 

저 고객사 식별코드는 연동정보 > 식별코드에서 고객사 식별코드 탭의 정보이다.

 

그리고 결제창 불러오기를 IMP.request_pay() 함수를 호출해서 열 수가 있다.

pg: 정보 기입란에 {PG사 코드}.{상점 ID}를 넣어줘야 하는데 포트원 콘솔에서 채널정보에서 알 수 있다.

PG사 코드값은 PG Provider의 값이고, 상점 ID는 PG상점 아이디의 값이다. 

위 정보를 토대로 하면 pg 값은 "html5_inicis.INIpayTest" 이고, 아래는 script 코드이다.

<script>
    var IMP = window.IMP;
    IMP.init("imp22283555");

    function requestPay() {
        const productId = document.getElementById("productId").value;
        const itemId = document.getElementById("itemId").value;
        const itemOption = document.getElementById("itemOption").value;
        const itemName = document.getElementById('itemName').value;
        const quantity = document.getElementById('quantity').value;
        const price = document.getElementById('price').value;
        const buyerEmail = document.getElementById('buyerEmail').value;
        const buyerName = document.getElementById('buyerName').value;
        const buyerTelNo = document.getElementById('buyerTelNo').value;
        const postCode = document.getElementById('sample4_postcode').value;
        const addr = document.getElementById('sample4_jibunAddress').value;

        IMP.request_pay(
            {
                pg: "html5_inicis.INIpayTest",
                pay_method: "card",
                merchant_uid: `payment-${crypto.randomUUID()}`, // 주문 고유 번호
                name: itemName,
                amount: price,
                buyer_email: buyerEmail,
                buyer_name: buyerName,
                buyer_tel: buyerTelNo,
                buyer_addr: addr,
                buyer_postcode: postCode,
            },
            function (response) {
                // 결제 종료 시 호출되는 콜백 함수
                // response.imp_uid 값으로 결제 단건조회 API를 호출하여 결제 결과를 확인하고,
                // 결제 결과를 처리하는 로직을 작성합니다.
                if (response.success) {
                    if (response.success) {
                        console.log("구매이력 저장 로직실행")
                    }
                } else {
                    console.log(response)
                    alert(response)
                }
            },
        );
    }

상품명과 금액, 구매자 정보는 UI에서 가져와서 인입해줬다. 주소는 카카오지도 API를 이용했다.

 

결제화면과 결제창 호출과 결제

 

 

 

html 화면을 간단하게 만들어서 KG 이니시스 결제창 불러오기를 성공하였고 결제를 하게 되면 포트원 콘솔 통합 결제 메뉴에서 확인이 가능하다.

필터에서 테스트 결제를 체크해야만 한다.

 

 

결제를 성공하면 아래 처럼 reponse가 오고 success필드는 true이다. 이 결제 내역의 imp_uid나 merchant_uid를 통해서 결제를 취소할 수 있다. 

[response JSON 값]

response 예제
{
    "success": true,
    "imp_uid": "imp_35168980131",
    "pay_method": "card",
    "merchant_uid": "payment-41693333-e87b-468e-be62-b4485123",
    "name": "오구 슬리퍼",
    "paid_amount": 1990,
    "currency": "KRW",
    "pg_provider": "html5_inicis",
    "pg_type": "payment",
    "pg_tid": "StdpayCARDINIpayTest20240907044147199999",
    "apply_num": "123415",
    "buyer_name": "토비",
    "buyer_email": "test@naver.com",
    "buyer_tel": "010-1111-1111",
    "buyer_addr": "서울 노원구 공릉동",
    "buyer_postcode": "01111",
    "custom_data": null,
    "status": "paid",
    "paid_at": 1725651707,
    "receipt_url": "https://iniweb.inicis.com/DefaultWebApp/mall/cr/cm/mCmReceipt_head.jsp?noTid=StdpayCARDINIpayTest20240907044147194653&noMethod=1",
    "card_name": "BC카드",
    "bank_name": null,
    "card_quota": 0,
    "card_number": "910003*********5"
}

 

결제가 성공하면 위 구매이력을 DB에 저장하고, 재고를 차감하였다.

[script 코드]

if (response.success) {
    console.log("구매이력 저장과 재고차감 로직실행")
    fetch("/v1/payments", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            productId: productId,
            itemId: itemId,
            impUid: response.imp_uid,
            merchantUid: response.merchant_uid,
            itemName: response.name,
            itemOption: itemOption,
            buyQuantity: quantity,
            paidAmount: response.paid_amount,
            buyerName: response.buyer_name,
            buyerEmail: response.buyer_email,
            buyerTelNo: response.buyer_tel,
            buyerAddr: response.buyer_addr,
            buyerPostCode: response.buyer_postcode,
        }),
    }).then((res) => res.json())
        .then((res) => {
            console.log(res)
            if (res.success) {
                alert(res.message);
            } else {
                alert(res.message);
            }
        }).catch(() => {
        alert("ajax 호출 에러")
    });
}

 

[Kotlin 코드]

@PostMapping("/v1/payments")
fun savePayment(@RequestBody savePaymentRequest: SavePaymentRequest): ResponseEntity<MessageDTO> {
    log.info { "/v1/payment post 요청 실행" }

    paymentServiceApplication.savePayment(savePaymentRequest)

    val messageDTO = MessageDTO(
        HttpStatus.OK.value(),
        "상품을 구매 완료 하였습니다."
    )

    return ResponseEntity(messageDTO, HttpStatus.OK)
}
@Transactional
fun savePayment(savePaymentRequest: SavePaymentRequest) {
    //재고차감
    log.info { "재고차감 로직 실행" }
    val item = itemService.getItemById(savePaymentRequest.itemId)
    itemService.deductItemStock(item, savePaymentRequest.buyQuantity)

    //구매이력 저장
    log.info { "구매이력 저장 실행" }
    paymentService.savePayment(savePaymentRequest)
}
@Transactional
fun deductItemStock(item: Item, quantity: Int) {
    val updatedStock: Int = item.stock - quantity
    item.updateStock(updatedStock)
}

@Transactional
fun savePayment(savePaymentRequest: SavePaymentRequest) {
    val payment = savePaymentRequest.toPayment()
    paymentRepository.save(payment)
}

댓글

Designed by JB FACTORY