[Project] 결제 페이지 레이어 구성 순서 ( Iamport )

김호정's avatar
Oct 09, 2024
[Project] 결제 페이지 레이어 구성 순서 ( Iamport )
 

1. 라이브러리 및 의존성 추가

plugins { id 'java' id 'org.springframework.boot' version '3.3.3' id 'io.spring.dependency-management' version '1.1.6' } group = 'shop.mtcoding' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() // 아임포트 관련 maven {url 'https://jitpack.io'} } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-mustache' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-validation' runtimeOnly 'com.mysql:mysql-connector-j' // 아임포트 관련 // // https://mvnrepository.com/artifact/com.github.iamport/iamport-rest-client-java implementation group: 'com.github.iamport', name: 'iamport-rest-client-java', version: '0.2.22' // https://mvnrepository.com/artifact/com.squareup.retrofit2/adapter-rxjava2 implementation group: 'com.squareup.retrofit2', name: 'adapter-rxjava2', version: '2.9.0' // https://mvnrepository.com/artifact/com.google.code.gson/gson implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.5' // https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.9.3' // https://mvnrepository.com/artifact/com.squareup.retrofit2/converter-gson implementation group: 'com.squareup.retrofit2', name: 'converter-gson', version: '2.3.0' } tasks.named('test') { useJUnitPlatform() }
 

2. Controller에서 request 임시 데이터 - view 영역

@GetMapping("/payment/view") public String paymentView(Model model) { // 임시 더미 model.addAttribute("reservationId", "1"); model.addAttribute("posterImg", "/img/inter.jpg"); model.addAttribute("movieTitle", "인터스텔라"); model.addAttribute("showTime", "2024-09-12 (목) 12:00 ~ 16:30"); model.addAttribute("cinema", "서면롯데시네마 Screen 1"); model.addAttribute("people", "성인 2"); model.addAttribute("seat", "1, 4"); model.addAttribute("price", "100원"); model.addAttribute("discount", "0 원"); model.addAttribute("payPrice", "10원"); model.addAttribute("userName", "신민재"); model.addAttribute("email", "example@example.com"); model.addAttribute("phone", "010-1234-5678"); return "payment/view"; }
 

3. view.mustache (controller 연동)

<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>결제 메인페이지</title> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"/> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <link rel="stylesheet" href="/css/payment.css"> <link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/header.css"> <script src="https://cdn.iamport.kr/v1/iamport.js"></script> </head> <body class="view-page"> <!-- 컨테이너 클래스 추가 --> {{> layout/header}} <!-- 메인 컨텐츠 --> <main id="main"> <div class="inner"> <!-- 예약정보 섹션 --> <div class="section__wrapper"> <div class="section__header">예약정보</div> <div class="section__content"> <div class="movie__info"> <div class="poster__box"> <img src="{{posterImg}}" alt="영화 포스터" class="img-fluid"> </div> <h3 class="movie__title">{{movieTitle}}</h3> <ul class="movie__details"> <li> <strong>일시</strong> <div>{{showTime}}</div> </li> <li> <strong>영화관</strong> <div>{{cinema}}관</div> </li> <li> <strong>인원</strong> <div>{{people}}</div> </li> <hr> <li> <strong>좌석</strong> <div>{{seat}}</div> </li> </ul> </div> </div> </div> <!-- 결제수단 섹션 --> <div class="section__wrapper"> <div class="section__header">결제수단</div> <div class="section__content"> <div class="payment__methods"> <h5>할인/포인트</h5> <!-- 관람권 버튼 --> <button id="viewing-ticket-btn" class="btn btn-outline-secondary">관람권</button> <!-- 할인권 버튼 --> <button id="discount-ticket-btn" class="btn btn-outline-secondary">할인권</button> <!-- 구분선 --> <hr> <div class="final__payment__method">최종 결제수단</div> <div class="payment__options"> <button class="btn">신용카드</button> <button class="btn">간편결제</button> </div> </div> </div> </div> <!-- 결제하기 섹션 --> <div class="section__wrapper"> <div class="section__header">결제하기</div> <div class="section__content"> <div class="payment__summary"> <ul> <li>상품금액 <span>{{price}}</span></li> <li>할인금액 <span>{{discount}}</span></li> <li>결제금액 <span>{{payPrice}}</span></li> </ul> <button class="btn" onclick="requestPay()">결제하기</button> </div> </div> </div> </div> </main> {{> layout/footer}} <script> var IMP = window.IMP; IMP.init("imp28446715"); // 고객사 식별코드 function requestPay() { var orderUid = '{{reservationId}}'; var itemName = '{{movieTitle}}'; var paymentPrice = '{{payPrice}}'; var buyerName = '{{userName}}'; var buyerEmail = '{{email}}'; var phone = '{{phone}}'; IMP.request_pay({ // m_redirect_url: "/payment", pg : 'html5_inicis.INIpayTest', pay_method : 'card', merchant_uid: orderUid, // 예매 번호 name : itemName, // 영화 제목 amount : paymentPrice, // 티켓 가격 buyer_email : buyerEmail, // 유저 이메일 buyer_name : buyerName, // 유저 이름 buyer_tel : phone, // 유저 연락처 buyer_postcode : '', // 임의의 값 }, function(rsp) { if (rsp.success) { alert('call back!!: ' + JSON.stringify(rsp)); // 결제 성공 시: 결제 승인 또는 가상계좌 발급에 성공한 경우 // jQuery로 HTTP 요청 jQuery.ajax({ url: "/payment", method: "POST", headers: {"Content-Type": "application/json"}, data: JSON.stringify({ "impUid": rsp.imp_uid, // 포트원 결제 고유번호 "reservationId": rsp.merchant_uid // 주문번호 (예매번호) }) }).done(function (response) { console.log(response); alert('결제가 완료되었습니다.'); window.location.href = "/payment/success"; }) } else { // alert("success? "+ rsp.success+ ", 결제에 실패하였습니다. 에러 내용: " + JSON.stringify(rsp)); alert('결제 실패. 좌석 페이지로 이동합니다.'); window.location.href = "/seat"; // TODO: 결제 실패 페이지를 만들지, 좌석 페이지로 redirect 할지 } }); } </script> </body> </html>
 
 

4. 결제창 요청 (Post)

@PostMapping("/payment") // 결제 프로세스 처리 public ResponseEntity<?> validationPayment(@RequestBody PaymentRequest.SaveDTO saveDTO) { paymentService.save(saveDTO); // 서비스로 전달해서 결제 정보 저장 // "http://localhost/payment?imp_uid=imp_359888216361&merchant_uid=255eae01-2b82-4cbf-b40b-49b12d793703&imp_success=true" return new ResponseEntity<>(Resp.ok(null), HttpStatus.OK); }
 

5. PaymentRequest

package shop.mtcoding.filmtalk.payment; import jakarta.persistence.EntityManager; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; public class PaymentRequest { @Data public static class SaveDTO { private Long reservationId; // 예매 ID (결제 고유 번호) = merchant_uid private String impUid; // 가맹점 ID } }
 

7. Service

💡
save() 메소드
  • 결제가 성공적으로 처리됐는지 포트원 API로 확인 후, 결제 미완료시 예약 데이터 삭제 및 예외 발생.
  • 결제 성공시 결제 정보를 DB에 Inser
package shop.mtcoding.filmtalk.payment; import com.siot.IamportRestClient.IamportClient; import com.siot.IamportRestClient.response.IamportResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import shop.mtcoding.filmtalk.core.error.ex.ExceptionApi404; import shop.mtcoding.filmtalk.core.error.ex.ExceptionApi500; import shop.mtcoding.filmtalk.reservation.Reservation; import shop.mtcoding.filmtalk.reservation.ReservationRepository; import java.sql.Timestamp; import java.time.LocalDateTime; @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class PaymentService { private final PaymentRepository paymentRepository; private final PaymentQueryRepository paymentQueryRepository; private final IamportClient iamportClient; private final ReservationRepository reservationRepository; @Transactional public void save(PaymentRequest.SaveDTO saveDTO) { try { // 클라이언트가 결제 됐다고 알림 -> SaveDTO로 / 예매ID(reservationId), 가맹점ID(impUid) IamportResponse<com.siot.IamportRestClient.response.Payment> iamportResponse = iamportClient.paymentByImpUid(saveDTO.getImpUid()); // 결제 완료가 아니면 if(!iamportResponse.getResponse().getStatus().equals("paid")) { // 티켓도 2장 삭제 // 주문&결제 삭제 (예매=부모 삭제하면, 티켓도 삭제 되는지 확인) reservationRepository.deleteById(saveDTO.getReservationId()); throw new ExceptionApi500("결제 미완료"); } // Payment Insert Reservation reservationPS = reservationRepository.findById(saveDTO.getReservationId()) .orElseThrow(() -> new ExceptionApi404("예매 내역이 존재하지 않아서 결제할 수 없습니다")); Payment payment = Payment.builder() .price(28000.0) // 임시 더미 금액 -> 티켓 2장 가격 .point(0) .state(2) // 결제 완료 .cnclDate(null) .payDate(Timestamp.valueOf(LocalDateTime.now())) .impUid(saveDTO.getImpUid()) .type("card") .mycoupon(null) .reservation(reservationPS) .build(); paymentRepository.save(payment); } catch (Exception e) { throw new ExceptionApi500(e.getMessage()); } } }
 

 

혼동할 수 있는 uid

💡
  • imp_uid : 포트원 결제 고유 번호, 결제 성공 시 포트원에서 반환하는 값
  • reservation_id : 예매 번호(merchant_uid)
 
 
 
 
Share article

keepgoing