본문 바로가기

ETC

15. 결제와 환불 (아임포트 모듈 사용)

 

 

12. Vuex Store ( 로그인 회원정보와 주문정보 Store )

index.js import Vue from 'vue'; import Vuex from "vuex"; import createPersistedState from 'vuex-persistedstate' import toOrderStore from "@/store/modules/toOrderStore"; import member from "@/store/..

dwc04112.tistory.com

위 Vuex Store에 저장된 toOrderStore state에서 책 정보 (bid와 수량)을 들고온다

 

결제의 동작순서는

 

1. vue에서 결제정보 입력

2. Springboot로 정보 전송 후 주문번호 생성 후 리턴

3. 주문번호 받아서 vue script에서 아임포트로 결제요청 후 

 

-> 결제가 성공하면 4번으로

-> 결제가 실패하면 2번에서 생성한 주문번호를 삭제 후 홈으로

 

4. Spring으로 결제정보를 넘겨서 처리

5. 처리 후 다시 Vue로 결과 전송

 

 

결제 시연영상은 아래로

 

 

 

들어가기 전 테이블 컬럼 정보

1. order

    @Id
    @Column(name = "orderId")
    private long orderId;         //1. 주문번호

    @Column(unique = true)
    private long mid;               //2. 사용자 id
    private String buyerName;
    private String postcode;           //3. 우편번호
    private String addr;            //4. 주소
    private String detailAddr;      //5. 상세주소
    private String phoneNum;        //6. 핸드폰 번호
    private LocalDate orderDate;    //7. 주문날짜
    private LocalTime orderTime;    //8. 주문시간
    private String orderState;      //9. 주문상태
    private int deliverCost;        //10. 배송료
    private String isDel;

 

2. orderItem

    @Id
    @Column(name = "orderItemId")
    private long orderItemId;     //1. 상품번호

   
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "orderId", insertable = false, updatable = false)
    @JsonIgnore
    private Orders orders;           //2. 주문번호

    @Column(unique = true)
    private long bid;               //3. 책 id
    private int bookCount;          //4. 책 수량
    private int bookSalePrice;      //5. 판매 가격
    private String bookTitle;
    private String bookThumb;

order테이블과 N대1관계를 가지고있는 테이블이다.

order목록을 호출하면 해당 order의 orderItem도 같이 출력된다.

 

3. payments

    @Id
    @Column(name = "paymentId")
    private String paymentId;          //1. imp_uid

    @Column(unique = true)
    private long orderId;            //2. order id = merchant_uid
    private String payMethod;        //3. 결제수단
    private String pgProvide;        //4. 결제승인된 pg
    private int paidAt;              //5. 결제시간
    private String payStatus;        //6. 결제상태
    private int payAmount;           //7. 결제금액
    private int cancelAmount;
    private String buyerName;        //8. 주문자 이름
    private String bankName;         //9. 가상계좌 은행명
    private String bankHorder;       //10. 가상계좌 예금주

결제상태는 ready, paid, cancel 아임포트에서 넘어오는 데이터

 

 

 

결제부분

1. vue에서 결제정보 입력

 //유효성 검사
    orderDataCheck(){

      let nameTest = /^[가-힣a-zA-Z]+$/.test(this.order.buyer_name)
      let nameCheck = false
      let addrCheck = false
      let phoneCheck =  /^01([0|1|6|7|8|9])-?([0-9]{3,4})-?([0-9]{4})$/.test(this.num)

      if(!nameTest){
        alert("이름을 확인해주세요")
        nameCheck = false;
        return false;
      }else{
        nameCheck = true;
      }

      if(!phoneCheck){
        alert("이름을 확인해주세요")
        phoneCheck = false;
      }

      if(this.order.buyer_postcode.length!==5){
        alert("우편번호는 5자리 입니다")
        addrCheck = false;
        return false;
      }else if(this.order.buyer_addr===''){
        alert("주소를 확인해주세요")
        addrCheck = false;
        return false;
      }else if(this.order.buyer_detail_addr==='') {
        alert("상세주소를 확인해주세요")
        addrCheck = false;
        return false;
      }else{
        addrCheck = true;
      }
      if(!this.orderCheck){
        alert("구매 동의를 체크해주세요")
        return false;
      }
      if(nameCheck && addrCheck && phoneCheck && this.orderCheck){
        this.createOrder();
      }
    },

 

 

14. 마이페이지-2 (댓글 관리와 개인정보 수정)

1. 댓글관리 (Comment Entity) https://dwc04112.tistory.com/172 11. 책 상세보기 미리미리 정리해뒀어야 했는데... 도서 사이트를 완성하고 이제서야 작성한다 책 상세보기에는 아래 사진과 같이  1. 책 정보..

dwc04112.tistory.com

(우편번호와 주소는 카카오 API를 통해 가져온다. 위 링크에 카카오 API코드 참고)

 

결제정보를 모두 입력후 정규식을 모두 통과하면?

createOrder() 메소드를 호출하여 새로운 주문번호를 생성한다.

 

//주문번호 생성
    createOrder(){
      let data = {};
      data.bookOrder = this.$store.state.toOrderStore.bookList; //책정보
      data.buyerName = this.order.buyer_name;  //받는사람으로 씀
      data.postcode = this.order.buyer_postcode;                 //우편번호
      data.addr = this.order.buyer_addr;                          //주소
      data.detailAddr = this.order.buyer_detail_addr;          //상세주소
      data.phoneNum = this.num        //구매자 번호

      this.$axios.post("order/",JSON.stringify(data),{
        headers: {
          "Content-Type": `application/json`,
        },
      }).then(response=>{
        this.requestPay(this.payRadios, response.data.data);
      }).catch(error =>{
        console.log(error.response);
      })
    },

 

 

 

2.  Springboot로 정보 전송 후 주문번호 생성

1.번에서 order/으로 요청을 보내면 (아래 코드는 Spring Order Controller)

    //주문번호 생성 및 주문아이템 등록
    //컨트롤러
    @PostMapping("/")
    public ApiResponse<Long> setCartList(@RequestBody SetOrderDTO setOrderDTO) throws Exception {
        log.debug("order controller : "+setOrderDTO);
        return orderService.setCartList(setOrderDTO);
    }

위 컨트롤러에서 정보를 받은 후 Seivce로 정보를 전달한다

 

(아래 코드는 Spring Order Service)

   
    //orders 관련 
    //주문 생성 서비스
    // transactional 의 default = error & runtime e
    @Transactional
    public ApiResponse<Long> setCartList(SetOrderDTO setOrderDTO) throws Exception {
        long newOrderId = newMerchantId(setOrderDTO);

        Orders postData = Orders.builder()
                .orderId(newOrderId)
                .mid(getMemberIdByEmail())
                .buyerName(setOrderDTO.getBuyerName())
                .postcode(setOrderDTO.getPostcode())
                .addr(setOrderDTO.getAddr())
                .detailAddr(setOrderDTO.getDetailAddr())
                .phoneNum(setOrderDTO.getPhoneNum())
                .orderDate(LocalDate.now())
                .orderTime(LocalTime.now())
                .orderState("결제대기중")
                .deliverCost(0)
                .isDel("N")
                .build();
        ordersRepository.save(postData);
        setCartListItem(setOrderDTO.getBookOrder(),newOrderId);
        return new ApiResponse<>(true,"주문번호 생성 성공", newOrderId);
    }
    
    
      // order id 만들기
    public Long newMerchantId(SetOrderDTO setOrderDTO){
        return Long.parseLong(nowDate6()+generateAuth5());   // 6+5 =11
    }
    
    public String nowDate6(){
        Date now = new Date();
        DateFormat formatter = new SimpleDateFormat("yyMMdd");
        return formatter.format(now);
    }
    //난수 생성
    public static int generateAuth5() {
        return ThreadLocalRandom.current().nextInt(10000, 100000);
    }
    // order id 만들기 끝

2. 서비스에서는 주문번호를 생성후 등록한다

 

주문번호는 총 11자리로 지금 날짜와 랜덤번호 5자리 (10000~99999)사이의 숫자

를 조합하여 생성하였다.

noewData6 = 현재 날짜 6자리로 

generateAuth5 = 랜덤 5자리 숫자

 

주문번호를 생성하면 해당 주문번호에 

Vue에서 받은 정보를 저장후 주문번호를 Vue로 넘긴다

 

* 왜 주문번호만 생성하지 않고 정보를 저장하나요?

결제처리가 완료되면 해당 결제내역과 주문정보를 매치하여 검증해야한다

(프론트엔드에서 정보를 무단으로 바꿔서 출력되는 금액과 실제 결제되는 금액이 다르지 않게 하기위해)

 

이 때 사용하는 주문정보가 지금 저장하는 주문정보이다

 

* 그러면 주문정보를 저장했는데 결제가 취소된다면?

결제가 취소된다면 저장한 주문번호를 통해 해당 주문정보를 삭제한다.

 

 

 

3. 아임포트로 결제요청 

const { IMP } = window;

export default {
...
	methods: {
    
    ...
        //import 주문관련
        requestPay(pgData, orderId) {
          //1. 객체 초기화 (가맹점 식별코드 삽입)
          IMP.init(this.impCode);
          //3. 결제창 호출
          IMP.request_pay({
            pg: pgData,					// 카카오결제를 할것인지? : kakaopay 
            							//kg이니시스 결제시 : html5_inicis
                                        
            pay_method: 'card',
            merchant_uid: orderId,
            name: this.order.name,
            amount: this.order.amount,
            buyer_email: this.order.buyer_email,
            buyer_name: this.order.buyer_name,
            buyer_tel: this.num,
            buyer_addr: this.order.buyer_addr,
            custom_data : this.order.buyer_detail_addr,
            buyer_postcode: this.order.buyer_postcode
          }, rsp => { // callback
            console.log(rsp);
            if (rsp.success) {
              let data = {};
              data.imp_uid= rsp.imp_uid;
              data.merchant_uid= rsp.merchant_uid;
              console.log(data)
              this.$axios.post("payments/complete/",JSON.stringify(data),{
                headers: {
                  "Content-Type": `application/json`,
                },
              }).then(response=>{
                console.log(response.data)
                //order state 초기화
                this.$store.dispatch('clearOrderState');
                alert("주문에 성공했습니다. 마이페이지로 이동합니다")
                this.$router.push({name: 'MyOrderComponent'})
              }).catch(error =>{
                console.log(error.response);
              })

            } else {
              let msg = '결제에 실패하였습니다.';
              msg += '에러내용 : ' + rsp.error_msg;
              console.log(msg)

              this.$axios.get("order/stop/"+rsp.merchant_uid)
                  .then(response=>{
                    console.log(response.data)
                    //order state 초기화
                    this.$store.dispatch('clearOrderState').then(()=>{
                      alert("결제가 취소되었습니다. 홈 화면으로 돌아갑니다")
                      this.$router.push({path:'/'})
                    })
                  }).catch(error =>{
                    console.log(error.response);
                  })
            }
          });
     }
 }

결제창에 입력정보를 넣고 결제를 요청한다.

 

성공하면 넘어오는 주문 아이디와 imp_uid를 payments/complete 로 넘긴다

 

실패하면? order/stop/{orderId} 으로 주문 아이디를 넘겨서 해당 주문 정보를 삭제한다.

 

(Spring의 order Controller) 에서 해당 주문아이디를 Service로 넘긴 후

   //결제 중단
    @GetMapping("/stop/{orderId}")
    public ApiResponse<Long> stopCartList(@PathVariable Long orderId) throws Exception {
        log.debug("order controller : "+orderId);
        return orderService.stopPayments(orderId);
    }

(Spring의 order Service)

    public ApiResponse<Long> stopPayments(Long orderId) {
        try {
            Optional<Orders> orderData = ordersRepository.findOrdersByOrderIdAndIsDel(orderId, "N");
            Orders data = orderData.orElseThrow(() -> new RuntimeException("no data : find order by order_id"));
            data.updateOrderState("결제중단");
            data.updateIsDel("Y");
            Orders result = ordersRepository.save(data);
            return new ApiResponse<>(true,"결제 중단된 주문정보를 처리했습니다. 주문번호 : "+result.getOrderId() + ", state : " + result.getOrderState());
        }catch (Exception e){
            log.debug("Exception (updateState) : "+ e);
        }
        return null;
    }

 

해당 주문번호로 주문정보를 가져오고 ->

주문상태를 변경한다.

 

 

 

4. Spring으로 결제정보를 넘겨서 처리

(아래는 Spring의  Payment Controller)

    @ResponseBody
    @PostMapping("/complete")
    public ApiResponse<String> getOrderInfo(@RequestBody ImportDTO importDTO)throws Exception{
        log.debug("imp data : " + importDTO);
        return paymentsService.getOrderInfo(importDTO);
    }

 

Service로 넘어가기전 

결제정보 처리는 

 

1. 결제부분에서 정보를 받은 후 

2. 아임포트 토큰을 요청후 받고 (getToken 에서 토큰 요청)

2. post 요청을 담당하는 postRequest로 토큰과 결제정보를 아임포트로 전송해서 아임포트에 저장된 결제정보를 받는다.

4. 받은 결제정보와 (2.  Springboot로 정보 전송 후 주문번호 생성) 에서 저장한 정보를 비교 후 

5-1. 결제정보가 맞으면? setPaymentsInfo로 정보를 넘겨 payments테이블에 결제정보를 저장 후 Vue로

5-2. 정보비교가 실패하면 결제실패 메시지 반환

 

1. 결제 정보를 처리하는 메인 서비스

final String API_URL = "https://api.iamport.kr";

	//1. 메인 서비스
    public ApiResponse<String> getOrderInfo(ImportDTO importDTO) {

        try {
            ApiResponse<PaymentsDTO> response = postRequest(getToken(), "/payments/"+importDTO.getImp_uid(), null);
            if(response==null){
                throw new Exception();
            }
            if(!response.isSuccess()){
                return new ApiResponse<>(false, "결제 정보 불러오기를 실패했습니다. 에러코드 "+ response.getData().getCode());
            }else{
                // 결제정보 불러오기에 성공하면 결제정보 비교
                return compareData( response.getData().getResponse(), importDTO);
            }
        }catch (Exception e){
            log.debug("Exception (getOrderInfo) : "+ e);
        }
        return null;
    }

 

 

 

2. 토큰을 발급받는 메소드

    // 토큰 사용
    public String getToken()  {
        int nowUnixTime = (int) System.currentTimeMillis() / 1000;

        AccessToken auth = new AccessToken();

        //토큰이 없으면?
        if(auth.getToken() == null){
            return Objects.requireNonNull(getAuth()).getToken();
        }

        //시간 지났으면 재발급
        if(auth.getExpired_at() <  nowUnixTime){
            return Objects.requireNonNull(getAuth()).getToken();
        }else{
            return auth.getToken();
        }
    }

토큰이 없다면? 토큰을 발급받고

토큰의 발급시간과 만료시간을 비교해 시간이 지났으면 다시 토큰을 발급해주는 메소드이다

 

아래는 AsseccToken

@Data
public class AccessToken {

    String token;
    int expired_at;
    int now;

    public String getToken() {
        return this.token;
    }
}

토큰 클래스에는 만료시간과 발급시간 그리고 String 타입의 토큰이 있다.

 

 

 

3. payments에서 모든 Post 요청을 담당하는 PostRequest

여기서는 아임포트에 해당 결제정보를 요청하는 PostRequest이다.

final String API_URL = "https://api.iamport.kr";

	// Post Request
    private ApiResponse<PaymentsDTO> postRequest(String auth,String url, JSONObject body){

        try {
            HttpHeaders headers = new HttpHeaders();
            headers.add("Content-Type", "application/json");
            headers.add("Authorization",auth);

            // body 있을때 없을때
            HttpEntity<String> entity = new HttpEntity<>(headers);
            if(body!=null){
                entity = new HttpEntity<>(body.toString(), headers);
            }
            ResponseEntity<PaymentsDTO> response = rt.postForEntity(API_URL+url, entity, PaymentsDTO.class);
            log.debug("post Rsp : " + response);

            if(response.getStatusCodeValue() == 200){
                return new ApiResponse<>(true, response.getBody() );
            }else{
                return new ApiResponse<>(false, response.getBody() );
            }

        }catch (Exception e){
            log.debug("Exception (postRequest) : "+ e);
        }
        return null;
    }

토큰과, url뒷부분, body를 담아서 post에 요청 후 결과를 리턴한다.

 

 

 

4. compareData에서는 위 요청에서 받은 결과를 비교한다.

 private ApiResponse<String> compareData(PaymentsRsp paymentsRsp, ImportDTO importDTO) {
        //결제 정보 비교
        int amountToBePaid = orderService.getTotalAmount(importDTO.getMerchant_uid());
        log.debug("compare : " + paymentsRsp.getAmount() + " and " + amountToBePaid);

        if(amountToBePaid == paymentsRsp.getAmount()){
            Payments saveData = setPaymentsInfo(paymentsRsp);           //결제정보 저장
            boolean orderUpdate = orderService.updateState(saveData.getOrderId(), saveData.getPayStatus());   //order 상태 업데이트
            if(!orderUpdate){
                return new ApiResponse<>(false, "결제 후, 주문상태 업데이트를 실패했습니다." );
            }
            switch (saveData.getPayStatus()){
                case ("ready") : {
                    return new ApiResponse<>(true, "가상계좌 발급 부분" );
                    //추가예정
                }
                case ("paid") : {
                    return new ApiResponse<>(true, "일반결제 완료." );
                }
            }
        } else { // 결제금액 불일치. 위/변조 된 결제
            return new ApiResponse<>(false, "위조된 결제 시도." );
        }
        return null;
    }

 

 

 

5. compareData에서 검증에 성공하면 Payments테이블에 저장

  // save payments
    public Payments setPaymentsInfo(PaymentsRsp data) {
        Payments payments = Payments.builder()
                .paymentId(data.getImp_uid())
                .orderId(data.getMerchant_uid())
                .payMethod(data.getPay_method())
                .pgProvide(data.getPg_provider())
                .paidAt(data.getPaid_at())
                .payStatus(data.getStatus())
                .payAmount(data.getAmount())
                .cancelAmount(0)
                .buyerName(data.getBuyer_name())
                .bankName(data.getVbank_name())
                .bankHorder(data.getVbank_holder())
                .build();
        return paymentsRepository.save(payments);
    }

그리고 다시 4.compareData -> 1. 결제 정보를 처리 순으로 이동한다

 

 

 

5. 처리 후 다시 Vue로 결과 전송

결과가 성공적으로 들어오면 해당 주문을 확인하기 위해서 마이페이지로 이동한다

 

 

 

 

환불부분     (마이페이지 주문내역 에서 실행가능)

  //환불관련
    @ResponseBody
    @PostMapping("/cancel")
    public ApiResponse<String> cancelPay(@RequestBody ImportDTO importDTO)throws Exception{
        log.debug("cancel Payments : " + importDTO);
        return paymentsService.cancelPay(importDTO);
    }

payments컨트롤러에서 cancelPay를 요청한다.

 

(아래는 payments Service 부분)

 //환불부분
    public ApiResponse<String> cancelPay(ImportDTO importDTO)throws Exception {
        Optional<Payments> payments = this.paymentsRepository.findPaymentsByOrderId(importDTO.getMerchant_uid());
        Payments data = payments.orElseThrow(() -> new RuntimeException("no data : find payments by order_id"));
        int cancelableAmount = data.getPayAmount() - data.getCancelAmount();
        if (cancelableAmount <= 0) { // 이미 전액 환불된 경우
            return new ApiResponse<>(false,"이미 전액환불된 주문입니다.");
        }

        JSONObject body= new JSONObject();
        body.put("imp_uid", data.getPaymentId());
        body.put("amount", data.getPayAmount());    //전액환불
        body.put("checksum", cancelableAmount);

        ApiResponse<PaymentsDTO> response = postRequest(getToken(),"/payments/cancel", body);
        if(response==null){
            throw new Exception();
        }
        if(!response.isSuccess()){
            return new ApiResponse<>(false, "결제취소 정보 불러오기를 실패했습니다. 에러코드 "+ response.getData().getCode());
        }else{
            if(cancelUpdate(data, response)){
                return new ApiResponse<>(true, "결제취소 완료");
            }else{
                return new ApiResponse<>(false, "결제취소 후, 주문상태 업데이트를 실패했습니다." );
            }
        }
    }

payments테이블에서 해당 주문번호로 결제정보를 불러온다

그리고 결제금액에서 환불금액을 뺀 금액이 0원보다 높을 시 환불을 진행한다.

결제부분 에서 (3. payments에서 모든 Post 요청을 담당하는 PostRequest) 으로

환불을 진행하는 url과 body 그리고 토큰을 넘겨준다

결제취소에 성공하면 아래 CancelUpdate를 수행한다

 

    @Transactional
    public boolean cancelUpdate(Payments data, ApiResponse<PaymentsDTO> response)throws Exception {
        // 결제정보 불러오기에 성공하면
        data.updateCancel("cancel", response.getData().getResponse().getCancel_amount());
        paymentsRepository.save(data);
        boolean result = orderService.updateState(data.getOrderId(), data.getPayStatus());
        return Objects.equals(data.getPayStatus(), "cancel") && result;
    }

payments의 정보를 변경하고 order의 주문상태를 변경해주는 작업을 한다.

 

 

 


참고한 내용

 

[결제연동] 일반결제

일반결제 연동하기 이 문서는 일반 결제 기능을 구현하는 방법을 설명합니다. STEP1아임포트 라이브러리 추가하기 client-side 주문 페이지에 아임포트 라이브러리를 추가합니다.최신 라이브러리

docs.iamport.kr

 

 

GitHub - iamport/iamport-rest-client-java-hc: Apache HttpClient기반의 java용 아임포트 REST API클라이언트입니다.

Apache HttpClient기반의 java용 아임포트 REST API클라이언트입니다. Contribute to iamport/iamport-rest-client-java-hc development by creating an account on GitHub.

github.com