쇼핑몰 프로젝트에 주문기능을 넣어보았다. 구현했던 것을 정리해보면 크게 3가지로 나눌 수 있었다.
- 아임포트를 이용한 결제연동& 금액확인을 위한 결제검증
- 배송정보에 대한 유효성 검증
- 결제 성공시에 작동하는 주문 비즈니스 로직
1. 아임포트를 이용한 결제연동 & 결제검증
우선 실제로 결제가 되어야 했기에 무료로 사용 가능한 결제대행 API인 아임포트를 이용했다.
원하는 PG사를 선택하고, 가맹점 식별 코드를 이용해서 내 프로젝트에서 사용가능하도록 했다.
우선 자바스크립트 라이브러리를 추가해준다.
<%--아임포트 라이브러리--%>
<script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-1.1.5.js"></script>
<script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.min.js" ></script>
결제페이지에 js를 연동해서 결제 함수를 작성해준다.
결제시에 form태그를 활용해서 필요한 데이터를 서버에 줄 수 있었지만, iamport매뉴얼에 자바스크립트를 활용한 예시가 있었기 때문에 모든 데이터를 불러와서 IMP.request_pay메서드에 넣었다.
자바스크립트를 활용했기 때문에 결제금액, 상태에 대해 변조가 이루어지기 쉽기 때문에 결제 검증을 넣어주는 것이 좋다고 한다. 따라서 처음 요청했던 금액에 대해서 결제가 올바르게 이루어졌는지에 대해서 아임포트 서버로 거래고유번호(imp_uid)를 보내 확인하는 검증과정을 추가했다.
function iamport(){
var flag = $("#flag").val();
var principalId = $("#principalId").val();
var name = $("#name").val();
var phone = $("#phone").val();
var email = $("#email").val();
var postcode = $("#postcode").val();
var address = $("#address").val() + " " + $("#detailAddress").val();
var productName;
var productId = $("#productId").val();
var detailName = $("#productName").val();
var cartName = $("#cartName").val();
var amount = $("#amount").val();
var price = $("#total-price").text();
//가맹점 식별코드
IMP.init("imp20807674");
IMP.request_pay({
pg : 'kcp',
pay_method : 'card',
merchant_uid : 'merchant_' + new Date().getTime(),
name : productName,
amount : price,
buyer_email : email,
buyer_name : name,
buyer_tel : phone,
buyer_addr : address,
buyer_postcode : postcode
}, function(res) {
// 결제검증
$.ajax({
type : "POST",
url : "/verifyIamport/" + res.imp_uid
}).done(function(data) {
if(res.paid_amount == data.response.amount){
alert("결제 및 결제검증완료");
//결제 성공 시 비즈니스 로직
} else {
alert("결제 실패");
}
});
});
}
검증 Token을 받기 위해서 아임포트 관리자페이지[시스템설정]에 있는 REST API키와 REST API secret을 복사해준다.
그 다음, iamport에서 제공하는 객체 및 함수를 사용하기 위해서 pom.xml에 다음 코드를 추가한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<!-- 아임포트 REST API연동 모듈 -->
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository> </repositories>
</repositories>
<!-- 아임포트 REST API연동 모듈 -->
<dependency>
<groupId>com.github.iamport</groupId>
<artifactId>iamport-rest-client-java</artifactId>
<version>0.2.14</version>
</dependency>
|
cs |
위의 자바크스크립트 코드의 ajax에 url로 /veryifyIamport/ + res.imp_uid로 POST방식으로 데이터를 보냈기 때문에 받아줄 Controller을 작성한다.(이 떄 복사해뒀던 RESTAPI KEY, RESTAPI secret을 사용한다.
@Controller
public class ImportApiController {
private IamportClient api;
public ImportApiController() {
// REST API 키와 REST API secret 를 아래처럼 순서대로 입력한다.
this.api = new IamportClient("[복사했던 REST API키]","[복사했던 REST API secret]");
}
@ResponseBody
@RequestMapping(value="/verifyIamport/{imp_uid}")
public IamportResponse<Payment> paymentByImpUid(
Model model
, Locale locale
, HttpSession session
, @PathVariable(value= "imp_uid") String imp_uid) throws IamportResponseException, IOException
{
return api.paymentByImpUid(imp_uid);
}
}
이 때, api.paymentByImpUid함수는 imp_uid를 검사하며, 데이터를 보내주게되는데, 이 데이터와 처음 금액이 일치하는지를 확인하면 된다. <- 이 부분은 맨 위의 자바스크립트 파일에 미리 명시한 부분을 통해 확인이 가능하다.
if(res.paid_amount == data.response.amount){
alert("결제 및 결제검증완료");
아임포트 결제구현은 다음 블로그를 참고했다. https://tyrannocoding.tistory.com/43
2. 배송정보에 대한 유효성 검증
배송정보에 대해서 유효성 검증이 이루어진 다음에 결제가 이루어져야 한다.
만약 그렇지 않다면, 배송지 주소와 같은 정보가 입력되지 않더라도 결제가 이루어지게 될 것이다,
배송정보에는 이름, 전화번호, 우편번호(주소)로 설정했고, 이에 대해서 ajax로 서버에 데이터를 전송한다.
//유효성 검증(배송정보-이름,연락처,우편번호에 대해)
var val_data = {
name: name,
phone: phone,
postcode: postcode
}
$.ajax({
type : "POST",
url : `/api/validation`,
data: JSON.stringify(val_data),
contentType: "application/json; charset=utf-8",
dataType: "json"
}).done(r=>{
alert("배송정보 유효성검사 성공");
//아까 구현했던 결제
배송정보를 받아줄 DeliveryDto를 아래와 같이 명시한 다음,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@AllArgsConstructor
@NoArgsConstructor
@Data
public class DeliveryDto {
@NotBlank(message="이름은 필수입니다.")
private String name;
@Phone //아직은 오류 -> 추후에 만들어 줄 것
private String phone;
@NotBlank(message = "주소는 필수입니다.")
private String postcode;
}
|
cs |
Controller에서 Spring이 지원하는 @Validated어노테이션을 사용해서 api data를 받는다. 참고로, 유효성 검증중 발생하는 오류에 대해서 저장하는 BindingResult객체를 활용해서 오류가 발생할 시에 RuntimeException을 발생시켰다.
@PostMapping("api/validation")
public ResponseEntity<?> validation(@Validated @RequestBody DeliveryDto deliveryDto, BindingResult bindingResult){
if(bindingResult.hasErrors()){
Map<String,String> errorMap = new HashMap<>();
for(FieldError error : bindingResult.getFieldErrors()){
errorMap.put(error.getField(), error.getDefaultMessage());
}
throw new CustomValidationException("배송정보 유효성 검사 실패",errorMap);
}else{
return new ResponseEntity<>(new CMResponseDto<>(1,"성공",""), HttpStatus.OK);
}
}
이름, 우편번호에 대해서는 NotBlank라는 spring지원 어노테이션을 사용해서 유효성검사를 완료하게 된 상태이다. 하지만 전화번호의 경우는 따로 마땅한 어노테이션이 없기 때문에, 별도로 Validation을 만들어 주어야 한다.
아래와 같이 interface를 만들어 주고, @Constrant를 활용해서 어느 클래스에서 사용할 것인지에 대한 명시를 한다.
@Target(FIELD)
@Retention(RUNTIME)
@Constraint(validatedBy = {PhoneValidator.class})
@Documented
public @interface Phone {
String message() default "전화번호 형식이 올바르지 않습니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
해당 인터페이스의 구현체로 PhoneValidator 클래스를 아래와 같이 작성한다. isValid함수를 오버라이딩하여 참이 되는 조건을 적어주면 되는데, 여기에서 내가 원하는 전화번호 형식을 정규표현식으로 compile해준다.
사실 전화번호 형식은 "\\d{3}-\\d{3,4}-\\d{4}"의 형태를 주로 사용하지만, 내 프로젝트의 경우 10~11자 길이만 제한하는 형식으로만 구성해보았다.
public class PhoneValidator implements ConstraintValidator<Phone, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
//10~11길이의 숫자만 허용
Pattern pattern = Pattern.compile("^[0-9]{10,11}$");
Matcher matcher = pattern.matcher(value);
return matcher.matches();
}
}
3. 결제 성공시에 작동하는 주문 비즈니스 로직
배송정보 유효성 검사가 끝났다. 이제 결제 성공을 했을 때 내 프로젝트에서 이루어지는 주문 비즈니스 로직을 구현해준다.
다음과 같이 검증이 완료된 다음 위에 작성한다.
if (res.paid_amount == data.response.amount) {
// alert("결제검증완료");
//비즈니스 로직
orderProcess(req_data, res.imp_uid);
} else {
//환불 요청
cancelPayment(res.imp_uid);
}
[OrderProcess]
//주문 비즈니스 로직
function orderProcess(req_data, imp_uid){
$.ajax({
type: "POST",
url: `/api/order`,
data:JSON.stringify(req_data),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (rsp) {
alert("결제되었습니다.");
location.href = "/mypage/"+rsp.data;
},
error: function (xhr) {
var errorResponse =JSON.parse(xhr.responseText);
var errorMessage = errorResponse.errorMessage;
alert(errorMessage);
// 결제 취소 요청
cancelPayment(imp_uid);
}
});
}
컨트롤러에서 아래와 같이 함수를 현재 세션의 userid와 dto를 받아 makeOrder()메서드를 호출한다.
Order order = orderService.makeOrder(principalDetail.getUser().getId(),paymentDto);
서비스에서는 장바구니 구매의 경우 장바구니에 등록된 상품을 모두 주문 하는 로직이기 때문에 상세페이지에서 주문을 하는 경우와 장바구니에서 주문을 하는 경우를 나눈다. 배송, 주문상품를 차례로 만들고 구매 시에 사용된 포인트를 고려해서 적절하게 주문 객체를 생성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
@Transactional
public Order makeOrder(int userId, PaymentDto paymentDto){
int maxPoint=0; //사용 제한 포인트
//회원 찾기
User user = userRepository.findById(userId).orElseThrow(()->{
return new CustomBusinessApiException(ErrorCode.NOT_FOUND_USER);
});
//delivery 생성
Delivery delivery = Delivery.createDelivery(paymentDto.getAddress(), DeliveryStatus.배송전);
//OrderItem 생성
List<OrderItem> orderItemList = new ArrayList<>();
if(paymentDto.getFlag() == 0){ //상세페이지
//상품 찾기
Product product = productRepository.findById(paymentDto.getProductId()).orElseThrow(()->{
return new CustomBusinessApiException(ErrorCode.NOT_FOUND_PRODUCT);
});
OrderItem orderItem = OrderItem.createOrderItem(product,product.getPrice(),paymentDto.getAmount());
orderItemList.add(orderItem);
maxPoint = product.getPrice()/2; //사용 제한 포인트 금액 구하기
}else{ //장바구니
List<Cart> cartList = cartRepository.loadCartByUserId(userId);
for(Cart cart : cartList){
OrderItem orderItem = OrderItem.createOrderItem(cart.getProduct(),cart.getProduct().getPrice(),cart.getProductCount());
orderItemList.add(orderItem);
maxPoint += orderItem.getOrderItemTotalPrice(); //사용 제한 포인트 금액 구하기
//장바구니에서는 삭제
cartRepository.deleteById(cart.getId());
user.getCarts().remove(cart);
}
maxPoint = maxPoint/2;
}
//포인트 차감
int usedPoint = paymentDto.getPointAmount(); //사용한 포인트
int userTotalPoint = user.getPoint().getAmount(); //회원의 총 포인트
if(usedPoint > userTotalPoint){ //총 포인트보다 많으면 에러발생
throw new CustomBusinessApiException(ErrorCode.EXCEED_POINT);
}
if(usedPoint > maxPoint){ //사용 제한 포인트보다 많으면 포인트 MAX로 전환
usedPoint = maxPoint;
}
user.getPoint().changePoint(user.getPoint().getAmount() - usedPoint); //회원의 포인트 차감
//Order 생성
Order order = Order.createOrder(user,delivery,orderItemList, usedPoint);
//포인트 적립
double EarnedPoint = order.getFinalOrderPrice() * 0.05; //최종 구매 금액의 5% 지급
user.getPoint().changePoint((int) (user.getPoint().getAmount() + EarnedPoint));
//DB에 저장
deliveryRepository.save(delivery);
for(OrderItem orderItem : order.getOrderItemList()){
orderItemRepository.save(orderItem);
}
orderRepository.save(order);
return order;
}
|
cs |
테스트 해본 결과 모두 초록불이 떴다.
'Projects > 쇼핑몰 프로젝트' 카테고리의 다른 글
AWS EC2를 이용한 Springboot + Mysql 서비스 배포 (0) | 2023.03.31 |
---|---|
Can not issue data manipulation statements with executeQuery() 에러 해결 (0) | 2023.03.30 |
트랜잭션 스크립트 패턴 > 도메인 모델 패턴으로 변환해보기 (0) | 2023.03.03 |
[JUnit] Spring Security 로그인 테스트 (0) | 2023.02.27 |
[장바구니] 장바구니 기능구현(상품추가, 수량변경) (0) | 2023.01.13 |
댓글