오늘은 아임포트에서 제공하는 결제API를 이용하여 이용자가 주문시
실제 카드결제가 가능하도록 구현해보도록 하겠습니다
아임포트 사이트에 들어가 회원가입을 하시면 다음과 같은 화면을 보실수 있습니다
https://www.iamport.kr/
테스트 연동에 KG이니시스를 선택하여 등록하고 오른쪽 위에 내식별코드를 누르면
가맹점 식별코드와 API키와 API시크릿키를 확인할수 있습니다
이제 사용자가 주문페이지에서 카드결제를 선택시 결제모듈이 뜨도록 하여야합니다
따라서 기존 order.jsp에 결제API라이브러리를 추가하도록 합시다
<!-- iamport.payment.js -->
<script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-1.1.8.js"></script>
// 주문번호 만들기
function createOrderNum(){
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
let orderNum = year + month + day;
for(let i=0;i<10;i++) {
orderNum += Math.floor(Math.random() * 8);
}
return orderNum;
}
// 카드 결제
function paymentCard(data) {
// 모바일로 결제시 이동페이지
const pathName = location.pathname;
const href = location.href;
const m_redirect = href.replaceAll(pathName, "");
IMP.init("가맹점 식별코드");
IMP.request_pay({ // param
pg: "html5_inicis",
pay_method: data.payMethod,
merchant_uid: data.orderNum,
name: data.name,
amount: data.amount,
buyer_email: "",
buyer_name: "",
buyer_tel: data.phone,
buyer_addr: data.deleveryAddress2 + " " + data.deleveryAddress3,
buyer_postcode: data.deleveryAddress1,
m_redirect_url: m_redirect,
},
function (rsp) { // callback
if (rsp.success) {
// 결제 성공 시 로직,
data.impUid = rsp.imp_uid;
data.merchant_uid = rsp.merchant_uid;
paymentComplete(data);
} else {
// 결제 실패 시 로직,
}
});
}
// 계산 완료
function paymentComplete(data) {
$.ajax({
url: "/api/order/payment/complete",
method: "POST",
data: data,
})
.done(function(result) {
messageSend();
swal({
text: result,
closeOnClickOutside : false
})
.then(function(){
location.replace("/orderList");
})
}) // done
.fail(function() {
alert("에러");
location.replace("/");
})
}
order.jsp에 payment() function을 다음과 같이 수정해주도록 합니다
우리는 기존에 orderNum을 서버단에서 생성해줬었는데 사용자가 카드결제 요청시
스크립트단에서 바로 API에 요청이 가므로 서버에서 orderNum을 생성하여
받아올수가 없습니다 따라서 프론트단에서 orderNum을 만들도록 추가하고 기존 서버단에
추가했던 orderNum을 만드는 부분은 모두 지워주도록 합니다
이제 실제 카드결제를 하기전에 데이터변조를 확인하기 위해 결제금액이 맞는지
확인해야 합니다. 이 부분은 예전 주문하기 구현하면서 추가했던 기억이 있으실겁니다
프론트에서의 결제금액이 실제 DB에서 계산된 금액이 일치해야만 주문이 가능하도록
해줘야 합니다. 만약 일치하지 않는다면 결제를 취소해줘야 합니다
일단 application.yml에 결제API키와 API시크릿키를 추가해줍니다
imp_key: 아임포트에서 발급받은 API KEY
imp_secret: 아임포트에서 발급받은 API SECRET KEY
private String impUid; // 아임포트 결제번호 추가
이제 결제에 관련된 부분을 처리해줘야하는데 몇가지 생각할 점이 있습니다
위에서 말했듯이 데이터변조가 있으면 결제를 중지해야 합니다 하지만 변조된 데이터를
확인하기 위해서는 서버에서 DB까지 조회해야하는데 결제는 프론트단에서 이루어집니다
따라서 일단 프론트단에서 카드결제가 완료되면 그 정보를 서버단으로 가져와 데이터가
일치하지 않으면 결제를 취소시키는 방법으로 구현을 할겁니다
// 카드 결제 성공 후
@PostMapping("/api/order/payment/complete")
public ResponseEntity<String> paymentComplete(HttpSession session, OrderInfoDto orderInfo, long totalPrice, @AuthenticationPrincipal CustomUserDetails user) throws IOException {
String token = paymentService.getToken();
System.out.println("토큰 : " + token);
// 결제 완료된 금액
int amount = paymentService.paymentInfo(orderInfo.getImpUid(), token);
try {
// 주문 시 사용한 포인트
int usedPoint = orderInfo.getUsedPoint();
if (user != null) {
int point = user.getPoint();
// 사용된 포인트가 유저의 포인트보다 많을 때
if (point < usedPoint) {
paymentService.payMentCancle(token, orderInfo.getImpUid(), amount, "유저 포인트 오류");
return new ResponseEntity<String>("유저 포인트 오류", HttpStatus.BAD_REQUEST);
}
} else {
// 로그인 하지않았는데 포인트사용 되었을 때
if (usedPoint != 0) {
paymentService.payMentCancle(token, orderInfo.getImpUid(), amount, "비회원 포인트사용 오류");
return new ResponseEntity<String>("비회원 포인트 사용 오류", HttpStatus.BAD_REQUEST);
}
}
CartListDto cartList = (CartListDto) session.getAttribute("cartList");
// 실제 계산 금액 가져오기
long orderPriceCheck = orderService.orderPriceCheck(cartList) - usedPoint;
// 계산 된 금액과 실제 금액이 다를 때
if (orderPriceCheck != amount) {
paymentService.payMentCancle(token, orderInfo.getImpUid(), amount, "결제 금액 오류");
return new ResponseEntity<String>("결제 금액 오류, 결제 취소", HttpStatus.BAD_REQUEST);
}
orderService.order(cartList, orderInfo, user, session);
session.removeAttribute("cartList");
return new ResponseEntity<>("주문이 완료되었습니다", HttpStatus.OK);
} catch (Exception e) {
paymentService.payMentCancle(token, orderInfo.getImpUid(), amount, "결제 에러");
return new ResponseEntity<String>("결제 에러", HttpStatus.BAD_REQUEST);
}
}
이제 Service패키지에 결제 로직을 처리할 PaymentService를 추가합니다
@Service
public class PaymentService {
@Value("${imp_key}")
private String impKey;
@Value("${imp_secret}")
private String impSecret;
@Data
private class Response{
private PaymentInfo response;
}
@Data
private class PaymentInfo{
private int amount;
}
public String getToken() throws IOException {
HttpsURLConnection conn = null;
URL url = new URL("https://api.iamport.kr/users/getToken");
conn = (HttpsURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-type", "application/json");
conn.setRequestProperty("Accept", "application/json");
conn.setDoOutput(true);
JsonObject json = new JsonObject();
json.addProperty("imp_key", impKey);
json.addProperty("imp_secret", impSecret);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
bw.write(json.toString());
bw.flush();
bw.close();
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
Gson gson = new Gson();
String response = gson.fromJson(br.readLine(), Map.class).get("response").toString();
System.out.println(response);
String token = gson.fromJson(response, Map.class).get("access_token").toString();
br.close();
conn.disconnect();
return token;
}
public int paymentInfo(String imp_uid, String access_token) throws IOException {
HttpsURLConnection conn = null;
URL url = new URL("https://api.iamport.kr/payments/" + imp_uid);
conn = (HttpsURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Authorization", access_token);
conn.setDoOutput(true);
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
Gson gson = new Gson();
Response response = gson.fromJson(br.readLine(), Response.class);
br.close();
conn.disconnect();
return response.getResponse().getAmount();
}
public void payMentCancle(String access_token, String imp_uid, int amount, String reason) throws IOException {
System.out.println("결제 취소");
System.out.println(access_token);
System.out.println(imp_uid);
HttpsURLConnection conn = null;
URL url = new URL("https://api.iamport.kr/payments/cancel");
conn = (HttpsURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-type", "application/json");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Authorization", access_token);
conn.setDoOutput(true);
JsonObject json = new JsonObject();
json.addProperty("reason", reason);
json.addProperty("imp_uid", imp_uid);
json.addProperty("amount", amount);
json.addProperty("checksum", amount);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
bw.write(json.toString());
bw.flush();
bw.close();
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
br.close();
conn.disconnect();
}
}
getToken메서드는 토큰을 생성하는 부분입니다 이 토큰을 가지고 결제정보를 가져올수 있는데
paymentInfo메서드 부분입니다 토큰으로 결제정보를 요청할 경우
{"code":0,"message":null,"response":{"amount":6000, .... } 형태의
중첩 JSON문을 받아오는데 우리는 여기서 amount(금액)부분만 가져오면 됩니다
payMentCancle메서드는 주문취소 로직을 가지는데 반드시 토큰정보와
imp_uid , 금액등이 필요합니다 현재 우리는 데이터 변조가 있을때 주문을
취소하는 부분을 구현하고 있으므로 imp_uid가 존재하므로 결제취소에 문제가 없습니다
하지만 일단 데이터 변조가 없이 정상적으로 결제가 완료된 이후에 만약 매장측에서
주문을 취소한경우 imp_uid가 DB상에 존재하지 않아 주문은 취소되지만 결제는
취소가 되지않아 결제한 금액이 붕 떠 버리는 문제가 발생합니다
그러므로 OrderMapper.xml에 order부분에 imp_uid를 추가해주도록 합시다
이제 관리자(사장님)가 주문을 취소할때 결제 토큰을 생성하여 impUid와 총금액으로
결제취소를 요청할수 있도록 기존 orderCancle메서드를 수정해줍니다
//주문취소
@PatchMapping("/api/admin/management/orderCancle")
public ResponseEntity<String> orderCancle(OrderCancelDto orderCancelDto) throws IOException {
System.out.println(orderCancelDto.toString());
if(!"".equals(orderCancelDto.getImpUid())) {
String token = paymentService.getToken();
int amount = paymentService.paymentInfo(orderCancelDto.getImpUid(), token);
paymentService.payMentCancle(token, orderCancelDto.getImpUid(), amount, "관리자 취소");
}
adminService.orderCancel(orderCancelDto);
return ResponseEntity.ok().body("주문취소완료");
}
이제 카드결제시 실제 카드에서 금액이 빠져나가며 관리자화면에서 주문취소시
다시 환불처리됨을 볼 수 있습니다. 결제API인 아임포트의 경우 연동하는 방법이
공식홈페이지에 나와있기 때문에 어렵지 않습니다. 다만 주의해야할점이 토큰과 U_id를
이용하여 주문취소를 하는것까지 정확히 구현하여야 문제가 생기지 않습니다 만약 끝까지
구현하지 않고 중간에 테스트한다고 카드결제 요청시 실제로 금액이 빠져나가고 결제취소를
구현하지 않았기 때문에 실제 그 금액이 붕 떠 버리게 됩니다
물론 몇시간정도 지나면 해당 결제건은 자동으로 취소가 되는걸로 알고 있지만 그렇지 않을경우
직접 고객센터에 연락해서 해결을 해야하니 꼭 끝까지 구현하고 나서 테스트 하는걸 추천드립니다