diff --git a/.DS_Store b/.DS_Store index b4a0214..f46dd4b 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/build.gradle b/build.gradle index a172b29..76d26d8 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ configurations { repositories { mavenCentral() + maven { url 'https://jitpack.io' } // 깃허브에 있는 오픈소스 프로젝트를 직접 의존성으로 가져와 사용할 수 있게 해주는 설정입니다. } dependencies { @@ -50,6 +51,9 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' implementation platform('software.amazon.awssdk:bom:2.20.56') implementation 'software.amazon.awssdk:s3' + //portone + implementation 'com.github.iamport:iamport-rest-client-java:0.2.21' + } diff --git a/src/main/java/friend/spring/apiPayload/code/status/ErrorStatus.java b/src/main/java/friend/spring/apiPayload/code/status/ErrorStatus.java index 2daa04f..ea6308e 100644 --- a/src/main/java/friend/spring/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/friend/spring/apiPayload/code/status/ErrorStatus.java @@ -78,7 +78,15 @@ public enum ErrorStatus implements BaseErrorCode { // 공지사항 관련 응답 - NOTICE_NOT_FOUND(HttpStatus.NOT_FOUND, "NOTICE4001", "공지사항이 없습니다."); + NOTICE_NOT_FOUND(HttpStatus.NOT_FOUND, "NOTICE4001", "공지사항이 없습니다."), + + // 결제 관련 응답 + ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "PAYMENT4001", "주문건을 찾을 수 없습니다"), + ORDER_ID_NOT_FOUND(HttpStatus.NOT_FOUND, "PAYMENT4002", "해당 주문 번호가 존재하지 않습니다"), + ORDER_PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "PAYMENT4003", "해당 상품을 찾을 수 없습니다."), + PRICE_NOT_OK(HttpStatus.CONFLICT, "PAYMENT4004", "결제 금액이 실제 결제 금액과 불일치합니다.(결제 금액 위변조 의심)"), + PAYMENT_NOT_PAID(HttpStatus.PAYMENT_REQUIRED, "PAYMENT4005", "결제 미완료 에러입니다."); + private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/friend/spring/apiPayload/handler/PaymentHandler.java b/src/main/java/friend/spring/apiPayload/handler/PaymentHandler.java new file mode 100644 index 0000000..d448f59 --- /dev/null +++ b/src/main/java/friend/spring/apiPayload/handler/PaymentHandler.java @@ -0,0 +1,10 @@ +package friend.spring.apiPayload.handler; + +import friend.spring.apiPayload.GeneralException; +import friend.spring.apiPayload.code.BaseErrorCode; + +public class PaymentHandler extends GeneralException { + public PaymentHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/friend/spring/config/IamportConfig.java b/src/main/java/friend/spring/config/IamportConfig.java new file mode 100644 index 0000000..bc0a27d --- /dev/null +++ b/src/main/java/friend/spring/config/IamportConfig.java @@ -0,0 +1,22 @@ +package friend.spring.config; + +import com.siot.IamportRestClient.IamportClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class IamportConfig { + // iamportclient를 빈으로 등록해줍니다. + + @Value("${iamport.api_key}") + private String apiKey; + + @Value("${iamport.api_secret}") + private String apiSecret; + + @Bean + public IamportClient iamportClient() { + return new IamportClient(apiKey, apiSecret); + } +} diff --git a/src/main/java/friend/spring/domain/Order.java b/src/main/java/friend/spring/domain/Order.java new file mode 100644 index 0000000..8193c69 --- /dev/null +++ b/src/main/java/friend/spring/domain/Order.java @@ -0,0 +1,38 @@ +package friend.spring.domain; + +import friend.spring.domain.common.BaseEntity; +import friend.spring.domain.enums.Product; +import lombok.*; + +import javax.persistence.*; +import java.math.BigDecimal; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) + +public class Order extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + private Product product; // POINT_1000, POINT_2000 일단 두개 설정해놨습니다. + + @Column(nullable = false) + private BigDecimal price; + + @Column(nullable = false) + private String orderUid; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "payment_id") + private Payment payment; +} diff --git a/src/main/java/friend/spring/domain/Payment.java b/src/main/java/friend/spring/domain/Payment.java index 2389dbd..ed5ac8d 100644 --- a/src/main/java/friend/spring/domain/Payment.java +++ b/src/main/java/friend/spring/domain/Payment.java @@ -1,35 +1,38 @@ -//package friend.spring.domain; -// -//import friend.spring.domain.common.BaseEntity; -//import friend.spring.domain.enums.PaymentState; -//import lombok.*; -// -//import javax.persistence.*; -//import java.math.BigDecimal; -// -//@Entity -//@Getter -//@Builder -//@AllArgsConstructor -//@NoArgsConstructor(access = AccessLevel.PROTECTED) -//public class Payment extends BaseEntity { -// -// @Id -// @GeneratedValue(strategy = GenerationType.IDENTITY) -// private Long id; -// -// @Column(nullable = false) -// private BigDecimal amount; -// -// //주문 고유 번호 -// private String merchantUid; -// -// //결제 상태 -// @Enumerated(EnumType.STRING) -// -// private PaymentState paymentState; -// -// -// -// -//} +package friend.spring.domain; + +import friend.spring.domain.common.BaseEntity; +import friend.spring.domain.enums.PaymentState; +import lombok.*; + +import javax.persistence.*; +import java.math.BigDecimal; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Payment extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private BigDecimal price; + + //결제 상태 + @Enumerated(EnumType.STRING) + private PaymentState paymentState; + + //주문 고유 번호 + private String paymentUid; + + + // 상태 변경 메서드 + public void changePaymentBySuccess(PaymentState paymentState, String paymentUid) { + this.paymentState = paymentState; + this.paymentUid = paymentUid; + } + +} diff --git a/src/main/java/friend/spring/domain/enums/PaymentState.java b/src/main/java/friend/spring/domain/enums/PaymentState.java index f6be152..7dad986 100644 --- a/src/main/java/friend/spring/domain/enums/PaymentState.java +++ b/src/main/java/friend/spring/domain/enums/PaymentState.java @@ -1,5 +1,5 @@ -//package friend.spring.domain.enums; -// -//public enum PaymentState { -// READY, PAID, FAILED, CANCEL -//} +package friend.spring.domain.enums; + +public enum PaymentState { + READY, ING, PAID, CANCEL +} diff --git a/src/main/java/friend/spring/domain/enums/Product.java b/src/main/java/friend/spring/domain/enums/Product.java new file mode 100644 index 0000000..31f7015 --- /dev/null +++ b/src/main/java/friend/spring/domain/enums/Product.java @@ -0,0 +1,6 @@ +package friend.spring.domain.enums; + +public enum Product { + // 물건 가격 임시로 설정 ( 추후에 pm 님과 상의 후 fix ) + POINT_1000, POINT_2000 +} diff --git a/src/main/java/friend/spring/repository/OrderRepository.java b/src/main/java/friend/spring/repository/OrderRepository.java new file mode 100644 index 0000000..eb78388 --- /dev/null +++ b/src/main/java/friend/spring/repository/OrderRepository.java @@ -0,0 +1,17 @@ +package friend.spring.repository; + +import friend.spring.domain.Order; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface OrderRepository extends JpaRepository { + @Query("SELECT o FROM Order o " + "LEFT JOIN FETCH o.payment p " + "LEFT JOIN FETCH o.user m " + "WHERE o.orderUid = :orderUid") + Optional findOrderAndPaymentAndMember(@Param("orderUid") String orderUid); + + @Query("SELECT o FROM Order o " + "LEFT JOIN FETCH o.payment p " + "WHERE o.orderUid = :orderUid") + Optional findOrderAndPayment(@Param("orderUid") String orderUid); +} + diff --git a/src/main/java/friend/spring/repository/PaymentRepository.java b/src/main/java/friend/spring/repository/PaymentRepository.java new file mode 100644 index 0000000..b3e91a5 --- /dev/null +++ b/src/main/java/friend/spring/repository/PaymentRepository.java @@ -0,0 +1,7 @@ +package friend.spring.repository; + +import friend.spring.domain.Payment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PaymentRepository extends JpaRepository { +} diff --git a/src/main/java/friend/spring/service/OrderService.java b/src/main/java/friend/spring/service/OrderService.java new file mode 100644 index 0000000..d3bda99 --- /dev/null +++ b/src/main/java/friend/spring/service/OrderService.java @@ -0,0 +1,13 @@ +package friend.spring.service; + +import friend.spring.domain.Order; +import friend.spring.web.dto.OrderRequestDTO; + +import javax.servlet.http.HttpServletRequest; + +public interface OrderService { + + Long createOrder(OrderRequestDTO orderRequestDTO, HttpServletRequest request); + + Order checkOrder(Long orderId); +} diff --git a/src/main/java/friend/spring/service/OrderServiceImpl.java b/src/main/java/friend/spring/service/OrderServiceImpl.java new file mode 100644 index 0000000..2f8944f --- /dev/null +++ b/src/main/java/friend/spring/service/OrderServiceImpl.java @@ -0,0 +1,81 @@ +package friend.spring.service; + +import friend.spring.apiPayload.code.status.ErrorStatus; +import friend.spring.apiPayload.handler.PaymentHandler; +import friend.spring.apiPayload.handler.UserHandler; +import friend.spring.domain.Order; +import friend.spring.domain.Payment; +import friend.spring.domain.User; +import friend.spring.domain.enums.PaymentState; +import friend.spring.domain.enums.Product; +import friend.spring.repository.OrderRepository; +import friend.spring.repository.PaymentRepository; +import friend.spring.repository.UserRepository; +import friend.spring.security.JwtTokenProvider; +import friend.spring.web.dto.OrderRequestDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; +import java.math.BigDecimal; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class OrderServiceImpl implements OrderService{ + + private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; + private final PaymentRepository paymentRepository; + private final OrderRepository orderRepository; + + + public Long createOrder(OrderRequestDTO orderRequestDTO, HttpServletRequest request) { + Product product = null; + long price = 0L; + + if(orderRequestDTO.getItemName().equals("POINT_1000")) { + product = Product.POINT_1000; + price = 1000L; + } else if (orderRequestDTO.getItemName().equals("POINT_2000")) { + product = Product.POINT_2000; + price = 2000L; + } + else { + throw new PaymentHandler(ErrorStatus.ORDER_PRODUCT_NOT_FOUND); + } + + Long userId = jwtTokenProvider.getCurrentUser(request); + User user = userRepository.findById(userId).orElseThrow(() -> { + throw new UserHandler(ErrorStatus.USER_NOT_FOUND); + }); + + Payment payment = Payment.builder() + .price(BigDecimal.valueOf(price)) + .paymentState(PaymentState.ING) + .build(); + + paymentRepository.save(payment); + + Order order = Order.builder() + .user(user) + .price(BigDecimal.valueOf(price)) + .product(product) + .orderUid(UUID.randomUUID().toString()) + .payment(payment) + .build(); + + Order result = orderRepository.save(order); + + return result.getId(); + } + + // 주문 체크 및 조회 + @Override + public Order checkOrder(Long orderId) { + return orderRepository.findById(orderId).orElseThrow(() -> { + throw new PaymentHandler(ErrorStatus.ORDER_NOT_FOUND); + }); + } + +} diff --git a/src/main/java/friend/spring/service/PaymentService.java b/src/main/java/friend/spring/service/PaymentService.java new file mode 100644 index 0000000..b2e008e --- /dev/null +++ b/src/main/java/friend/spring/service/PaymentService.java @@ -0,0 +1,16 @@ +package friend.spring.service; + +import com.siot.IamportRestClient.response.IamportResponse; + +import com.siot.IamportRestClient.response.Payment; +import friend.spring.web.dto.PaymentCallback; +import friend.spring.web.dto.PaymentResponseDTO; + +public interface PaymentService { + + String previewOrderUid(Long orderId); + + PaymentResponseDTO previewOrderResponse(String orderUid); + + IamportResponse paymentByCallBack(PaymentCallback paymentCallback); +} diff --git a/src/main/java/friend/spring/service/PaymentServiceImpl.java b/src/main/java/friend/spring/service/PaymentServiceImpl.java new file mode 100644 index 0000000..626aff7 --- /dev/null +++ b/src/main/java/friend/spring/service/PaymentServiceImpl.java @@ -0,0 +1,105 @@ +package friend.spring.service; + +import com.siot.IamportRestClient.IamportClient; +import com.siot.IamportRestClient.exception.IamportResponseException; +import com.siot.IamportRestClient.request.CancelData; +import com.siot.IamportRestClient.response.IamportResponse; +import com.siot.IamportRestClient.response.Payment; +import friend.spring.apiPayload.code.status.ErrorStatus; +import friend.spring.apiPayload.handler.PaymentHandler; +import friend.spring.domain.Order; + +import friend.spring.domain.enums.PaymentState; +import friend.spring.repository.OrderRepository; +import friend.spring.repository.PaymentRepository; +import friend.spring.web.dto.PaymentCallback; +import friend.spring.web.dto.PaymentResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.math.BigDecimal; + +@Service +@RequiredArgsConstructor +public class PaymentServiceImpl implements PaymentService{ + + private final OrderRepository orderRepository; + private final PaymentRepository paymentRepository; + private final IamportClient iamportClient; + + @Override + public String previewOrderUid(Long orderId) { + Order result = orderRepository.findById(orderId).orElseThrow(() -> { + throw new PaymentHandler(ErrorStatus.ORDER_ID_NOT_FOUND); + }); + return result.getOrderUid(); + } + + @Override + public PaymentResponseDTO previewOrderResponse(String orderUid) { + Order order = orderRepository.findOrderAndPaymentAndMember(orderUid).orElseThrow(() -> { + throw new PaymentHandler(ErrorStatus.ORDER_NOT_FOUND); + }); + + return PaymentResponseDTO.builder() + .buyerName(order.getUser().getNickname()) + .buyerEmail(order.getUser().getEmail()) + .paymentPrice(order.getPayment().getPrice()) + .product(order.getProduct()) + .orderUid(order.getOrderUid()) + .build(); + } + + @Override + public IamportResponse paymentByCallBack(PaymentCallback paymentCallback) { + try{ + // 결제 단건 조회 + IamportResponse iamportResponse = iamportClient.paymentByImpUid(paymentCallback.getPaymentUid()); + // 주문 내역 조회 + Order order = orderRepository.findOrderAndPayment(paymentCallback.getOrderUid()).orElseThrow(() -> new RuntimeException("주문이 없습니다.")); + + if(!iamportResponse.getResponse().getStatus().equals("paid")) { + orderRepository.delete(order); + paymentRepository.delete(order.getPayment()); + throw new PaymentHandler(ErrorStatus.PAYMENT_NOT_PAID); + } + + // 데이터 베이스상에 있는 결제금액 + BigDecimal price = order.getPayment().getPrice(); + // 실제 결제금액 + int iamportPrice = iamportResponse.getResponse().getAmount().intValue(); + + // 결제 금액 검증 + if (price.compareTo(BigDecimal.valueOf(iamportPrice)) != 0) { + + // 금액이 다를경우 주문 및 결제정보를 삭제합니다. + orderRepository.delete(order); + paymentRepository.delete(order.getPayment()); + + // 결제금액 위변조로 의심되는 결제 금액을 취소 + iamportClient.cancelPaymentByImpUid(new CancelData(iamportResponse.getResponse().getImpUid(), true, new BigDecimal(iamportPrice))); + + throw new PaymentHandler(ErrorStatus.PRICE_NOT_OK); + } + // 결제 상태 변경 + order.getPayment().changePaymentBySuccess(PaymentState.PAID, iamportResponse.getResponse().getImpUid()); + + // 멤버 포인트 변경 + int currentPoint = order.getUser().getPoint(); + + if(price.compareTo(BigDecimal.valueOf(1000)) == 0) { + order.getUser().setPoint(currentPoint + 1000); + } else if (price.compareTo(BigDecimal.valueOf(2000)) == 0) { + order.getUser().setPoint(currentPoint + 2000); + } else { + // 추후 pm님과 상의 후 포인트 로직 추가하거나 수정하면 될 것 같습니다. + } + return iamportResponse; + } catch (IamportResponseException | IOException e) { + throw new RuntimeException(e); + } + } + + +} diff --git a/src/main/java/friend/spring/web/controller/OrderController.java b/src/main/java/friend/spring/web/controller/OrderController.java new file mode 100644 index 0000000..50e2f37 --- /dev/null +++ b/src/main/java/friend/spring/web/controller/OrderController.java @@ -0,0 +1,32 @@ +package friend.spring.web.controller; + +import friend.spring.apiPayload.ApiResponse; +import friend.spring.domain.Order; +import friend.spring.service.OrderService; +import friend.spring.web.dto.OrderRequestDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/order") +public class OrderController { + + private final OrderService orderService; + + + // 주문 번호 생성 api + @PostMapping("/request") + public ApiResponse createOrder(@RequestBody OrderRequestDTO orderRequestDTO, @RequestHeader("atk") String atk, HttpServletRequest request) { + Long orderId = orderService.createOrder(orderRequestDTO, request); + return ApiResponse.onSuccess(orderId); + } + + // 생성된 주문 id 로 체크 + @GetMapping("/check") + public Order checkOrder(Long orderId, @RequestHeader("atk") String atk, HttpServletRequest request) { + return orderService.checkOrder(orderId); + } +} diff --git a/src/main/java/friend/spring/web/controller/PaymentController.java b/src/main/java/friend/spring/web/controller/PaymentController.java new file mode 100644 index 0000000..3d250bb --- /dev/null +++ b/src/main/java/friend/spring/web/controller/PaymentController.java @@ -0,0 +1,45 @@ +package friend.spring.web.controller; + +import com.siot.IamportRestClient.IamportClient; +import com.siot.IamportRestClient.response.IamportResponse; +import com.siot.IamportRestClient.response.Payment; +import friend.spring.apiPayload.ApiResponse; +import friend.spring.domain.Order; +import friend.spring.repository.OrderRepository; +import friend.spring.service.PaymentService; +import friend.spring.web.dto.PaymentCallback; +import friend.spring.web.dto.PaymentResponseDTO; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; + +@RestController +@RequiredArgsConstructor +public class PaymentController { + + private final PaymentService paymentService; + private static final Logger log = LoggerFactory.getLogger(PaymentController.class); // 로그 출력을 위한 선언입니다. + + + + @GetMapping("/payment/{orderId}") + public ApiResponse paymentPreview(@PathVariable Long orderId, @RequestHeader("atk") String atk, HttpServletRequest request) { + + String orderUid = paymentService.previewOrderUid(orderId); + PaymentResponseDTO paymentResponseDTO = paymentService.previewOrderResponse(orderUid); + return ApiResponse.onSuccess(paymentResponseDTO); + } + + @PostMapping("/payment") + public ApiResponse> validationPayment(@RequestBody PaymentCallback paymentCallback, @RequestHeader("atk") String atk, HttpServletRequest request) { + IamportResponse iamportResponse = paymentService.paymentByCallBack(paymentCallback); + log.info("결제 응답입니다.", iamportResponse.getResponse().toString()); // 결제 응답 로그 출력입니다. + return ApiResponse.onSuccess(iamportResponse); + } + + +} diff --git a/src/main/java/friend/spring/web/dto/OrderRequestDTO.java b/src/main/java/friend/spring/web/dto/OrderRequestDTO.java new file mode 100644 index 0000000..f1f9a9f --- /dev/null +++ b/src/main/java/friend/spring/web/dto/OrderRequestDTO.java @@ -0,0 +1,17 @@ +package friend.spring.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Pattern; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class OrderRequestDTO { + + @Pattern(regexp = "^(POINT_1000|POINT_2000)$", message = "포인트는 POINT_1000 또는 POINT_2000 중 하나여야 합니다.") + private String itemName; + +} diff --git a/src/main/java/friend/spring/web/dto/PaymentCallback.java b/src/main/java/friend/spring/web/dto/PaymentCallback.java new file mode 100644 index 0000000..16b5613 --- /dev/null +++ b/src/main/java/friend/spring/web/dto/PaymentCallback.java @@ -0,0 +1,12 @@ +package friend.spring.web.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; + +@Data +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class PaymentCallback { + private String paymentUid; // 결제 고유 번호 + private String orderUid; // 주문 고유 번호 +} diff --git a/src/main/java/friend/spring/web/dto/PaymentResponseDTO.java b/src/main/java/friend/spring/web/dto/PaymentResponseDTO.java new file mode 100644 index 0000000..d600b79 --- /dev/null +++ b/src/main/java/friend/spring/web/dto/PaymentResponseDTO.java @@ -0,0 +1,20 @@ +package friend.spring.web.dto; + +import friend.spring.domain.enums.Product; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Getter +@Builder +public class PaymentResponseDTO { + private String orderUid; + private Product product; + private String buyerName; + private String buyerEmail; + private BigDecimal paymentPrice; + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4f9a80a..5cfdb22 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -67,4 +67,7 @@ kakao: client: ${KAKAO_AUTH_CLIENT} redirect_uri: ${KAKAO_AUTH_REDIRECT_URI} secret_key: ${KAKAO_AUTH_SECRET_KEY} +iamport: + api_key: ${iamport_Rest_Api_Key} + api_secret: ${iamport_Rest_Api_Secret}