본 포스트는 테스트 환경 기준으로 작성되었습니다.
토이 프로젝트로 쇼핑몰 프로젝트를 진행중이다. 결제를 구현하기 위해 가장 유명하고 정보가 많은 포트원을 사용한 적용 과정을 기록할려고 한다.
포트원 사이트 회원가입 후 결제 연동 페이지로 이동.
REST API KEY, REST API SECRET 값 확인.
결제대행사 추가.
포트원 결제창 연동하기 가이드에 예시가 있어 쉽게 만들 수 있다.
import React, { useEffect } from 'react';
const RequestPay = () => {
// 결제 요청 함수
const requestPay = () => {
window.IMP.request_pay({
pg: "html5_inicis",
pay_method: "card",
merchant_uid: "1234578",
name: "스파게티면 500g",
amount: 200,
buyer_email: "gildong@gmail.com",
buyer_name: "홍길동",
buyer_tel: "010-4242-4242",
buyer_addr: "서울특별시 강남구 신사동",
buyer_postcode: "01181"
}, rsp => {
if (rsp.success) {
// 결제 성공 시 로직
console.log('Payment succeeded');
// 추가로 실행할 로직을 여기에 작성
} else {
// 결제 실패 시 로직
console.log('Payment failed', rsp.error_msg);
// 추가로 실행할 로직을 여기에 작성
}
});
};
useEffect(() => {
// 외부 스크립트 로드 함수
const loadScript = (src, callback) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = src;
script.onload = callback;
document.head.appendChild(script);
};
// 스크립트 로드 후 실행
loadScript('https://code.jquery.com/jquery-1.12.4.min.js', () => {
loadScript('https://cdn.iamport.kr/js/iamport.payment-1.2.0.js', () => {
const IMP = window.IMP;
// 가맹점 식별코드
IMP.init("impXXXXXXXX");
});
});
// 컴포넌트가 언마운트될 때 스크립트를 제거하기 위한 정리 함수
return () => {
const scripts = document.querySelectorAll('script[src^="https://"]');
scripts.forEach((script) => script.remove());
};
}, []);
return (
<div>
{/* 결제하기 버튼 */}
<button onClick={requestPay}>Pay Now</button>
</div>
);
};
export default RequestPay;
위와 같이 결제창이 뜨면 성공입니다.
서버에서 아임포트와 통신할 때 아임포트의 API를 참고하여 직접 통신해도 되지만 아임포트에서 제공하는 임포트 REST API 연동 모듈용하는 것이 훨씬 편리하다.
allprojects {
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
dependencies {
// 포트원 REST API 연동 모듈
implementation 'com.github.iamport:iamport-rest-client-java:0.2.23'
}
결제창을 띄우는 프론트엔드를 보여주기 전에 어떤 주문번호로 얼마만큼의 결제가 이루어져야 하는지를 아래의 API를 사용하여 사전에 등록할 수 있습니다.
IMP.request_pay의 인자로 들어온 금액이 위의 API로 사전 등록해둔 금액과 일치하지 않으면 SDK 수준에서 결제 요청이 차단됩니다.
OrderController.java
@Operation(summary = "사전 검증")
@PostMapping("/preparation")
public Response<PreparationResponse> prepareValid(@RequestBody PreparationRequest preparationRequest, Authentication authentication) throws IamportResponseException, IOException {
return Response.success(paymentService.prepareValid(preparationRequest));
}
OrderService.java
public PreparationResponse prepareValid(PreparationRequest request) throws IamportResponseException, IOException {
PrepareData prepareData = new PrepareData(request.getMerchantUid(), request.getTotalPrice());
IamportResponse<Prepare> iamportResponse = iamportClient.postPrepare(prepareData);
log.info("결과 코드 : {}", iamportResponse.getCode());
log.info("결과 메시지 : {}", iamportResponse.getMessage());
if (iamportResponse.getCode() != 0) {
throw new AppException(FAILED_PREPARE_VALID, iamportResponse.getMessage());
}
return PreparationResponse.builder().merchantUid(request.getMerchantUid()).build();
}
결제된 실 금액과 요청 금액을 비교하여 결제금액 위변조여부 검증합니다.(쿠폰이나 멤버쉽에 의한 할인이 있다면 적용시켜야 한다).
금액이 다르다면 주문을 취소하고 예외를 발생시키도록 작업했습니다.
OrderController.java
@Operation(summary = "사후 검증")
@PostMapping("/verification")
public Response<MessageResponse> postVerification(@RequestBody PostVerificationRequest postVerificationRequest, Authentication authentication) throws IamportResponseException, IOException {
log.info("imp_uid:{}", postVerificationRequest.getImpUid());
return Response.success(orderService.postVerification(postVerificationRequest));
}
OrderService.java
// 사후 검증
public MessageResponse postVerification(PostVerificationRequest request) throws IamportResponseException, IOException {
//DB에 merchant_uid가 중복되었는지 확인
Order order = validOrder(request.getMerchantUid());
//DB에 있는 금액과 사용자가 결제한 금액이 같은지 확인
BigDecimal dbAmount = calcDbAmount(order.getOrderItemList()); // db에서 가져온 금액
IamportResponse<Payment> iamResponse = iamportClient.paymentByImpUid(request.getImpUid());
BigDecimal paidAmount = iamResponse.getResponse().getAmount(); // 사용자가 결제한 금액
// 금액이 다르면 결제 취소
if (paidAmount.compareTo(dbAmount) != 0) {
IamportResponse<Payment> response = iamportClient.paymentByImpUid(request.getImpUid());
CancelData cancelData = createCancelData(response, BigDecimal.ZERO);
iamportClient.cancelPaymentByImpUid(cancelData);
throw new AppException(WRONG_PAYMENT_AMOUNT, WRONG_PAYMENT_AMOUNT.getMessage());
}
order.setImpUid(request.getImpUid());
return new MessageResponse("사후 검증 완료되었습니다.");
}
private BigDecimal calcDbAmount(List<OrderItem> orderItemList) {
BigDecimal totalPrice = BigDecimal.ZERO;
for (OrderItem orderItem : orderItemList) {
totalPrice = totalPrice.add(orderItem.getTotalPrice()); // 값을 누적하기 위해 totalPrice를 업데이트
}
return totalPrice;
}
private CancelData createCancelData(IamportResponse<Payment> response, BigDecimal refundAmount) {
if (refundAmount.compareTo(BigDecimal.ZERO) == 0) { //전액 환불일 경우
return new CancelData(response.getResponse().getImpUid(), true);
}
//부분 환불일 경우 checksum을 입력해 준다.
return new CancelData(response.getResponse().getImpUid(), true, refundAmount);
}
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const RequestPay = () => {
const [isPaymentRequested, setIsPaymentRequested] = useState(false);
const [merchantUid, setMerchantUid] = useState(null);
// 임의의 6자리 숫자를 생성하는 함수
const generateRandomNumber = () => {
return Math.floor(100000 + Math.random() * 900000).toString();
};
// 결제 요청 함수
const requestPay = () => {
window.IMP.request_pay({
pg: "html5_inicis",
pay_method: "card",
merchant_uid: merchantUid,
name: "스파게티면 200g",
amount: 200,
buyer_email: "string@naver.com",
buyer_name: "string",
buyer_tel: "string",
buyer_addr: "string",
buyer_postcode: "01181"
}, rsp => {
if (rsp.success) {
// 결제 성공 시 로직
console.log('결제 성공');
console.log(rsp);
createOrder(rsp.imp_uid);
} else {
// 결제 실패 시 로직
console.log('결제 실패', rsp.error_msg);
// 추가로 실행할 로직을 여기에 작성
}
});
};
// Axios POST 요청 함수 (주문 생성)
const createOrder = (imp_uid) => {
console.log(merchantUid);
axios.post('/api/v1/orders', {
itemId : 1,
itemCnt : 1,
recipientName : "string",
recipientTel : "string",
recipientCity : "string",
recipientStreet : "string",
recipientDetail : "string",
recipientZipcode : "string",
merchantUid : merchantUid,
totalPrice : 0,
})
.then((orderResponse) => {
console.log(orderResponse);
if (orderResponse.status === 200) {
console.log('주문이 성공적으로 생성되었습니다.');
// 성공한 경우 사후 검증 API 호출
sendPostVerificationRequest(imp_uid);
} else {
console.error('주문 생성 실패');
}
})
.catch((error) => {
console.error('주문 생성 요청 오류', error);
});
};
// Axios POST 요청 함수 (사전 검증)
const sendPreVerificationRequest = async () => {
try {
const currentDate = new Date();
const year = currentDate.getFullYear();
const month = (currentDate.getMonth() + 1).toString().padStart(2, '0');
const day = currentDate.getDate().toString().padStart(2, '0');
const response = await axios.post('/api/v1/orders/preparation', {
merchantUid: `${year}.${month}.${day}_${generateRandomNumber()}`, // 가맹점 주문번호
totalPrice: 200 // 결제 예정금액
});
if (response.data.resultCode === "SUCCESS") {
console.log(response);
// 사전 검증 성공 시 결제 요청 실행
setIsPaymentRequested(true);
setMerchantUid(response.data.result.merchantUid);
} else {
console.error('사전 검증 실패');
}
} catch (error) {
console.error('사전 검증 요청 오류', error);
}
};
// Axios POST 요청 함수 (사후 검증)
const sendPostVerificationRequest = async (imp_uid) => {
try {
const response = await axios.post('/api/v1/orders/verification', {
merchantUid : merchantUid,
impUid : imp_uid
});
if (response.data.resultCode === "SUCCESS") {
alert(response.data.result.msg);
} else {
console.error('사후 검증 실패');
}
} catch (error) {
console.error('사후 검증 요청 오류', error);
}
};
useEffect(() => {
// 외부 스크립트 로드 함수
const loadScript = (src, callback) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = src;
script.onload = callback;
document.head.appendChild(script);
};
// 스크립트 로드 후 실행
loadScript('https://code.jquery.com/jquery-1.12.4.min.js', () => {
loadScript('https://cdn.iamport.kr/js/iamport.payment-1.2.0.js', () => {
const IMP = window.IMP;
// 아임포트 초기화
IMP.init("impXXXXXXXX");
});
});
// 컴포넌트가 언마운트될 때 스크립트를 제거하기 위한 정리 함수
return () => {
const scripts = document.querySelectorAll('script[src^="https://"]');
scripts.forEach((script) => script.remove());
};
}, []);
useEffect(() => {
// 컴포넌트가 마운트될 때 사전 검증 API 호출
sendPreVerificationRequest();
}, []);
return (
<div>
{/* 결제하기 버튼 */}
<button onClick={requestPay}>지금 결제하기</button>
</div>
);
};
export default RequestPay;
Reference