본문 바로가기

국비교육/국비교육

day79 - 1118

결제 내역 전체 조회

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">

	<!-- 회원 아이디로 해당 회원의 결제 내역 전체 조회-->
	<select id="paymentHistory" resultType="PaymentDto" parameterType="String">
		select * from payment where member_id = #{memberId} order by payment_no desc
	</select>

</mapper>

PaymentDao

- 결제 내역 전체 조회

public interface PaymentDao {

	// 추상 메소드 - 결제 내역 전체 조회
	List<PaymentDto> paymentHistory(String memberId);
}

PaymentDaoImpl

- 결제 내용 전체 조회

@Repository
public class PaymentDaoImpl implements PaymentDao {

	// 의존성 주입
	@Autowired
	private SqlSession sqlSession;
	
	// 추상 메소드 오버라이딩 - 결제 내역 전체 조회
	@Override
	public List<PaymentDto> paymentHistory(String memberId) {
		return sqlSession.selectList("payment.paymentHistory", memberId);
	}
}

PayController

@Controller
public class PayController {

	// 의존성 주입 - 결제 내역 조회를 위함
	@Autowired
	private PaymentDao paymentDao;
	
	// 주문 내역 조회 Mapping
	@GetMapping("/list")
	public String list(HttpSession session, Model model) {

		// HttpSession에서 로그인 중인 회원 아이디 반환
		String memberId = (String)session.getAttribute("loginId");

		// 반환한 회원 아이디로 결제 정보 단일 조회
		List<PaymentDto> list = paymentDao.paymentHistory(memberId);

		// 조회의 결과를 model에 첨부
		model.addAttribute("list", list);

		// 주문 내역 목록 페이지(list.jsp)로 연결
		return "list";
	}
}

list.jsp

- '더보기'를 누르면 해당 결제에 대한 상세 정보 페이지로 이동

  (결제 번호(paymentNo)를 통해 PayController에서 상세 조회 후 상세 조회 페이지로 연결)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<h1>결제 내역 조회</h1>

<c:forEach var="paymentDto" items="${list}">
<div>
	상품명 : ${paymentDto.itemName} <br>
	결제금액 : <fmt:formatNumber value="${paymentDto.totalAmount}" pattern="#,##0"/> 원 <br>
	결제시각 : <fmt:formatDate value="${paymentDto.approveAt}" pattern="y년 M월 d일 E H시 m분 s초"/> 
	<br>
	상태 : ${paymentDto.paymentStatus}<br>
	<a href="detail?paymentNo=${paymentDto.paymentNo}">더보기</a>
</div>
</c:forEach>

 


 

결제 내역 상세 조회

기본 정보

결제 조회 요청에 포함되는 정보

String cid 가맹점 코드, 10자
String tid 결제 고유 번호, 20자

** CID는 테스트 결제용 CID인 TC0ONETIME로 한다

결제 조회 응답에 포함되는 정보

String cid 가맹점 코드, 10자
String tid 결제 고유 번호, 20자
String status 결제 상태
String partner_order_id 가맹점 주문 번호
String partner_user_id 가맹점 회원 ID
String payment_method_type 결제 수단 (CARD / MONEY)
Amount amount 결제 금액
Amount canceled_amount 취소된 금액
Amount cancel_available_amount 취소 가능 금액
String item_name 상품 이름, 최대 100자
String item_code 상품 코드, 최대 100자
Integer quantity 상품 수량
Date created_at 결제 준비 요청 시각
Date approved_at 결제 승인 시각
Date canceled_at 결제 취소 시각
SelectedCardInfo selected_card_info 결제 카드 정보
PaymentActionDetails[] payment_action_details 결제/취소 상세

Amount

Integer total 전체 결제 금액
Integer tax_free 비과세 금액
Integer vat 부과세 금액
Integer point 포인트 금액
Integer discount 할인 금액
Integer green_deposit 컵 보증금

SelectedCardInfo

String card_bin 카드 BIN
Integer install_month 할부 개월 수
String card_corp_name 카드사 정보
String interest_free_install 무이자 할부 여부 (Y/N)

PaymentActionDetails[]

String aid Request 고유 번호
String approved_at 거래 시간 (String 형태로 반환)
Integer amount 결제/취소 금액
Integer point_amount 결제/취소 포인트 금액
Integer discount_amount 할인 금액
Integer green_deposit 컵 보증금
String payment_action_type 결제 타입 - PAYMENT(결제) / CANCEL(취소) / ISSUE_SID(SID 발급) 
String payload Request로 전달한 값

 

application.properties

- 보안을 위해 Admin Key와 cid(가맹점 코드)를 사용자 정의 속성으로 설정한 후 .gitignore에 추가(업로드 방지)

- 설정값을 직접적으로 사용하기 위해서는 해당 속성의 값을 바인딩할 VO를 만들어야 한다 (KakaoPayProperties)

 

KakaoPayProperties

- application.properties의 사용자 속성의 값을 바인딩하기 위한 VO (@ConfigurationProperties)

- application.properties의 key 값을 key 필드에 바인딩하여 REST 요청의 header에 포함될 Admin Key로 사용

- application.properties의 cid 값을 cid 필드에 바인딩하여 REST 요청의 body에 포함될 tid로 사용

@Data
@Component
@ConfigurationProperties(prefix = "custom.pay")
public class KakaoPayProperties {
	private String key; // REST API 요청의 header에서 Authorization 값
	private String cid; // REST API 요청의 body에서 cid 값
}

 

KakaoPayOrderRequestVO

- 카카오페이 결제 조회 요청을 위한 VO

@Data 
@NoArgsConstructor 
@AllArgsConstructor
@Builder 
public class KakaoPayOrderRequestVO {
	private String tid; // 결제 고유 번호
}

 

KakaoPayOrderResponseVO

- 카카오페이 결제 조회 응답을 위한 VO

@Data 
@NoArgsConstructor 
@AllArgsConstructor 
@Builder
public class KakaoPayOrderResponseVO {
	private String tid; // 결제 고유 번호
	private String cid; // 가맹점 코드
	private String status; // 결제 상태
	private String partner_order_id; // 가맹점 주문번호
	private String partner_user_id; // 가맹점 회원 ID
	private String payment_method_type; // 결제 수단(CARD/MONEY)
	private AmountVO amount; // 결제 금액
	private AmountVO canceled_amount; // 취소된 금액
	private AmountVO cancel_available_amount; // 취소 가능 금액
	private String item_name; // 상품 이름
	private String item_code; // 상품 코드
	private int quantity; // 상품 수량
	private Date created_at; // 결제 준비 요청 시각
	private Date approved_at; // 결제 승인 시각
	private Date canceled_at; // 결제 취소 시각
	private SelectedCardInfoVO selected_card_info; // 결제 카드 정보
	private PaymentActionDetailsVO[] payment_action_details; // 결제/취소 상세
}

AmountVO

@Data 
@NoArgsConstructor 
@AllArgsConstructor 
@Builder
public class AmountVO {
	private int total; // 전체 결제 금액
	private int tax_free; // 비과세 금액
	private int vat; // 부가세 금액
	private int point; // 사용한 포인트 금액
	private int discount; // 할인 금액
	private int green_cup_deposit; // 컵 보증금
}

SelectedCardInfo

@Data 
@NoArgsConstructor 
@AllArgsConstructor 
@Builder
public class SelectedCardInfoVO {
	private String card_bin; // 카드 BIN
	private int install_month; // 할부 개월 수
	private String card_corp_name; // 카드사 정보
	private String interest_free_install; // 무이자 할부 여부 (Y/N)
}

PaymentActionDetailsVO

@Data 
@NoArgsConstructor 
@AllArgsConstructor
@Builder 
public class PaymentActionDetailsVO {
	private String aid; // 요청 고유 번호
	private String approved_at; // 거래시간
	private int amount; // 결제/취소 총액
	private int point_amount; // 결제/취소 포인트 금액
	private int discount_amount; // 할인 금액
	private String payment_action_type; // 결제 타입 - PAYMEYT(결제) / CANCEL(결제취소) / ISSUED_SID(SID발급)
	private String payload; // 요청에 추가로 전달한 값
	private String green_deposit; // 컵 보증금
}

 

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">

	<!-- 결제 번호를 이용하여 결제 정보 조회 -->
	<select id = "findPayment" resultType = "PaymentDto" parameterType = "int">
		select * from payment where payment_no = #{paymentNo}
	</select>
	
	<!-- 결제 번호를 이용하여 결제 상세정보 조회 -->
	<select id = "findPaymentDetail" resultType = "PaymentDetailDto" parameterType = "int">
		select * from payment_detail where payment_no = #{paymentNo} order by payment_detail_no asc
	</select>

</mapper>

 

PaymentDao

- 결제 번호를 이용하여 결제 정보 및 결제 상세정보 조회

public interface PaymentDao {

	// 추상 메소드 - 결제 번호를 이용하여 결제 정보 조회
	PaymentDto findPayment(int paymentNo);
	
	// 추상 메소드 - 결제 번호를 이용하여 결제 상세 정보 조회
	List<PaymentDetailDto> findPaymentDetail(int paymentNo);
}

 

PaymentDaoImpl

- 결제 번호를 이용하여 결제 정보 및 결제 상세정보 조회

@Repository
public class PaymentDaoImpl implements PaymentDao {

	// 의존성 주입
	@Autowired
	private SqlSession sqlSession;
	
	// 추상 메소드 오버라이딩 - 결제 번호를 이용하여 결제 정보 조회
	@Override
	public PaymentDto findPayment(int paymentNo) {
		return sqlSession.selectOne("payment.findPayment", paymentNo);
	}
	
	// 추상 메소드 오버라이딩 - 결제 번호를 이용하여 결제 상세 정보 조회
	@Override
	public List<PaymentDetailDto> findPaymentDetail(int paymentNo) {
		return sqlSession.selectList("payment.findPaymentDetail", paymentNo);
	}
}

 

KakaoPayService

- 결제 조회 요청을 보낸 후 결제 조회 응답을 반환

public interface KakaoPayService {

	// 추상 메소드 - 결제 조회 요청을 보낸 후 결제 조회 응답을 반환
	KakaoPayOrderResponseVO order(KakaoPayOrderRequestVO request) throws URISyntaxException;
}

 

KakaoPayServiceImpl

- 결제 조회 요청을 보낸 후 결제 조회 응답을 반환

- Admin Key와 CID는 application.properties의 custom.pay.key와 custom.pay.cid의 값을 바인딩한 값으로 한다

@Slf4j
@Service
public class KakaoPayServiceImpl implements KakaoPayService {
	
	// REST 방식의 API를 호출하기 위한 RestTemplate의 인스턴스 생성
	private RestTemplate template = new RestTemplate();
	
	// 의존성 주입 - Admin Key와 cid를 반환하기 위함
	@Autowired
	private KakaoPayProperties kakaoPayProperties

	// 추상 메소드 오버라이딩 - 결제 조회 요청을 보낸 후 결제 조회 응답을 반환
	@Override
	public KakaoPayOrderResponseVO order(KakaoPayOrderRequestVO request) throws URISyntaxException {
		
		// 결제 조회 요청을 보낼 주소 설정
		URI uri = new URI("https://kapi.kakao.com/v1/payment/order");

		// REST API 요청의 header 설정
		HttpHeaders headers = new HttpHeaders();
		headers.add("Authorization", "KakaoAK " + kakaoPayProperties.getKey());
		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());

		// REST API 요청을 위한 HttpEntity 생성 - header와 body 결합
		HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers);
		
		// 요청 전송 후 KakaoPayOrderResponseVO 형태로 응답 반환
		KakaoPayOrderResponseVO response = template.postForObject(uri, entity, KakaoPayOrderResponseVO.class);
		
		// 응답 반환
		return response;
	}
}

 

PayController

@Controller
public class PayController {
	
	// 의존성 주입
	@Autowired
	private KakaoPayService kakaoPayService;
	
	// 의존성 주입
	@Autowired
	private PaymentDao paymentDao;
	
	// 카카오페이 결제 조회 Mapping
	@GetMapping("/detail")
	public String detail(@RequestParam int paymentNo, Model model) throws URISyntaxException {
		
		// 주문 내역 목록 페이지에서 전달받은 결제 번호(paymentNo)를 통해 결제 정보 조회
		PaymentDto paymentDto = paymentDao.findPayment(paymentNo);
		
		// 결제 조회 요청을 위한 VO에 정보 설정
		KakaoPayOrderRequestVO request = KakaoPayOrderRequestVO.builder().tid(paymentDto.getTid()).build();
		
		// 카카오페이로 결제 조회 요청 전송 후 응답 반환
		KakaoPayOrderResponseVO response = kakaoPayService.order(request);
		
		// 카카오페이로부터 받은 결제 조회 응답을 model에 첨부
		model.addAttribute("info", response);
		
		// 결제 번호로 조회한 결제 정보를 model에 첨부
		model.addAttribute("paymentDto", paymentDto);
		
		// 결제 번호로 조회한 결제 상세 정보를 model에 첨부
		model.addAttribute("paymentDetailList", paymentDao.findPaymentDetail(paymentNo));
		
		// 결제 상세 페이지(detail.jsp)로 연결
		return "detail";
	}
}

 

detail.jsp

<%@ 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>

<h2><a href="${pageContext.request.contextPath}/">홈</a></h2>
<h2><a href="list">목록</a></h2>

<!-- DB에서 조회한 결제 정보 -->
<div>
	<h1>대표 정보</h1>
	<ul>
		<li>paymentNo : ${paymentDto.paymentNo}</li>
		<li>itemName : ${paymentDto.itemName}</li>
		<li>totalAmount : ${paymentDto.totalAmount}</li>
		<li>approveAt : ${paymentDto.approveAt}</li>
		<li>paymentStatus : ${paymentDto.paymentStatus}</li>
		<li>tid : ${paymentDto.tid}</li>
	</ul>
	<!-- 전체 취소 버튼 -->
	<h2><a href="#">취소</a></h2>
</div>

<div>
	<h1>세부 내역</h1>
	<ul>
		<c:forEach var="paymentDetailDto" items="${paymentDetailList}">
		<li>
			${paymentDetailDto.productName}
			(${paymentDetailDto.qty}개)
			-
			${paymentDetailDto.productPrice}원
			[${paymentDetailDto.paymentDetailStatus}]
			<a href="#">취소</a>
		</li>
		</c:forEach>
	</ul>
</div>

<!-- 카카오페이에서 조회한 정보 -->
<h1>카카오페이 조회 정보</h1>
<ul>
	<li>거래번호 : ${info.tid}</li>
	<li>결제상태 : ${info.status}</li>
	<li>주문번호 : ${info.partner_order_id}</li>
	<li>결제방법 : ${info.payment_method_type}</li>
	<li>
		결제금액 : 
		총 ${info.amount.total} 원 , 
		취소 ${info.canceled_amount.total} 원, 
		잔액 ${info.cancel_available_amount.total} 원
	</li>
	<li>상품명 : ${info.item_name}</li>
	<li>
		내역 : 
		<ul>
			<c:forEach var="detailsVO" items="${info.payment_action_details}">
				<li>${detailsVO}</li>
			</c:forEach>
		</ul>
	</li>
</ul>

<!-- response의 값 확인 -->
<div>${info}</div>

 


 

결제 취소 (전체)

기본 정보

 

결제 취소 요청에 포함되는 정보

** CID는 테스트 결제용 CID인 TC0ONETIME로 한다

String cid 가맹점 코드, 10자
String tid 결제 고유 번호, 20자
Integer cancel_amount 취소 금액
Integer cancel_tax_free_amount 취소 비과세 금액

 

결제 취소 응답에 포함되는 정보

String cid 가맹점 코드, 10자
String tid 결제 고유 번호, 20자
String aid 요청 고유번호
String status 결제 상태
String partner_order_id 가맹점 주문 번호
String partner_user_id 가맹점 회원 ID
String payment_method_type 결제 수단 (CARD / MONEY)
Amount amount 결제 금액
Amount approved_cancel_amount 이번 요청으로 취소된 금액
Amount canceled_amount 누계 취소 금액
Amount cancel_available_amount 남은 취소 가능 금액
String item_name 상품 이름, 최대 100자
String item_code 상품 코드, 최대 100자
Integer quantity 상품 수량
Date approved_at 결제 승인 시각
Date canceled_at 결제 취소 시각
String payload 취소 요청시 전달한 값

Amount

Integer total 전체 결제 금액
Integer tax_free 비과세 금액
Integer vat 부과세 금액
Integer point 포인트 금액
Integer discount 할인 금액
Integer green_deposit 컵 보증금

 

application.properties

- 보안을 위해 Admin Key와 cid(가맹점 코드)를 사용자 정의 속성으로 설정한 후 .gitignore에 추가(업로드 방지)

- 설정값을 직접적으로 사용하기 위해서는 해당 속성의 값을 바인딩할 VO를 만들어야 한다 (KakaoPayProperties)

 

KakaoPayProperties

- application.properties의 사용자 속성의 값을 바인딩하기 위한 VO (@ConfigurationProperties)

- application.properties의 key 값을 key 필드에 바인딩하여 REST 요청의 header에 포함될 Admin Key로 사용

- application.properties의 cid 값을 cid 필드에 바인딩하여 REST 요청의 body에 포함될 tid로 사용

@Data
@Component
@ConfigurationProperties(prefix = "custom.pay")
public class KakaoPayProperties {
	private String key; // REST API 요청의 header에서 Authorization 값
	private String cid; // REST API 요청의 body에서 cid 값
}

 

KakaoPayCancelRequestVO

- 카카오페이 결제 취소 요청을 위한 VO

@Data 
@NoArgsConstructor 
@AllArgsConstructor 
@Builder
public class KakaoPayCancelRequestVO {
	private String tid; // 결제 고유 번호
	private int cancel_amount; // 취소 금액
	private int cancel_tax_free_amount; // 취소 비과세 금액
}

 

KakaoPayCancelResponseVO

- 카카오페이 결제 취소 응답을 위한 VO

@Data 
@NoArgsConstructor 
@AllArgsConstructor 
@Builder
public class KakaoPayCancelResponseVO {
	private String aid; // 요청 고유번호
	private String tid; // 결제 고유번호
	private String cid; // 가맹점 코드
	private String status; // 결제 상태
	private String partner_order_id; // 가맹점 주문 번호
	private String partner_user_id; // 가맹점 회원 번호
	private String payment_method_type; // 결제 수단(CARD/MONEY)
	private AmountVO amount ; // 결제 금액 정보
	private AmountVO approved_cancel_amount; // 이번에 취소한 금액 정보
	private AmountVO cancel_available_amount; // 취소 가능 금액 정보
	private String item_name; // 상품명
	private String item_code; // 상품 코드
	private int quantity; // 상품 수량
	private Date created_at; // 결제 준비 요청 시각
	private Date approved_at; // 승인 시각
	private Date canceled_at; // 취소 시각
	private String payload; // 취소 요청시 전달한 값
}

 

KakaoPayService

- 결제 취소 요청을 보낸 후 결제 취소 응답을 반환

public interface KakaoPayService {

	// 추상 메소드 - 결제 취소 요청을 보낸 후 결제 취소 응답을 반환
	KakaoPayCancelResponseVO cancel(KakaoPayCancelRequestVO 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;
	
	// 의존성 주입 - 결제 정보 등록을 위함
	@Autowired
	private PaymentDao paymentDao;

	// 추상 메소드 오버라이딩 - 결제 취소 요청을 보낸 후 결제 취소 응답을 반환
	@Override
	public KakaoPayCancelResponseVO cancel(KakaoPayCancelRequestVO request) throws URISyntaxException {
		
		// 결제 취소 요청을 보낼 주소 설정
		URI uri = new URI("https://kapi.kakao.com/v1/payment/cancel");
		
		// REST API 요청의 header 설정
		HttpHeaders headers = new HttpHeaders();
		headers.add("Authorization", "KakaoAK "+kakaoPayProperties.getKey());
		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("cancel_amount", String.valueOf(request.getCancel_amount()));//취소 금액
		body.add("cancel_tax_free_amount", "0");//취소 비과세액
		
		// REST API 요청을 위한 HttpEntity 생성 - header와 body 결합
		HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers);
		
		// 요청 전송 후 KakaoPayCancelResponseVO 형태로 응답 반환
		KakaoPayCancelResponseVO response = template.postForObject(uri, entity, KakaoPayCancelResponseVO.class);
		
		// 응답 반환
		return response;
	}
}

 

PaymentDto

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PaymentDto {
	private int paymentNo; // 결제 번호
	private String memberId; // 결제 회원 ID
	private String itemName; // 상품명
	private int totalAmount; // 상품 가격 총액
	private Date approveAt; // 결제 승인 시각
	private String paymentStatus; // 결제 상태
	private String tid; // 결제 고유 번호
}

 

payment-mapper.xml

- 결제 번호(paymentNo)를 이용하여 해당 결제 번호를 가진 결제 상품의 결제 상태를 '취소'로 변경

- 결제 번호(paymentNo)를 이용하여 해당 결제 번호에 묶여있는 세부 결제 상품의 결제 상태를 '취소'로 변경

<?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">

	<!-- 결제 번호를 이용하여 해당 결제 상품의 결제 상태를 '취소'로 변경 -->
	<update id = "cancelPayment" parameterType = "int">
		update payment set payment_status = '취소' where payment_no = #{paymentNo}
	</update>
	
	<!-- 결제 번호를 이용하여 해당 번호의 세부 결제 상품의 결제 상태를 '취소'로 변경 -->
	<update id = "cancelPaymentDetail" parameterType = "int">
		update payment_detail set payment_detail_status = '취소' where payment_no = #{paymentNo}
	</update>

</mapper>

 

PaymentDao

- 결제 번호(paymentNo)를 이용하여 해당 결제 번호를 가진 결제 상품의 결제 상태를 '취소'로 변경

- 결제 번호(paymentNo)를 이용하여 해당 결제 번호에 묶여있는 세부 결제 상품의 결제 상태를 '취소'로 변경

public interface PaymentDao {

	// 추상 메소드 - 해당 결제 번호의 결제 상품의 결제 상태를 '취소'로 변경
	void cancelPayment(int paymentNo);
	
	// 추상 메소드 - 해당 결제 번호의 세부 결제 상품의 결제 상태를 '취소'로 변경
	void cancelPaymentDetail(int paymentNo);
}

 

PaymentDaoImpl

- 결제 번호(paymentNo)를 이용하여 해당 결제 번호를 가진 결제 상품의 결제 상태를 '취소'로 변경

- 결제 번호(paymentNo)를 이용하여 해당 결제 번호에 묶여있는 세부 결제 상품의 결제 상태를 '취소'로 변경

@Repository
public class PaymentDaoImpl implements PaymentDao {

	// 의존성 주입
	@Autowired
	private SqlSession sqlSession;
	
	// 추상 메소드 오버라이딩 - 해당 결제 번호의 결제 상품의 결제 상태를 '취소'로 변경
	@Override
	public void cancelPayment(int paymentNo) {
		sqlSession.update("payment.cancelPayment", paymentNo);
	}
	
	// 추상 메소드 오버라이딩 - 해당 결제 번호의 세부 결제 상품의 결제 상태를 '취소'로 변경
	@Override
	public void cancelPaymentDetail(int paymentNo) {
		sqlSession.update("payment.cancelPaymentDetail", paymentNo);
	}
}

 

 

PayController

@Controller
public class PayController {
	
	// 의존성 주입
	@Autowired
	private KakaoPayService kakaoPayService;

	// 의존성 주입
	@Autowired
	private PaymentDao paymentDao;
	
	// 카카오페이 전체 취소 Mapping
	@GetMapping("/cancel_all")
	public String cancelAll(@RequestParam int paymentNo, RedirectAttributes attr) throws URISyntaxException {

		// 결제 번호(paymentNo)로 결제 정보 조회
		PaymentDto paymentDto = paymentDao.findPayment(paymentNo);

		// 결제 취소 요청을 위한 VO에 정보 설정
		KakaoPayCancelRequestVO request 
			= KakaoPayCancelRequestVO
				.builder()
					.tid(paymentDto.getTid())
					.cancel_amount(paymentDto.getTotalAmount())
				.build();
		
		// 카카오페이로 결제 취소 요청 전송 후 응답 반환
		KakaoPayCancelResponseVO response = kakaoPayService.cancel(request);

		// 결제 정보 테이블(payment)에서 해당 결제 번호 상품의 결제 상태를 '취소'로 변경
		paymentDao.cancelPayment(paymentNo);
		
		// 세부 결제 정보 테이블(payment_detail)에서 해당 결제 번호의 모든 세부 결제 상품의 결제 상태를 '취소'로 변경
		paymentDao.cancelPaymentDetail(paymentNo);

		// 해당 결제 번호의 카카오페이 결제 조회 Mapping으로 강제 이동(redirect)
		attr.addAttribute("paymentNo", paymentNo);
		return "redirect:detail";
	}
}

 

detail.jsp

- 대표 결제 정보의 전체 취소 버튼을 누르면 해당 결제 번호(paymentNo)의 결제 상품의 결제 상태와

  해당 결제 번호를 갖는 세부 결제 상품의 결제 상태를 모두 '취소'로 변경

<%@ 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>

<h2><a href="${pageContext.request.contextPath}/">홈</a></h2>
<h2><a href="list">목록</a></h2>

<!-- DB에서 조회한 결제 정보 -->
<div>
	<h1>대표 정보</h1>
	<ul>
		<li>paymentNo : ${paymentDto.paymentNo}</li>
		<li>itemName : ${paymentDto.itemName}</li>
		<li>totalAmount : ${paymentDto.totalAmount}</li>
		<li>approveAt : ${paymentDto.approveAt}</li>
		<li>paymentStatus : ${paymentDto.paymentStatus}</li>
		<li>tid : ${paymentDto.tid}</li>
	</ul>
	<!-- 전체 취소 버튼 -->
	<h2><a href="cancel_all?paymentNo=${paymentDto.paymentNo}">취소</a></h2>
</div>

<div>
	<h1>세부 내역</h1>
	<ul>
		<c:forEach var="paymentDetailDto" items="${paymentDetailList}">
		<li>
			${paymentDetailDto.productName}
			(${paymentDetailDto.qty}개)
			-
			${paymentDetailDto.productPrice}원
			[${paymentDetailDto.paymentDetailStatus}]
			<a href="#">취소</a>
		</li>
		</c:forEach>
	</ul>
</div>

<!-- 카카오페이에서 조회한 정보 -->
<h1>카카오페이 조회 정보</h1>
<ul>
	<li>거래번호 : ${info.tid}</li>
	<li>결제상태 : ${info.status}</li>
	<li>주문번호 : ${info.partner_order_id}</li>
	<li>결제방법 : ${info.payment_method_type}</li>
	<li>
		결제금액 : 
		총 ${info.amount.total} 원 , 
		취소 ${info.canceled_amount.total} 원, 
		잔액 ${info.cancel_available_amount.total} 원
	</li>
	<li>상품명 : ${info.item_name}</li>
	<li>
		내역 : 
		<ul>
			<c:forEach var="detailsVO" items="${info.payment_action_details}">
				<li>${detailsVO}</li>
			</c:forEach>
		</ul>
	</li>
</ul>

<!-- response의 값 확인 -->
<div>${info}</div>

 


부분 결제 취소

부분 결제 취소시 반드시 필요한 처리 : 결제 상태 변경

1) 세부 결제 테이블의 결제 상태 변경

- 부분 취소시 해당 세부 결제의 결제 상태를 '취소'로 변경

- 부분 취소시 해당 세부 결제의 결제 번호(paymentNo)를 이용하여 결제의 결제 상태를 '부분취소'로 변경

 

2) 결제 테이블의 결제 상태 변경

- 모든 세부 결제의 결제 상태가 '취소'이면 결제의 결제 상태를 '취소'로 변경

 

** mapper.xml에 Oracle의 case when ~ then 문을 사용하여 상태를 변경하는 update 태그를 추가한다

** Oracle에서 case when ~ then 문

case
	when [조건 1] then [리턴값 1] -- 조건 1을 만족하면 리턴값 1을 반환
	when [조건 2] then [리턴값 2] -- 조건 2를 만족하면 리턴값 2를 반환
	...
	else [조건 N] then [리턴값 N] -- 조건 N을 만족하면 리턴값 N을 반환
end

 

payment-mapper.xml

- 세부 결제 번호(paymentDetailNo)를 이용하여 세부 결제 정보 조회

- 세부 결제 번호(paymentDetailNo)를 이용하여 해당 세부 결제 상품의 결제 상태를 '취소'로 변경

- 부분 취소 결과에 따라 전체 결제 상태를 '취소' 또는 '부분취소'로 변경

<?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">

	<!-- 세부 결제 번호를 이용하여 세부 결제 정보 조회  -->
	<select id = "findPaymentDetailItem" resultType = "PaymentDetailDto" parameterType = "int">
		select * from payment_detail where payment_detail_no = #{paymentDetailNo}
	</select>

	<!-- 세부 결제 번호를 이용하여 해당 세부 결제 상품의 결제 상태를 '취소'로 변경 -->
	<update id = "cancelPaymentDetailItem" parameterType = "int">
		update payment_detail set payment_detail_status = '취소' where payment_detail_no = #{paymentDetailNo}
	</update>

	<!-- 부분 취소 결과에 따라 전체 결제의 상태를 '취소' 또는 '부분취소'로 변경 -->
	<update id = "refreshPayment" parameterType = "int">
		update payment set payment_status=(
		    select 
		        case
		            when "전체"="취소" then '취소'
		            when "취소"=0 then '승인'
		            else '부분취소'
		        end
		    from
		    (
		        select 
		            (select count(*) from payment_detail where payment_no = #{paymentNo}) "전체",
		            (select count(*) from payment_detail where payment_no = #{paymentNo} and payment_detail_status='취소') "취소" from dual
		    )
		)
		where payment_no = #{paymentNo}
	</update>

</mapper>

 

PaymentDao

- 세부 결제 번호(paymentDetailNo)를 이용하여 세부 결제 정보 조회

- 세부 결제 번호(paymentDetailNo)를 이용하여 해당 세부 결제 상품의 결제 상태를 '취소'로 변경

- 부분 취소 결과에 따라 전체 결제 상태를 '취소' 또는 '부분취소'로 변경

public interface PaymentDao {

	// 추상 메소드 - 세부 결제 번호를 이용하여 세부 결제 정보 조회
	PaymentDetailDto findPaymentDetailItem(int paymentDetailNo);
	
	// 추상 메소드 - 해당 세부 결제 번호의 세부 결제 상품의 결제 상태를 '취소'로 변경
	void cancelPaymentDetailItem(int paymentDetailNo);
	
	// 추상 메소드 - 부분 취소 결과에 따라 전체 결제의 상태를 '취소' 또는 '부분취소'로 변경
	void refreshPayment(int paymentNo);
}

 

PaymentDaoImpl

- 세부 결제 번호(paymentDetailNo)를 이용하여 세부 결제 정보 조회

- 세부 결제 번호(paymentDetailNo)를 이용하여 해당 세부 결제 상품의 결제 상태를 '취소'로 변경

- 부분 취소 결과에 따라 전체 결제 상태를 '취소' 또는 '부분취소'로 변경

@Repository
public class PaymentDaoImpl implements PaymentDao {

	// 의존성 주입
	@Autowired
	private SqlSession sqlSession;
	
	// 추상 메소드 오버라이딩 - 세부 결제 번호를 이용하여 세부 결제 정보 조회
	@Override
	public PaymentDetailDto findPaymentDetailItem(int paymentDetailNo) {
		return sqlSession.selectOne("payment.findPaymentDetailItem", paymentDetailNo);
	}
	
	// 추상 메소드 오버라이딩 - 해당 세부 결제 번호의 세부 결제 상품의 결제 상태를 '취소'로 변경
	@Override
	public void cancelPaymentDetailItem(int paymentDetailNo) {
		sqlSession.update("payment.cancelPaymentDetailItem", paymentDetailNo);
	}
	
	// 추상 메소드 오버라이딩 - 부분 취소 결과에 따라 전체 결제의 상태를 '취소' 또는 '부분취소'로 변경
	@Override
	public void refreshPayment(int paymentNo) {
		sqlSession.update("payment.refreshPayment", paymentNo);
	}
}

 

KakaoPayService

- 결제 취소 요청을 보낸 후 결제 취소 응답을 반환

public interface KakaoPayService {

	// 추상 메소드 - 결제 취소 요청을 보낸 후 결제 취소 응답을 반환
	KakaoPayCancelResponseVO cancel(KakaoPayCancelRequestVO 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;
	
	// 의존성 주입 - 결제 정보 변경을 위함
	@Autowired
	private PaymentDao paymentDao;

	// 추상 메소드 오버라이딩 - 결제 취소 요청을 보낸 후 결제 취소 응답을 반환
	@Override
	public KakaoPayCancelResponseVO cancel(KakaoPayCancelRequestVO request) throws URISyntaxException {
		
		// 결제 취소 요청을 보낼 주소 설정
		URI uri = new URI("https://kapi.kakao.com/v1/payment/cancel");
		
		// REST API 요청의 header 설정
		HttpHeaders headers = new HttpHeaders();
		headers.add("Authorization", "KakaoAK "+kakaoPayProperties.getKey());
		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("cancel_amount", String.valueOf(request.getCancel_amount()));//취소 금액
		body.add("cancel_tax_free_amount", "0");//취소 비과세액
		
		// REST API 요청을 위한 HttpEntity 생성 - header와 body 결합
		HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers);
		
		// 요청 전송 후 KakaoPayCancelResponseVO 형태로 응답 반환
		KakaoPayCancelResponseVO response = template.postForObject(uri, entity, KakaoPayCancelResponseVO.class);
		
		// 응답 반환
		return response;
	}
}

 

PayController

@Controller
public class PayController {
	
	// 의존성 주입
	@Autowired
	private KakaoPayService kakaoPayService;
	
	// 의존성 주입
	@Autowired
	private PaymentDao paymentDao;
	
	// 카카오페이 부분 취소 Mapping
	@GetMapping("/cancel_item")
	public String cancelItem(@RequestParam int paymentDetailNo, RedirectAttributes attr) throws URISyntaxException {
		
		// 해당 세부 결제 번호(paymentDetailNo)의 세부 결제 상품의 세부 결제 정보를 조회
		PaymentDetailDto paymentDetailDto = paymentDao.findPaymentDetailItem(paymentDetailNo);
		
		// 세부 결제 상품의 결제 번호(paymentNo)를 반환하여 결제 정보를 조회
		PaymentDto paymentDto = paymentDao.findPayment(paymentDetailDto.getPaymentNo());

		// 결제 취소 요청을 위한 VO에 정보 설정
		KakaoPayCancelRequestVO request 
			= KakaoPayCancelRequestVO
				.builder()
					.tid(paymentDto.getTid())
					.cancel_amount(paymentDetailDto.getProductPrice())
				.build();
		
		// 카카오페이로 결제 취소 요청 전송 후 응답 반환
		KakaoPayCancelResponseVO response = kakaoPayService.cancel(request);

		// 세부 결제 번호에 해당하는 세부 결제 상품의 결제 상태를 '취소'로 변경
		paymentDao.cancelPaymentDetailItem(paymentDetailNo);
		
		// 부분 취소의 갯수에 따라 전체 결제 상태 변경
		paymentDao.refreshPayment(paymentDto.getPaymentNo());

		// 해당 결제 번호의 카카오페이 결제 조회 Mapping으로 강제 이동(redirect)
		attr.addAttribute("paymentNo", paymentDto.getPaymentNo());
		return "redirect:detail";
	}
}

 

 


 

계층형 구조

- 데이터가 트리 형태의 구조로 조직된 구조

- 반복적인 부모 - 자식 관계 정보를 표현

- 부모는 다수의 자식을 가질 수 있지만, 자식은 오직 하나의 부모만을 가질 수 있다

- 대표적인 계층형 구조) 계층형 게시판 - 게시글과 답글의 구조 (https://floating-branch.tistory.com/153)

 

계층형 조회에서 발생하는 문제점 : N+1 문제

- 계층형 구조에서 부모는 다수의 자식을 가질 수 있다

- 1회의 조회 결과로 N개의 부모 정보를 얻었을 때

  모든 부모 정보에 엮인 모든 자식 정보를 얻기 위해서는 N번(부모 정보의 수)만큼 더 조회해야 한다

 

Mybatis를 이용한 계층형 조회

- 모든 결제 정보와 각각의 결제 정보에 엮인 모든 세부 결제 정보 조회

PaymentVO

- 결제 정보와 결제 정보에 엮인 세부 결제 정보를 조회하기 위한 VO

@Data 
@NoArgsConstructor 
@AllArgsConstructor 
@Builder
public class PaymentVO {
	private PaymentDto paymentDto; // 결제 정보
	private List<PaymentDetailDto> paymentDetailList; // 결제 정보 하나에 엮인 세부 결제 정보 집합
}

 

PaymentDetailDto

- 세부 결제 정보 하나에 대한 VO

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PaymentDetailDto {
	private int paymentDetailNo; // 세부 결제 번호
	private int paymentNo; // 결제 번호
	private int productNo; // 상품 번호
	private String productName; // 상품명
	private int productPrice; // 상품 가격
	private int qty; // 상품 수량
	private String paymentDetailStatus; // 상품 결제 상품
}

 

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">

	<!-- 결제 번호를 이용하여 결제 상세정보 조회 -->
	<select id = "findPaymentDetail" resultType = "PaymentDetailDto" parameterType = "int">
		select * from payment_detail where payment_no = #{paymentNo} order by payment_detail_no asc
	</select>

	<!-- 계층형 조회 - 결제 정보와 상세 결제 정보 -->
	<select id = "paymentGroupList" resultMap = "vo" parameterType = "String">
		select * from payment
	</select>

	<!-- 계층형 조회를 위한 추가 설정 -->	
	<resultMap type = "PaymentVO" id = "vo">
		<association property = "paymentDto">
			<result column = "payment_no" property = "paymentNo"/>
			<result column = "member_id" property = "memberId"/>
			<result column = "item_name" property = "itemName"/>
			<result column = "total_amount" property = "totalAmount"/>
			<result column = "approve_at" property = "approveAt" javaType = "java.sql.Date"/>
			<result column = "payment_status" property = "paymentStatus"/>
			<result column = "tid" property = "tid"/>
		</association>
		
		<collection column = "payment_no" property = "paymentDetailList"
				javaType = "java.util.List" ofType = "PaymentDetailDto"
				select = "findPaymentDetail">
			<result column = "payment_detail_no" property = "paymentDetailNo"/>
			<result column = "payment_no" property = "paymentNo"/>
			<result column = "product_no" property = "productNo"/>
			<result column = "product_name" property = "productName"/>
			<result column = "qty" property = "qty"/>
			<result column = "product_price" property = "productPrice"/>
			<result column = "payment_detail_status" property = "paymentDetailStatus"/>
		</collection>
	</resultMap>

</mapper>

 

<select>

- resultMap : 외부 resultMap의 참조명

 

<resultMap>

- id : resultMap의 참조명 (조회하기 위한 <select>에서의 resultMap의 값과 같아야 한다)

- type : 자료형

 

<association>

- property : 결과 Mapping을 위한 필드나 자바빈 프로퍼티 이름 (여기서는 DTO)

 

<collection>

- column : DB의 컬럼명

- property : 결과 Mapping을 위한 필드나 자바빈 프로퍼티 이름 (여기서는 DTO의 collection)

- javaType : 패키지 경로를 포함한 클래스명 또는 타입

- ofType : collection 안에 들어있는 데이터 타입

- select : 실행할 select 태그의 id

 

<result>

- column : DB의 컬럼명

- property : 결과 Mapping을 위한 필드 (여기서는 DTO의 필드)

- javaType : 패키지 경로를 포함한 클래스명 또는 자료형

 

PayTest08 (Test에서 진행)

@SpringBootTest
public class PayTest09 {

	// 의존성 주입
	@Autowired
	private SqlSession sqlSession;
	
	@Test
	public void test() {
		
		// 계층형 조회 구문 실행
		List<PaymentVO> list= sqlSession.selectList("payment.paymentGroupList");
		
		// 조회 결과
		System.out.println(list.size());
	}
}

 

Test 결과

- select * from payment의 결과로 총 2개의 레코드 조회

- select * from payment_detail where payment_no = 23의 결과로 총 3개의 레코드 조회

- select * from payment_detail where payment_no = 28의 결과로 총 3개의 레코드 조회

 

실제 DB의 레코드 갯수와 비교

payment 테이블의 데이터

- payment_no=23인 레코드와 payment_no=28인 레코드 2개가 존재

 

payment_detail 테이블의 데이터

- 각각의 payment_no에 대하여 payment_detail 레코드가 3개씩 총 6개 존재

'국비교육 > 국비교육' 카테고리의 다른 글

Oracle DB Import  (0) 2022.12.27
day80 - 1121  (0) 2022.11.21
day78 - 1117  (0) 2022.11.18
day77 - 1116  (0) 2022.11.16
day76 - 1115  (0) 2022.11.16