[스프링]카카오페이 단건결제 api 적용하기

     

    https://developers.kakao.com/console/app

     

    카카오계정

     

    accounts.kakao.com

    카카오 디벨로퍼에 카카오 아이디로 로그인

    https://developers.kakaopay.com/docs/payment/online/single-payment

     

    카카오페이 | 개발자센터

    새로운 기회와 가치를 함께 만들어봐요

    developers.kakaopay.com

     

    위 api를 적용해 보자.


    일단 카카오페이 컨트롤러를 만듦. 

    그리코 카카오 결제 넣을 jsp 페이지 만들어서 연결함. 

    결제 예시 페이지

    그리고 ajax로 보내기 전 데이터 전처리 (및 확인) 을 해줌. 

       $("#btnPay").on("click", function() {
    		$("#btnPay").prop("disabled",true);
    		if($.trim($("#itemCode").val().length<=0)){
    			alert("상품코드를 입력하세요.");
    			$("#itemCode").val("");
    			$("#itemCode").focus();
    			
    			$("#btnPay").prop("disabled",false);
    			return;
    		}
    		if($.trim($("#itemName").val().length<=0)){
    			alert("상품명을 입력하세요");
    			$("#itemName").val("");
    			$("#itemName").focus();
    			
    			$("#btnPay").prop("disabled",false);
    			return;
    		}
    		if($.trim($("#quantity").val().length<=0)){
    			alert("수량을 입력하세요.");
    			$("#quantity").val("");
    			$("#quantity").focus();
    			
    			$("#btnPay").prop("disabled",false);
    			return;
    		}
    		//수량에 숫자가 아닌 다른 문자를 적었을 때 처리.
    		if(!icia.common.isNumber($("#quantity").val())){
    			alert("수량은 숫자만 입력 가능합니다.");
    			$("#quantity").val("");
    			$("#quantity").focus();
    			
    			$("#btnPay").prop("disabled",false);
    			return;
    		}
    		if($.trim($("#totalAmount").val().length<=0)){
    			alert("금액을 입력하세요.");
    			$("#totalAmount").val("");
    			$("#totalAmount").focus();
    			
    			$("#btnPay").prop("disabled",false);
    			return;
    		}
    		if(!icia.common.isNumber($("#totalAmount").val())){
    			alert("금액은 숫자만 입력 가능합니다.");
    			$("#totalAmount").val("");
    			$("#totalAmount").focus();
    			
    			$("#btnPay").prop("disabled",false);
    			return;
    		}

    컨트롤러에 매핑함

    	@RequestMapping (value="/kakao/payReady", method=RequestMethod.POST)
    	@ResponseBody
    	public Response<Object> payReady (HttpServletRequest request, HttpServletResponse response){
    		Response<Object> ajaxRes = new Response<Object>();
    		
    		return ajaxRes;
    	}
    }

    여기까지 해놓고 카카오페이지 개발자 페이지를 띁어보자. 

    우리가 봐야 할 것은 테스트 결제 코드(가맹점 코드는 카카오 측에서 만들어서 주는 코드.)다. 

    페이로드에 필요한것들 설명해줌. 

    required 에 o 되어 있는 것들은 필수적으로 있어야 하는 것. x는 선택.

    예제도 잘 나와 있으니 위 링크는 필수적으로 잘 읽어보고 시작해보자. 

    우선 카카오에서 필요로 하는 정보를 담을 모델을 정의해줌.

    package com.sist.web.model;
    
    import java.io.Serializable;
    
    public class KakaoPayOrder implements Serializable{
    
    	private static final long serialVersionUID = 1L;
    	
    	private String partnerOrderId;	//String	O	가맹점 주문번호, 최대 100자
    	private String partnerUserId;	//String	O	가맹점 회원 id, 최대 100자
    	private String itemName;		//String	O	상품명, 최대 100자
    	private String itemCode;		//String	X	상품코드, 최대 100자
    	private int quantity;			//Integer	O	상품 수량
    	private int totalAmount;		//Integer	O	상품 총액
    	private int taxFreeAmount;		//Integer	O	상품 비과세 금액
    	private int vatAmount;			//Integer	X	상품 부가세 금액(값을 보내지 않을 경우 다음과 같이 VAT 자동 계산(상품총액 - 상품 비과세 금액)/11 : 소숫점 이하 반올림)
    	
    	private String tId;				//결제 고유번호
    	private String pgToken;			//결제 승인 요청을 인증하는 토큰
    									//사용자 결제 수단 선택 완료시, approval_url로 redirection 했을 때 pg_token을 query String으로 변경.
    }

    이렇게 정의함(생성자, getter setter 생략)

    그 다음 다시 컨트롤러로 가서, 필요한 정보를 받아서 객체에 세팅해줌.

    	@RequestMapping (value="/kakao/payReady", method=RequestMethod.POST)
    	@ResponseBody
    	public Response<Object> payReady (HttpServletRequest request, HttpServletResponse response){
    		Response<Object> ajaxRes = new Response<Object>();
    		
    		String orderId = StringUtil.uniqueValue();	//자체 주문번호
    		String userId = CookieUtil.getHexValue(request, AUTH_COOKIE_NAME);
    		String itemCode= HttpUtil.get(request, "itemCode","");
    		String itemName = HttpUtil.get(request, "itemName","");
    		int quantity = HttpUtil.get(request, "quantity", 0);
    		int totalAmount = HttpUtil.get(request, "totalAmount", 0);
    		int taxFreeAmount = HttpUtil.get(request, "taxFreeAmount", 0);
    		int vatAmount = HttpUtil.get(request, "vatAmount", 0);
    		
    		KakaoPayOrder kakaoPayOrder = new KakaoPayOrder();
    		kakaoPayOrder.setPartnerOrderId(orderId);
    		kakaoPayOrder.setPartnerUserId(userId);
    		kakaoPayOrder.setItemCode(itemCode);
    		kakaoPayOrder.setItemName(itemName);
    		kakaoPayOrder.setQuantity(quantity);
    		kakaoPayOrder.setTotalAmount(totalAmount);
    		kakaoPayOrder.setTaxFreeAmount(taxFreeAmount);
    		kakaoPayOrder.setVatAmount(vatAmount);
    		
    		return ajaxRes;
            }

    카카오페이랑 보내고 받는 서비스(Service) 만들고, env.xml에 카카오페이 추가하기. 

    	<!-- ########## 카카오페이 시작 ########## -->
    	<entry key="kakao.pay.host">https://kapi.kakao.com</entry>
    	<entry key="kakao.pay.admin.key">본인이 할당받은 admin key</entry>
    	<entry key="kakao.pay.cid">TC0ONETIME</entry> <!-- 가맹점 코드(10자) 테스트 결제시 사용 -->
    	<entry key="kakao.pay.ready.url">/v1/payment/ready</entry>
    	<entry key="kakao.pay.approve.url">/v1/payment/approve</entry>
    	<entry key="kakao.pay.success.url">http://hiboard.sist.co.kr:8088/kakao/paySuccess</entry>
    	<entry key="kakao.pay.cancel.url">http://hiboard.sist.co.kr:8088/kakao/payCancel</entry><!-- 결제 취소시 url -->
    	<entry key="kakao.pay.fail.url">http://hiboard.sist.co.kr:8088/kakao/payFail</entry>
    	<!-- ########## 카카오페이 종료 ########## -->

    그리고 실제로 사용할 service.java에다가 @Value 어노테이션으로 상수 정의해준다.  

    	//카카오페이 호스트
    	@Value("#{env['kakao.pay.host']}")
    	private String KAKAO_PAY_HOST;
    	//관리자 키
    	@Value("#{env['kakao.pay.admin.key']}")
    	private String KAKAO_PAY_ADMIN_KEY;
    	//가맹점 코드
    	@Value("#{env['kakao.pay.cid']}")
    	private String KAKAO_PAY_CID;
    	//결제 url
    	@Value("#{env['kakao.pay.ready.url']}")
    	private String KAKAO_PAY_READY_URL;
    	//카카오페이 결제 요청 url
    	@Value("#{env['kakao.pay.approve.url']}")
    	private String KAKAO_PAY_APPROVE_URL;
    	//카카오페이 결제 성공 url
    	@Value("#{env['kakao.pay.success.url']}")
    	private String KAKAO_PAY_SUCCESS_URL;
    	//카카오페이 결제 취소 url
    	@Value("#{env['kakao.pay.cancel.url']}")
    	private String KAKAO_PAY_CANCEL_URL;
    	//카카오페이 결제 실패 url
    	@Value("#{env['kakao.pay.fail.url']}")
    	private String KAKAO_PAY_FAIL_URL;

    *Sample 페이지에는 이런 다소 번거로운 과정 없이 바로 사용했는데, 의존성을 낮추기 위해 env.xml 파일을 활용하는 것 같다. 

    이제 카카오 디밸로퍼에 내가 카카오페이를 사용할 도메인을 등록해줌. 

    kakao developer > 내 애플리케이션>플랫폼 등록 에서 도메인 등록할 수 있음.

    객체 하나 더 정의함... 

    public class KakaoPayReady implements Serializable{
    
    	private static final long serialVersionUID = 1L;
    	
    	private String tid;							//String	결제 고유 번호, 20자
    	private String next_redirect_app_url;		//String	요청한 클라이언트(Client)가 모바일 앱일 경우 카카오톡 결제 페이지 Redirect URL
    	private String next_redirect_mobile_url;	//String	요청한 클라이언트가 모바일 웹일 경우 카카오톡 결제 페이지 Redirect URL
    	private String next_redirect_pc_url;		//String	요청한 클라이언트가 PC 웹일 경우 카카오톡으로 결제 요청 메시지(TMS)를 보내기 위한 사용자 정보 입력 화면 Redirect URL
    	private String android_app_scheme;			//String	카카오페이 결제 화면으로 이동하는 Android 앱 스킴(Scheme) - 내부 서비스용
    	private String ios_app_scheme;				//String	카카오페이 결제 화면으로 이동하는 iOS 앱 스킴 - 내부 서비스용
    	private Date created_at;

     

    정의한 객체를 매개변수로 써서 이제 진짜 서비스 로직 만듦. (여기서 사용한 정의하지 않은 객체들은 모두 Spring 프레임워크에서 편리한 rest api 사용을 위해 제공하는 객체들)

    package com.sist.web.service;
    
    import java.net.URI;
    import java.net.URISyntaxException;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.http.HttpEntity;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.MediaType;
    import org.springframework.stereotype.Service;
    import org.springframework.util.LinkedMultiValueMap;
    import org.springframework.util.MultiValueMap;
    import org.springframework.web.client.RestTemplate;
    
    import com.sist.web.model.KakaoPayOrder;
    import com.sist.web.model.KakaoPayReady;
    
    @Service("KaokaoPayService")
    public class KakaoPayService {
    	private static Logger logger = LoggerFactory.getLogger(KakaoPayService.class);
    	
    	//카카오페이 호스트
    	@Value("#{env['kakao.pay.host']}")
    	private String KAKAO_PAY_HOST;
    	//관리자 키
    	@Value("#{env['kakao.pay.admin.key']}")
    	private String KAKAO_PAY_ADMIN_KEY;
    	//가맹점 코드
    	@Value("#{env['kakao.pay.cid']}")
    	private String KAKAO_PAY_CID;
    	//결제 url
    	@Value("#{env['kakao.pay.ready.url']}")
    	private String KAKAO_PAY_READY_URL;
    	//카카오페이 결제 요청 url
    	@Value("#{env['kakao.pay.approve.url']}")
    	private String KAKAO_PAY_APPROVE_URL;
    	//카카오페이 결제 성공 url
    	@Value("#{env['kakao.pay.success.url']}")
    	private String KAKAO_PAY_SUCCESS_URL;
    	//카카오페이 결제 취소 url
    	@Value("#{env['kakao.pay.cancel.url']}")
    	private String KAKAO_PAY_CANCEL_URL;
    	//카카오페이 결제 실패 url
    	@Value("#{env['kakao.pay.fail.url']}")
    	private String KAKAO_PAY_FAIL_URL;
    	
    	
    	
    	public KakaoPayReady kakaoPayReady(KakaoPayOrder order) {
    		
    		
    		KakaoPayReady ready = new KakaoPayReady();
    		
    		if(order!=null) {
    			//RestTemplate = 스프링에서 지원하는 내장 클래스 객체로, 간편하게 rest 방식 API 호출 가능
    			RestTemplate restTemplet = new RestTemplate();
    			
    			//서버로 요청할 헤더 (스프링에서 지원해 줌)
    			HttpHeaders headers = new HttpHeaders();
    			headers.add("Authorization","KakaoAK"+KAKAO_PAY_ADMIN_KEY);
    			headers.add("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE+";charset=utf-8");
    			
    			//서버로 요청할 바디(다형성)
    			MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
    			
    			params.add("cid",KAKAO_PAY_CID);
    			params.add("partner_order_id", order.getPartnerOrderId());
    			params.add("partner_user_id", order.getPartnerUserId());
    			params.add("item_name", order.getItemName());
    			params.add("item_code", order.getItemCode());
    			params.add("quantity", String.valueOf(order.getQuantity()));
    			params.add("total_amount", String.valueOf(order.getTotalAmount()));
    			params.add("tax_free_amount",String.valueOf(order.getTaxFreeAmount()));
    			params.add("approval_url", KAKAO_PAY_SUCCESS_URL);
    			params.add("cancel_url", KAKAO_PAY_CANCEL_URL);
    			params.add("fail_url", KAKAO_PAY_FAIL_URL);
    			
    			//요청하기 위해서 header와 body 합치기
    			//Spring fraimwork에서 제공하는 HttpEntity class에 header와 body 합치기
    			HttpEntity<MultiValueMap<String, String>> body =
    					new HttpEntity<MultiValueMap<String,String>>(params,headers);
    			
    			
    			try {
    				//postForObject 메서드는 POST 요청을 보내고 객체로 결과를 반환받음.
    				ready = restTemplet.postForObject(new URI(KAKAO_PAY_HOST+KAKAO_PAY_READY_URL), body, KakaoPayReady.class);
    				if(ready!=null) {
    					order.settId(ready.getTid());
    					logger.debug("!!!!!!!!!!!ready.getTid:"+ready.getTid());
    					logger.debug("!!!!!!!!!!![kakaoPayService] ready:"+ready);
    				}
    			} 
    			catch (URISyntaxException e) {
    				logger.error("[kakaoService]kakaoPayReady URISyntaxException",e);
    			} 
    	
    		}
    		else {
    			logger.error("[kakaoPayService] kakaoPayOrder is null");
    		}
    		return ready;
    	}
    	
    	
    }

    RestTemplete : rest 형식 api 호출에 필요함. 

    MultivalueMap, HttpEntity : 각각 body, header+body 합쳐주는 거

    postForObject : post로 요청을 보내고 객체로 결과를 받아옴 

    URI:

    • URI 클래스: URI 객체를 생성하고 관련된 기능(상대적, 절대적 URI 처리, URI 구성 요소 가져오기 등)을 제공합니다.
    • 출처: https://wikidocs.net/207550
     

    URL, URI

    ## **URL (Uniform Resource Locator)** **`URL`**은 웹 리소스에 대한 참조로 사용되며, 해당 리소스의 위치와 사용할 프로토콜을 나타냅니다. …

    wikidocs.net

    uri 때문에 예외처리 해줘야 함. 

    그리고 서비스 호출하고 결과 받는 컨트롤러 설계하기

    	
    	@RequestMapping (value="/kakao/payReady", method=RequestMethod.POST)
    	@ResponseBody
    	public Response<Object> payReady (HttpServletRequest request, HttpServletResponse response){
    		Response<Object> ajaxRes = new Response<Object>();
    		
    		String orderId = StringUtil.uniqueValue();	//자체 주문번호
    		String userId = CookieUtil.getHexValue(request, AUTH_COOKIE_NAME);
    		String itemCode= HttpUtil.get(request, "itemCode","");
    		String itemName = HttpUtil.get(request, "itemName","");
    		int quantity = HttpUtil.get(request, "quantity", 0);
    		int totalAmount = HttpUtil.get(request, "totalAmount", 0);
    		int taxFreeAmount = HttpUtil.get(request, "taxFreeAmount", 0);
    		int vatAmount = HttpUtil.get(request, "vatAmount", 0);
    		
    		KakaoPayOrder kakaoPayOrder = new KakaoPayOrder();
    		kakaoPayOrder.setPartnerOrderId(orderId);
    		kakaoPayOrder.setPartnerUserId(userId);
    		kakaoPayOrder.setItemCode(itemCode);
    		kakaoPayOrder.setItemName(itemName);
    		kakaoPayOrder.setQuantity(quantity);
    		kakaoPayOrder.setTotalAmount(totalAmount);
    		kakaoPayOrder.setTaxFreeAmount(taxFreeAmount);
    		kakaoPayOrder.setVatAmount(vatAmount);
    		
    		KakaoPayReady ready= kakaoPayService.kakaoPayReady(kakaoPayOrder);
    		
    		if(ready!=null) {
    			logger.debug("[KakaoPayController] payReady:"+ready);
    			
    			//service에서 kakaopayReady와 중복 (근데 이걸 왜 중복해서 하냐고)
    			kakaoPayOrder.settId(ready.getTid());
    			
    			JsonObject json = new JsonObject();
    			json.addProperty("orderId", orderId);
    			json.addProperty("tId",ready.getTid());
    			json.addProperty("appUrl", ready.getNext_redirect_app_url());
    			json.addProperty("mobileUrl", ready.getNext_redirect_mobile_url());
    			json.addProperty("pcUrl", ready.getNext_redirect_pc_url());
    			
    			ajaxRes.setResponse(0, "success", json);
    			
    		}
    		else {
    			ajaxRes.setResponse(-1, "fail", null);
    		}
    		
    		return ajaxRes;
    	}

    ajax로 jsp 단에서 결과 받음

    icia.ajax.post({
    			url:"/kakao/payReady",
    			data:{
    				itemCode:$("#itemCode").val(),
    				itemName:$("#itemName").val(),
    				quantity:$("#quantity").val(),
    				totalAmount:$("#totalAmount").val(),
    				success:function(response){
    					icia.common.log(response)
    					if(response.code==0){
    						var orderId = response.data.orderId;
    						var tId = response.data.tId;
    						var pcUrl = response.data.pcUrl;
    						
    						$("#orderId").val(orderId);
    						$("#tId").val(tId);
    						$("#pcUrl").val(pcUrl);
    						
    						var win = window.open('','kakaoPopUp','toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=no,width=540,height=700,left=100,top=100');
    						$("#kakaoForm").submit();
    					}
    					else{
    						alert("오류가 발생하였습니다.");
    						$("#btnPay").prop("disabled",false);
    					}
    				},
    				error:function(error){
    					icia.common.error(error);
    					$("#btnPay").prop("disabled",false);
    				}

    이렇게 해준 다음, ifram으로 새로운 창을 띄워서 보여줌 (controller로 jsp 연결) 

    아래 네 개는 model 로 보내주

    		model.addAttribute("pcUrl",pcUrl);
    		model.addAttribute("orderId", orderId);
    		model.addAttribute("tId", tId);
    		model.addAttribute("userId",userId);

     

    댓글