결제 api인 아임포트(iamport)가 포트원으로 이름을 바꿨다! 아무튼 나는 이걸 사용해서 결제를 구현했다.
결제 기능을 구현하기 전에, 당연히 장바구니/주문 기능부터 구현해야 한다.
이제 주문 -> 결제를 구현하면 된다. 참고로 주문테이블과 결제(또는 결제 내역) 테이블은 따로 관리되어야 한다.
기본적으로 정규화를 따르기 위함이다. (데이터 일관성, 무결성 보장)
물론 개념적으로도 주문 정보와 결제 정보는 별도의 엔티티이다.
주문 정보는 제품, 수량, 가격 등을 포함하고, 결제 정보는 결제 방법 등 서로 다른 정보가 저장되기 때문.
주소, 이름 등을 사용자에게 입력받고, 가격이나 상품 이름, 주문 번호 등은 테이블에서 가져옴으로써 일관성을 유지하는 방법
주문 -> 결제로 이루어지는 동안 동일한 테이블을 사용한다면, 아무리 트랜잭션을 분리해도 한계가 생기며, 쿼리를 따로 관리할 수가 없다.
또한, 주문 후 결제가 제대로 이루어지지 않는 상황 등의 문제를 관리할 수 있다.
포트원에서는 실제 결제 기능과 테스트결제 기능을 모두 제공한다. 실결제는 사업자등록증이 필요하므로 우리가 연결할 것은 포트원 테스트 결제.
포트원(아임포트) 테스트 결제 연동
1. 포트원 관리자 콘솔 회원가입 및 로그인
여기에서 회원가입 후 로그인을 완료한다.
2. 결제 연동
3. 테스트 결제를 연동할 거니까 테스트를 눌러주고,
4. 결제대행사를 선택한다.
대행사, 간편결제 등 엄청 많은데, KG이니시스를 사용하기로 했다. (대행사마다 클라이언트 코드에 들어갈 상점 아이디(MID)가 다르다.
5. 결제 모듈과 MID를 선택하고 저장을 누르면 채널이 만들어진다.
연동관리 > 식별코드에 들어가면 식별코드와 API key들을 볼 수 있는데, 얘네들이 API를 사용할때 필요한 것들이다.
서비스 로직 설계
포트원 동작 과정은 클라이언트에서 결제 -> 완료 -> 결제 정보를 서버로 전송 -> 서버 API에서 결제 완료 처리이다.
- 사용자가 장바구니에서 주문 클릭 -> 장바구니 정보 중 주문 테이블에 필요한 값을 세션에 저장 (주문 준비)
- 나머지 필요한 정보를 사용자에게 입력 받음
- (주문 완료) 결제 클릭 -> 세션+입력값 주문 테이블에 저장 -> 프론트에서 결제 API 실행
- 결제 완료 -> 백엔드에서 결제 완료 진행 -> 결제내역 테이블 저장
따라서 우리 프로젝트의 서비스는 이런 과정으로 구현했다.
엔티티
주문 엔티티
@Getter
@Setter
@Entity
@Table(name = "orders")
public class Orders {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
private Long orderId; // PK
@ManyToOne
@JoinColumn(name = "member_id", nullable = false)
private Member member; // 사용자
// 상품 - 주문 테이블 다대다 구현, 중간 테이블 생성
@ManyToMany
@JoinTable(
name = "orders_product_management",
joinColumns = @JoinColumn(name = "orders_id"),
inverseJoinColumns = @JoinColumn(name = "product_management_id")
)
private List<ProductManagement> productManagements = new ArrayList<>();
@Column(name = "order_name")
private String ordererName; // 주문자 이름
@Column(name = "product_names")
private String productName; // 상품 이름
@Enumerated(EnumType.STRING)
PayMethod payMethod; // 결제 방식
@Column(length = 100, name = "merchant_uid")
private String merchantUid; // 주문번호
@Column(name = "total_price")
private BigDecimal totalPrice; // 가격
@Column(name = "address")
private String address; // 주소
@Column(name = "detail_address")
private String detailAddress; // 상세주소
@Column(name = "post_code", length = 100)
private String postCode; // 우편번호
@Column(name = "phone_number")
private String phoneNumber; // 전화번호
@CreationTimestamp
@Column(name = "created_at", nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime orderDay; // 주문시각
@Column(name = "payment_status")
private Boolean paymentStatus = false; // 결제 상태
@OneToMany(mappedBy = "orders")
private List<PaymentHistory> paymentHistories = new ArrayList<>(); // 결제내역과 일대다
public void orderConfirm(String merchantUid, OrderDto orderDto) {
this.merchantUid = merchantUid;
this.postCode = orderDto.getPostCode();
this.address = orderDto.getAddress();
this.detailAddress = orderDto.getDetailAddress();
this.ordererName = orderDto.getOrdererName();
this.phoneNumber = orderDto.getPhoneNumber();
this.payMethod = orderDto.getPayMethod();
this.orderDay = LocalDateTime.now();
}
public void setPaymentStatus(Boolean paymentStatus) {
this.paymentStatus = paymentStatus;
}
}
결제 엔티티
@Entity
@Getter
@Setter
@Table(name = "payment_history")
public class PaymentHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "payment_history_id")
private Long id; // PK
@ManyToOne
@JoinColumn(name = "member", nullable = false)
private Member member; // 사용자
@ManyToOne
@JoinColumn(name = "orders", nullable = false)
private Orders orders; // 주문 테이블과 다대일 (연관관계 주인은 주문)
@ManyToOne
@JoinColumn(name = "product", nullable = false)
private Product product; // 상품
@Column(name = "product_name")
private String productName; // 상품 이름
@Column(name = "product_option")
private String productOption; // 상품 옵션
@Column(name = "product_price", nullable = false)
private Integer price; // 가격
@Column(name = "total_price", nullable = false)
private Long totalPrice; // 결제한 총 가격
@Column(name = "paid_at", nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime paidAt; // 결제시각
@Column(name = "status")
private Boolean status = true; // 상태
@Column(name = "review")
private Boolean review = false; // 리뷰 작성 여부
public PaymentHistory(Member member, Orders orders, Product product, String productName, String productOption, Integer price, Long totalPrice) {
this.member = member;
this.orders = orders;
this.product = product;
this.productName = productName;
this.productOption = productOption;
this.price = price;
this.totalPrice = totalPrice;
this.paidAt = LocalDateTime.now();
}
public void setReview(Boolean review) {
this.review = review;
}
}
주문하기
Order - 주문 준비
먼저 사용자가 장바구니에서 주문을 클릭하면, 해당 장바구니들의 id를 리스트로 받는다. 그리고 그 안에서 상품 정보 등 필요한 정보를 찾아 주문 테이블을 생성하고, 이를 세션에 임시저장한다.
컨트롤러
@RestController
@RequestMapping("/api/v1/order")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final HttpSession httpSession;
private final ModelMapper modelMapper;
/**
* 주문서에 나타낼 정보
* @param payload "cartIds" : [1,2,3]
* @return
*/
@PostMapping("/create")
public ResponseEntity<String> createOrder(@RequestBody Map<String, Object> payload) {
List<Integer> cartIdsInteger = (List<Integer>) payload.get("cartIds");
List<Long> cartIds = cartIdsInteger.stream().map(Long::valueOf).collect(Collectors.toList());
// 요청받은 장바구니id로 주문 테이블 생성
Orders temporaryOrder = orderService.createOrder(cartIds);
// 세션에 임시 주문 정보를 저장
httpSession.setAttribute("temporaryOrder", temporaryOrder);
httpSession.setAttribute("cartIds", cartIds); // 장바구니 id 저장
Object cartIdsAttribute = httpSession.getAttribute("cartIds");
return ResponseEntity.ok("주문 임시 저장 완료");
}
}
서비스
먼저 장바구니에서 주문을 누를 때 서비스 메서드. 주문 테이블에 필요한 사용자, 상품, 가격 등의 정보를 저장한다.
상품 이름은 단순히 주문하고 결제할때 사용자에게 보여줄 내용이므로, 각각 가져와서 묶어 '상품1, 상품2, 상품3' 와 같이 하나의 문자열로 만들었다.
public Orders createOrder(List<Long> cartIds) {
List<Cart> carts = cartRepository.findByCartIdIn(cartIds);
Long memberId = carts.get(0).getMember().getId();
verifyUserIdMatch(memberId); // 로그인 된 사용자와 요청 사용자 비교
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new NoSuchElementException(MEMBER_NOT_FOUND));
// 주문할 상품들
List<ProductManagement> productMgts = new ArrayList<>();
for (Cart cart : carts) {
ProductManagement productMgt = cart.getProductManagement();
productMgts.add(productMgt);
}
// 모든 장바구니의 memberID가 동일한지 확인
boolean sameMember = carts.stream()
.allMatch(cart -> cart.getMember().getId().equals(memberId));
if (!sameMember || member == null) {
// 동일하지 않거나 회원이 존재하지 않는 경우, 주문 생성 실패
return null;
}
// 주문서 내용 중 사용자에게 입력받지 않고 자동으로 가져올 값 반환
return new Orders(member, productMgts,member.getUsername(),getProductNames(carts),calculateTotalPrice(carts),getMemberPhoneNumber(carts));
}
참고로 주문 테이블 생성시 가격 계산, 상품 이름, 전화번호 가져오는 메서드는 따로 분리했다.
// 주문 상품 이름들을 가져오는 메서드
private String getProductNames(List<Cart> carts) {
StringBuilder productNamesBuilder = new StringBuilder();
for (Cart cart : carts) {
Long productId = cart.getProductManagement().getProduct().getProductId();
Product product = productRepository.findById(productId).orElse(null);
if (product != null) {
if (!productNamesBuilder.isEmpty()) {
productNamesBuilder.append(", ");
}
productNamesBuilder.append(product.getProductName());
}
}
return productNamesBuilder.toString();
}
// 회원 전화번호를 가져오는 메서드
private String getMemberPhoneNumber(List<Cart> carts) {
Long memberId = carts.get(0).getMember().getId();
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new NoSuchElementException(MEMBER_NOT_FOUND));
return (member != null && member.getPhone() != null) ? member.getPhone() : null;
}
// 총 가격을 계산하는 메서드
private BigDecimal calculateTotalPrice(List<Cart> carts) {
BigDecimal totalPrice = BigDecimal.ZERO;
for (Cart cart : carts) {
BigDecimal cartPrice = BigDecimal.valueOf(cart.getPrice());
totalPrice = totalPrice.add(cartPrice);
}
return totalPrice;
}
Order - 주문 완료
그리고 사용자가 이름, 주소 등 주문 정보 입력 후 결제하기를 누르면 주문 테이블이 먼저 저장된다.
컨트롤러
/**
* 주문서에서 입력받아 최종 주문 테이블 생성
* @param request
* @return
*/
@PostMapping("/done")
public ResponseEntity<Object> completeOrder(@RequestBody OrderDto request) {
OrderDto orders = modelMapper.map(request, OrderDto.class);
// 세션에서 임시 주문 정보를 가져옴
Orders temporaryOrder = (Orders) httpSession.getAttribute("temporaryOrder");
if (temporaryOrder == null) {
return ResponseEntity.badRequest().body("임시 주문 정보를 찾을 수 없습니다.");
}
Orders completedOrder = orderService.orderConfirm(temporaryOrder, orders);
OrderResponseDto orderResponseDto = new OrderResponseDto(completedOrder);
return ResponseEntity.ok(orderResponseDto);
}
서비스
/**
* 주문 테이블 저장
* @param temporaryOrder 세션에 저장된 주문서
* @param orders 사용자에게 입력받은 주문 정보
* @return 주문 테이블 저장
*/
public Orders orderConfirm(Orders temporaryOrder, OrderDto orders) {
String merchantUid = generateMerchantUid(); //주문번호 생성
// 세션 주문서와 사용자에게 입력받은 정보 합치기
temporaryOrder.orderConfirm(merchantUid, orders);
return orderRepository.save(temporaryOrder);
}
이 메서드는 단순히 세션에 저장된 상품 정보 + 사용자에게 입력받은 정보로 주문 테이블을 저장하는 메서드이고, 이 때 주문번호를 생성한다. 주문번호는 날짜+UUID로 중복되지 않으면서도 주문번호로 주문일을 확인할 수 있도록 했다.
// 주문번호 생성 메서드
private String generateMerchantUid() {
// 현재 날짜와 시간을 포함한 고유한 문자열 생성
String uniqueString = UUID.randomUUID().toString().replace("-", "");
LocalDateTime today = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String formattedDay = today.format(formatter).replace("-", "");
// 무작위 문자열과 현재 날짜/시간을 조합하여 주문번호 생성
return formattedDay +'-'+ uniqueString;
}
주문번호는 20240418-6f3b1333fe7049aeb3eed702f8f1cef3
의 형태로 나온다.
OrderController 전체 코드
@RestController
@RequestMapping("/api/v1/order")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final HttpSession httpSession;
private final ModelMapper modelMapper;
/**
* 주문서에 나타낼 정보
* @param payload "cartIds" : [1,2,3]
* @return
*/
@PostMapping("/create")
public ResponseEntity<String> createOrder(@RequestBody Map<String, Object> payload) {
List<Integer> cartIdsInteger = (List<Integer>) payload.get("cartIds");
List<Long> cartIds = cartIdsInteger.stream().map(Long::valueOf).collect(Collectors.toList());
Orders temporaryOrder = orderService.createOrder(cartIds);
// 세션에 임시 주문 정보를 저장
httpSession.setAttribute("temporaryOrder", temporaryOrder);
httpSession.setAttribute("cartIds", cartIds); // 장바구니 id 저장
Object cartIdsAttribute = httpSession.getAttribute("cartIds");
return ResponseEntity.ok("주문 임시 저장 완료");
}
/**
* 주문서에서 입력받아 최종 주문 테이블 생성
* @param request
* @return
*/
@PostMapping("/done")
public ResponseEntity<Object> completeOrder(@RequestBody OrderDto request) {
OrderDto orders = modelMapper.map(request, OrderDto.class);
// 세션에서 임시 주문 정보를 가져옴
Orders temporaryOrder = (Orders) httpSession.getAttribute("temporaryOrder");
if (temporaryOrder == null) {
return ResponseEntity.badRequest().body("임시 주문 정보를 찾을 수 없습니다.");
}
Orders completedOrder = orderService.orderConfirm(temporaryOrder, orders);
OrderResponseDto orderResponseDto = new OrderResponseDto(completedOrder);
return ResponseEntity.ok(orderResponseDto);
}
}
OrderSerivce 전체 코드
@Service
@RequiredArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class OrderService {
public final CartRepository cartRepository;
public final OrderRepository orderRepository;
public final ProductRepositoryV1 productRepository;
public final MemberRepositoryV1 memberRepository;
/**
* 주문서 화면에 나타날 정보 (사용자에게 입력받지 않고 자동으로 가져와 화면에 띄워주거나 저장할 값)
* @param cartIds card id 리스트
* @return order 객체 반환
*/
public Orders createOrder(List<Long> cartIds) {
List<Cart> carts = cartRepository.findByCartIdIn(cartIds);
Long memberId = carts.get(0).getMember().getId();
verifyUserIdMatch(memberId); // 로그인 된 사용자와 요청 사용자 비교
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new NoSuchElementException(MEMBER_NOT_FOUND));
// 주문할 상품들
List<ProductManagement> productMgts = new ArrayList<>();
for (Cart cart : carts) {
ProductManagement productMgt = cart.getProductManagement();
productMgts.add(productMgt);
}
// 모든 장바구니의 memberID가 동일한지 확인
boolean sameMember = carts.stream()
.allMatch(cart -> cart.getMember().getId().equals(memberId));
if (!sameMember || member == null) {
// 동일하지 않거나 회원이 존재하지 않는 경우, 주문 생성 실패
return null;
}
// 주문서 반환
return new Orders(member, productMgts,member.getUsername(),getProductNames(carts),calculateTotalPrice(carts),getMemberPhoneNumber(carts));
}
// 주문 상품 이름들을 가져오는 메서드
private String getProductNames(List<Cart> carts) {
StringBuilder productNamesBuilder = new StringBuilder();
for (Cart cart : carts) {
Long productId = cart.getProductManagement().getProduct().getProductId();
Product product = productRepository.findById(productId).orElse(null);
if (product != null) {
if (!productNamesBuilder.isEmpty()) {
productNamesBuilder.append(", ");
}
productNamesBuilder.append(product.getProductName());
}
}
return productNamesBuilder.toString();
}
// 회원 전화번호를 가져오는 메서드
private String getMemberPhoneNumber(List<Cart> carts) {
Long memberId = carts.get(0).getMember().getId();
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new NoSuchElementException(MEMBER_NOT_FOUND));
return (member != null && member.getPhone() != null) ? member.getPhone() : null;
}
// 총 가격을 계산하는 메서드
private BigDecimal calculateTotalPrice(List<Cart> carts) {
BigDecimal totalPrice = BigDecimal.ZERO;
for (Cart cart : carts) {
BigDecimal cartPrice = BigDecimal.valueOf(cart.getPrice());
totalPrice = totalPrice.add(cartPrice);
}
return totalPrice;
}
/**
* 주문 테이블 저장
* @param temporaryOrder 세션에 저장된 주문서
* @param orders 사용자에게 입력받은 주문 정보
* @return 주문 테이블 저장
*/
public Orders orderConfirm(Orders temporaryOrder, OrderDto orders) {
String merchantUid = generateMerchantUid(); //주문번호 생성
// 세션 주문서와 사용자에게 입력받은 정보 합치기
temporaryOrder.orderConfirm(merchantUid, orders);
return orderRepository.save(temporaryOrder);
}
// 주문번호 생성 메서드
private String generateMerchantUid() {
// 현재 날짜와 시간을 포함한 고유한 문자열 생성
String uniqueString = UUID.randomUUID().toString().replace("-", "");
LocalDateTime today = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String formattedDay = today.format(formatter).replace("-", "");
// 무작위 문자열과 현재 날짜/시간을 조합하여 주문번호 생성
return formattedDay +'-'+ uniqueString;
}
}
결제 구현
PaymentController
포트원에서는 JAVA API 연동 모듈을 지원하므로 이걸 사용하면 된다.
https://github.com/iamport/iamport-rest-client-java
@RestController
@RequestMapping("api/v1")
@RequiredArgsConstructor
@Slf4j
public class PaymentController {
@Value("${IMP_API_KEY}")
private String apiKey;
@Value("${imp.api.secretkey}")
private String secretKey;
@PostConstruct
public void init() {
this.iamportClient = new IamportClient(apiKey, secretKey);
}
}
new IamportClient(apiKey, secretKey)
에 위에서 연동하고 받은 REST API key 와 REST API Secret을 넣어주면 된다. 민감한 정보이므로 환경변수로 관리하는 것을 추천한다.
그리고 포트원에서 제공하는 api 모듈을 사용한다. URL의 imp_uid
는 역시 연동 시에 받아온 고객사 식별코드이다.
@PostMapping("/order/payment/{imp_uid}")
public IamportResponse<Payment> validateIamport(@PathVariable String imp_uid, @RequestBody PaymentRequestDto request) throws IamportResponseException, IOException {
IamportResponse<Payment> payment = iamportClient.paymentByImpUid(imp_uid);;
log.info("결제 요청 응답. 결제 내역 - 주문 번호: {}", payment.getResponse().getMerchantUid());
paymentService.processPaymentDone(request);
return payment;
}
결제가 완료되면 세션에 저장했던 값들을 삭제하고, 장바구니에 저장된 값도 삭제한다.
// 결제 완료 화면에서 세션 저장값, 장바구니 삭제하는 로직
@GetMapping("/order/paymentconfirm")
public void deleteSession() {
List<Long>cartIds = (List<Long>) httpSession.getAttribute("cartIds");
for(Long cartId : cartIds){
Cart cart = cartRepository.findById(cartId)
.orElseThrow(() -> new NoSuchElementException("삭제할 장바구니를 찾을 수 없습니다."));
cartRepository.delete(cart);
}
// 세션에서 임시 주문 정보 삭제
httpSession.removeAttribute("temporaryOrder");
httpSession.removeAttribute("cartIds");
=
}
전체 컨트롤러 클래스
@RestController
@RequestMapping("api/v1")
@RequiredArgsConstructor
@Slf4j
public class PaymentController {
private final HttpSession httpSession;
public final OrderRepository orderRepository;
private final PaymentService paymentService;
private final CartRepository cartRepository;
private IamportClient iamportClient;
@Value("${IMP_API_KEY}")
private String apiKey;
@Value("${imp.api.secretkey}")
private String secretKey;
@PostConstruct
public void init() {
this.iamportClient = new IamportClient(apiKey, secretKey);
}
@PostMapping("/order/payment/{imp_uid}")
public IamportResponse<Payment> validateIamport(@PathVariable String imp_uid, @RequestBody PaymentRequestDto request) throws IamportResponseException, IOException {
IamportResponse<Payment> payment = iamportClient.paymentByImpUid(imp_uid);;
log.info("결제 요청 응답. 결제 내역 - 주문 번호: {}", payment.getResponse().getMerchantUid());
paymentService.processPaymentDone(request);
return payment;
}
// 결제 완료 화면에서 세션 저장값, 장바구니 삭제하는 로직
@GetMapping("/order/paymentconfirm")
public void deleteSession() {
List<Long>cartIds = (List<Long>) httpSession.getAttribute("cartIds");
for(Long cartId : cartIds){
Cart cart = cartRepository.findById(cartId)
.orElseThrow(() -> new NoSuchElementException("삭제할 장바구니를 찾을 수 없습니다."));
cartRepository.delete(cart);
}
// 세션에서 임시 주문 정보 삭제
httpSession.removeAttribute("temporaryOrder");
httpSession.removeAttribute("cartIds");
}
/**
* 결제내역 조회
* @param memberId
* @return
*/
@GetMapping("/paymenthistory/{memberId}")
public ResponseEntity<List<PaymentHistoryDto>> paymentList(@PathVariable Long memberId) {
return ResponseEntity.status(HttpStatus.OK).body(paymentService.paymentHistoryList(memberId));
}
}
PaymentService
validateIamport
컨트롤러에서 사용하는 서비스 메서드
@Service
@RequiredArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class PaymentService {
private final OrderRepository orderRepository;
private final MemberRepositoryV1 memberRepository;
private final PaymentRepository paymentRepository;
private final ProductManagementRepository productMgtRepository;
public void processPaymentDone(PaymentRequestDto request) {
Long orderId = request.getOrderId();
Long memberId = request.getMemberId();
verifyUserIdMatch(memberId); // 로그인 된 사용자와 요청 사용자 비교
Long totalPrice = request.getPrice();
List<Long> productMgtIdList = request.getInventoryIdList();
//orders 테이블에서 해당 부분 결제true 처리
Orders currentOrder = orderRepository.findById(orderId)
.orElseThrow(() -> new NoSuchElementException("주문 정보를 찾을 수 없습니다."));
currentOrder.setPaymentStatus(true);
// PaymentHistory 테이블에 저장할 Member 객체
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new NoSuchElementException(ResponseMessageConstants.MEMBER_NOT_FOUND));
// PaymentHistory 테이블에 저장할 Orders 객체
Orders order = orderRepository.findById(orderId)
.orElseThrow(() -> new NoSuchElementException("해당 주문서를 찾을 수 없습니다. Id : " + orderId));
// 주문한 상품들에 대해 각각 결제내역 저장
createPaymentHistory(productMgtIdList, order, member, totalPrice);
}
// 결제내역 테이블 저장하는 메서드
private void createPaymentHistory(List<Long> productMgtIdList, Orders order, Member member, Long totalPrice) {
for (Long productMgtId : productMgtIdList) {
ProductManagement productMgt = productMgtRepository.findById(productMgtId)
.orElseThrow(() -> new NoSuchElementException(ResponseMessageConstants.PRODUCT_NOT_FOUND));
Product product = productMgt.getProduct();
String option = productMgt.getColor().getColor() + ", " + productMgt.getSize().toString(); // 상품옵션 문자열로 저장
PaymentHistory paymentHistory = new PaymentHistory(member, order, product, product.getProductName(),option,product.getPrice(),totalPrice);
paymentRepository.save(paymentHistory);
}
}
사실 사전 검증 단계도 포트원에서 지원하기 때문에 있으면 좋은데, 우선 결제 기능이 완벽히 끝난 후에 추가해볼 생각이다.
클라이언트 구현 - React
이제 프론트 코드를 구현해보자. 결제가 제대로 되는지 확인하려면 있어야 한다..!
해당 코드는 아래 가이드를 참고했다. JavaScript, React.js, Vue.js 모두 예시를 보여준다.
https://developers.portone.io/docs/ko/auth/guide/3?v=v1
장바구니 -> 주문하기 -> 정보입력 -> 결제하기
import React, { useState } from 'react';
import axios from 'axios';
function OrdersPage() {
const [cartIds, setCartIds] = useState('');
const [postCode, setPostCode] = useState('');
const [address, setAddress] = useState('');
const [detailAddress, setDetailAddress] = useState('');
const [ordererName, setOrdererName] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
const [payMethod, setPayMethod] = useState('CARD');
const axiosInstance = axios.create({ withCredentials: true }); // 세션값을 저장하고 사용하기 위해 호출
// 장바구니 -> 주문하기 api 호출 : 세션에 주문정보 저장하는 함수 필요
// 결제하기 누르면 사용자 입력 + 세션 저장값 -> 주문 테이블 -> 결제 함수 호출
async function completeOrder() {
try { // 세션값 사용해야 하므로 axiosInstance 사용
const response = await axiosInstance.post('http://localhost:8080/api/v1/order/done', {
postCode,
address,
detailAddress,
ordererName,
phoneNumber,
payMethod // CARD, TRANS, VBANK, PHONE 중 하나로
});
console.log(response.data);
handlePayment(response.data); // 결제 함수 호출 - 리턴값을 파라미터로
} catch (error) {
console.error('Error completing order:', error);
}
}
결제하기를 누르면 주문 테이블이 저장되고, 저장된 객체를 response로 받아 결제를 진행한다. 여기서 결제가 완료되면 서버에 있는 결제 API가 호출된다.
// 프론트에서 결제 진행 -> 완료되면 그때 백엔드 전달(결제 api 호출)
async function handlePayment(orderInfo) {
// index.html에 iamport CDN 불러와야 사용할 수 있음
window.IMP.init('imp04081725'); // 이 값은 계정 고유번호이므로 고정
window.IMP.request_pay({
pg: "html5_inicis", // 고정
pay_method : orderInfo.payMethod,
merchant_uid : orderInfo.merchantUid,
name : orderInfo.productName,
amount: orderInfo.totalPrice,
buyer_name : orderInfo.ordererName,
buyer_tel : orderInfo.phoneNumber,
buyer_postcode : orderInfo.postCode,
buyer_addr : orderInfo.address
}, (rsp) => {
if (rsp.success) { // 프론트에서 결제가 완료되면
axios.post(`http://localhost:8080/api/v1/order/payment/${rsp.imp_uid}`, {
memberId: orderInfo.memberId ,
orderId:orderInfo.orderId,
price : orderInfo.totalPrice,
inventoryIdList : orderInfo.productMgtIds
}) // 백엔드 결제 api 호출 orderInfo.member.id
.then((res) => {
// 결제완료
})
.catch((error) => {
// 에러발생시
});
} else {
// 에러발생시
}
});
}
테스트
장바구니에 담겨있는 상품을 주문한다. '주문결제' 버튼을 누르면 주문 페이지로 이동한다.
주문 정보가 세션에 저장된다.
사용자가 나머지 정보를 입력하고, 결제수단을 선택해 주문하기를 누르면 주문 테이블이 저장되고, 해당 객체가 response로 반환되면서 이를 결제 API로 보낸다. -> 결제 진행
이때 주문 테이블은 정상적으로 저장된 것을 확인할 수 있고, 결제 상태는 아직 false
이다.
결제를 완료하면 결제 완료 페이지로 넘어가고, 결제내역 테이블이 저장된다. 그리고 주문 테이블의 결제 상태가 true
로 업데이트 된 것을 확인할 수 있다.
또한 장바구니와 세션에 저장된 값이 삭제된다.
당연히 이 상태에서 /order 엔드포인트를 실행하면
세션에 저장된 값이 없으므로 주문결제가 진행되지 않는다.
GitHub 댓글