Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/main/java/com/brainpix/api/code/error/KakaoPayErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.brainpix.api.code.error;

import org.springframework.http.HttpStatus;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum KakaoPayErrorCode implements ErrorCode {

// 400 Bad Request - 잘못된 요청
IDEA_OWNER_PURCHASE_INVALID(HttpStatus.BAD_REQUEST, "KAKAOPAY400", "글 작성자는 구매할 수 없습니다."),
QUANTITY_INVALID(HttpStatus.BAD_REQUEST, "KAKAOPAY400", "구매 수량은 재고를 초과할 수 없습니다."),

// 500 Internal Server Error - 서버 내부 오류
API_RESPONSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "KAKAOPAY500", "카카오페이 API 호출 중 오류가 발생하였습니다.");

private final HttpStatus httpStatus;
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,12 @@ public BaseQuantity(Long totalQuantity, Long occupiedQuantity) {
this.totalQuantity = totalQuantity;
this.occupiedQuantity = occupiedQuantity;
}

public Long getRemainingQuantity() {
return this.totalQuantity - this.occupiedQuantity;
}

public void increaseOccupiedQuantity(Long quantity) {
this.occupiedQuantity += quantity;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
import com.brainpix.user.entity.User;

@Repository
public interface IdeaMarketPurchasingRepository
extends JpaRepository<IdeaMarketPurchasing, Long> {
public interface IdeaMarketPurchasingRepository extends JpaRepository<IdeaMarketPurchasing, Long> {
Page<IdeaMarketPurchasing> findByBuyer(User buyer, Pageable pageable);

List<IdeaMarketPurchasing> findByIdeaMarket(IdeaMarket ideaMarket);
Expand Down
110 changes: 110 additions & 0 deletions src/main/java/com/brainpix/kakaopay/api_client/KakaoPayApiClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.brainpix.kakaopay.api_client;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import com.brainpix.kakaopay.dto.KakaoPayApproveDto;
import com.brainpix.kakaopay.dto.KakaoPayReadyDto;
import com.brainpix.kakaopay.entity.KakaoPaymentData;
import com.brainpix.post.entity.idea_market.IdeaMarket;
import com.brainpix.user.entity.User;

@Component
public class KakaoPayApiClient {

private final RestTemplate restTemplate = new RestTemplate();

@Value("${kakao.pay.ready-url}")
private String READY_URL;
@Value("${kakao.pay.approve-url}")
private String APPROVE_URL;

@Value("${kakao.pay.cid}")
private String cid; // TC0ONETIME (테스트용 가맹점 cid)
@Value("${kakao.pay.secret-key}")
private String secretKey; // secret-key(dev) 값 (테스트용 시크릿 값)

// 카카오페이 결제 준비 API
public KakaoPayReadyDto.KakaoApiResponse requestPaymentReady(
KakaoPayReadyDto.Parameter parameter, String orderId, User buyer, IdeaMarket ideaMarket) {

Map<String, String> parameters = getReadyParams(parameter, orderId, buyer, ideaMarket);
HttpHeaders headers = getHeaders();

HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(parameters, headers);

ResponseEntity<KakaoPayReadyDto.KakaoApiResponse> response = restTemplate.postForEntity(
READY_URL, requestEntity, KakaoPayReadyDto.KakaoApiResponse.class
);

return response.getBody();
}

// 카카오페이 결제 승인 API
public KakaoPayApproveDto.KakaoApiResponse requestPaymentApprove(
KakaoPayApproveDto.Parameter parameter, KakaoPaymentData kakaoPaymentData) {

Map<String, String> parameters = getApproveParams(parameter, kakaoPaymentData);
HttpHeaders headers = getHeaders();

HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(parameters, headers);

ResponseEntity<KakaoPayApproveDto.KakaoApiResponse> response = restTemplate.postForEntity(
APPROVE_URL, requestEntity, KakaoPayApproveDto.KakaoApiResponse.class
);

return response.getBody();
}

// 결제 준비 파라미터 생성
private Map<String, String> getReadyParams(
KakaoPayReadyDto.Parameter parameter, String orderId, User buyer, IdeaMarket ideaMarket) {

Map<String, String> params = new HashMap<>();

params.put("cid", cid);
params.put("partner_order_id", orderId);
params.put("partner_user_id", String.valueOf(buyer.getId()));
params.put("item_name", ideaMarket.getTitle());
params.put("quantity", String.valueOf(parameter.getQuantity()));
params.put("total_amount", String.valueOf(parameter.getTotalPrice()));
params.put("tax_free_amount", "0");
params.put("vat_amount", String.valueOf(parameter.getVat()));
params.put("approval_url",
"https://www.brainpix.net/purchase/approve?ideaId=" + ideaMarket.getId() + "&orderId=" + orderId);
params.put("cancel_url", "https://www.brainpix.net/purchase/cancel?ideaId=" + ideaMarket.getId());
params.put("fail_url", "https://www.brainpix.net/purchase/fail?ideaId=" + ideaMarket.getId());

return params;
}

// 결제 승인 파라미터 생성
private Map<String, String> getApproveParams(KakaoPayApproveDto.Parameter parameter,
KakaoPaymentData kakaoPaymentData) {

Map<String, String> params = new HashMap<>();

params.put("cid", cid);
params.put("tid", kakaoPaymentData.getTid());
params.put("partner_order_id", parameter.getOrderId());
params.put("partner_user_id", String.valueOf(kakaoPaymentData.getBuyer().getId()));
params.put("pg_token", parameter.getPgToken());

return params;
}

// API 호출에 필요한 헤더 생성
private HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "SECRET_KEY " + secretKey);
headers.set("Content-type", "application/json");
return headers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.brainpix.kakaopay.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.brainpix.api.ApiResponse;
import com.brainpix.kakaopay.converter.KakaoPayApproveDtoConverter;
import com.brainpix.kakaopay.converter.KakaoPayReadyDtoConverter;
import com.brainpix.kakaopay.dto.KakaoPayApproveDto;
import com.brainpix.kakaopay.dto.KakaoPayReadyDto;
import com.brainpix.kakaopay.service.KakaoPayFacadeService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequestMapping("/kakao-pay")
@RequiredArgsConstructor
@Tag(name = "카카오페이 아이디어 결제 API", description = "아이디어를 카카오페이로 결제하는 API 입니다.<br>결제 과정 : 결제 준비 API 호출 -> 리다이렉트된 사용자의 결제 진행 -> 결제 승인 API 호출")
public class KakaoPayController {

private final KakaoPayFacadeService kakaoPayService;

@Operation(summary = "결제 준비 API", description = "아이디어 식별자, 판매자 식별자, 상품 수량, 총 결제금액(vat 포함), vat를 json 본문으로 입력받아 카카오페이 쪽에 결제 정보를 생성합니다.<br>사용자를 결제 화면으로 이동시킬 수 있도록 리다이렉트 url과 주문 번호가 응답값으로 제공됩니다.<br>주문 번호는 이후 승인 단계에서 pgToken과 함께 전달해주시면 됩니다.")
@PostMapping("/ready")
public ResponseEntity<ApiResponse<KakaoPayReadyDto.Response>> kakaoPayReady(@RequestParam Long userId,
@RequestBody KakaoPayReadyDto.Request request) {
log.info("카카오페이 결제 준비 API 호출");
KakaoPayReadyDto.Parameter parameter = KakaoPayReadyDtoConverter.toParameter(userId, request);
KakaoPayReadyDto.Response response = kakaoPayService.kakaoPayReady(parameter);
return ResponseEntity.ok(ApiResponse.success(response));
}

@Operation(summary = "결제 승인 API", description = "pgToken과 주문 번호를 json 본문으로 입력받아 최종 승인을 처리합니다.")
@PostMapping("/approve")
public ResponseEntity<ApiResponse<KakaoPayApproveDto.Response>> kakaoPayApprove(
@RequestParam Long userId,
@RequestBody KakaoPayApproveDto.Request request) {
log.info("카카오페이 결제 최종 승인 API 호출");
KakaoPayApproveDto.Parameter parameter = KakaoPayApproveDtoConverter.toParameter(userId, request);
KakaoPayApproveDto.Response response = kakaoPayService.kakaoPayApprove(parameter);
return ResponseEntity.ok(ApiResponse.success(response));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.brainpix.kakaopay.converter;

import com.brainpix.joining.entity.purchasing.IdeaMarketPurchasing;
import com.brainpix.joining.entity.purchasing.Payment;
import com.brainpix.joining.entity.quantity.PaymentDuration;
import com.brainpix.kakaopay.dto.KakaoPayApproveDto;
import com.brainpix.post.entity.idea_market.IdeaMarket;
import com.brainpix.user.entity.User;

public class KakaoPayApproveDtoConverter {

public static KakaoPayApproveDto.Parameter toParameter(Long userId, KakaoPayApproveDto.Request request) {

return KakaoPayApproveDto.Parameter.builder()
.userId(userId)
.orderId(request.getOrderId())
.pgToken(request.getPgToken())
.build();
}

public static IdeaMarketPurchasing toIdeaMarketPurchasing(KakaoPayApproveDto.KakaoApiResponse kakaoApiResponse,
User user,
IdeaMarket ideaMarket, PaymentDuration paymentDuration) {

return IdeaMarketPurchasing.builder()
.buyer(user)
.ideaMarket(ideaMarket)
.paymentDuration(paymentDuration)
.price(kakaoApiResponse.getAmount().getTotal())
.vat(kakaoApiResponse.getAmount().getVat())
.quantity(kakaoApiResponse.getQuantity())
.payment(Payment.KAKAO_PAY)
.build();
}

public static KakaoPayApproveDto.Response toResponse(IdeaMarketPurchasing ideaMarketPurchasing) {

return KakaoPayApproveDto.Response.builder()
.ideaMarketPurchasingId(ideaMarketPurchasing.getId())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.brainpix.kakaopay.converter;

import com.brainpix.kakaopay.dto.KakaoPayReadyDto;
import com.brainpix.kakaopay.entity.KakaoPaymentData;
import com.brainpix.post.entity.idea_market.IdeaMarket;
import com.brainpix.user.entity.User;

public class KakaoPayReadyDtoConverter {

public static KakaoPayReadyDto.Parameter toParameter(Long userId, KakaoPayReadyDto.Request request) {

return KakaoPayReadyDto.Parameter.builder()
.buyerId(userId)
.sellerId(request.getSellerId())
.ideaId(request.getIdeaId())
.quantity(request.getQuantity())
.totalPrice(request.getTotalPrice())
.vat(request.getVat())
.build();
}

public static KakaoPayReadyDto.Response toResponse(KakaoPayReadyDto.KakaoApiResponse kakaoApiResponse,
String orderId) {

return KakaoPayReadyDto.Response.builder()
.nextRedirectPcUrl(kakaoApiResponse.getNext_redirect_pc_url())
.build();
}

public static KakaoPaymentData toKakaoPaymentData(KakaoPayReadyDto.KakaoApiResponse kakaoApiResponse,
KakaoPayReadyDto.Parameter parameter,
String orderId, User buyer,
IdeaMarket ideaMarket) {

return KakaoPaymentData.builder()
.tid(kakaoApiResponse.getTid())
.quantity(parameter.getQuantity())
.orderId(orderId)
.buyer(buyer)
.ideaMarket(ideaMarket)
.build();
}
}
57 changes: 57 additions & 0 deletions src/main/java/com/brainpix/kakaopay/dto/KakaoPayApproveDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.brainpix.kakaopay.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

public class KakaoPayApproveDto {

@NoArgsConstructor
@Getter
public static class Request {
@NotBlank(message = "토큰을 입력해주세요.")
private String pgToken; // 결제 최종 완료를 위한 토큰
@NotBlank(message = "주문 번호를 입력해주세요.")
private String orderId; // 주문 번호 (세션에서 결제 정보 탐색 Key)
@NotNull(message = "아이디어 게시글 ID를 입력해주세요.")
private Long ideaId; // 아이디어 ID
}

@Getter
@Builder
public static class Parameter {
private Long userId; // 유저 ID
private String orderId; // 주문 번호 (결제 정보 탐색 Key)
private String pgToken; // 결제 최종 완료를 위한 토큰
private Long ideaId; // 아이디어 ID
}

@Getter
@Builder
public static class KakaoApiResponse {
private String aid; // 요청 고유 번호
private String tid; // 결제 고유 번호
private String cid; // 가맹점 코드
private String partner_order_id; // 주문 번호 (DB에 저장)
private String partner_user_id; // 구매자 ID
private String item_name; // 상품 이름
private Long quantity; // 상품 수량
private Amount amount; // 결제 금액 관련
private String approved_at; // 결제 승인 시각
}

@Getter
@Builder
public static class Amount {
private Long total; // 총 결제 금액
private Long vat; // 부가세 금액
}

@Getter
@Builder
public static class Response {
private Long ideaMarketPurchasingId;
}
}
Loading