다중 항목 결제 + 결제 정보 DB 등록
- 체크박스가 체크된 상품만 결제하도록 구현
1. DB 등록을 위한 준비
1) PAYMENT 테이블 생성
-- 테이블 생성
create table payment(
payment_no number primary key, -- 결제 번호
member_id varchar2(20) not null, -- 회원 번호
item_name varchar2(300) not null, -- 상품명
total_amount number not null check(total_amount >= 0), -- 결제 금액
approve_at date not null, -- 결제 시각
payment_status varchar2(12) not null check(payment_status in ('승인', '취소', '부분취소')), -- 결제 상태
tid varchar2(20) not null -- 거래 번호
);
-- 테이블 삭제
drop table payment;
-- 시퀀스 생성
create sequence payment_seq;
-- 시퀀스 삭제
drop sequence payment_seq;
2) PAYMENT_DETAIL 테이블 생성
-- 테이블 생성
create table payment_detail(
payment_detail_no number primary key, -- 세부 결제 번호
payment_no references payment(payment_no) on delete cascade, -- 결제 번호
product_no number not null, -- 상품 번호
product_name varchar2(30) not null, -- 상품명
qty number not null check(qty >= 1), -- 상품 수량
product_price number not null check(product_price >= 0), -- 상품 가격
payment_detail_status varchar2(6) not null check(payment_detail_status in('승인', '취소')) -- 결제 상태
);
-- 테이블 삭제
drop table payment_detail;
-- 시퀀스 생성
create sequence payment_detail_seq;
-- 시퀀스 삭제
drop sequence payment_detail_seq;
3) payment-mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace = "payment">
<!-- payment 번호 생성 -->
<select id = "paymentSequence" resultType = "int">
select payment_seq.nextval from dual
</select>
<!-- payment 등록 -->
<insert id = "paymentInsert" parameterType = "PaymentDto">
insert into payment(payment_no, member_id, item_name, total_amount, approve_at, payment_status, tid)
values(#{paymentNo}, #{memberId}, #{itemName}, #{totalAmount}, #{approveAt}, '승인', #{tid})
</insert>
<!-- payment_detail 번호 생성 -->
<select id = "paymentDetailSequence" resultType = "int">
select payment_detail_seq.nextval from dual
</select>
<!-- payment_detail 등록 -->
<insert id = "paymentDetailInsert" parameterType = "PaymentDetailDto">
insert into payment_detail(payment_detail_no, payment_no, product_no, product_name, qty, product_price, payment_detail_status)
values(#{paymentDetailNo}, #{paymentNo}, #{productNo}, #{productName}, #{qty}, #{productPrice}, '승인')
</insert>
</mapper>
4) PaymentDao
- 시퀀스 번호 반환 후 그 값을 결제 번호로 하여 결제 정보 등록
- 시퀀스 번호 반환 후 그 값을 결제 상세 번호로 하여 결제 상세 정보 등록
public interface PaymentDao {
// 추상 메소드 - 결제 번호 반환
int paymentSequence();
// 추상 메소드 - 결제 정보 등록
void paymentInsert(PaymentDto paymentDto);
// 추상 메소드 - 세부 결제 번호 반환
int paymentDetailSequence();
// 추상 메소드 - 세부 결제 정보 등록
void paymentDetailInsert(PaymentDetailDto paymentDetailDto);
}
5) PaymentDaoImpl
- 시퀀스 번호 반환 후 그 값을 결제 번호로 하여 결제 정보 등록
- 시퀀스 번호 반환 후 그 값을 결제 상세 번호로 하여 결제 상세 정보 등록
@Repository
public class PaymentDaoImpl implements PaymentDao {
// 의존성 주입
@Autowired
private SqlSession sqlSession;
// 추상 메소드 오버라이딩 - 결제 번호 반환
@Override
public int paymentSequence() {
return sqlSession.selectOne("payment.paymentSequence");
}
// 추상 메소드 오버라이딩 - 결제 정보 등록
@Override
public void paymentInsert(PaymentDto paymentDto) {
sqlSession.insert("payment.paymentInsert", paymentDto);
}
// 추상 메소드 오버라이딩 - 세부 결제 번호 반환
@Override
public int paymentDetailSequence() {
return sqlSession.selectOne("payment.paymentDetailSequence");
}
// 추상 메소드 오버라이딩 - 세부 결제 정보 등록
@Override
public void paymentDetailInsert(PaymentDetailDto paymentDetailDto) {
sqlSession.insert("payment.paymentDetailInsert", paymentDetailDto);
}
}
2. 카카오페이 결제 준비 요청 및 결제 승인 요청
KakaoPayService
- 결제 준비 요청을 보낸 후 결제 준비 응답을 반환
- 결제 승인 요청을 보낸 후 결제 승인 응답을 반환
public interface KakaoPayService {
// 추상 메소드 - 결제 준비 요청을 보낸 후 결제 준비 응답을 반환
KakaoPayReadyResponseVO ready(KakaoPayReadyRequestVO request) throws URISyntaxException;
// 추상 메소드 - 결제 승인 요청을 보낸 후 결제 승인 응답을 반환
KakaoPayApproveResponseVO approve(KakaoPayApproveRequestVO request) throws URISyntaxException;
}
KakaoPayServiceImpl
- 결제 준비 요청을 보낸 후 결제 준비 응답을 반환
- 결제 승인 요청을 보낸 후 결제 승인 응답을 반환
@Slf4j
@Service
public class KakaoPayServiceImpl implements KakaoPayService {
// REST 방식의 API를 호출하기 위한 RestTemplate의 인스턴스 생성
private RestTemplate template = new RestTemplate();
// 의존성 주입 - Admin Key와 cid를 반환하기 위함
@Autowired
private KakaoPayProperties kakaoPayProperties;
// 추상 메소드 오버라이딩 - 결제 준비 요청을 보낸 후 결제 준비 응답을 반환
@Override
public KakaoPayReadyResponseVO ready(KakaoPayReadyRequestVO request) throws URISyntaxException {
// 결제 준비 요청을 보낼 주소 설정
URI uri = new URI("https://kapi.kakao.com/v1/payment/ready");
// REST API 요청의 header 설정
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "KakaoAK " + kakaoPayProperties.getKey()); // Admin Key
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// REST API 요청의 body 설정
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("cid", kakaoPayProperties.getCid()); // 가맹점 코드
body.add("partner_order_id", request.getPartner_order_id()); // 가맹점 주문 번호
body.add("partner_user_id", request.getPartner_user_id()); // 가맹점 회원 ID
body.add("item_name", request.getItem_name()); // 상품명
body.add("quantity", "1"); // 상품 수량
body.add("total_amount", String.valueOf(request.getTotal_amount())); // 상품 총액
body.add("tax_free_amount", "0"); // 상품 비과세 금액
body.add("approval_url", "http://localhost:8888/pay/result/success"); // 결제 성공시 Redirect 주소
body.add("cancel_url", "http://localhost:8888/pay/result/cancel"); // 결제 취소시 Redirect 주소
body.add("fail_url", "http://localhost:8888/pay/result/fail"); // 결제 실패시 Redirect 주소
// REST API 요청을 위한 HttpEntity 생성 - header와 body 결합
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers);
// 요청 전송 후 KakaoPayReadyResponseVO 형태로 응답 반환
KakaoPayReadyResponseVO response = template.postForObject(uri, entity, KakaoPayReadyResponseVO.class);
// 응답 반환
return response;
}
// 추상 메소드 오버라이딩 - 결제 승인 요청을 보낸 후 결제 승인 응답을 반환
@Override
public KakaoPayApproveResponseVO approve(KakaoPayApproveRequestVO request) throws URISyntaxException {
// 결제 승인 요청을 보낼 주소 설정
URI uri = new URI("https://kapi.kakao.com/v1/payment/approve");
// REST API 요청의 header 설정
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "KakaoAK "+kakaoPayProperties.getKey()); // Admin Key
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// REST API 요청의 body 설정
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("cid", kakaoPayProperties.getCid()); // 가맹점 코드
body.add("tid", request.getTid()); // 결제 고유 번호
body.add("partner_order_id", request.getPartner_order_id()); // 가맹점 주문 번호
body.add("partner_user_id", request.getPartner_user_id()); // 가맹점 회원 ID
body.add("pg_token", request.getPg_token()); // 결제 승인을 인증하는 토큰
log.debug("partner_order_id = {}", request.getPartner_order_id());
log.debug("partner_user_id = {}", request.getPartner_user_id());
log.debug("tid = {}", request.getTid());
log.debug("pg_token = {}", request.getPg_token());
// REST API 요청을 위한 HttpEntity 생성 - header와 body 결합
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers);
// 요청 전송 후 KakaoPayApproveResponseVO 형태로 응답 반환
KakaoPayApproveResponseVO response = template.postForObject(uri, entity, KakaoPayApproveResponseVO.class);
// 응답 반환
return response;
}
}
PurchaseVO
- pay2.jsp에서 form으로 전송한 값을 전달하기 위한 VO
@Data
public class PurchaseVO {
// 체크된 구매 상품의 상품 번호와 수량을 List 형태로 저장
private List<PurchaseItemVO> data;
}
PurchaseItemVO
- PurchaseVO의 data에 들어갈 체크 상품 하나에 대한 상품 번호 및 수량 정보
@Data
public class PurchaseItemVO {
private int no; // 구매 상품 번호
private int qty; // 구매 상품 수량
}
PayController
- 구매 페이지(pay2.jsp)로 연결
- 구매 페이지에서 구매할 상품 체크 및 수량을 입력한 후 구매하기 버튼 클릭
- 구매하기 버튼 클릭시 구매할 상품의 번호와 수량을 전송하도록 설정
@Controller
public class PayController {
// 의존성 주입
@Autowired
private KakaoPayService kakaoPayService;
// 의존성 주입 - 상품 목록 조회를 위함
@Autowired
private ProductDao productDao;
// 의존성 주입 - 결제 정보 등록을 위함
@Autowired
private PaymentDao paymentDao;
// 다중 상품 결제 Mapping
@GetMapping("/pay2")
public String pay2(Model model) {
// 상품 목록을 조회하여 Model에 첨부
model.addAttribute("list", productDao.list());
// 상품 목록 페이지(pay2.jsp)로 연결
return "pay2";
}
// 카카오페이 상품 결제 준비 Mapping
@PostMapping("/pay2")
public String pay2(HttpSession session, @ModelAttribute PurchaseVO purchaseVO) throws URISyntaxException {
// 체크된 상품 정보가 없으면 상품 구메 Mapping으로 강제 이동(redirect)
if(purchaseVO.getData().isEmpty()) {
return "redirect:pay2";
}
// 다중 항목 구매에 대한 처리
// 구매 정보를 DB에 등록하기 위한 List
List<ProductDto> list = new ArrayList<>();
// 결제 준비 요청을 위한 총액(total_amount)
int total_amount = 0;
// PurchaseVO의 List 형태인 data 안의 PurchaseItemVO 각각에 대해 다음 시행을 반복
for(PurchaseItemVO vo : purchaseVO.getData()) {
// 상품 번호로 단일 조회하여 ProductDto 반환
ProductDto productDto = productDao.find(vo.getNo());
// 반환한 ProductDto를 List에 저장
list.add(productDto);
// ProductDto에서 반환한 상품 가격과 PurchaseItemVO에서 반환한 갯수의 곱을 총액에 덧셈
total_amount += productDto.getPrice() * vo.getQty();
}
// 다중 항목 구매에 대한 상품명 처리
// 첫 번째 상품의 상품명 반환
String item_name = list.get(0).getName();
// 상품이 2개 이상이라면
if(list.size() >= 2) {
// 상품명이 "[첫 번째 상품명] 외 N개"의 형태가 되도록 문자열 덧셈
item_name += " 외 " + (list.size() - 1) + "개";
}
// 결제 번호 반환
int paymentNo = paymentDao.paymentSequence();
// 결제 준비 요청을 위한 VO에 정보 설정
KakaoPayReadyRequestVO request
= KakaoPayReadyRequestVO
.builder()
.partner_order_id(String.valueOf(paymentNo))
.partner_user_id((String)session.getAttribute("loginId"))
.item_name(item_name)
.total_amount(total_amount)
.build();
// 카카오페이로 결제 준비 요청 전송 후 응답 반환
KakaoPayReadyResponseVO response = kakaoPayService.ready(request);
// 결제 승인을 위한 값을 미리 HttpSession에 저장
session.setAttribute("tid", response.getTid());
session.setAttribute("partner_order_id", request.getPartner_order_id());
session.setAttribute("partner_user_id", request.getPartner_user_id());
// 결제 승인 후 DB 등록을 위한 값을 HttpSession에 저장
session.setAttribute("list", list); // 구매 상품의 List<ProductDto>
session.setAttribute("data", purchaseVO.getData()); // 구매 상품의 List<PurchaseItemVO>
// 사용자를 결제 준비 응답에 포함된 PC URL로 강제 이동(redirect)
return "redirect:" + response.getNext_redirect_pc_url();
}
// 카카오페이 결제 승인 Mapping
@GetMapping("/pay/result/success")
public String paySuccess(HttpSession session, @RequestParam String pg_token) throws URISyntaxException {
// 결제 승인을 위해 HttpSession에 저장했던 값을 반환
String tid = (String)session.getAttribute("tid");
String partner_order_id = (String)session.getAttribute("partner_order_id");
String partner_user_id = (String)session.getAttribute("partner_user_id");
List<ProductDto> list = (List<ProductDto>)session.getAttribute("list");
List<PurchaseItemVO> data = (List<PurchaseItemVO>)session.getAttribute("data");
// 반환이 끝난 값을 HttpSession에서 삭제
session.removeAttribute("tid");
session.removeAttribute("partner_order_id");
session.removeAttribute("partner_user_id");
session.removeAttribute("list");
session.removeAttribute("data");
// 결제 승인 요청을 위한 VO에 정보 설정
KakaoPayApproveRequestVO request
= KakaoPayApproveRequestVO
.builder()
.tid(tid)
.partner_order_id(partner_order_id)
.partner_user_id(partner_user_id)
.pg_token(pg_token)
.build();
// 카카오페이로 결제 승인 요청 전송 후 응답 반환
KakaoPayApproveResponseVO response = kakaoPayService.approve(request);
// Integer 형태의 partner_order_id 값을 int로 변환
int paymentNo = Integer.parseInt(partner_order_id);
// payment 테이블에 결제 정보 등록
paymentDao.paymentInsert(
PaymentDto
.builder()
.paymentNo(paymentNo)
.memberId((String)session.getAttribute("loginId"))
.itemName(response.getItem_name())
.totalAmount(response.getAmount().getTotal())
.approveAt(response.getApproved_at())
.tid(tid)
.build()
);
// payment_detail 테이블에 세부 결제 정보 등록
for(int i = 0 ; i < list.size() ; i ++) {
// 구매 상품 List<ProductDto>에서 ProductDto 반환
ProductDto productDto = list.get(i);
// 구매 상품 List<PurchaseItemVO>에서 PurchaseItemVO 반환
PurchaseItemVO itemVO = data.get(i);
// 세부 결제 번호 반환
int paymentDetailNo = paymentDao.paymentDetailSequence();
// 반환한 값들로 payment_detail 테이블에 정보 등록
paymentDao.paymentDetailInsert(
PaymentDetailDto
.builder()
.paymentDetailNo(paymentDetailNo)
.paymentNo(paymentNo)
.productNo(productDto.getNo())
.productName(productDto.getName())
.qty(itemVO.getQty())
.productPrice(productDto.getPrice() * itemVO.getQty())
.build()
);
}
// 결제 승인 완료 Mapping으로 강제 이동(redirect)
return "redirect:/pay/result/success_view";
}
// 카카오페이 결제 승인 완료 Mapping
@GetMapping("/pay/result/success_view")
public String successView() {
// 최종 결제 성공 페이지(succes_view.jsp)로 연결
return "success_view";
}
}
pay2.jsp
- 체크박스 형태의 input 태그를 생성하여 체크된 상품만 결제
- 구매하기 버튼을 누를 때 새로 form을 생성한 후 체크된 상품의 상품 번호와 갯수를 전송
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %>
<h1>골라서 결제</h1>
<c:forEach var = "productDto" items = "${list}">
<div>
<!-- 체크 박스와 수량 입력란을 생성 -->
<input type = "checkbox" class = "item-check" data-no = "${productDto.no}">
<input type = "number" class = "item-qty" data-no = "${productDto.no}" value = "0">
${productDto.no} /
${productDto.name} /
${productDto.type} /
${productDto.price} /
${productDto.made} /
${productDto.expire} /
</div>
</c:forEach>
<button class = "purchase-btn">구매하기</button>
<!-- JQuery CDN -->
<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
<script>
$(function(){
// 구매하기 버튼에 클릭 이벤트 설정
$(".purchase-btn").click(function(){
// POST 방식의 form 태그 지정
var form = $("<form>").attr("method", "post");
// 값을 배열 형태로 전송하기 위한 배열 index 카운트
var count = 0;
// 체크박스 각각에 대해
$(".item-check").each(function(){
// 체크 여부를 변수로 지정
var checked = $(this).prop("checked");
// 체크 박스가 체크되어 있다면
if(checked) {
// 체크된 상품의 상품 번호를 변수로 지정
var no = $(this).data("no");
// 체크된 상품의 상품 갯수를 변수로 지정
var qty = $(".item-qty[data-no="+ no +"]").val();
// 배열 형태로 값을 전달
// <input type = "hidden" name = "data[i].no"> 형태이며 값은 no의 값
var noTag = $("<input>").attr("type", "hidden").attr("name", "data[" + count + "].no").val(no);
// <input type = "hidden" name = "data[i].qty"> 형태이며 값은 qty의 값
var qtyTag = $("<input>").attr("type", "hidden").attr("name", "data[" + count + "].qty").val(qty);
// form 태그에 해당 input 태그를 연결
form.append(noTag).append(qtyTag);
// 배열 index 카운트를 증가
count ++;
}
});
// 최종 완성된 form 태그를 body에 생성
$("body").append(form);
// form 전송
form.submit();
});
});
</script>
'국비교육 > 국비교육' 카테고리의 다른 글
day80 - 1121 (0) | 2022.11.21 |
---|---|
day79 - 1118 (0) | 2022.11.18 |
day77 - 1116 (0) | 2022.11.16 |
day76 - 1115 (0) | 2022.11.16 |
day75 - 1114 (0) | 2022.11.14 |