diff --git a/build.gradle b/build.gradle index 1007515f..931f9ee2 100644 --- a/build.gradle +++ b/build.gradle @@ -47,6 +47,8 @@ dependencies { testImplementation 'org.mockito:mockito-core:5.2.0' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + implementation 'net.coobird:thumbnailator:0.4.19' } tasks.named('bootBuildImage') { diff --git a/src/main/java/com/ll/netmong/base/config/TossPaymentConfig.java b/src/main/java/com/ll/netmong/base/config/TossPaymentConfig.java new file mode 100644 index 00000000..9fcfdc48 --- /dev/null +++ b/src/main/java/com/ll/netmong/base/config/TossPaymentConfig.java @@ -0,0 +1,31 @@ +package com.ll.netmong.base.config; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Getter +public class TossPaymentConfig { + public static final String TOSS_COMMON_URL = "https://api.tosspayments.com/v1/payments/"; + + @Value("${payment.toss.test-client-api-key}") + private String testClientApiKey; + + @Value("${payment.toss.test-secrete-api-key}") + private String testSecretKey; + + /** + * 실제 전자 결제 신청 후 사용할 API 키 + * @Value("${payment.toss.live_client_api_key}") + * private String liveClientApiKey; + * @Value("${payment.toss.live_secrete_api_key}") + * private String liveSecretKey; + */ + + @Value("${payment.success-url}") + private String successUrl; + + @Value("${payment.fail-url}") + private String failUrl; +} diff --git a/src/main/java/com/ll/netmong/domain/cart/controller/CartController.java b/src/main/java/com/ll/netmong/domain/cart/controller/CartController.java index 9cbe32bd..4eb9ffe4 100644 --- a/src/main/java/com/ll/netmong/domain/cart/controller/CartController.java +++ b/src/main/java/com/ll/netmong/domain/cart/controller/CartController.java @@ -2,13 +2,18 @@ import com.ll.netmong.common.RsData; import com.ll.netmong.domain.cart.dto.request.ProductCountRequest; +import com.ll.netmong.domain.cart.dto.response.ViewCartResponse; import com.ll.netmong.domain.cart.itemCart.service.ItemCartService; import com.ll.netmong.domain.cart.service.CartService; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequiredArgsConstructor @RequestMapping("api/v1/products/cart") @@ -18,16 +23,27 @@ public class CartController { private final ItemCartService itemCartService; @GetMapping - public RsData readProductCartByUser(@AuthenticationPrincipal UserDetails userDetails) { + public ResponseEntity readCartByUser(@AuthenticationPrincipal UserDetails userDetails) { String findMemberEmail = userDetails.getUsername(); - return RsData.successOf(itemCartService.readMemberCartByUser(findMemberEmail)); + RsData> responseBody = RsData.successOf(itemCartService.readMemberCartByUser(findMemberEmail)); + return ResponseEntity.ok(responseBody); } @PostMapping("{productId}") - public RsData addMyCart(@AuthenticationPrincipal UserDetails currentUser, + public ResponseEntity addProductToCart(@AuthenticationPrincipal UserDetails currentUser, @PathVariable(name = "productId") Long productId, @RequestBody ProductCountRequest productCountRequest) { cartService.addProductByCart(currentUser, productId, productCountRequest); - return RsData.of("S-1", CART_SUCCESS_PRODUCT, "create"); + RsData responseBody = RsData.of("S-1", CART_SUCCESS_PRODUCT, "create"); + return new ResponseEntity<>(responseBody, HttpStatus.CREATED); + + } + + @DeleteMapping("{productId}") + public ResponseEntity removeProductFromCart(@AuthenticationPrincipal UserDetails currentUser, + @PathVariable(name = "productId") Long productId) { + cartService.deleteByProduct(currentUser, productId); + RsData responseBody = RsData.successOf("delete"); + return ResponseEntity.ok(responseBody); } } diff --git a/src/main/java/com/ll/netmong/domain/cart/dto/request/ProductCountRequest.java b/src/main/java/com/ll/netmong/domain/cart/dto/request/ProductCountRequest.java index 7f43ab1c..520acf55 100644 --- a/src/main/java/com/ll/netmong/domain/cart/dto/request/ProductCountRequest.java +++ b/src/main/java/com/ll/netmong/domain/cart/dto/request/ProductCountRequest.java @@ -1,10 +1,14 @@ package com.ll.netmong.domain.cart.dto.request; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import lombok.Getter; import lombok.Setter; @Getter @Setter public class ProductCountRequest { + @Min(value = 1, message = "상품의 수량은 최소 1개 이상이어야 합니다.") + @Max(value = 30, message = "상품의 수량은 최대 30개까지 가능합니다.") private Integer count; } diff --git a/src/main/java/com/ll/netmong/domain/cart/entity/Cart.java b/src/main/java/com/ll/netmong/domain/cart/entity/Cart.java index 1b19ba0a..ab2b631a 100644 --- a/src/main/java/com/ll/netmong/domain/cart/entity/Cart.java +++ b/src/main/java/com/ll/netmong/domain/cart/entity/Cart.java @@ -10,9 +10,7 @@ @Entity @Getter -@Table(name = "product_cart", indexes = { - @Index(name = "idx_member_id", columnList = "member_id") -}) +@Table(name = "product_cart") @NoArgsConstructor(access = AccessLevel.PROTECTED) @SuperBuilder(toBuilder = true) public class Cart extends BaseEntity { @@ -34,4 +32,8 @@ public static Cart createCart(Member member) { public void addCount(Integer count) { this.totalCount += count; } + + public void minusCount(Integer count){ + this.totalCount -= count; + } } diff --git a/src/main/java/com/ll/netmong/domain/cart/itemCart/repository/ItemCartRepository.java b/src/main/java/com/ll/netmong/domain/cart/itemCart/repository/ItemCartRepository.java index e11ff50a..1600765f 100644 --- a/src/main/java/com/ll/netmong/domain/cart/itemCart/repository/ItemCartRepository.java +++ b/src/main/java/com/ll/netmong/domain/cart/itemCart/repository/ItemCartRepository.java @@ -2,7 +2,14 @@ import com.ll.netmong.domain.cart.itemCart.entity.ItemCart; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface ItemCartRepository extends JpaRepository { ItemCart findByCartIdAndProductId(Long cartId, Long productId); + + @Query("select ic from ItemCart ic join fetch ic.cart c join fetch c.member m join fetch ic.product where m.email = :email") + List findByMemberEmail(@Param("email")String findMemberEmail); } diff --git a/src/main/java/com/ll/netmong/domain/cart/itemCart/service/ItemCartService.java b/src/main/java/com/ll/netmong/domain/cart/itemCart/service/ItemCartService.java index fb9f11dd..403c5404 100644 --- a/src/main/java/com/ll/netmong/domain/cart/itemCart/service/ItemCartService.java +++ b/src/main/java/com/ll/netmong/domain/cart/itemCart/service/ItemCartService.java @@ -14,6 +14,8 @@ public interface ItemCartService { void addToCartForExistingProduct(ItemCart findItemCart, Cart cart, ProductCountRequest productCountRequest); + void deleteByProduct(Cart cart, Long productId); + ItemCart getItemCart(Cart cart, Long productId); String findMemberEmailByProductId(Long productId); diff --git a/src/main/java/com/ll/netmong/domain/cart/itemCart/service/ItemCartServiceImpl.java b/src/main/java/com/ll/netmong/domain/cart/itemCart/service/ItemCartServiceImpl.java index 43bf487b..623591b3 100644 --- a/src/main/java/com/ll/netmong/domain/cart/itemCart/service/ItemCartServiceImpl.java +++ b/src/main/java/com/ll/netmong/domain/cart/itemCart/service/ItemCartServiceImpl.java @@ -31,7 +31,7 @@ public class ItemCartServiceImpl implements ItemCartService { @Override public List readMemberCartByUser(String findMemberEmail) { - List findItemCart = itemCartRepository.findAll(); + List findItemCart = itemCartRepository.findByMemberEmail(findMemberEmail); List memberProducts = new ArrayList<>(); for (ItemCart itemCart : findItemCart) { @@ -73,13 +73,27 @@ public void addToCartForExistingProduct(ItemCart findItemCart, Cart cart, Produc findItemCart.addCount(productCountRequest.getCount()); } + @Override + public void deleteByProduct(Cart cart, Long productId) { + ItemCart itemCart = getItemCart(cart, productId); + int stackCount = itemCart.getStackCount(); + + Product product = productRepository.findById(itemCart.getProduct().getId()) + .orElseThrow(() -> new ProductException("존재하지 않는 상품입니다.", ProductErrorCode.NOT_EXIST_PRODUCT)); + product.setCount(product.getCount() + stackCount); + + cart.minusCount(stackCount); + itemCartRepository.deleteById(itemCart.getId()); + productRepository.save(product); + } + @Transactional public void removeStock(Long productId, Integer count) { try { transactionTemplate.execute(status -> { // Pessimistic Lock을 걸고 상품을 조회합니다. Product findByProduct = productRepository.findByIdWithPessimisticLock(productId) - .orElseThrow(() -> new ProductException("존재하지 않는 상품입니다.", ProductErrorCode.NOT_EXIST_PRODUCT_NAME)); + .orElseThrow(() -> new ProductException("존재하지 않는 상품입니다.", ProductErrorCode.NOT_EXIST_PRODUCT)); int restStock = findByProduct.getCount() - count; diff --git a/src/main/java/com/ll/netmong/domain/cart/service/CartService.java b/src/main/java/com/ll/netmong/domain/cart/service/CartService.java index 75495a92..5922bf71 100644 --- a/src/main/java/com/ll/netmong/domain/cart/service/CartService.java +++ b/src/main/java/com/ll/netmong/domain/cart/service/CartService.java @@ -8,4 +8,6 @@ public interface CartService { void createCart(Member member); void addProductByCart(UserDetails currentUser, Long productId, ProductCountRequest productCountRequest); + + void deleteByProduct(UserDetails currentUser, Long productId); } diff --git a/src/main/java/com/ll/netmong/domain/cart/service/CartServiceImpl.java b/src/main/java/com/ll/netmong/domain/cart/service/CartServiceImpl.java index acce7fce..ad933f47 100644 --- a/src/main/java/com/ll/netmong/domain/cart/service/CartServiceImpl.java +++ b/src/main/java/com/ll/netmong/domain/cart/service/CartServiceImpl.java @@ -44,6 +44,13 @@ public void addProductByCart(UserDetails currentUser, Long productId, ProductCou itemCartService.addToCartForExistingProduct(findItemCart.get(), cart, productCountRequest); } + @Override + @Transactional + public void deleteByProduct(UserDetails currentUser, Long productId) { + Cart cart = validateExistMember(currentUser); + itemCartService.deleteByProduct(cart, productId); + } + private Cart validateExistMember(UserDetails currentUser) { return cartRepository.findByMemberEmail(currentUser.getUsername()) .orElseThrow(() -> new ProductException("회원이 존재하지 않습니다.", ProductErrorCode.NOT_EXIST_PRODUCT)); diff --git a/src/main/java/com/ll/netmong/domain/image/service/ImageServiceImpl.java b/src/main/java/com/ll/netmong/domain/image/service/ImageServiceImpl.java index 20afd558..ce9876ba 100644 --- a/src/main/java/com/ll/netmong/domain/image/service/ImageServiceImpl.java +++ b/src/main/java/com/ll/netmong/domain/image/service/ImageServiceImpl.java @@ -7,11 +7,14 @@ import com.ll.netmong.domain.post.entity.Post; import com.ll.netmong.domain.product.entity.Product; import lombok.RequiredArgsConstructor; +import net.coobird.thumbnailator.Thumbnails; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Optional; @@ -21,15 +24,18 @@ public class ImageServiceImpl implements ImageService { private final AmazonS3Client amazonS3Client; private final ImageRepository imageRepository; + @Value("${cloud.aws.s3.resized-bucket}") + private String resizedBucket; + @Value("${cloud.aws.s3.bucket}") - private String bucket; + private String originalBucket; @Value("${cloud.aws.s3.url}") - private String bucketUrl; + private String originalBucketUrl; @Transactional public Optional uploadImage(T requestType, MultipartFile file) throws IOException { - String imageLocation = bucketUrl; + String imageLocation = originalBucketUrl; String imageName = file.getOriginalFilename(); String requestTypeSimpleName = requestType.getClass().getSimpleName() + "/"; @@ -51,16 +57,35 @@ public Optional uploadImage(T requestType, MultipartFile file) throws if (image.isPresent()) { imageRepository.save(image.get()); - createS3Bucket(fileName, file); + uploadOriginalImage(originalBucket, fileName, file); + uploadResizedImage(resizedBucket, fileName, file); } return image; } - private void createS3Bucket(String fileName, MultipartFile image) throws IOException { + private void uploadOriginalImage(String bucketName, String fileName, MultipartFile image) throws IOException { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(image.getContentType()); + metadata.setContentLength(image.getSize()); + amazonS3Client.putObject(bucketName, fileName, image.getInputStream(), metadata); + } + + private void uploadResizedImage(String bucketName, String fileName, MultipartFile image) throws IOException { ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentType(image.getContentType()); metadata.setContentLength(image.getSize()); - amazonS3Client.putObject(bucket, fileName, image.getInputStream(), metadata); + + byte[] buffer = getResizedImageStream(image); + metadata.setContentLength(buffer.length); + ByteArrayInputStream inputStream = new ByteArrayInputStream(buffer); + + amazonS3Client.putObject(bucketName, fileName, inputStream, metadata); + } + + private byte[] getResizedImageStream(MultipartFile image) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Thumbnails.of(image.getInputStream()).size(400, 400).toOutputStream(outputStream); + return outputStream.toByteArray(); } } diff --git a/src/main/java/com/ll/netmong/domain/order/controller/OrderController.java b/src/main/java/com/ll/netmong/domain/order/controller/OrderController.java new file mode 100644 index 00000000..1aea222b --- /dev/null +++ b/src/main/java/com/ll/netmong/domain/order/controller/OrderController.java @@ -0,0 +1,51 @@ +package com.ll.netmong.domain.order.controller; + +import com.ll.netmong.base.config.TossPaymentConfig; +import com.ll.netmong.common.RsData; +import com.ll.netmong.domain.order.dto.request.PaymentRequest; +import com.ll.netmong.domain.order.dto.response.PaymentFailResponse; +import com.ll.netmong.domain.order.dto.response.PaymentResponse; +import com.ll.netmong.domain.order.dto.response.PaymentSuccessResponse; +import com.ll.netmong.domain.order.service.OrderService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import java.util.Optional; + +@RestController +@RequiredArgsConstructor +@RequestMapping("api/v1/order") +public class OrderController { + private final OrderService orderService; + private final TossPaymentConfig tossPaymentConfig; + + @PostMapping + public RsData requestPaymentByToss(@AuthenticationPrincipal UserDetails currentUser, + @RequestBody PaymentRequest paymentRequest) throws Exception { + PaymentResponse paymentResponse = orderService.requestPayment(paymentRequest.createOrder(), currentUser.getUsername()); + paymentResponse.setSuccessUrl(Optional.ofNullable(paymentRequest.getSuccessUrl()).orElse(tossPaymentConfig.getSuccessUrl())); + paymentResponse.setFailUrl(Optional.ofNullable(paymentRequest.getFailUrl()).orElse(tossPaymentConfig.getFailUrl())); + + return RsData.successOf(paymentResponse); + } + + @GetMapping("/success") + public RsData successPaymentByToss(@RequestParam String paymentKey, + @RequestParam String orderId, + @RequestParam Long amount) { + PaymentSuccessResponse paymentSuccessResponse = orderService.paymentSuccess(paymentKey, orderId, amount); + + return RsData.successOf(paymentSuccessResponse); + } + + @GetMapping("/fail") + public RsData failPaymentByToss(@RequestParam String code, + @RequestParam String orderId, + @RequestParam String message) { + PaymentFailResponse paymentFailResponse = orderService.failPayment(code, orderId, message); + + return RsData.successOf(paymentFailResponse); + } +} diff --git a/src/main/java/com/ll/netmong/domain/order/dto/request/PaymentRequest.java b/src/main/java/com/ll/netmong/domain/order/dto/request/PaymentRequest.java new file mode 100644 index 00000000..bcc27008 --- /dev/null +++ b/src/main/java/com/ll/netmong/domain/order/dto/request/PaymentRequest.java @@ -0,0 +1,29 @@ +package com.ll.netmong.domain.order.dto.request; + +import com.ll.netmong.domain.order.entity.Order; +import com.ll.netmong.domain.order.util.PayType; +import lombok.Getter; +import lombok.Setter; + +import java.util.UUID; + +@Getter +@Setter +public class PaymentRequest { + private PayType payType; + private String orderName; // 주문명, ex : 포인트 충전 + private Long amount; + private String successUrl; // 성공 시 리다이렉트 될 URL + private String failUrl; // 실패 시 리다이렉트 될 URL + + public Order createOrder() { + return Order + .builder() + .payType(payType) + .amount(amount) + .orderName(orderName) + .orderId(UUID.randomUUID().toString()) + .paySuccessYN(false) + .build(); + } +} diff --git a/src/main/java/com/ll/netmong/domain/order/dto/response/PaymentFailResponse.java b/src/main/java/com/ll/netmong/domain/order/dto/response/PaymentFailResponse.java new file mode 100644 index 00000000..9b6ecd99 --- /dev/null +++ b/src/main/java/com/ll/netmong/domain/order/dto/response/PaymentFailResponse.java @@ -0,0 +1,16 @@ +package com.ll.netmong.domain.order.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@Builder +public class PaymentFailResponse { + private String errorCode; + private String errorMessage; + private String orderId; +} diff --git a/src/main/java/com/ll/netmong/domain/order/dto/response/PaymentResponse.java b/src/main/java/com/ll/netmong/domain/order/dto/response/PaymentResponse.java new file mode 100644 index 00000000..36a18f85 --- /dev/null +++ b/src/main/java/com/ll/netmong/domain/order/dto/response/PaymentResponse.java @@ -0,0 +1,25 @@ +package com.ll.netmong.domain.order.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@Builder +public class PaymentResponse { + private String payType; + private Long amount; + private String orderName; + private String orderId; + private String customerEmail; + private String customerName; + private String successUrl; + private String failUrl; + private String failReason; + private boolean cancelYN; + private String cancelReason; + private String createdAt; +} diff --git a/src/main/java/com/ll/netmong/domain/order/dto/response/PaymentSuccessResponse.java b/src/main/java/com/ll/netmong/domain/order/dto/response/PaymentSuccessResponse.java new file mode 100644 index 00000000..6ef1c9da --- /dev/null +++ b/src/main/java/com/ll/netmong/domain/order/dto/response/PaymentSuccessResponse.java @@ -0,0 +1,27 @@ +package com.ll.netmong.domain.order.dto.response; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class PaymentSuccessResponse { + private String mid; // 가맹점 Id -> tosspayments + private String version; // Payment 객체 응답 버전 + private String paymentKey; + private String orderId; + private String orderName; + private String currency; // "KRW" + private String method; // 결제 수단 + private String totalAmount; + private String balanceAmount; + private String suppliedAmount; + private String vat; // 부가가치세 + private String status; // 결제 처리 상태 + private String requestedAt; + private String approvedAt; + private String useEscrow; // false + private String cultureExpense; // false +// private PaymentSuccessCardDto card; // 결제 카드 정보 (아래 자세한 정보 있음) + private String type; // 결제 타입 정보 (NOMAL / BILLING / CONNECTPAY) +} diff --git a/src/main/java/com/ll/netmong/domain/order/entity/Order.java b/src/main/java/com/ll/netmong/domain/order/entity/Order.java new file mode 100644 index 00000000..db6085e4 --- /dev/null +++ b/src/main/java/com/ll/netmong/domain/order/entity/Order.java @@ -0,0 +1,84 @@ +package com.ll.netmong.domain.order.entity; + +import com.ll.netmong.common.BaseEntity; +import com.ll.netmong.domain.member.entity.Member; +import com.ll.netmong.domain.order.dto.response.PaymentResponse; +import com.ll.netmong.domain.order.util.PayType; +import com.ll.netmong.domain.product.entity.Product; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Entity +@Getter +@Table(name = "payment") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SuperBuilder(toBuilder = true) +public class Order extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @Column(name = "pay_type") + @Enumerated(EnumType.STRING) + private PayType payType; + + @Column(name = "pay_amount") + private Long amount; + + @Column(name = "pay_name") + private String orderName; + + @Column(name = "order_id") + private String orderId; + + @Column(name = "payment_key") + private String paymentKey; + + @Column(name = "pay_success_status") + private boolean paySuccessYN; + + @Column(name = "cancel_status") + private boolean cancelYN; + + @Column(name = "fail_reason") + private String failReason; + + + public void setMember(Member member) { + this.member = member; + } + + + public void paymentSuccess(String paymentKey) { + this.paymentKey = paymentKey; + this.paySuccessYN = true; + } + + public void paymentFail(String message) { + this.failReason = message; + this.paySuccessYN = false; + } + + public PaymentResponse toPaymentResponse() { + return PaymentResponse + .builder() + .payType(payType.getPaymentType()) + .amount(amount) + .orderName(orderName) + .orderId(orderId) + .customerEmail(member.getEmail()) + .customerName(member.getRealName()) + .createdAt(String.valueOf(getCreateDate())) + .cancelYN(cancelYN) + .failReason(failReason) + .build(); + } +} diff --git a/src/main/java/com/ll/netmong/domain/order/repository/OrderRepository.java b/src/main/java/com/ll/netmong/domain/order/repository/OrderRepository.java new file mode 100644 index 00000000..06e6110b --- /dev/null +++ b/src/main/java/com/ll/netmong/domain/order/repository/OrderRepository.java @@ -0,0 +1,11 @@ +package com.ll.netmong.domain.order.repository; + +import com.ll.netmong.domain.order.entity.Order; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface OrderRepository extends JpaRepository { + + Optional findByOrderId(String orderId); +} diff --git a/src/main/java/com/ll/netmong/domain/order/service/OrderService.java b/src/main/java/com/ll/netmong/domain/order/service/OrderService.java new file mode 100644 index 00000000..55e2f46c --- /dev/null +++ b/src/main/java/com/ll/netmong/domain/order/service/OrderService.java @@ -0,0 +1,15 @@ +package com.ll.netmong.domain.order.service; + +import com.ll.netmong.domain.order.dto.response.PaymentFailResponse; +import com.ll.netmong.domain.order.dto.response.PaymentResponse; +import com.ll.netmong.domain.order.dto.response.PaymentSuccessResponse; +import com.ll.netmong.domain.order.entity.Order; + +public interface OrderService { + + PaymentResponse requestPayment(Order order, String userEmail) throws Exception; + + PaymentSuccessResponse paymentSuccess(String paymentKey, String orderId, Long amount); + + PaymentFailResponse failPayment(String code, String orderId, String message); +} diff --git a/src/main/java/com/ll/netmong/domain/order/service/OrderServiceImpl.java b/src/main/java/com/ll/netmong/domain/order/service/OrderServiceImpl.java new file mode 100644 index 00000000..ec3b337d --- /dev/null +++ b/src/main/java/com/ll/netmong/domain/order/service/OrderServiceImpl.java @@ -0,0 +1,127 @@ +package com.ll.netmong.domain.order.service; + +import com.ll.netmong.base.config.TossPaymentConfig; +import com.ll.netmong.domain.member.entity.Member; +import com.ll.netmong.domain.member.service.MemberService; +import com.ll.netmong.domain.order.dto.response.PaymentFailResponse; +import com.ll.netmong.domain.order.dto.response.PaymentResponse; +import com.ll.netmong.domain.order.dto.response.PaymentSuccessResponse; +import com.ll.netmong.domain.order.entity.Order; +import com.ll.netmong.domain.order.repository.OrderRepository; +import lombok.RequiredArgsConstructor; +import net.minidev.json.JSONObject; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; + + +@Service +@RequiredArgsConstructor +public class OrderServiceImpl implements OrderService { + private final MemberService memberService; + private final OrderRepository orderRepository; + private final TossPaymentConfig tossPaymentConfig; + + @Override + @Transactional + public PaymentResponse requestPayment(Order order, String userEmail) throws Exception { + Member findMemberByEmail = memberService.findByEmail(userEmail); + order.setMember(findMemberByEmail); + + return orderRepository.save(order).toPaymentResponse(); + } + + @Override + @Transactional + public PaymentSuccessResponse paymentSuccess(String paymentKey, String orderId, Long amount) { + Order order = verifyPayment(orderId, amount); // 요청가격 == 결제된 금액 + PaymentSuccessResponse result = requestPaymentAccept(paymentKey, orderId, amount); + order.paymentSuccess(paymentKey); + + // todo : 회원의 포인트 갱신으로 인해 Member 테이블에 Point 컬럼 추가 +// order.getMember(). + + return result; + } + + @Override + @Transactional + public PaymentFailResponse failPayment(String code, String orderId, String message) { + Order order = findByOrder(orderId); + order.paymentFail(message); + + return requestPaymentFail(code, message, orderId); + } + + private PaymentFailResponse requestPaymentFail(String code, String message, String orderId) { + return PaymentFailResponse + .builder() + .errorCode(code) + .errorMessage(message) + .orderId(orderId) + .build(); + } + + private Order verifyPayment(String orderId, Long amount) { + Order order = findByOrder(orderId); + + if (!order.getAmount().equals(amount)) { + throw new IllegalArgumentException(); + } + return order; + } + + private Order findByOrder(String orderId) { + return orderRepository.findByOrderId(orderId).orElseThrow(() -> { + throw new IllegalArgumentException(); + }); + } + + private PaymentSuccessResponse requestPaymentAccept(String paymentKey, String orderId, Long amount) { + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = getHeaders(); + JSONObject params = createParmas(orderId, amount); + + PaymentSuccessResponse result; + + try { + result = restTemplate.postForObject(TossPaymentConfig.TOSS_COMMON_URL + paymentKey, + new HttpEntity<>(params, headers), + PaymentSuccessResponse.class); + } catch (Exception exception) { + throw new IllegalArgumentException(); + } + + return result; + } + + private HttpHeaders getHeaders() { + HttpHeaders headers = new HttpHeaders(); + + // 토스에서 받은 시크릿 키를 Base64를 이용하여 인코딩 한다. + // 이때, {시크릿 키 + ":"} 조합으로 인코딩 해야함 + String encodeAuthKey = new String( + Base64.getEncoder().encode((tossPaymentConfig.getTestSecretKey() + ":").getBytes(StandardCharsets.UTF_8)) + ); + + headers.setBasicAuth(encodeAuthKey); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + + return headers; + } + + private JSONObject createParmas(String orderId, Long amount) { + JSONObject params = new JSONObject(); + params.put("orderId", orderId); + params.put("amount", amount); + return params; + } +} diff --git a/src/main/java/com/ll/netmong/domain/order/util/PayType.java b/src/main/java/com/ll/netmong/domain/order/util/PayType.java new file mode 100644 index 00000000..149ecdc2 --- /dev/null +++ b/src/main/java/com/ll/netmong/domain/order/util/PayType.java @@ -0,0 +1,20 @@ +package com.ll.netmong.domain.order.util; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum PayType { + CARD("카드"), + CASH("현금"), + POINT("포인트"); + + PayType(String paymentType) { + this.paymentType = paymentType; + } + + private final String paymentType; + + @JsonValue + public String getPaymentType() { + return paymentType; + } +} diff --git a/src/main/java/com/ll/netmong/domain/product/controller/ProductController.java b/src/main/java/com/ll/netmong/domain/product/controller/ProductController.java index 479d1262..9dd48e16 100644 --- a/src/main/java/com/ll/netmong/domain/product/controller/ProductController.java +++ b/src/main/java/com/ll/netmong/domain/product/controller/ProductController.java @@ -64,7 +64,7 @@ public RsData findByProductCategory(@PathVariable(name = "category") String cate @GetMapping("/name/{name}") public RsData findByProductName(@PathVariable(name = "name") String name) { - return RsData.successOf(productService.findByProductName(name)); + return RsData.successOf(productService.findProductsByProductName(name)); } @GetMapping("/all") diff --git a/src/main/java/com/ll/netmong/domain/product/entity/Product.java b/src/main/java/com/ll/netmong/domain/product/entity/Product.java index 8a29dff2..bf108af3 100644 --- a/src/main/java/com/ll/netmong/domain/product/entity/Product.java +++ b/src/main/java/com/ll/netmong/domain/product/entity/Product.java @@ -47,7 +47,7 @@ public class Product extends BaseEntity { @JoinColumn(name = "member_id") private Member member; - @OneToOne(orphanRemoval = true) + @OneToOne(orphanRemoval = true, fetch = FetchType.LAZY) @JoinColumn(name = "image_id") private Image image; diff --git a/src/main/java/com/ll/netmong/domain/product/service/ProductService.java b/src/main/java/com/ll/netmong/domain/product/service/ProductService.java index 4bf05d40..dd45cd63 100644 --- a/src/main/java/com/ll/netmong/domain/product/service/ProductService.java +++ b/src/main/java/com/ll/netmong/domain/product/service/ProductService.java @@ -22,7 +22,7 @@ public interface ProductService { List findByProductCategory(Category category); - List findByProductName(String productName); + List findProductsByProductName(String productName); Page readPageByProduct(Pageable pageable); diff --git a/src/main/java/com/ll/netmong/domain/product/service/ProductServiceImpl.java b/src/main/java/com/ll/netmong/domain/product/service/ProductServiceImpl.java index aac7f631..686ecd6e 100644 --- a/src/main/java/com/ll/netmong/domain/product/service/ProductServiceImpl.java +++ b/src/main/java/com/ll/netmong/domain/product/service/ProductServiceImpl.java @@ -38,11 +38,8 @@ public class ProductServiceImpl implements ProductService { @Transactional public void createProductWithImage(UserDetails currentUser, CreateRequest createRequest, MultipartFile images) throws IOException { - if (!isImageExists(images)) { - initProduct(currentUser, createRequest); - } + Product product = initProduct(currentUser, createRequest); if (isImageExists(images)) { - Product product = initProduct(currentUser, createRequest); product.addProductImage(imageService.uploadImage(product, images).orElseThrow()); } } @@ -63,7 +60,7 @@ public List findByProductCategory(Category category) { } @Override - public List findByProductName(String productName) { + public List findProductsByProductName(String productName) { List products = productRepository.findByProductName(productName); if (products.isEmpty()) { throw new ProductException("존재 하지 않는 상품 이름 입니다.", ProductErrorCode.NOT_EXIST_PRODUCT_NAME); @@ -92,7 +89,8 @@ public void updateProduct(UserDetails currentUser, @Transactional public void softDeleteProduct(UserDetails currentUser, Long productId) { - validateCurrentUser(currentUser, validateExistProduct(productId)); + Product findProduct = validateExistProduct(productId); + validateCurrentUser(currentUser, findProduct); productRepository.deleteById(productId); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6f2dbcee..23546396 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -28,8 +28,8 @@ spring: multipart: enabled: true location: '${custom.image.url}' - max-request-size: 500MB - max-file-size: 500MB + max-request-size: '${custom.image.request-size}' + max-file-size: '${custom.image.file-size}' security: oauth2: client: @@ -44,6 +44,7 @@ spring: cloud: aws: s3: + resized-bucket: '${cloud.aws.s3.resized-bucket}' bucket: '${cloud.aws.s3.bucket}' url: '${cloud.aws.s3.url}' stack.auto: false @@ -57,4 +58,11 @@ logging: root: INFO org.hibernate.orm.jdbc.bind: trace +payment: + toss: + test-client-api-key: '${payment.toss.test-client-api-key}' + test-secrete-api-key: '${payment.toss.test-secrete-api-key}' + success-url: '${payment.success-url}' + fail-url: '${payment.fail-url}' + domain: '${custom.image.domain}'