이상형 월드컵, 굿즈 판매 개인 프로젝트를 진행하는 도중, 굿즈 결제 부분은 포트원 카카오페이 API를 활용하기로 했다.
포트원 | 온라인 비즈니스 성장을 돕는 기업
포트원이 제공하는 단 한 줄의 코드로 세상의 모든 결제를 손쉽게 연동해보세요. PG사 통합결제 연동, 해외결제, 파트너 정산 관리, 결제 애널리틱스, 수수료 혜택까지, 포트원의 맞춤 컨설팅을
portone.io
우선 포트원 사이트에 가서 회원가입을 하고 로그인을 한다.
연동 정보에서 식별코드 탭에 가면 본인의 식별코드, 시크릿 키 등을 확인할 수 있다. 이 코드들은 유출되지 않도록 주의한다.
그리고 채널 관리 탭에 가서 사용할 결제 대행사를 선택 후, 등록을 한다. 단순 테스트를 하고 싶으면 테스트, 실제로 연동을 하고 싶으면 실연동으로. 내 프로젝트는 그저 포트폴리오용이니까 당연히 테스트로
<!-- jQuery CDN 추가 -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<!-- 아임포트 스크립트 추가 -->
<script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-1.2.0.js"></script>
해당 API를 사용할 HTML의 헤드 부분에 추가한다.
<input class="purchase-button" onclick="handlePayment('kakaopay', 'card')" value="구매하기" type="button">
// 아임포트 코드
var impCode = '본인 아임포트 코드';
function handlePayment(pg, payMethod) {
const checkAgree = document.querySelector('.check-agree');
if (!checkAgree.checked) {
checkAgree.nextElementSibling.style.color = 'red';
return;
}
const url = new URL(window.location.href); // url 객체
const urlParams = url.searchParams; // 해당 주소에서 파라미터들 가져오기
const merchantUid = urlParams.get("index"); // 주문 고유 index
console.log("handlePayment");
console.log(pg);
console.log(payMethod);
// 결제하기 버튼 클릭 시 결제 요청
IMP.init(impCode);
IMP.request_pay({
pg: pg,
pay_method: payMethod,
merchant_uid: merchantUid, // 주문번호 생성
name: document.querySelector('.product-name').value, // 제품 이름
amount: document.querySelector('.price').value, // 결제 가격
buyer_name: document.querySelector('.buyerName').value // 주문한 사람
}, function(rsp) {
if (rsp.success) {
// 결제 성공 시
console.log(rsp.imp_uid);
$.ajax({
type: 'POST',
url: '/api/v1/payment/validation/' + rsp.imp_uid
}).done(function(data) {
console.log(data);
// 결제 금액 일치. 결제 성공 처리
$.ajax({
url: "/api/v1/payment/check/" + merchantUid,
method: "post",
contentType: "application/json"
}).then(function(res) {
// saveOrder(merchantUid);
alert("[결제 완료] 제품을 기다려주십시오.");
location.href = '/store/';
}).catch(function(error) {
// 금액이 맞지 않아 결제 실패시 환불처리 및 주문취소
$.ajax({
url: "/api/v1/payment/cancel/" + rsp.imp_uid,
method: "post",
contentType: "application/json"
}).done(function(data) {
cancelOrder('no');
}).catch(function(error) {
alert('환불에 실패하였습니다.');
});
});
}).catch(function(error) {
alert('결제에 실패하였습니다. ' + rsp.error_msg);
});
} else {
alert(rsp.error_msg);
}
});
}
자바스크립트 로직. 일단 결제를 성공한 후, 만약 유저가 악의적으로 HTML로 금액을 조작한 후, 결제한 것이라면 바로 결제가 취소되고 주문이 취소되도록 하였다.
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.iamport</groupId>
<artifactId>iamport-rest-client-java</artifactId>
<version>0.2.22</version>
</dependency>
자바에서 아임포트 클라이언트를 활용하기 위해 pom.xml에 레포시토리와 의존성을 추가하였다.
#iamport
imp.code=본인 아임포트 코드
imp.api.key=본인 api 키
imp.api.secretkey=본인 시크릿키
애플리케이션 프로퍼티로 가서 위에서 발급받은 본인의 아임포트 코드, api 키, 시크릿키를 작성한다. 자바에서 그대로 사용하면 유출의 위험이 있기 때문이다.
@Service
@Slf4j
@RequiredArgsConstructor
public class PaymentService {
private final AdminMapper adminMapper;
private final StoreMapper storeMapper;
private IamportClient iamportClient;
@Value("${imp.api.key}")
private String apiKey;
@Value("${imp.api.secretkey}")
private String secretKey;
@PostConstruct
public void init() {
this.iamportClient = new IamportClient(apiKey, secretKey);
}
/**
* 아임포트 서버로부터 결제 정보를 검증
* @param imp_uid
*/
public IamportResponse<Payment> validateIamport(String imp_uid, HttpSession session) {
try {
IamportResponse<Payment> payment = iamportClient.paymentByImpUid(imp_uid);
log.info("결제 요청 응답. 결제 내역 - 주문 번호: {}", payment.getResponse());
// 내가 결제한 정보들을 객체에 집어넣기
GoodsOrderDto userOrder = new GoodsOrderDto();
userOrder.setIndex(Integer.parseInt(payment.getResponse().getMerchantUid()));
userOrder.setTitle(payment.getResponse().getName());
userOrder.setUserEmail(payment.getResponse().getBuyerName());
userOrder.setPrice(payment.getResponse().getAmount().intValue());
session.setAttribute("userOrder", userOrder); // 세션에 등록
return payment;
} catch (Exception e) {
log.info(e.getMessage());
return null;
}
}
/**
* 아임포트 서버로부터 결제 취소 요청
*
* @param imp_uid
* @return
*/
public IamportResponse<Payment> cancelPayment(String imp_uid) {
try {
CancelData cancelData = new CancelData(imp_uid, true);
IamportResponse<Payment> payment = iamportClient.cancelPaymentByImpUid(cancelData);
return payment;
} catch (Exception e) {
log.info(e.getMessage());
return null;
}
}
/**
* 금액 맞는지 아닌지 체크
*/
public Result check(int goodsOrderId, HttpSession session){
GoodsOrderDto dbGoodsOrder = this.adminMapper.selectGoodsOrderByIndex(goodsOrderId); // DB에 있는 값
GoodsOrderDto userGoodsOrder = (GoodsOrderDto) session.getAttribute("userOrder"); // 유저 결제 내용
UserEntity user = (UserEntity) session.getAttribute("user"); // 유저
if (dbGoodsOrder.getPrice() - dbGoodsOrder.getPrice() * dbGoodsOrder.getDiscount()/100 != userGoodsOrder.getPrice()) {
System.out.println("가격 문제");
return CommonResult.FAILURE;
}
if (goodsOrderId != userGoodsOrder.getIndex()) {
System.out.println("인덱스 문제");
return CommonResult.FAILURE;
}
if (!dbGoodsOrder.getTitle().equals(userGoodsOrder.getTitle())) {
System.out.println("굿즈 이름 문제");
return CommonResult.FAILURE;
}
if (!dbGoodsOrder.getUserEmail().equals(user.getEmail())) {
System.out.println("유저 이름 문제");
return CommonResult.FAILURE;
}
if (dbGoodsOrder.isPaid()) {
System.out.println("이미 결제");
return CommonResult.FAILURE;
}
dbGoodsOrder.setPaid(true);
this.storeMapper.updateGoodsOrder(dbGoodsOrder);
return CommonResult.SUCCESS;
}
}
@RestController
@RequestMapping("/api/v1/payment")
@RequiredArgsConstructor
@Slf4j
public class PaymentController {
private final PaymentService paymentService;
// 결제하기
@PostMapping("/validation/{imp_uid}")
public IamportResponse<Payment> validateIamport(
@PathVariable String imp_uid,
HttpSession session
) throws IamportResponseException, IOException {
log.info("imp_uid: {}", imp_uid);
log.info("validateIamport");
return paymentService.validateIamport(imp_uid, session);
}
// 금액 맞는지 체크
@PostMapping("/check/{goodsOrderId}")
public ResponseEntity<String> processOrder(
@PathVariable int goodsOrderId,
Exception ex,
HttpSession session
) {
Result result = this.paymentService.check(goodsOrderId, session);
if (result == CommonResult.FAILURE) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.ok("성공");
}
// 결제 취소(환불)
@PostMapping("/cancel/{imp_uid}")
public IamportResponse<Payment> cancelPayment(@PathVariable String imp_uid) throws IamportResponseException, IOException {
return paymentService.cancelPayment(imp_uid);
}
}
전체 서비스와 컨트롤러. 우선 해당 결제을 식별하는 주문 번호를 카테고리로 가져와서 결제를 진행한다. 그러면 결제 완료.
하지만 결제 부분만 완성한다면, 어떤 유저가 악의적으로 금액을 수정해서 결제하는 것을 막을 수 없다. 특히 내 프로젝트는 장바구니 시스템이 곧 주문이다. 그러므로 상대적으로 금액이 낮은 제품을 우선 주문하고, 그 후 금액 높은 제품을 다시 주문하여 그 제품의 정보들을 이미 등록한 금액 낮은 제품의 정보로 등록하여 결제하는 편법을 쓸 수 있다.
그래서 우선 결제 부분에서 내가 주문한 정보들을 세션에 집어넣고, 검토 부분에서 내 주문의 정보들과 DB에 등록된 주문 정보가 맞는지 확인하는 컨트롤러 작성 후, ResponseEntity 방식으로 금액이 맞다면 ok, 틀리다면 일부러 오류를 내서 환불처리 POST를 진행하도록 한다. 위와 같은 편법을 쓸 경우 금액 높은 제품이 아닌 편법으로 등록해놓았던 금액 낮은 제품이 대신 결제완료 된다.
시연 결과 제대로 작동하는 것을 확인하였다.
약간 아쉬운 부분은, 포트원 api의 구현방식을 염두해 두지 않고 주문 테이블을 만들었기에, 고유 주문번호를 랜덤 숫자문자 16자리가 아닌 그냥 인덱스 식별값으로 집어넣은 점이 아쉽고, 이미 주문하기 부분을 구현해 놓은 상태였기에 포트원 api에서 권장하는 주문하기 기능을 활용하지 못한 점이 아쉽다.
포트원의 KG 이니시스 방식도 이 처럼 구현하는 방식이 같기 때문에 나중에 똑같은 방식으로 구현하면 좋을 것 같다.
'API' 카테고리의 다른 글
[API] 카카오 로그인 API 구현 (0) | 2024.05.13 |
---|---|
[API] 카카오 지도 API 추가하기 (0) | 2024.03.25 |
[API] 다음 주소 찾기 API (0) | 2024.01.24 |