본문 바로가기

국비교육/국비교육

day78 - 1117

다중 항목 결제 + 결제 정보 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