코딩 노트

결제04 - 마무리 본문

Spring

결제04 - 마무리

newbyeol 2023. 10. 27. 13:30

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="sequence" resultType="int">

select payment_seq.nextval from dual

</select>

<insert id="save">

insert into payment(

payment_no, payment_member, payment_tid,

payment_name, payment_price, payment_remain

)

values(

#{paymentNo}, #{paymentMember}, #{paymentTid},

#{paymentName}, #{paymentPrice}, #{paymentRemain}

)

</insert>

 

<!-- 결제 상세정보 등록 -->

<insert id="saveDetail">

insert into payment_detail (

payment_detail_no, payment_detail_origin, payment_detail_product,

payment_detail_product_name, payment_detail_product_price,

payment_detail_product_qty, payment_detail_status

)

values(

payment_detail_seq.nextval,

#{paymentDetailOrigin}, #{paymentDetailProduct}, #{paymentDetailProductName},

#{paymentDetailProductPrice}, #{paymentDetailProductQty},

'완료'

)

</insert>

 

<!-- 결제 대표정보만 조회(사용자별/전체) -->

<select id="list" resultType="PaymentDto">

select * from payment order by payment_no asc

</select>

 

<!--

결제 대표정보와 상세정보를 계층형으로 조회

- resultType은 필드명으로 데이터베이스 조회 결과를 매핑하여 처리한다

- 지금과 같이 복잡한 구조는 처리가 불가능하다

- resultMap 항목을 선언하여 수동으로 매핑하도록 처리한다

- association 은 객체를 의미

- collection 은 리스트를 의미

- property는 항목명(필드명)을 의미

- column은 DB의 조회결과 열명을 의미

-->

<resultMap type="PaymentListVO" id="paymentListVO">

<!-- 일단 조회 결과는 내가 지정한 객체에 필드에 맞게 배치해라! -->

<association property="paymentDto">

<result column="payment_no" property="paymentNo"/>

<result column="payment_member" property="paymentMember"/>

<result column="payment_tid" property="paymentTid"/>

<result column="payment_name" property="paymentName"/>

<result column="payment_price" property="paymentPrice"/>

<result column="payment_remain" property="paymentRemain"/>

<result column="payment_time" property="paymentTime"/>

</association>

 

<!--

List가 하나 있는데 구문 알려줄테니까 니가 알아서 조회해서 채워라!

- select에 적힌 구문을 부르면 내용을 채울 수 있을거야!

- 구문을 실행하는 데 필요한 값은 column에 적혀있는 항목이야!

- 이 저장소는 javaType에 적혀 있는 형태야!

- 이 저장소에 들어갈 수 있는 데이터는 ofType에 적혀 있는 형태야!

-->

<collection property="paymentDetailList" select="listDetail" column="payment_no"

javaType="java.util.List" ofType="PaymentDto">

<result column="payment_detail_no" property="paymentDetailNo"/>

<result column="payment_detail_origin" property="paymentDetailOrigin"/>

<result column="payment_detail_product" property="paymentDetailProduct"/>

<result column="payment_detail_product_name" property="paymentDetailProductName"/>

<result column="payment_detail_product_price" property="paymentDetailProductPrice"/>

<result column="payment_detail_product_qty" property="paymentDetailProductQty"/>

<result column="payment_detail_status" property="paymentDetailStatus"/>

</collection>

</resultMap>

 

<select id="listAll" resultMap="paymentListVO">

select * from payment

<if test="paymentMember != null">

where payment_member = #{paymentMember}

</if>

order by payment_no asc

</select>

 

<!-- 결제 상세정보를 조회(결제 대표번호별) -->

<select id="listDetail" resultType="PaymentDetailDto">

select * from payment_detail

where payment_detail_origin = #{paymentDetailOrigin}

order by payment_detail_no asc

</select>

 

 

<!-- payment 상세조회 -->

<select id="find" resultType="PaymentDto">

select * from payment where payment_no = #{paymentNo}

</select>

 

<!-- payment_detail 상세조회 -->

<select id="selectDetail" resultType="PaymentDetailDto">

select * from payment_detail where payment_detail_no = #{paymentDetailNo}

</select>

 

<!-- payment 잔여금액처리 -->

<update id="cancel">

update payment

set payment_remain = #{paymentRemain}

where payment_no = #{paymentNo}

</update>

 

<!-- payment_detail 취소처리 -->

<update id="cancelDetail">

update payment_detail

set payment_detail_status = '취소'

where payment_detail_no = #{paymentDetailNo}

</update>

 

<!-- 특정 payment_no에 속한 payment_detail에 대한 취소처리 -->

<update id="cancelDetailGroup">

update payment_detail

set payment_detail_status = '취소'

where payment_detail_origin = #{paymentDetailOrigin}

</update>

 

</mapper>

KakaoPayController 구문 추가

@Slf4j

@Controller

@RequestMapping("/pay")

public class KakaoPayController {

 

@Autowired

private KakaoPayService kakaoPayService;

 

@Autowired

private PaymentDao paymentDao;

 

@GetMapping("/test1")

public String test1() {

//return "/WEB-INF/views/pay/test1.jsp";

return "pay/test1";

}

 

@PostMapping("/test1")

public String test1(@ModelAttribute KakaoPayReadyRequestVO request,

HttpSession session) throws URISyntaxException {

request.setPartnerOrderId(UUID.randomUUID().toString());

KakaoPayReadyResponseVO response = kakaoPayService.ready(request);

//session에 flash value를 저장(잠시 쓰고 지우는 데이터)

//- 사용자를 거치지 않는 범위 내에서 사용해야 안전하게 쓸 수 있다

session.setAttribute("approve", KakaoPayApproveRequestVO.builder()

.partnerOrderId(request.getPartnerOrderId())

.partnerUserId(request.getPartnerUserId())

.tid(response.getTid())

.build());

return "redirect:" + response.getNextRedirectPcUrl();

}

 

@GetMapping("/test1/success")

public String test1Success(HttpSession session, @RequestParam String pg_token) throws URISyntaxException {

//session에 저장되어 있는 flash value를 꺼내어 pg_token을 추가한 뒤 승인 요청

KakaoPayApproveRequestVO request =

(KakaoPayApproveRequestVO) session.getAttribute("approve");

session.removeAttribute("approve");

 

request.setPgToken(pg_token);//토큰 추가

 

//결제 승인 요청

KakaoPayApproveResponseVO response = kakaoPayService.approve(request);

return "redirect:successResult";

}

 

@GetMapping("/test1/successResult")

public String successResult() {

//return "/WEB-INF/views/pay/successResult.jsp";

return "pay/successResult";

}

 

@GetMapping("/test1/detail")

public String detail(Model model, @RequestParam String tid) throws URISyntaxException {

 

KakaoPayDetailResponseVO response =

kakaoPayService.detail(KakaoPayDetailRequestVO.builder()

.tid(tid)

.build());

model.addAttribute("vo", response);

 

//return "/WEB-INF/views/pay/detail.jsp";

return "pay/detail";

}

 

@RequestMapping("/test1/cancel")

public String cancel(@ModelAttribute KakaoPayCancelRequestVO request) throws URISyntaxException {

KakaoPayCancelResponseVO response = kakaoPayService.cancel(request);

return "redirect:detail?tid="+request.getTid();

}

 

///////////////////////////////////////////////////////////////////////////////////////////

@Autowired

private ProductDao productDao;

 

@RequestMapping("/test2")

public String test2(Model model) {

model.addAttribute("list", productDao.selectList());

//return "/WEB-INF/views/pay2/home.jsp";

return "pay2/home";

}

 

@GetMapping("/test2/purchase")

public String purchase(HttpSession session, @RequestParam int productNo) throws URISyntaxException {

//상품정보 조회

ProductDto productDto = productDao.selectOne(productNo);

//상품정보를 이용하여 결제준비요청

KakaoPayReadyRequestVO request = KakaoPayReadyRequestVO.builder()

.itemName(productDto.getProductName())

.itemPrice(productDto.getProductPrice())

.partnerOrderId(UUID.randomUUID().toString())

.partnerUserId("testuser1")

.build();

 

KakaoPayReadyResponseVO response = kakaoPayService.ready(request);

 

//session에 flash value를 저장(잠시 쓰고 지우는 데이터)

//- 사용자를 거치지 않는 범위 내에서 사용해야 안전하게 쓸 수 있다

session.setAttribute("approve", KakaoPayApproveRequestVO.builder()

.partnerOrderId(request.getPartnerOrderId())

.partnerUserId(request.getPartnerUserId())

.tid(response.getTid())

.build());

session.setAttribute("productNo", productNo);

 

//결제페이지를 사용자에게 안내

return "redirect:"+response.getNextRedirectPcUrl();

}

 

//결제 성공

@GetMapping("/test2/purchase/success")

public String test2Success(HttpSession session, @RequestParam String pg_token) throws URISyntaxException {

//session에 저장되어 있는 flash value를 꺼내어 pg_token을 추가한 뒤 승인 요청

KakaoPayApproveRequestVO request =

(KakaoPayApproveRequestVO) session.getAttribute("approve");

int productNo = (int)session.getAttribute("productNo");

session.removeAttribute("approve");

session.removeAttribute("productNo");

 

request.setPgToken(pg_token);//토큰 추가

 

//결제 승인 요청

KakaoPayApproveResponseVO response = kakaoPayService.approve(request);

 

//결제 승인이 완료되었다면 DB에 결제 정보를 저장

int paymentNo = paymentDao.sequence();

paymentDao.insert(PaymentDto.builder()

.paymentNo(paymentNo)

.paymentMember(response.getPartnerUserId())

.paymentTid(response.getTid())

.paymentName(response.getItemName())

.paymentPrice(response.getAmount().getTotal())

.paymentRemain(response.getAmount().getTotal())

.build());

 

return "redirect:successResult";

}

 

@RequestMapping("/test2/purchase/successResult")

public String test2SuccessResult() {

return "pay2/successResult";

}

 

///////////////////////////////////////////////////////////////////////////////

 

@RequestMapping("/test3")

public String test3(Model model) {

model.addAttribute("list", productDao.selectList());

return "pay3/home";

}

 

//결제 확인화면

@GetMapping("/test3/purchase")

public String test3Purchase(@ModelAttribute PurchaseListVO listVO, Model model) {

List<PurchaseVO> purchaseList = listVO.getProduct();

 

List<PurchaseConfirmVO> confirmList = new ArrayList<>();//옮겨담을 리스트

int total = 0;

for(PurchaseVO vo : purchaseList) {//사용자가 선택한 번호와 수량의 목록을 반복하며

ProductDto productDto = productDao.selectOne(vo.getProductNo());//상품정보를 구한다

 

//vodto를 신규객체로 만들어 추가

PurchaseConfirmVO confirmVO = PurchaseConfirmVO.builder()

.purchaseVO(vo).productDto(productDto)

.build();

confirmList.add(confirmVO);//화면에 출력할 데이터 추가

total += confirmVO.getTotal();//총 구매금액 합산

}

 

model.addAttribute("list", confirmList);

model.addAttribute("total", total);

return "pay3/purchase";

}

 

//결제 처리

@PostMapping("/test3/purchase")

public String test3Purchse(HttpSession session,

@ModelAttribute PurchaseListVO listVO) throws URISyntaxException {

log.debug("listVO = {}", listVO);

 

//listVO에 들어있는 product 항목들을 이용해서 결제 준비 요청 처리 후 결제 페이지로 안내

//- 결제이름은 대표 상품명 외 ?개 와 같이 작성

//- 결제금액은 모든 상품의 가격과 수량의 총합계

//- 결론적으로 만들어야 하는 데이터는 KakaoPayReadyRequestVO

KakaoPayReadyRequestVO request = kakaoPayService.convert(listVO);

 

String memberId = (String)session.getAttribute("name");

request.setPartnerUserId(memberId);

 

KakaoPayReadyResponseVO response = kakaoPayService.ready(request);

 

//session에 flash value를 저장(잠시 쓰고 지우는 데이터)

//- 사용자를 거치지 않는 범위 내에서 사용해야 안전하게 쓸 수 있다

session.setAttribute("approve", KakaoPayApproveRequestVO.builder()

.partnerOrderId(request.getPartnerOrderId())

.partnerUserId(request.getPartnerUserId())

.tid(response.getTid())

.build());//승인요청을 위한 준비데이터 --> 카카오페이

session.setAttribute("listVO", listVO);//구매한 상품의 번호와 수량 목록 --> 우리 DB

 

return "redirect:" + response.getNextRedirectPcUrl();

}

 

@GetMapping("/test3/purchase/success")

public String test3Success(HttpSession session, @RequestParam String pg_token) throws URISyntaxException {

//session에 저장한 flash value 추출 및 삭제

KakaoPayApproveRequestVO request =

(KakaoPayApproveRequestVO)session.getAttribute("approve");

PurchaseListVO listVO = (PurchaseListVO) session.getAttribute("listVO");

 

session.removeAttribute("approve");

session.removeAttribute("listVO");

 

request.setPgToken(pg_token);//토큰 설정

KakaoPayApproveResponseVO response = kakaoPayService.approve(request);//승인요청

 

//DB작업

//- 상품을 3개 구매했다면 payment 1회, payment_detail 3회의 insert가 필요(N+1)

 

//[1] 결제번호 생성

//int paymentNo = paymentDao.sequence();

int paymentNo = Integer.parseInt(response.getPartnerOrderId());

 

//[2] 결제정보 등록

paymentDao.insert(PaymentDto.builder()

.paymentNo(paymentNo)//결제고유번호

.paymentMember(response.getPartnerUserId())//결제자ID

.paymentTid(response.getTid())//PG사 거래번호

.paymentName(response.getItemName())//PG사 결제상품명

.paymentPrice(response.getAmount().getTotal())//총 결제액

.paymentRemain(response.getAmount().getTotal())//총 취소가능액

.build());

//[3] 상품 개수만큼 결제 상세정보를 등록

List<PurchaseVO> list = listVO.getProduct();

for(PurchaseVO vo : list) {

ProductDto productDto = productDao.selectOne(vo.getProductNo());//상품정보 조회

paymentDao.insertDetail(PaymentDetailDto.builder()

.paymentDetailOrigin(paymentNo)//상위결제번호

.paymentDetailProduct(vo.getProductNo())//상품번호 (vo, productDto)

.paymentDetailProductName(productDto.getProductName())//상품명 (productDto)

.paymentDetailProductPrice(productDto.getProductPrice())//상품가격 (productDto)

.paymentDetailProductQty(vo.getQty())//구매수량 (vo)

.build());

}

 

return "redirect:successResult";

}

 

@RequestMapping("/test3/purchase/successResult")

public String test3SuccessResult() {

return "pay3/successResult";

}

 

@RequestMapping("/test3/list")

public String test3list(Model model) {

model.addAttribute("list", paymentDao.selectList());

return "pay3/list";

}

 

@RequestMapping("/test3/list2")

public String test3list2(HttpSession session, Model model) {

String memberId = (String)session.getAttribute("name");

 

//model.addAttribute("list", paymentDao.selectTotalList());//전체내역

model.addAttribute("list", paymentDao.selectTotalListByMember(memberId));//나의내역

return "pay3/list2";

}

 

//[1] 결제 상세 번호로 PaymentDetailDto를 조회

//[2] 1번에서 조회한 PaymentDetailDto의 정보로 PaymentDto를 조회

//[3] 1번에서 취소금액을 알 수 있고, 2번에서 거래번호(tid)를 알 수 있다

//[4] 3번의 정보로 카카오페이 취소 요청을 보낸다

//[5] DB의 정보를 업데이트

// - 현재 항목에 대한 상태를 취소로 변경해야 한다(payment_detail)

// - 결제 대표 정보의 잔여 금액을 차감해야 한다(payment)

@RequestMapping("/test3/cancel")

public String test3cancel(@RequestParam int paymentDetailNo) throws URISyntaxException {

PaymentDetailDto paymentDetailDto = paymentDao.selectDetail(paymentDetailNo);//1

//if(paymentDetailDto.getPaymentDetailStatus().equals("취소")) {

if(paymentDetailDto == null || paymentDetailDto.isCanceled()) {

throw new NoTargetException();

}

 

PaymentDto paymentDto =

paymentDao.selectOne(paymentDetailDto.getPaymentDetailOrigin());//2

 

//3

KakaoPayCancelRequestVO request = KakaoPayCancelRequestVO.builder()

.tid(paymentDto.getPaymentTid())

.cancelAmount(

paymentDetailDto.getPaymentDetailProductPrice()//상품판매가

* paymentDetailDto.getPaymentDetailProductQty()//구매수량

)

.build();

 

//4

KakaoPayCancelResponseVO response = kakaoPayService.cancel(request);

 

//5

paymentDao.cancelDetail(paymentDetailNo);

paymentDao.cancel(PaymentDto.builder()

.paymentNo(paymentDto.getPaymentNo())//결제대표번호

.paymentRemain(response.getCancelAvailableAmount().getTotal())//잔여금액

.build());

 

return "redirect:list2";

}

 

//결제 그룹 전체 취소

//[1] 전달받은 paymentNo로 PaymentDto를 조회

// - 잔여금액이 0이라면 차단

//[2] 1번에서 거래번호(tid)와 잔여금액을 꺼내서 카카오페이에 취소 요청을 전송

//[3] 취소가 성공하였다면 DB에서 다음의 항목을 수정

// - payment에서 잔여금액을 0으로 변경

// - payment_detail에서 해당 payment_no에 대한 모든 상태를 취소로 변경

@RequestMapping("/test3/cancelAll")

public String test3cancelAll(@RequestParam int paymentNo) throws URISyntaxException {

//1

PaymentDto paymentDto = paymentDao.selectOne(paymentNo);

if(paymentDto == null || paymentDto.getPaymentRemain() == 0) {

throw new NoTargetException("이미 취소된 결제입니다");

}

 

//2

KakaoPayCancelRequestVO request = KakaoPayCancelRequestVO.builder()

.tid(paymentDto.getPaymentTid())//거래번호

.cancelAmount(paymentDto.getPaymentRemain())//잔여금액

.build();

KakaoPayCancelResponseVO response = kakaoPayService.cancel(request);

 

//3

paymentDao.cancel(PaymentDto.builder()

.paymentNo(paymentNo).paymentRemain(0)

.build());

paymentDao.cancelDetailGroup(paymentNo);

 

return "redirect:list2";

}

 

//결제취소와 결제실패의 경우에도 세션에 저장한 flash value를 삭제해야 한다

@RequestMapping("/test3/purchase/cancel")

public String test3cancel(HttpSession session) {

session.removeAttribute("approve");

session.removeAttribute("listVO");

return "취소했을때 보여줄 페이지";

}

 

@RequestMapping("/test3/purchase/fail")

public String test3fail(HttpSession session) {

session.removeAttribute("approve");

session.removeAttribute("listVO");

return "실패했을때 보여줄 페이지";

}

 

}

PaymentDao 구문 추가

public interface PaymentDao {

int sequence();

void insert(PaymentDto paymentDto);

List<PaymentDto> selectList();

PaymentDto selectOne(int paymentNo);

 

void insertDetail(PaymentDetailDto paymentDetailDto);

 

List<PaymentListVO> selectTotalList();

List<PaymentListVO> selectTotalListByMember(String paymentMember);

 

PaymentDetailDto selectDetail(int paymentDetailNo);

void cancelDetail(int paymentDetailNo);

void cancel(PaymentDto paymentDto);

void cancelDetailGroup(int paymentDetailOrigin);

}

PaymentDaoImpl 구문 추가

@Repository

public class PaymentDaoImpl implements PaymentDao{

@Autowired

private SqlSession sqlSession;

@Override

public int sequence() {

return sqlSession.selectOne("payment.sequence");

}

@Override

public void insert(PaymentDto paymentDto) {

sqlSession.insert("payment.save", paymentDto);

}

@Override

public void insertDetail(PaymentDetailDto paymentDetailDto) {

sqlSession.insert("payment.saveDetail", paymentDetailDto);

}

@Override

public List<PaymentDto> selectList() {

return sqlSession.selectList("payment.list");

}

@Override

public List<PaymentListVO> selectTotalList() {

return sqlSession.selectList("payment.listAll");

}

@Override

public PaymentDetailDto selectDetail(int paymentDetailNo) {

return sqlSession.selectOne("payment.selectDetail", paymentDetailNo);

}

@Override

public PaymentDto selectOne(int paymentNo) {

return sqlSession.selectOne("payment.find", paymentNo);

}

@Override

public void cancel(PaymentDto paymentDto) {

sqlSession.update("payment.cancel", paymentDto);

}

@Override

public void cancelDetail(int paymentDetailNo) {

sqlSession.update("payment.cancelDetail", paymentDetailNo);

}

@Override

public void cancelDetailGroup(int paymentDetailOrigin) {

sqlSession.update("payment.cancelDetailGroup", paymentDetailOrigin);

}

@Override

public List<PaymentListVO> selectTotalListByMember(String paymentMember) {

return sqlSession.selectList("payment.listAll", paymentMember);

}

}

HomeController 생성

@Controller

public class HomeController {

 

@RequestMapping("/")

public String home() {

return "home";

}

 

@RequestMapping("/login")

public String login(HttpSession session,

@RequestParam String memberId,

@RequestParam String memberPw) {

if(memberId.equals("testuser999") && memberPw.equals("Testuser999!")) {

session.setAttribute("name", memberId);

session.setAttribute("level", "VIP");

}

return "redirect:/";

}

 

@RequestMapping("/logout")

public String logout(HttpSession session) {

session.removeAttribute("name");

session.removeAttribute("level");

return "redirect:/";

}

 

}

KakaoPayServiceImpl 구문 추가

@Slf4j

@Service

public class KakaoPayServiceImpl implements KakaoPayService{

 

@Autowired

private KakaoPayProperties kakaoPayProperties;

 

@Autowired

private RestTemplate template;

 

@Autowired

private HttpHeaders headers;

 

@Autowired

private ProductDao productDao;

 

@Autowired

private PaymentDao paymentDao;

 

@Override

public KakaoPayReadyResponseVO ready(KakaoPayReadyRequestVO request) throws URISyntaxException {

 

//주소 설정

URI uri = new URI("https://kapi.kakao.com/v1/payment/ready");

 

//바디 설정

MultiValueMap<String, String> body = new LinkedMultiValueMap<>();

body.add("cid", kakaoPayProperties.getCid());

body.add("partner_order_id", request.getPartnerOrderId());

body.add("partner_user_id", request.getPartnerUserId());

body.add("item_name", request.getItemName());

body.add("quantity", "1");

body.add("total_amount", String.valueOf(request.getItemPrice()));

body.add("tax_free_amount", "0");

 

//현재 페이지 주소 계산

String path = ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString();

body.add("approval_url", path+"/success");

body.add("cancel_url", path+"/cancel");

body.add("fail_url", path+"/fail");

 

//요청 발송

HttpEntity entity = new HttpEntity(body, headers);

 

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

 

MultiValueMap<String, String> body = new LinkedMultiValueMap<>();

body.add("cid", kakaoPayProperties.getCid());

body.add("tid", request.getTid());

body.add("partner_order_id", request.getPartnerOrderId());

body.add("partner_user_id", request.getPartnerUserId());

body.add("pg_token", request.getPgToken());

 

HttpEntity entity = new HttpEntity(body, headers);

 

KakaoPayApproveResponseVO response =

template.postForObject(uri, entity, KakaoPayApproveResponseVO.class);

 

log.debug("결제 승인 완료 = {}", response.getTid());

 

return response;

}

 

@Override

public KakaoPayDetailResponseVO detail(KakaoPayDetailRequestVO request) throws URISyntaxException {

URI uri = new URI("https://kapi.kakao.com/v1/payment/order");

 

MultiValueMap<String, String> body = new LinkedMultiValueMap<>();

body.add("cid", kakaoPayProperties.getCid());

body.add("tid", request.getTid());

 

HttpEntity entity = new HttpEntity(body, headers);

 

KakaoPayDetailResponseVO response =

template.postForObject(uri, entity, KakaoPayDetailResponseVO.class);

return response;

}

 

@Override

public KakaoPayCancelResponseVO cancel(KakaoPayCancelRequestVO request) throws URISyntaxException {

URI uri = new URI("https://kapi.kakao.com/v1/payment/cancel");

 

MultiValueMap<String, String> body = new LinkedMultiValueMap<>();

body.add("cid", kakaoPayProperties.getCid());

body.add("tid", request.getTid());

body.add("cancel_amount", String.valueOf(request.getCancelAmount()));

body.add("cancel_tax_free_amount", "0");

 

HttpEntity entity = new HttpEntity(body, headers);

 

KakaoPayCancelResponseVO response =

template.postForObject(uri, entity, KakaoPayCancelResponseVO.class);

return response;

}

 

@Override

public KakaoPayReadyRequestVO convert(PurchaseListVO listVO) {

//구매목록 추출

List<PurchaseVO> list = listVO.getProduct();

 

//결제 사용할 정보를 저장할 변수들

String name = null;

int total = 0;

 

//구매목록을 모두 조사하여 상품정보 추출 후 필요한 항목을 계산

for(PurchaseVO vo : list) {

//vo 안에는 상품번호(productNo)와 구매수량(qty)가 있다

ProductDto dto = productDao.selectOne(vo.getProductNo());

if(name == null) {//이름이 없을때만 이름을 넣어라(최초 이름만 들어감)

name = dto.getProductName();

}

total += dto.getProductPrice() * vo.getQty();//상품가격과 수량을 곱해서 합산해라!

}

 

//구매 수량이 2개 이상이라면 이름에 "외 ?건"를 추가

if(list.size() >= 2) {

name += " 외 "+(list.size()-1)+"건";

}

 

//partner_order_id에 결제번호를 집어넣으면 충돌도 없고 좋지 않을까?

int paymentNo = paymentDao.sequence();

 

return KakaoPayReadyRequestVO.builder()

//.partnerOrderId(UUID.randomUUID().toString())

.partnerOrderId(String.valueOf(paymentNo))

.itemName(name)

.itemPrice(total)

.build();

}

}

PaymentDetailDto 구문 수정

@Data @Builder @NoArgsConstructor @AllArgsConstructor

public class PaymentDetailDto {

private int paymentDetailNo;//하위 결제번호

private int paymentDetailOrigin;//상위 결제번호

private int paymentDetailProduct;//구매상품번호

private String paymentDetailProductName;//구매상품명

private int paymentDetailProductPrice;//구매상품가격

private int paymentDetailProductQty;//구매상품수량

private String paymentDetailStatus;//구매상품상태(완료/취소)

 

public boolean isCanceled() {

return paymentDetailStatus.equals("취소");

}

}

error 패키지 생성 후 NoTargetException 파일 생성

package com.kh.spring21.error;

 

import lombok.NoArgsConstructor;

 

/**

* 대상이 없을 경우 발생하는 예외

* 1. Exception을 상속받는다

* - RuntimeException을 상속받으면 예외 전가를 작성하지 않아도 된다

* 2. 기본생성자와 메세지를 설정할 수 있는 생성자를 구현한다

*/

@NoArgsConstructor

public class NoTargetException extends RuntimeException {

public NoTargetException(String message) {

super(message);

}

}

successResult.jsp 수정

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<h1>결제가 완료되었습니다</h1>

 

<h2><a href="../list2">나의 결제 내역 보기</a></h2>

<h2><a href="${pageContext.request.contextPath}/pay/test3/list2">나의 결제 내역 보기</a></h2>

home.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>

 

<!-- 전체 목록 -->

<c:forEach var="paymentListVO" items="${list}">

 

<div style="border:1px solid black; margin:30px 0px; padding:10px">

 

<!-- 대표 정보 -->

<div style="border:1px solid blue; padding:10px">

${paymentListVO.paymentDto}

<hr>

 

<%-- 전체취소 버튼은 잔여금액이 있을 때만 출력되어야 한다 --%>

<c:if test="${paymentListVO.paymentDto.paymentRemain > 0}">

<a href="cancelAll?paymentNo=${paymentListVO.paymentDto.paymentNo}">전체(잔여) 금액 취소</a>

</c:if>

</div>

 

<!-- 상세 목록 정보 -->

<div style="border:1px solid red; padding:10px; margin-top:10px">

<c:forEach var="paymentDetailDto" items="${paymentListVO.paymentDetailList}">

<div style="border:1px solid gray; padding:10px; margin-top:10px;">

${paymentDetailDto}

<hr>

 

<%-- 취소가 가능한 상황일 경우에만 개별내역취소 버튼을 출력 --%>

<c:if test="${paymentDetailDto.paymentDetailStatus == '완료'}">

<a href="cancel?paymentDetailNo=${paymentDetailDto.paymentDetailNo}">

개별내역취소

</a>

</c:if>

</div>

</c:forEach>

</div>

 

</div>

</c:forEach>