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