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
'ETC' 카테고리의 다른 글
Book Store 수정사항 목록 (0) | 2022.07.12 |
---|---|
그 외 정리 등... (0) | 2022.07.12 |
14. 마이페이지-2 (댓글 관리와 개인정보 수정) (0) | 2022.07.11 |
13. 마이페이지-1 (장바구니, 위시 리스트, 주문목록) (0) | 2022.07.11 |
12. Vuex Store ( 로그인 회원정보와 주문정보 Store ) (0) | 2022.07.11 |