Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
280aa8c
Feat: 이미지 리사이징을 위한 의존성 추가
shinjaewon99 Dec 28, 2023
024f6a6
Feat: 리사이징한 이미지를 담기 위한 S3 버킷 추가
shinjaewon99 Dec 28, 2023
fd6412d
Feat: 이미지 리사이징 기능 추가
shinjaewon99 Dec 28, 2023
562377f
Feat: 장바구니에 담긴 삭품 삭제 기능 추가
shinjaewon99 Dec 30, 2023
e15b6e0
Feat: 상품 재고 감소 기능 추가
shinjaewon99 Dec 30, 2023
616f50a
Feat: 장바구니에 등록된 상품 삭제 기능 추가
shinjaewon99 Dec 30, 2023
a9d38d8
Feat: 장바구니에 등록된 상품 삭제 기능 추가
shinjaewon99 Dec 31, 2023
ee2f96e
Feat: 토스 페이먼츠 설정
shinjaewon99 Jan 3, 2024
e61a90c
Feat: X To One 관계 Lazy 적용
shinjaewon99 Jan 3, 2024
fccc71c
Feat: 결제 방식에 대한 enum 추가
shinjaewon99 Jan 4, 2024
06706ae
Feat: 주문 Entity 클래스 추가
shinjaewon99 Jan 4, 2024
89c4650
Feat: 결제요청, 성공시 롤백 URL, 실패시 롤백 URL 기능 추가
shinjaewon99 Jan 4, 2024
77ea678
Feat: 결제요청 기능, 성공시 롤백 기능, 실패시 롤백 기능 추가
shinjaewon99 Jan 4, 2024
24f5699
Feat: 결제관련 응답 DTO 추가
shinjaewon99 Jan 4, 2024
a55665b
Feat: 결제 요청 DTO 추가
shinjaewon99 Jan 4, 2024
40cc396
Feat: 결제 Repository 추가
shinjaewon99 Jan 4, 2024
d07b41a
Refactor : isImageExists 메소드를 한번만 호출하게 코드 최적화
shinjaewon99 Jan 11, 2024
7f65cf3
Refactor : 불필요한 유효성 검사 리펙토링
shinjaewon99 Jan 11, 2024
57d6bcb
Refactor : 메소드명 변경
shinjaewon99 Jan 11, 2024
46654c3
Feat : 유저의 장바구니 조회시 N + 1 문제 해결 기능 추가
shinjaewon99 Jan 14, 2024
ec90246
Refactor : 사용하지 않는 Index 제거
shinjaewon99 Jan 15, 2024
9796a09
Refactor : 유효성 검증 추가
shinjaewon99 Jan 15, 2024
e5124ab
Refactor : 반환시 ResponseEntity를 감싼후 return
shinjaewon99 Jan 15, 2024
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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/ll/netmong/base/config/TossPaymentConfig.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -18,16 +23,27 @@ public class CartController {
private final ItemCartService itemCartService;

@GetMapping
public RsData readProductCartByUser(@AuthenticationPrincipal UserDetails userDetails) {
public ResponseEntity<RsData> readCartByUser(@AuthenticationPrincipal UserDetails userDetails) {
String findMemberEmail = userDetails.getUsername();
return RsData.successOf(itemCartService.readMemberCartByUser(findMemberEmail));
RsData<List<ViewCartResponse>> responseBody = RsData.successOf(itemCartService.readMemberCartByUser(findMemberEmail));
return ResponseEntity.ok(responseBody);
}

@PostMapping("{productId}")
public RsData addMyCart(@AuthenticationPrincipal UserDetails currentUser,
public ResponseEntity<RsData> 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<String> responseBody = RsData.of("S-1", CART_SUCCESS_PRODUCT, "create");
return new ResponseEntity<>(responseBody, HttpStatus.CREATED);

}

@DeleteMapping("{productId}")
public ResponseEntity<RsData> removeProductFromCart(@AuthenticationPrincipal UserDetails currentUser,
@PathVariable(name = "productId") Long productId) {
cartService.deleteByProduct(currentUser, productId);
RsData responseBody = RsData.successOf("delete");
return ResponseEntity.ok(responseBody);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
8 changes: 5 additions & 3 deletions src/main/java/com/ll/netmong/domain/cart/entity/Cart.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, Long> {
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<ItemCart> findByMemberEmail(@Param("email")String findMemberEmail);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class ItemCartServiceImpl implements ItemCartService {

@Override
public List<ViewCartResponse> readMemberCartByUser(String findMemberEmail) {
List<ItemCart> findItemCart = itemCartRepository.findAll();
List<ItemCart> findItemCart = itemCartRepository.findByMemberEmail(findMemberEmail);
List<ViewCartResponse> memberProducts = new ArrayList<>();

for (ItemCart itemCart : findItemCart) {
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 <T> Optional<Image> uploadImage(T requestType, MultipartFile file) throws IOException {
String imageLocation = bucketUrl;
String imageLocation = originalBucketUrl;
String imageName = file.getOriginalFilename();
String requestTypeSimpleName = requestType.getClass().getSimpleName() + "/";

Expand All @@ -51,16 +57,35 @@ public <T> Optional<Image> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Loading