결제 내역 전체 조회
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 |