Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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 @@ -27,14 +27,24 @@ public class IdeaMarketPurchasing extends BaseTimeEntity {
private Long price;
private PaymentDuration paymentDuration;

private Payment payment; // 결제 방식
private Long vat; // 수수료
private Long quantity; // 구매 수량

@ManyToOne
private IdeaMarket ideaMarket;

@Builder
public IdeaMarketPurchasing(User buyer, Long price, PaymentDuration paymentDuration, IdeaMarket ideaMarket) {
public IdeaMarketPurchasing(Long id, User buyer, Long price, PaymentDuration paymentDuration, Payment payment,
Long vat,
Long quantity, IdeaMarket ideaMarket) {
this.id = id;
this.buyer = buyer;
this.price = price;
this.paymentDuration = paymentDuration;
this.payment = payment;
this.vat = vat;
this.quantity = quantity;
this.ideaMarket = ideaMarket;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.brainpix.joining.entity.purchasing;

public enum Payment {
KAKAO_PAY
}
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 @@ -9,8 +9,6 @@
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);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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.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.KakaoPayService;
import com.brainpix.security.authorization.AllUser;
import com.brainpix.security.authorization.UserId;

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 KakaoPayService kakaoPayService;

@AllUser
@Operation(summary = "결제 준비 API", description = "아이디어 식별자, 판매자 식별자, 상품 수량, 총 결제금액(vat 포함), vat를 json 본문으로 입력받아 카카오페이 쪽에 결제 정보를 생성합니다.<br>사용자를 결제 화면으로 이동시킬 수 있도록 리다이렉트 url과 주문 번호가 응답값으로 제공됩니다.<br>주문 번호는 이후 승인 단계에서 pgToken과 함께 전달해주시면 됩니다.")
@PostMapping("/ready")
public ResponseEntity<ApiResponse<KakaoPayReadyDto.Response>> kakaoPayReady(@UserId 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));
}

@AllUser
@Operation(summary = "결제 승인 API", description = "pgToken과 주문 번호를 json 본문으로 입력받아 최종 승인을 처리합니다.")
@PostMapping("/approve")
public ResponseEntity<ApiResponse<KakaoPayApproveDto.Response>> kakaoPayApprove(
@UserId 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,44 @@
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())
.orderId(orderId)
.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();
}
}
53 changes: 53 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,53 @@
package com.brainpix.kakaopay.dto;

import jakarta.validation.constraints.NotBlank;
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)
}

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

@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;
}
}
49 changes: 49 additions & 0 deletions src/main/java/com/brainpix/kakaopay/dto/KakaoPayReadyDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.brainpix.kakaopay.dto;

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

public class KakaoPayReadyDto {

@NoArgsConstructor
@Getter
public static class Request {
@NotNull(message = "아이디어 ID가 필요합니다.")
private Long ideaId; // 아이디어 ID
@NotNull(message = "판매자 ID가 필요합니다.")
private Long sellerId; // 판매자 ID
@NotNull(message = "상품 수량 필수입니다.")
private Long quantity; // 상품 수량
@NotNull(message = "총 결제 금액은 필수입니다.")
private Long totalPrice; // 총 결제금액
@NotNull(message = "VAT는 필수입니다.")
private Long vat; // VAT
}

@Getter
@Builder
public static class Parameter {
private Long buyerId; // 구매자 ID
private Long ideaId; // 아이디어 ID
private Long sellerId; // 판매자 ID
private Long quantity; // 상품 수량
private Long totalPrice; // 총 결제금액
private Long vat; // VAT
}

@Getter
@Builder
public static class KakaoApiResponse {
private String tid; // 카카오페이 쪽 결제 고유 번호
private String next_redirect_pc_url; // 카카오페이가 생성한 결제 경로, 사용자는 해당 경로로 이동하여 결제 진행
}

@Getter
@Builder
public static class Response {
private String nextRedirectPcUrl;
private String orderId; // 최종 승인(approve)때, 세션에서 결제 정보를 가져오는데 Key로 사용하기 위함
}
}
43 changes: 43 additions & 0 deletions src/main/java/com/brainpix/kakaopay/entity/KakaoPaymentData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.brainpix.kakaopay.entity;

import com.brainpix.jpa.BaseTimeEntity;
import com.brainpix.post.entity.idea_market.IdeaMarket;
import com.brainpix.user.entity.User;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
public class KakaoPaymentData extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String orderId;
private String tid;
private Long quantity;

@ManyToOne
private User buyer;
@ManyToOne
private IdeaMarket ideaMarket;

@Builder
public KakaoPaymentData(Long id, String orderId, String tid, Long quantity, User buyer, IdeaMarket ideaMarket) {
this.id = id;
this.orderId = orderId;
this.tid = tid;
this.quantity = quantity;
this.buyer = buyer;
this.ideaMarket = ideaMarket;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.brainpix.kakaopay.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.brainpix.kakaopay.entity.KakaoPaymentData;

public interface KakaoPaymentDataRepository extends JpaRepository<KakaoPaymentData, Long> {

Optional<KakaoPaymentData> findByOrderId(String orderId);
}
Loading