diff --git a/backend/JiShop/build.gradle b/backend/JiShop/build.gradle index fa537f3e..afc10d4e 100644 --- a/backend/JiShop/build.gradle +++ b/backend/JiShop/build.gradle @@ -46,7 +46,7 @@ dependencies { // Querydsl implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' - annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" @@ -80,6 +80,9 @@ dependencies { implementation 'org.springframework.session:spring-session-data-redis' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + // Apple Silicon native DNS resolver + implementation 'io.netty:netty-resolver-dns-native-macos:4.1.100.Final:osx-aarch_64' + // Logstash를 위한 logstash-logback-encoder // implementation 'net.logstash.logback:logstash-logback-encoder:7.2' @@ -97,3 +100,13 @@ dependencyManagement { tasks.named('test') { useJUnitPlatform() } + +def generated = 'build/generated/sources/annotationProcessor/java/main' + +tasks.withType(JavaCompile).configureEach { + options.getGeneratedSourceOutputDirectory().set(file(generated)) +} + +clean { + delete file(generated) +} \ No newline at end of file diff --git a/backend/JiShop/gradlew b/backend/JiShop/gradlew old mode 100644 new mode 100755 diff --git a/backend/JiShop/src/main/java/com/jishop/cart/dto/CartDetailResponse.java b/backend/JiShop/src/main/java/com/jishop/cart/dto/CartDetailResponse.java index b1baef45..4f13e47e 100644 --- a/backend/JiShop/src/main/java/com/jishop/cart/dto/CartDetailResponse.java +++ b/backend/JiShop/src/main/java/com/jishop/cart/dto/CartDetailResponse.java @@ -29,37 +29,37 @@ public static CartDetailResponse from( return new CartDetailResponse( cartId, product.getId(), - product.getProduct().getName(), + product.getProduct().getProductInfo().getName(), product.getOption() != null ? product.getOption().getOptionValue() : "기본옵션", paymentPrice, orderPrice, discountPrice, quantity, paymentPrice * quantity, - product.getProduct().getMainImage(), - product.getProduct().getBrand(), + product.getProduct().getImage().getMainImage(), + product.getProduct().getProductInfo().getBrand(), isExisted ); } public static CartDetailResponse of(Cart cart, boolean isExisted) { SaleProduct saleProduct = cart.getSaleProduct(); - int paymentPrice = saleProduct.getProduct().getDiscountPrice(); - int orderPrice = saleProduct.getProduct().getOriginPrice(); + int paymentPrice = saleProduct.getProduct().getProductInfo().getDiscountPrice(); + int orderPrice = saleProduct.getProduct().getProductInfo().getOriginPrice(); int discountPrice = orderPrice - paymentPrice; return new CartDetailResponse( cart.getId(), saleProduct.getId(), - saleProduct.getProduct().getName(), + saleProduct.getProduct().getProductInfo().getName(), saleProduct.getOption() != null ? saleProduct.getOption().getOptionValue() : "기본옵션", paymentPrice, orderPrice, discountPrice, cart.getQuantity(), paymentPrice * cart.getQuantity(), - saleProduct.getProduct().getMainImage(), - saleProduct.getProduct().getBrand(), + saleProduct.getProduct().getImage().getMainImage(), + saleProduct.getProduct().getProductInfo().getBrand(), isExisted ); } diff --git a/backend/JiShop/src/main/java/com/jishop/cart/service/Impl/CartServiceImpl.java b/backend/JiShop/src/main/java/com/jishop/cart/service/Impl/CartServiceImpl.java index aba860c0..632b6ad5 100644 --- a/backend/JiShop/src/main/java/com/jishop/cart/service/Impl/CartServiceImpl.java +++ b/backend/JiShop/src/main/java/com/jishop/cart/service/Impl/CartServiceImpl.java @@ -133,15 +133,16 @@ public CartResponse getGuestCart(List guestCartRequests) { return new CartDetailResponse( null, //장바구니 ID는 null (비회원이니까) saleProduct.getId(), - saleProduct.getProduct().getName(), + saleProduct.getProduct().getProductInfo().getName(), saleProduct.getOption() != null ? saleProduct.getOption().getOptionValue() : "기본옵션", - saleProduct.getProduct().getDiscountPrice(), - saleProduct.getProduct().getOriginPrice(), - saleProduct.getProduct().getOriginPrice() - saleProduct.getProduct().getDiscountPrice(), + saleProduct.getProduct().getProductInfo().getDiscountPrice(), + saleProduct.getProduct().getProductInfo().getOriginPrice(), + saleProduct.getProduct().getProductInfo().getOriginPrice() - + saleProduct.getProduct().getProductInfo().getDiscountPrice(), quantity, - saleProduct.getProduct().getDiscountPrice() * quantity, - saleProduct.getProduct().getMainImage(), - saleProduct.getProduct().getBrand(), + saleProduct.getProduct().getProductInfo().getDiscountPrice() * quantity, + saleProduct.getProduct().getImage().getMainImage(), + saleProduct.getProduct().getProductInfo().getBrand(), false ); }) diff --git a/backend/JiShop/src/main/java/com/jishop/category/controller/CategoryController.java b/backend/JiShop/src/main/java/com/jishop/category/controller/CategoryController.java index f5f22bb0..f40101c0 100644 --- a/backend/JiShop/src/main/java/com/jishop/category/controller/CategoryController.java +++ b/backend/JiShop/src/main/java/com/jishop/category/controller/CategoryController.java @@ -1,18 +1,17 @@ package com.jishop.category.controller; import com.jishop.category.dto.CategoryResponse; -import com.jishop.product.dto.response.ProductResponse; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.data.web.PagedModel; import java.util.List; @Tag(name = "카테고리 API") public interface CategoryController { - PagedModel getProductListByCategory(Long CategoryId, int page); - + @Operation(summary = "최상위 카테고리의 ID와 이름 조회") List getCategoryFilterInfo(); + @Operation(summary = "해당 카테고리의 한단계 하위카테고리 ID 조회") List getSubcategoriesByParentId(Long categoryId); } diff --git a/backend/JiShop/src/main/java/com/jishop/category/controller/CategoryControllerImpl.java b/backend/JiShop/src/main/java/com/jishop/category/controller/CategoryControllerImpl.java index 57c162e1..3f96c2a3 100644 --- a/backend/JiShop/src/main/java/com/jishop/category/controller/CategoryControllerImpl.java +++ b/backend/JiShop/src/main/java/com/jishop/category/controller/CategoryControllerImpl.java @@ -2,9 +2,7 @@ import com.jishop.category.dto.CategoryResponse; import com.jishop.category.service.CategoryService; -import com.jishop.product.dto.response.ProductResponse; import lombok.RequiredArgsConstructor; -import org.springframework.data.web.PagedModel; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -19,17 +17,6 @@ public class CategoryControllerImpl implements CategoryController { private final CategoryService categoryService; - @Override - @GetMapping("/products") - public PagedModel getProductListByCategory( - @RequestParam(defaultValue = "50000000L") Long categoryId, - @RequestParam(defaultValue = "0") int page) { - if (page < 0 || page > 100) {page = 0;} - - return categoryService.getProductsByCategory(categoryId, page); - } - - // 진입 시점에 부르면 내려감 헤더용 @Override @GetMapping("/root") public List getCategoryFilterInfo() { diff --git a/backend/JiShop/src/main/java/com/jishop/category/repository/CategoryRepository.java b/backend/JiShop/src/main/java/com/jishop/category/repository/CategoryRepository.java index 6c904cd7..103da42b 100644 --- a/backend/JiShop/src/main/java/com/jishop/category/repository/CategoryRepository.java +++ b/backend/JiShop/src/main/java/com/jishop/category/repository/CategoryRepository.java @@ -17,21 +17,21 @@ public interface CategoryRepository extends JpaRepository { /** * 상위(1뎁스) 카테고리 ID로 그 하위(2,3뎁스) 카테고리 상품 목록 모두 조회 - * @param CategoryId 1뎁스 카테고리 ID + * @param categoryId 1뎁스 카테고리 ID * @return 해당 카테고리에 속한 상품 페이지 */ @Query(value = "SELECT p FROM Product p " + "LEFT JOIN p.category c1 " + "LEFT JOIN Category c2 ON c1.parent = c2 " + - "WHERE c1.parent.currentId = :CategoryId OR " + - "c2.parent.currentId = :CategoryId", + "WHERE c1.parent.currentId = :categoryId OR " + + "c2.parent.currentId = :categoryId", countQuery = "SELECT COUNT(p) FROM Product p " + "LEFT JOIN p.category c1 " + "LEFT JOIN Category c2 ON c1.parent = c2 " + - "WHERE c1.parent.currentId = :CategoryId OR " + - "c2.parent.currentId = :CategoryId") + "WHERE c1.parent.currentId = :categoryId OR " + + "c2.parent.currentId = :categoryId") Page findProductsByCategoryWithAllDescendants( - @Param("CategoryId") Long CategoryId, Pageable pageable); + @Param("categoryId") Long categoryId, Pageable pageable); /** * 특정 카테고리의 모든 하위 카테고리 ID를 재귀적으로 조회 diff --git a/backend/JiShop/src/main/java/com/jishop/category/service/CategoryService.java b/backend/JiShop/src/main/java/com/jishop/category/service/CategoryService.java index 18a82d89..cb315c59 100644 --- a/backend/JiShop/src/main/java/com/jishop/category/service/CategoryService.java +++ b/backend/JiShop/src/main/java/com/jishop/category/service/CategoryService.java @@ -1,15 +1,11 @@ package com.jishop.category.service; import com.jishop.category.dto.CategoryResponse; -import com.jishop.product.dto.response.ProductResponse; -import org.springframework.data.web.PagedModel; import java.util.List; public interface CategoryService { - PagedModel getProductsByCategory(Long categoryId, int page); - List getCategoryFilterInfo(); List getSubcategoriesByParentId(Long categoryId); diff --git a/backend/JiShop/src/main/java/com/jishop/category/service/Impl/CategoryServiceImpl.java b/backend/JiShop/src/main/java/com/jishop/category/service/Impl/CategoryServiceImpl.java index 5b9ea43c..978d516a 100644 --- a/backend/JiShop/src/main/java/com/jishop/category/service/Impl/CategoryServiceImpl.java +++ b/backend/JiShop/src/main/java/com/jishop/category/service/Impl/CategoryServiceImpl.java @@ -4,12 +4,8 @@ import com.jishop.category.dto.CategoryResponse; import com.jishop.category.repository.CategoryRepository; import com.jishop.category.service.CategoryService; -import com.jishop.product.domain.Product; -import com.jishop.product.dto.response.ProductResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.*; -import org.springframework.data.web.PagedModel; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,29 +13,11 @@ @Slf4j @Service -@Transactional +@Transactional(readOnly = true) @RequiredArgsConstructor public class CategoryServiceImpl implements CategoryService { private final CategoryRepository categoryRepository; -// private final CategoryRedisService categoryRedisService; - - @Override - public PagedModel getProductsByCategory(Long categoryId, int page) { - - Pageable pageable = PageRequest.of(page, 12, Sort.by(Sort.Direction.DESC, "wishListCount")); - Page productPage = categoryRepository.findProductsByCategoryWithAllDescendants(categoryId, pageable); - - List productsResponse = productPage.getContent().stream() - .map(ProductResponse::from) - .toList(); - - return new PagedModel<>(new PageImpl<>( - productsResponse, - pageable, - productPage.getTotalElements() - )); - } @Override public List getCategoryFilterInfo() { diff --git a/backend/JiShop/src/main/java/com/jishop/logshop/dto/RecentProductResponse.java b/backend/JiShop/src/main/java/com/jishop/logshop/dto/RecentProductResponse.java index 60837f15..e6888105 100644 --- a/backend/JiShop/src/main/java/com/jishop/logshop/dto/RecentProductResponse.java +++ b/backend/JiShop/src/main/java/com/jishop/logshop/dto/RecentProductResponse.java @@ -39,14 +39,14 @@ public static class ProductResponse { public static ProductResponse from(Product product) { return new ProductResponse( product.getId(), - product.getName(), - product.getMallSeq(), - product.getBrand(), - product.getDescription(), - product.getOriginPrice(), - product.getDiscountPrice(), - product.getDiscountRate(), - product.getMainImage() + product.getProductInfo().getName(), + product.getProductInfo().getMallSeq(), + product.getProductInfo().getBrand(), + product.getProductInfo().getDescription(), + product.getProductInfo().getOriginPrice(), + product.getProductInfo().getDiscountPrice(), + product.getProductInfo().getDiscountRate(), + product.getImage().getMainImage() ); } } diff --git a/backend/JiShop/src/main/java/com/jishop/option/dto/FashionClothesOptionResponse.java b/backend/JiShop/src/main/java/com/jishop/option/dto/FashionClothesOptionResponse.java index 84dd9a81..11d75ecc 100644 --- a/backend/JiShop/src/main/java/com/jishop/option/dto/FashionClothesOptionResponse.java +++ b/backend/JiShop/src/main/java/com/jishop/option/dto/FashionClothesOptionResponse.java @@ -8,29 +8,22 @@ public record FashionClothesOptionResponse( List option ) { - public static FashionClothesOptionResponse from(List> productOptions) { + public static FashionClothesOptionResponse from(List productOptions) { if (productOptions == null || productOptions.isEmpty()) { return new FashionClothesOptionResponse(List.of()); } Map> fashionClothesOptions = new HashMap<>(); - for (Map option : productOptions) { - String optionValue = (String) option.get("optionValue"); - String[] colorAndSize = optionValue.split("/"); - - if (colorAndSize.length == 2) { - String color = colorAndSize[0]; - String size = colorAndSize[1]; + for (SizeOption option : productOptions) { + if (option.isValidOption()) { + String color = option.extractColor(); if (!fashionClothesOptions.containsKey(color)) { fashionClothesOptions.put(color, new ArrayList<>()); } - SizeOption sizeOption = new SizeOption( - (Long) option.get("saleProductId"), - size, - (int) option.get("optionExtra") - ); + + SizeOption sizeOption = option.withSize(); fashionClothesOptions.get(color).add(sizeOption); } } diff --git a/backend/JiShop/src/main/java/com/jishop/option/dto/GeneralOptionResponse.java b/backend/JiShop/src/main/java/com/jishop/option/dto/GeneralOptionResponse.java index 979f9f50..82c99a55 100644 --- a/backend/JiShop/src/main/java/com/jishop/option/dto/GeneralOptionResponse.java +++ b/backend/JiShop/src/main/java/com/jishop/option/dto/GeneralOptionResponse.java @@ -2,19 +2,18 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; public record GeneralOptionResponse( List options ) { - public static GeneralOptionResponse from(final List> productOptions) { + public static GeneralOptionResponse from(final List productOptions) { final List generalOptions = new ArrayList<>(); - for (final Map option : productOptions) { + for (final SizeOption option : productOptions) { ProductOption productOption = new ProductOption( - (Long) option.get("saleProductId"), - (String) option.get("optionValue"), - (int) option.get("optionExtra") + option.saleProductId(), + option.optionValue(), + option.optionExtra() ); generalOptions.add(productOption); } diff --git a/backend/JiShop/src/main/java/com/jishop/option/dto/SizeOption.java b/backend/JiShop/src/main/java/com/jishop/option/dto/SizeOption.java index b380b5fd..32c00746 100644 --- a/backend/JiShop/src/main/java/com/jishop/option/dto/SizeOption.java +++ b/backend/JiShop/src/main/java/com/jishop/option/dto/SizeOption.java @@ -5,4 +5,25 @@ public record SizeOption( String optionValue, int optionExtra ) { + public boolean isValidOption() { + return (optionValue != null) && optionValue.split("/").length == 2; + } + + public String extractColor() { + String[] colorAndSize = optionValue.split("/"); + return colorAndSize[0]; + } + + public String extractSize() { + String[] colorAndSize = optionValue.split("/"); + return colorAndSize[1]; + } + + public SizeOption withSize() { + return new SizeOption( + saleProductId(), + extractSize(), + optionExtra() + ); + } } diff --git a/backend/JiShop/src/main/java/com/jishop/order/domain/OrderDetail.java b/backend/JiShop/src/main/java/com/jishop/order/domain/OrderDetail.java index bb403281..3079b09a 100644 --- a/backend/JiShop/src/main/java/com/jishop/order/domain/OrderDetail.java +++ b/backend/JiShop/src/main/java/com/jishop/order/domain/OrderDetail.java @@ -53,8 +53,8 @@ public OrderDetail(Order order, String orderNumber, SaleProduct saleProduct, int } public static OrderDetail from(Order order, SaleProduct saleProduct, int quantity){ - int orderPrice = saleProduct.getProduct().getOriginPrice(); - int paymentPrice = saleProduct.getProduct().getDiscountPrice(); + int orderPrice = saleProduct.getProduct().getProductInfo().getOriginPrice(); + int paymentPrice = saleProduct.getProduct().getProductInfo().getDiscountPrice(); int discountPrice = orderPrice - paymentPrice; if(saleProduct.getOption() != null){ diff --git a/backend/JiShop/src/main/java/com/jishop/order/dto/OrderProductResponse.java b/backend/JiShop/src/main/java/com/jishop/order/dto/OrderProductResponse.java index d636f1a4..4527db5e 100644 --- a/backend/JiShop/src/main/java/com/jishop/order/dto/OrderProductResponse.java +++ b/backend/JiShop/src/main/java/com/jishop/order/dto/OrderProductResponse.java @@ -20,7 +20,7 @@ public static OrderProductResponse from(OrderDetail detail, boolean canReview) { return new OrderProductResponse( detail.getId(), detail.getSaleProduct().getId(), - detail.getSaleProduct().getProduct().getMainImage(), + detail.getSaleProduct().getProduct().getImage().getMainImage(), detail.getSaleProduct().getName(), detail.getSaleProduct().getOption() != null ? detail.getSaleProduct().getOption().getOptionValue() : null, detail.getPaymentPrice(), @@ -29,7 +29,7 @@ public static OrderProductResponse from(OrderDetail detail, boolean canReview) { detail.getQuantity(), detail.getPaymentPrice() * detail.getQuantity(), canReview, - detail.getSaleProduct().getProduct().getBrand() + detail.getSaleProduct().getProduct().getProductInfo().getBrand() ); } } diff --git a/backend/JiShop/src/main/java/com/jishop/order/service/impl/OrderGetServiceImpl.java b/backend/JiShop/src/main/java/com/jishop/order/service/impl/OrderGetServiceImpl.java index 211f65bd..cab76698 100644 --- a/backend/JiShop/src/main/java/com/jishop/order/service/impl/OrderGetServiceImpl.java +++ b/backend/JiShop/src/main/java/com/jishop/order/service/impl/OrderGetServiceImpl.java @@ -146,8 +146,8 @@ public CartResponse getCheckout(User user, List orderDetailR .map(OrderDetailRequest::quantity) .orElseThrow(() -> new DomainException(ErrorType.INVALID_QUANTITY)); // 기본값은 1로 설정 - int paymentPrice = product.getProduct().getDiscountPrice(); - int orderPrice = product.getProduct().getOriginPrice(); + int paymentPrice = product.getProduct().getProductInfo().getDiscountPrice(); + int orderPrice = product.getProduct().getProductInfo().getOriginPrice(); int discountPrice = orderPrice - paymentPrice; return CartDetailResponse.from( diff --git a/backend/JiShop/src/main/java/com/jishop/popular/service/PopularCalculationServiceImpl.java b/backend/JiShop/src/main/java/com/jishop/popular/service/PopularCalculationServiceImpl.java index 9fc04f43..97810f10 100644 --- a/backend/JiShop/src/main/java/com/jishop/popular/service/PopularCalculationServiceImpl.java +++ b/backend/JiShop/src/main/java/com/jishop/popular/service/PopularCalculationServiceImpl.java @@ -96,8 +96,8 @@ public List findPopularProductsByKeyword(String keyword, List products; // 브랜드 이름과 정확히 일치하는 경우 - if(productRepository.existsByBrand(keyword)){ - products = productRepository.findAllByBrand(keyword); + if(productRepository.existsByProductInfo_Brand(keyword)){ + products = productRepository.findAllByProductInfo_Brand(keyword); } // 그 외에는 Fulltext 검색으로 통합 처리 else { @@ -140,12 +140,12 @@ public PopularProductResponse convertToPopularProductResponse(ProductScore produ Product product = productScore.getProduct(); return new PopularProductResponse( product.getId(), - product.getMainImage(), - product.getBrand(), - product.getName(), - product.getOriginPrice(), - product.getDiscountPrice(), - productScore.getProduct().getDiscountRate(), + product.getImage().getMainImage(), + product.getProductInfo().getBrand(), + product.getProductInfo().getName(), + product.getProductInfo().getOriginPrice(), + product.getProductInfo().getDiscountPrice(), + productScore.getProduct().getProductInfo().getDiscountRate(), productScore.getTotalOrderCount(), productScore.getReviewRating().doubleValue() ); @@ -160,12 +160,12 @@ public PopularProductResponse convertToPopularProductResponse(ProductScore produ public PopularProductResponse convertToPopularProductResponseFromProduct(Product product){ return new PopularProductResponse( product.getId(), - product.getMainImage(), - product.getBrand(), - product.getName(), - product.getOriginPrice(), - product.getDiscountPrice(), - product.getDiscountRate(), + product.getImage().getMainImage(), + product.getProductInfo().getBrand(), + product.getProductInfo().getName(), + product.getProductInfo().getOriginPrice(), + product.getProductInfo().getDiscountPrice(), + product.getProductInfo().getDiscountRate(), 0, 0.0 ); diff --git a/backend/JiShop/src/main/java/com/jishop/product/controller/ProductController.java b/backend/JiShop/src/main/java/com/jishop/product/controller/ProductController.java index 846ca021..2e2e4a53 100644 --- a/backend/JiShop/src/main/java/com/jishop/product/controller/ProductController.java +++ b/backend/JiShop/src/main/java/com/jishop/product/controller/ProductController.java @@ -22,8 +22,11 @@ public interface ProductController { ProductDetailResponse getProduct(final User user, final Long productId); @Operation(summary = "인기순(찜순) 상품 조회") - List getProductByWishTopTen(final int page, final int size); + List getProductsByWishTopTen(final int page, final int size); @Operation(summary = "오늘의 특가 상품 조회") - PagedModel getProductByTodaySpecial(final int page, final int size); + PagedModel getProductsByTodaySpecial(final int page, final int size); + + @Operation(summary = "헤더 카테고리 상품 조회") + PagedModel getProductsByCategory(final Long CategoryId, final int page, final int size); } diff --git a/backend/JiShop/src/main/java/com/jishop/product/controller/ProductControllerImpl.java b/backend/JiShop/src/main/java/com/jishop/product/controller/ProductControllerImpl.java index 57fd6703..be1324a4 100644 --- a/backend/JiShop/src/main/java/com/jishop/product/controller/ProductControllerImpl.java +++ b/backend/JiShop/src/main/java/com/jishop/product/controller/ProductControllerImpl.java @@ -6,7 +6,10 @@ import com.jishop.product.dto.response.ProductDetailResponse; import com.jishop.product.dto.response.ProductResponse; import com.jishop.product.dto.response.TodaySpecialListResponse; +import com.jishop.product.service.ProductCategoryService; +import com.jishop.product.service.ProductDiscountService; import com.jishop.product.service.ProductService; +import com.jishop.product.service.ProductWishlistService; import lombok.RequiredArgsConstructor; import org.springframework.data.web.PagedModel; import org.springframework.validation.annotation.Validated; @@ -20,6 +23,9 @@ public class ProductControllerImpl implements ProductController { private final ProductService productService; + private final ProductWishlistService productWishListService; + private final ProductCategoryService productCategoryService; + private final ProductDiscountService productDiscountService; @Override @GetMapping @@ -41,28 +47,39 @@ public ProductDetailResponse getProduct(@CurrentUser final User user, @PathVari return productService.getProduct(user, productId); } - // 상위 찜순 데이터 @Override @GetMapping("/popular") - public List getProductByWishTopTen( + public List getProductsByWishTopTen( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "8") int size ) { if (page < 0 || page > 100) {page = 0;} if (size <= 0 || size > 100) {size = 8;} - return productService.getProductsByWishList(page, size); + return productWishListService.getProductsByWishList(page, size); } @Override @GetMapping("/specialPrices") - public PagedModel getProductByTodaySpecial( + public PagedModel getProductsByTodaySpecial( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "8") int size ) { if (page < 0 || page > 100) {page = 0;} if (size <= 0 || size > 100) {size = 8;} - return productService.getProductsByTodaySpecial(page, size); + return productDiscountService.getProductsByDailyDeal(page, size); + } + + @Override + @GetMapping("/category/{categoryId}") + public PagedModel getProductsByCategory( + @PathVariable final Long categoryId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "12") int size) { + if (page < 0 || page > 100) {page = 0;} + if (size <= 0 || size > 100) {size = 12;} + + return productCategoryService.getProductsByCategory(categoryId, page, size); } } diff --git a/backend/JiShop/src/main/java/com/jishop/product/domain/Product.java b/backend/JiShop/src/main/java/com/jishop/product/domain/Product.java index 5a3b298c..ec313276 100644 --- a/backend/JiShop/src/main/java/com/jishop/product/domain/Product.java +++ b/backend/JiShop/src/main/java/com/jishop/product/domain/Product.java @@ -2,6 +2,10 @@ import com.jishop.category.domain.Category; import com.jishop.common.util.BaseEntity; +import com.jishop.product.domain.embed.CategoryInfo; +import com.jishop.product.domain.embed.ImageUrl; +import com.jishop.product.domain.embed.ProductInfo; +import com.jishop.product.domain.embed.Status; import com.jishop.productscore.domain.ProductScore; import com.jishop.saleproduct.domain.SaleProduct; import jakarta.persistence.*; @@ -9,7 +13,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -19,111 +22,40 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Product extends BaseEntity { - // 상품 정보 - @Column(name = "name", nullable = false) - private String name; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "category_id", nullable = false) - private Category category; - @Column(name = "mall_seq", nullable = false) - private String mallSeq; - @Column(name = "manufacture_date", nullable = false) - private LocalDateTime manufactureDate; - @Column(name = "brand", nullable = false) - private String brand; - @Column(name = "description", nullable = false) - private String description; - @Column(name = "origin_price", nullable = false) - private int originPrice; - @Column(name = "discount_price", nullable = false) - private int discountPrice; - @Column(name = "discount_rate", nullable = false) - private int discountRate; - - // 상품 상태 - @Column(name = "secret", nullable = false) - private Boolean secret; - @Enumerated(EnumType.STRING) - @Column(name = "sale_status", nullable = false) - private SaleStatus saleStatus; - @Enumerated(EnumType.STRING) - @Column(name = "labels", length = 50) - private Labels labels; - @Enumerated(EnumType.STRING) - @Column(name = "discount_status", nullable = false) - private DiscountStatus discountStatus; - - // "오늘의 특가" 구분 필드값 - @Column(name = "is_discount", nullable = false) - private Boolean isDiscount; + @Embedded + private ProductInfo productInfo; + @Embedded + private CategoryInfo categoryInfo; + @Embedded + private ImageUrl image; + @Embedded + private Status status; @Column(name = "wish_list_count", nullable = false, columnDefinition = "int default 0") private int wishListCount; @Column(name = "product_view_count", nullable = false, columnDefinition = "int default 0") private int productViewCount; - @Column(name = "main_image", nullable = false) - private String mainImage; - @Column(name = "image1") - private String image1; - @Column(name = "image2") - private String image2; - @Column(name = "image3") - private String image3; - @Column(name = "image4") - private String image4; - @Column(name = "detail_image") - private String detailImage; - - // 카테고리 분류 - @Column(name = "l_cat_id", nullable = false) - private Long lCatId; - @Column(name = "m_cat_id") - private Long mCatId; - @Column(name = "s_cat_id") - private Long sCatId; - + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id", nullable = false) + private Category category; @OneToMany(mappedBy = "product", cascade = CascadeType.ALL) - private List saleProducts = new ArrayList<>(); - - // 상품 점수(ProductScore)와 연관관게 추가 + private final List saleProducts = new ArrayList<>(); @OneToOne(mappedBy = "product", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) private ProductScore productScore; - public Product(Category category, Long lCatId, Long mCatId, Long sCatId, - String mallSeq, String name, String description, int originPrice, int discountPrice, - int discountRate, LocalDateTime manufactureDate, Boolean secret, SaleStatus saleStatus, - DiscountStatus discountStatus, Boolean isDiscount, String brand, int wishListCount, Labels labels, - String mainImage, String image1, String image2, String image3, String image4, String detailImage, - int productViewCount + public Product(ProductInfo productInfo, CategoryInfo categoryInfo, Status status, ImageUrl image, + Category category, int wishListCount, int productViewCount ) { + this.productInfo = productInfo; + this.categoryInfo = categoryInfo; + this.status = status; + this.image = image; this.category = category; - this.lCatId = lCatId; - this.mCatId = mCatId; - this.sCatId = sCatId; - this.mallSeq = mallSeq; - this.name = name; - this.description = description; - this.originPrice = originPrice; - this.discountPrice = discountPrice; - this.discountRate = discountRate; - this.manufactureDate = manufactureDate; - this.secret = secret; - this.saleStatus = saleStatus; - this.discountStatus = discountStatus; - this.isDiscount = isDiscount; - this.brand = brand; this.wishListCount = wishListCount; - this.labels = labels; - this.mainImage = mainImage; - this.image1 = image1; - this.image2 = image2; - this.image3 = image3; - this.image4 = image4; - this.detailImage = detailImage; this.productViewCount = productViewCount; } @@ -135,7 +67,6 @@ public void decrementWishCount() { if (this.wishListCount > 0) { this.wishListCount--;} } - // 상품 점수(ProductScore)와 연관관계 편의 메서드 public void setProductScore(ProductScore productScore) { this.productScore = productScore; if(productScore != null && productScore.getProduct() != this){ diff --git a/backend/JiShop/src/main/java/com/jishop/product/domain/embed/CategoryInfo.java b/backend/JiShop/src/main/java/com/jishop/product/domain/embed/CategoryInfo.java new file mode 100644 index 00000000..7ff4473f --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/domain/embed/CategoryInfo.java @@ -0,0 +1,26 @@ +package com.jishop.product.domain.embed; + +import com.jishop.category.domain.Category; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CategoryInfo { + + @Column(name = "l_cat_id", nullable = false) + private Long lCatId; + @Column(name = "m_cat_id") + private Long mCatId; + @Column(name = "s_cat_id") + private Long sCatId; + + public CategoryInfo(Category category, Long lCatId, Long mCatId, Long sCatId) { + this.lCatId = lCatId; + this.mCatId = mCatId; + this.sCatId = sCatId; + } +} \ No newline at end of file diff --git a/backend/JiShop/src/main/java/com/jishop/product/domain/embed/ImageUrl.java b/backend/JiShop/src/main/java/com/jishop/product/domain/embed/ImageUrl.java new file mode 100644 index 00000000..88218008 --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/domain/embed/ImageUrl.java @@ -0,0 +1,36 @@ +package com.jishop.product.domain.embed; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ImageUrl { + + @Column(name = "main_image", nullable = false) + private String mainImage; + @Column(name = "image1") + private String image1; + @Column(name = "image2") + private String image2; + @Column(name = "image3") + private String image3; + @Column(name = "image4") + private String image4; + @Column(name = "detail_image") + private String detailImage; + + public ImageUrl(String mainImage, String image1, String image2, String image3, + String image4, String detailImage) { + this.mainImage = mainImage; + this.image1 = image1; + this.image2 = image2; + this.image3 = image3; + this.image4 = image4; + this.detailImage = detailImage; + } +} diff --git a/backend/JiShop/src/main/java/com/jishop/product/domain/embed/ProductInfo.java b/backend/JiShop/src/main/java/com/jishop/product/domain/embed/ProductInfo.java new file mode 100644 index 00000000..dabb92fd --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/domain/embed/ProductInfo.java @@ -0,0 +1,46 @@ +package com.jishop.product.domain.embed; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ProductInfo { + + @Column(name = "name", nullable = false) + private String name; + @Column(name = "mall_seq", nullable = false) + private String mallSeq; + @Column(name = "manufacture_date", nullable = false) + private LocalDateTime manufactureDate; + @Column(name = "brand", nullable = false) + private String brand; + @Column(name = "description", nullable = false) + private String description; + + @Column(name = "origin_price", nullable = false) + private int originPrice; + @Column(name = "discount_price", nullable = false) + private int discountPrice; + @Column(name = "discount_rate", nullable = false) + private int discountRate; + + public ProductInfo(String name, String mallSeq, LocalDateTime manufactureDate, + String brand, String description, int originPrice, int discountPrice, int discountRate + ) { + this.name = name; + this.mallSeq = mallSeq; + this.manufactureDate = manufactureDate; + this.brand = brand; + this.description = description; + this.originPrice = originPrice; + this.discountPrice = discountPrice; + this.discountRate = discountRate; + } +} diff --git a/backend/JiShop/src/main/java/com/jishop/product/domain/embed/Status.java b/backend/JiShop/src/main/java/com/jishop/product/domain/embed/Status.java new file mode 100644 index 00000000..f6d9119a --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/domain/embed/Status.java @@ -0,0 +1,43 @@ +package com.jishop.product.domain.embed; + +import com.jishop.product.domain.DiscountStatus; +import com.jishop.product.domain.Labels; +import com.jishop.product.domain.SaleStatus; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Status { + + @Column(name = "secret", nullable = false) + private Boolean secret; + @Enumerated(EnumType.STRING) + @Column(name = "sale_status", nullable = false) + private SaleStatus saleStatus; + @Enumerated(EnumType.STRING) + @Column(name = "labels", length = 50) + private Labels labels; + @Enumerated(EnumType.STRING) + @Column(name = "discount_status", nullable = false) + private DiscountStatus discountStatus; + + // "오늘의 특가" 구분 필드값 + @Column(name = "is_discount", nullable = false) + private Boolean isDiscount; + + public Status(Boolean secret, SaleStatus saleStatus, Labels labels, Boolean isDiscount, DiscountStatus discountStatus + ) { + this.secret = secret; + this.saleStatus = saleStatus; + this.labels = labels; + this.isDiscount = isDiscount; + this.discountStatus = discountStatus; + } +} diff --git a/backend/JiShop/src/main/java/com/jishop/product/dto/request/ProductRequest.java b/backend/JiShop/src/main/java/com/jishop/product/dto/request/ProductRequest.java index ddcf0b28..da51d7a6 100644 --- a/backend/JiShop/src/main/java/com/jishop/product/dto/request/ProductRequest.java +++ b/backend/JiShop/src/main/java/com/jishop/product/dto/request/ProductRequest.java @@ -1,17 +1,13 @@ package com.jishop.product.dto.request; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; -import java.util.Arrays; import java.util.List; public record ProductRequest( - @NotBlank String keyword, - @Pattern(regexp = "^(wish|discount|latest|priceAsc|priceDesc)$", - message = "정렬 기준은 'wish', 'discount', 'latest', 'priceAsc', 'priceDesc' 중 하나여야 합니다") + message = "정렬 기준은 'wish', 'discount', 'latest', 'priceAsc', 'priceDesc' 중 하나여야 합니다") String sort, Long categoryId, // 가격 필터 (값 범위: 0~25000, 25000~50000, 50000~100000, 100000~) @@ -20,10 +16,6 @@ public record ProductRequest( List ratings ){ public ProductRequest { - if (sort == null || sort.isEmpty()) {sort = "wish";} - if (priceRanges == null || priceRanges.isEmpty()) { - priceRanges = Arrays.asList(0, 25000, 50000, 100000); - } - if (ratings == null || ratings.isEmpty()) {ratings = Arrays.asList(1, 2, 3, 4, 5);} + if (keyword == null) { keyword = ""; } } } diff --git a/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductCategoryInfoResponse.java b/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductCategoryInfoResponse.java new file mode 100644 index 00000000..88556733 --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductCategoryInfoResponse.java @@ -0,0 +1,17 @@ +package com.jishop.product.dto.response; + +import com.jishop.product.domain.embed.CategoryInfo; + +public record ProductCategoryInfoResponse( + Long lCatId, + Long mCatId, + Long sCatId +) { + public static ProductCategoryInfoResponse from(CategoryInfo categoryInfo) { + return new ProductCategoryInfoResponse( + categoryInfo.getLCatId(), + categoryInfo.getMCatId(), + categoryInfo.getSCatId() + ); + } +} diff --git a/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductDetailResponse.java b/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductDetailResponse.java index 5acde8d8..72b85a64 100644 --- a/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductDetailResponse.java +++ b/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductDetailResponse.java @@ -3,6 +3,9 @@ import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.jishop.product.domain.Product; +import java.util.List; +import java.util.stream.Stream; + public record ProductDetailResponse( @JsonUnwrapped ProductResponse productResponse, @@ -12,7 +15,7 @@ public record ProductDetailResponse( Object option, int reviewCount, double reviewRate, - String[] images, + List images, String detailImage, Object isWished ) { @@ -21,15 +24,26 @@ public static ProductDetailResponse from(final ProductResponse productResponse, final Object option) { final Object wishStatus = isWished != null && isWished; + ProductImageUrlResponse imageUrlResponse = ProductImageUrlResponse.from(product.getImage()); + + List imageList = Stream.of( + imageUrlResponse.image1(), + imageUrlResponse.image2(), + imageUrlResponse.image3(), + imageUrlResponse.image4() + ) + .filter(img -> img != null && !img.isEmpty()) + .toList(); + return new ProductDetailResponse( productResponse, - product.getDescription(), - product.getIsDiscount(), + product.getProductInfo().getDescription(), + product.getStatus().getIsDiscount(), option, reviewCount, reviewRate, - new String[] { product.getImage1(), product.getImage2(), product.getImage3(), product.getImage4() }, - product.getDetailImage(), + imageList, + imageUrlResponse.detailImage(), wishStatus ); } diff --git a/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductImageUrlResponse.java b/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductImageUrlResponse.java new file mode 100644 index 00000000..7dc87394 --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductImageUrlResponse.java @@ -0,0 +1,23 @@ +package com.jishop.product.dto.response; + +import com.jishop.product.domain.embed.ImageUrl; + +public record ProductImageUrlResponse( + String mainImage, + String image1, + String image2, + String image3, + String image4, + String detailImage +) { + public static ProductImageUrlResponse from(ImageUrl imageUrl) { + return new ProductImageUrlResponse( + imageUrl.getMainImage(), + imageUrl.getImage1(), + imageUrl.getImage2(), + imageUrl.getImage3(), + imageUrl.getImage4(), + imageUrl.getDetailImage() + ); + } +} diff --git a/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductInfoResponse.java b/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductInfoResponse.java new file mode 100644 index 00000000..b39bf393 --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductInfoResponse.java @@ -0,0 +1,23 @@ +package com.jishop.product.dto.response; + +import com.jishop.product.domain.embed.ProductInfo; + +public record ProductInfoResponse( + String name, + String brand, + String description, + int originPrice, + int discountPrice, + int discountRate +) { + public static ProductInfoResponse from(ProductInfo productInfo) { + return new ProductInfoResponse( + productInfo.getName(), + productInfo.getBrand(), + productInfo.getDescription(), + productInfo.getOriginPrice(), + productInfo.getDiscountPrice(), + productInfo.getDiscountRate() + ); + } +} \ No newline at end of file diff --git a/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductResponse.java b/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductResponse.java index 8ef6f723..c1d30f11 100644 --- a/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductResponse.java +++ b/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductResponse.java @@ -16,13 +16,13 @@ public record ProductResponse( public static ProductResponse from(final Product product) { return new ProductResponse( product.getId(), - product.getName(), - product.getLabels(), - product.getOriginPrice(), - product.getDiscountPrice(), - product.getBrand(), - product.getDiscountRate(), - product.getMainImage() + product.getProductInfo().getName(), + product.getStatus().getLabels(), + product.getProductInfo().getOriginPrice(), + product.getProductInfo().getDiscountPrice(), + product.getProductInfo().getBrand(), + product.getProductInfo().getDiscountRate(), + product.getImage().getMainImage() ); } } diff --git a/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductStatusResponse.java b/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductStatusResponse.java new file mode 100644 index 00000000..4e898e25 --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/dto/response/ProductStatusResponse.java @@ -0,0 +1,24 @@ +package com.jishop.product.dto.response; + +import com.jishop.product.domain.DiscountStatus; +import com.jishop.product.domain.Labels; +import com.jishop.product.domain.SaleStatus; +import com.jishop.product.domain.embed.Status; + +public record ProductStatusResponse( + Boolean secret, + SaleStatus saleStatus, + Labels labels, + DiscountStatus discountStatus, + Boolean isDiscount +) { + public static ProductStatusResponse from(Status status) { + return new ProductStatusResponse( + status.getSecret(), + status.getSaleStatus(), + status.getLabels(), + status.getDiscountStatus(), + status.getIsDiscount() + ); + } +} diff --git a/backend/JiShop/src/main/java/com/jishop/product/implementation/ProductQueryHelper.java b/backend/JiShop/src/main/java/com/jishop/product/implementation/ProductQueryHelper.java deleted file mode 100644 index 11fdbb5a..00000000 --- a/backend/JiShop/src/main/java/com/jishop/product/implementation/ProductQueryHelper.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.jishop.product.implementation; - -import com.jishop.product.domain.QProduct; -import com.jishop.product.dto.request.ProductRequest; -import com.jishop.reviewproduct.domain.QReviewProduct; -import com.querydsl.core.BooleanBuilder; - -public interface ProductQueryHelper { - - BooleanBuilder findProductsByCondition(final ProductRequest request, - final QProduct product, final QReviewProduct reviewProduct); -} diff --git a/backend/JiShop/src/main/java/com/jishop/product/implementation/ProductQueryHelperImpl.java b/backend/JiShop/src/main/java/com/jishop/product/implementation/ProductQueryHelperImpl.java deleted file mode 100644 index 3d948d52..00000000 --- a/backend/JiShop/src/main/java/com/jishop/product/implementation/ProductQueryHelperImpl.java +++ /dev/null @@ -1,143 +0,0 @@ -package com.jishop.product.implementation; - -import com.jishop.category.repository.CategoryRepository; -import com.jishop.product.domain.QProduct; -import com.jishop.product.dto.request.ProductRequest; -import com.jishop.reviewproduct.domain.QReviewProduct; -import com.querydsl.core.BooleanBuilder; -import com.querydsl.jpa.JPAExpressions; -import com.querydsl.jpa.JPQLQuery; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.util.List; - -@Slf4j -@Component -@RequiredArgsConstructor -public class ProductQueryHelperImpl implements ProductQueryHelper { - - private final CategoryRepository categoryRepository; - - @Override - public BooleanBuilder findProductsByCondition( - final ProductRequest productRequest, final QProduct product, final QReviewProduct reviewProduct) { - final BooleanBuilder builder = new BooleanBuilder(); - - addPriceRangesFiltering(productRequest.priceRanges(), product, builder); - addRatingsFilter(productRequest.ratings(), reviewProduct, product, builder); - addCategory(productRequest.categoryId(), product, builder); - addKeyword(productRequest.keyword(), product, builder); - return builder; - } - - private static void addPriceRangesFiltering(final List priceRanges, - final QProduct product, final BooleanBuilder builder) { - final BooleanBuilder priceBuilder = new BooleanBuilder(); - - for (final Integer range : priceRanges) { - switch (range) { - case 0 -> - priceBuilder.or(product.discountPrice.between(0, 25000)); - case 25000 -> - priceBuilder.or(product.discountPrice.between(25001, 50000)); - case 50000 -> - priceBuilder.or(product.discountPrice.between(50001, 100000)); - case 100000 -> - priceBuilder.or(product.discountPrice.gt(100000)); - default -> { } - } - } - - if (priceBuilder.hasValue()) { - builder.and(priceBuilder); - } - } - - private static void addRatingsFilter(final List ratings, final QReviewProduct reviewProduct, - final QProduct product, final BooleanBuilder builder) { - if (ratings.size() != 5 || - !ratings.contains(1) || - !ratings.contains(2) || - !ratings.contains(3) || - !ratings.contains(4) || - !ratings.contains(5)) { - - final BooleanBuilder ratingBuilder = new BooleanBuilder(); - - // 나눗셈 오류 방지 - ratingBuilder.and(reviewProduct.reviewCount.gt(0)); - - // 평점 = 리뷰총점 / 리뷰개수 - for (final Integer rating : ratings) { - switch (rating) { - case 1 -> ratingBuilder.or( - reviewProduct.reviewScore.divide(reviewProduct.reviewCount) - .goe(1.0).and( - reviewProduct.reviewScore.divide(reviewProduct.reviewCount).lt(2.0) - ) - ); - case 2 -> ratingBuilder.or( - reviewProduct.reviewScore.divide(reviewProduct.reviewCount) - .goe(2.0).and( - reviewProduct.reviewScore.divide(reviewProduct.reviewCount).lt(3.0) - ) - ); - case 3 -> ratingBuilder.or( - reviewProduct.reviewScore.divide(reviewProduct.reviewCount) - .goe(3.0).and( - reviewProduct.reviewScore.divide(reviewProduct.reviewCount).lt(4.0) - ) - ); - case 4 -> ratingBuilder.or( - reviewProduct.reviewScore.divide(reviewProduct.reviewCount) - .goe(4.0).and( - reviewProduct.reviewScore.divide(reviewProduct.reviewCount).lt(5.0) - ) - ); - case 5 -> ratingBuilder.or( - reviewProduct.reviewScore.divide(reviewProduct.reviewCount).goe(5.0) - ); - default -> {} - } - } - - if (ratingBuilder.hasValue()) { - final JPQLQuery subQuery = JPAExpressions - .select(reviewProduct.product.id) - .from(reviewProduct) - .where(ratingBuilder); - builder.and(product.id.in(subQuery)); - - // 아래는 직접 JOIN 방식 (메인 쿼리에 JOIN 조건 추가 필요) - // queryFactory.selectFrom(product) - // .join(reviewProduct).on(product.id.eq(reviewProduct.productId)) - // .where(builder.and(ratingBuilder)) - } - } - } - - private void addCategory(final Long categoryId, final QProduct product, final BooleanBuilder builder) { - if (categoryId == null) return; - - final List categoryIds = categoryRepository.findById(categoryId) - .map(category -> { - final List subCategoryPKs = categoryRepository.findIdsByCurrentIds( - categoryRepository.findAllSubCategoryIds(categoryId) - ); - return subCategoryPKs.isEmpty() - ? List.of(category.getId()) - : subCategoryPKs; - }) - .orElse(List.of(-1L)); - - builder.and(product.category.id.in(categoryIds)); - } - - private static void addKeyword(final String keyword, final QProduct product, final BooleanBuilder builder) { - builder.and( - product.name.containsIgnoreCase(keyword).or(product.description.containsIgnoreCase(keyword)) - ); - } -} diff --git a/backend/JiShop/src/main/java/com/jishop/product/repository/ProductRepository.java b/backend/JiShop/src/main/java/com/jishop/product/repository/ProductRepository.java index 26d40ffd..39fb139b 100644 --- a/backend/JiShop/src/main/java/com/jishop/product/repository/ProductRepository.java +++ b/backend/JiShop/src/main/java/com/jishop/product/repository/ProductRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -12,10 +13,30 @@ public interface ProductRepository extends JpaRepository, ProductRepositoryQueryDsl { - @Query("SELECT p FROM Product p WHERE p.discountStatus = :status AND p.isDiscount = true") - Page findDailDealProducts(@Param("status") DiscountStatus status, Pageable pageable); + @Query("SELECT p FROM Product p WHERE p.status.discountStatus = :status AND p.status.isDiscount = true") + Page findDailyDealProducts(@Param("status") DiscountStatus status, Pageable pageable); - boolean existsByBrand(String keyword); + @Modifying + @Query(value = "UPDATE products SET discount_status = 'NONE', is_discount = false " + + "WHERE discount_status = 'DAILY_DEAL'", nativeQuery = true) + int resetPreviousDailyDeals(); + + @Modifying + @Query(value = "UPDATE products p " + + "JOIN ( SELECT id " + + "FROM products " + + "WHERE discount_price < origin_price " + + "AND sale_status = 'SELLING' " + + "AND secret = false " + + "ORDER BY RAND() " + + "LIMIT 100 ) " + + "AS selected_products ON p.id = selected_products.id " + + "SET p.discount_status = 'DAILY_DEAL', p.is_discount = true", nativeQuery = true) + int updateNewDailyDeals(); + + boolean existsByProductInfo_Brand(String keyword); + + List findAllByProductInfo_Brand(String keyword); // "남성 셔츠" 가 검색어로 들어온 경우 +남성 +셔츠로 변환(BOOLEAN MODE) // "남성셔츠" 가 검색으로 들어온 경우 남성, 성셔, 셔츠로 변환(ngram parser) @@ -25,9 +46,6 @@ public interface ProductRepository extends JpaRepository, Product ) Long existsByFulltext(String keyword); - List findAllByBrand(String keyword); - - @Query( value = "SELECT * FROM products WHERE MATCH(name, brand) AGAINST(:keyword IN BOOLEAN MODE)", nativeQuery = true diff --git a/backend/JiShop/src/main/java/com/jishop/product/repository/ProductRepositoryQueryDsl.java b/backend/JiShop/src/main/java/com/jishop/product/repository/ProductRepositoryQueryDsl.java index 01544cc6..40e4890e 100644 --- a/backend/JiShop/src/main/java/com/jishop/product/repository/ProductRepositoryQueryDsl.java +++ b/backend/JiShop/src/main/java/com/jishop/product/repository/ProductRepositoryQueryDsl.java @@ -7,7 +7,10 @@ public interface ProductRepositoryQueryDsl { - List findProductsByCondition(final ProductRequest request, final int page, final int size); + List findProductsByCondition(final ProductRequest request, + final int page, + final int size, + final List categoryIds); - long countProductsByCondition(final ProductRequest request); + long countProductsByCondition(final ProductRequest request, final List categoryIds); } diff --git a/backend/JiShop/src/main/java/com/jishop/product/repository/ProductRepositoryQueryDslImpl.java b/backend/JiShop/src/main/java/com/jishop/product/repository/ProductRepositoryQueryDslImpl.java index 23ae18ab..30effbdb 100644 --- a/backend/JiShop/src/main/java/com/jishop/product/repository/ProductRepositoryQueryDslImpl.java +++ b/backend/JiShop/src/main/java/com/jishop/product/repository/ProductRepositoryQueryDslImpl.java @@ -3,57 +3,143 @@ import com.jishop.product.domain.Product; import com.jishop.product.domain.QProduct; import com.jishop.product.dto.request.ProductRequest; -import com.jishop.product.implementation.ProductQueryHelper; import com.jishop.reviewproduct.domain.QReviewProduct; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; + +import static com.jishop.product.domain.embed.QProductInfo.productInfo; @Repository @RequiredArgsConstructor public class ProductRepositoryQueryDslImpl implements ProductRepositoryQueryDsl { private final JPAQueryFactory queryFactory; - private final ProductQueryHelper productQueryHelper; - private final QProduct product = QProduct.product; - private final QReviewProduct reviewProduct = QReviewProduct.reviewProduct; + private static final QProduct product = QProduct.product; + private static final QReviewProduct reviewProduct = QReviewProduct.reviewProduct; @Override - public List findProductsByCondition(final ProductRequest productRequest, final int page, final int size) { - final BooleanBuilder filterBuilder = productQueryHelper - .findProductsByCondition(productRequest, product, reviewProduct); - - final OrderSpecifier orderSpecifier = addSorting(productRequest.sort(), product); + public List findProductsByCondition(final ProductRequest productRequest, + final int page, + final int size, + final List categoryIds) { return queryFactory.selectFrom(product) - .where(filterBuilder) - .orderBy(orderSpecifier) + .where(buildProductConditions(productRequest, categoryIds)) + .orderBy(getSortOrderSpecifier(productRequest.sort())) .offset((long) page * size) .limit(size) .fetch(); } @Override - public long countProductsByCondition(final ProductRequest productRequest) { - final BooleanBuilder filterBuilder = productQueryHelper - .findProductsByCondition(productRequest, product, reviewProduct); + public long countProductsByCondition(final ProductRequest productRequest, final List categoryIds) { - return queryFactory.selectFrom(product) - .where(filterBuilder) - .fetchCount(); + return Optional.ofNullable( + queryFactory.select(product.count()) + .from(product) + .where(buildProductConditions(productRequest, categoryIds)) + .fetchOne() + ).orElse(0L); + } + + private BooleanExpression buildProductConditions(final ProductRequest request, final List categoryIds) { + return Expressions.allOf( + priceRangesFilter(request.priceRanges()), + ratingsFilter(request.ratings()), + categoryFilter(categoryIds), + keywordFilter(request.keyword()) + ); } - private OrderSpecifier addSorting(final String sort, final QProduct product) { + private OrderSpecifier getSortOrderSpecifier(final String sort) { return switch (sort) { case "latest" -> product.createdAt.desc(); - case "priceAsc" -> product.discountPrice.asc(); - case "priceDesc" -> product.discountPrice.desc(); - case "discount" -> product.discountRate.desc(); + case "priceAsc" -> product.productInfo.discountPrice.asc(); + case "priceDesc" -> product.productInfo.discountPrice.desc(); + case "discount" -> product.productInfo.discountRate.desc(); default -> product.wishListCount.desc(); }; } + + private BooleanExpression priceRangesFilter(final List priceRanges) { + if (priceRanges == null || priceRanges.isEmpty()) { + return null; + } + + BooleanBuilder priceBuilder = new BooleanBuilder(); + + for (final Integer range : priceRanges) { + switch (range) { + case 0 -> priceBuilder.or(productInfo.discountPrice.between(0, 25000)); + case 25000 -> priceBuilder.or(productInfo.discountPrice.between(25001, 50000)); + case 50000 -> priceBuilder.or(productInfo.discountPrice.between(50001, 100000)); + case 100000 -> priceBuilder.or(productInfo.discountPrice.gt(100000)); + default -> {} + } + } + + return priceBuilder.hasValue() ? Expressions.asBoolean(priceBuilder.getValue()) : null; + } + + private BooleanExpression ratingsFilter(final List ratings) { + if (ratings == null || ratings.isEmpty() || ratings.size() == 5) { + return null; + } + // 카운트 > 0 조건 builder + BooleanBuilder ratingBuilder = new BooleanBuilder(); + ratingBuilder.and(reviewProduct.reviewCount.gt(0)); + + NumberExpression avgRating = Expressions.numberTemplate( + Double.class, + "1.0 * {0} / {1}", + reviewProduct.reviewScore, + reviewProduct.reviewCount + ); + + // 평점 범위 조건 builder + BooleanBuilder ratingRangeBuilder = new BooleanBuilder(); + for (final Integer rating : ratings) { + switch (rating) { + case 1 -> ratingRangeBuilder.or(avgRating.goe(1.0).and(avgRating.lt(2.0))); + case 2 -> ratingRangeBuilder.or(avgRating.goe(2.0).and(avgRating.lt(3.0))); + case 3 -> ratingRangeBuilder.or(avgRating.goe(3.0).and(avgRating.lt(4.0))); + case 4 -> ratingRangeBuilder.or(avgRating.goe(4.0).and(avgRating.lt(5.0))); + case 5 -> ratingRangeBuilder.or(avgRating.goe(5.0)); + default -> { + } + } + } + if (ratingRangeBuilder.hasValue()) { + ratingBuilder.and(ratingRangeBuilder); + } + return ratingBuilder.hasValue() ? + product.id.in( + JPAExpressions.select(reviewProduct.product.id) + .from(reviewProduct) + .where(ratingBuilder) + ) : null; + } + + private BooleanExpression categoryFilter(final List categoryIds) { + if (categoryIds == null || categoryIds.isEmpty()) { + return null; + } + return product.category.id.in(categoryIds); + } + + private BooleanExpression keywordFilter(final String keyword) { + + return product.productInfo.name.containsIgnoreCase(keyword) + .or(product.productInfo.description.containsIgnoreCase(keyword)); + } } \ No newline at end of file diff --git a/backend/JiShop/src/main/java/com/jishop/product/service/DiscountSatusScheduler.java b/backend/JiShop/src/main/java/com/jishop/product/service/DiscountSatusScheduler.java deleted file mode 100644 index eab6d316..00000000 --- a/backend/JiShop/src/main/java/com/jishop/product/service/DiscountSatusScheduler.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.jishop.product.service; - -import com.jishop.product.repository.ProductRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.transaction.annotation.Transactional; - -@Slf4j -@Configuration -@EnableScheduling -@RequiredArgsConstructor -public class DiscountSatusScheduler { - - private final ProductRepository productRepository; - private final JdbcTemplate jdbcTemplate; - - @Transactional - @Scheduled(cron = "0 0 0 * * *") // 매일 자정에 실행 - public void randomPickDailyHotDeal() { - log.info("오늘의 특가 랜덤 Pick Pick 시작합니다!!! 두구두구두구..."); - - try { - // 1. 기존 일일 특가 상품 초기화 - resetPreviousDailyDeals(); - - // 2. 새로운 일일 특가 상품 설정 - updateNewDailyDeals(); - - log.info("오늘의 특가 뽑기가 잘 완료되었습니다. 박수!!"); - } catch (Exception e) { - log.error("오늘의 특가 뽑기 도중 에러가 발생했습니다.", e); - } - } - - private void resetPreviousDailyDeals() { - String resetQuery = "UPDATE products SET discount_status = 'NONE', is_discount = false " + - "WHERE discount_status = 'DAILY_DEAL'"; - - int updatedCount = jdbcTemplate.update(resetQuery); - log.info(" {} 개의 상품이 리셋되었습니다. ", updatedCount); - } - - private void updateNewDailyDeals() { - String updateQuery = " UPDATE products p " + - " JOIN ( SELECT id " + - " FROM products " + - " WHERE discount_price < origin_price " + - " AND sale_status = 'SELLING' " + - " AND secret = false " + - " ORDER BY RAND() " + - " LIMIT 100 )" + - " AS selected_products ON p.id = selected_products.id " + - " SET p.discount_status = 'DAILY_DEAL', p.is_discount = true"; - - int updatedCount = jdbcTemplate.update(updateQuery); - log.info(" {}개의 상품이 오늘의 특가로 뽑혔습니다.", updatedCount); - } -} diff --git a/backend/JiShop/src/main/java/com/jishop/product/service/DiscountStatusScheduler.java b/backend/JiShop/src/main/java/com/jishop/product/service/DiscountStatusScheduler.java new file mode 100644 index 00000000..b54f18ba --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/service/DiscountStatusScheduler.java @@ -0,0 +1,28 @@ +package com.jishop.product.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +@Slf4j +@Configuration +@EnableScheduling +@RequiredArgsConstructor +public class DiscountStatusScheduler { + + private final ProductDiscountService productDiscountService; + + @Scheduled(cron = "0 0 0 * * *") // 매일 자정에 실행 + public void randomPickDailyHotDeal() { + log.info("오늘의 특가 랜덤 Pick Pick 시작합니다!!! 두구두구두구..."); + + try { + productDiscountService.updateDailyDeals(); + log.info("오늘의 특가 뽑기가 잘 완료되었습니다. 박수!!"); + } catch (Exception e) { + log.error("오늘의 특가 뽑기 도중 에러가 발생했습니다.", e); + } + } +} diff --git a/backend/JiShop/src/main/java/com/jishop/product/service/ProductCategoryService.java b/backend/JiShop/src/main/java/com/jishop/product/service/ProductCategoryService.java new file mode 100644 index 00000000..da1b8f65 --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/service/ProductCategoryService.java @@ -0,0 +1,13 @@ +package com.jishop.product.service; + +import com.jishop.product.dto.response.ProductResponse; +import org.springframework.data.web.PagedModel; + +import java.util.List; + +public interface ProductCategoryService { + + PagedModel getProductsByCategory(final Long categoryId, final int page, final int size); + + List getCategoryIdsWithSubcategories(final Long categoryId); +} diff --git a/backend/JiShop/src/main/java/com/jishop/product/service/ProductDiscountService.java b/backend/JiShop/src/main/java/com/jishop/product/service/ProductDiscountService.java new file mode 100644 index 00000000..66b69e8c --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/service/ProductDiscountService.java @@ -0,0 +1,11 @@ +package com.jishop.product.service; + +import com.jishop.product.dto.response.TodaySpecialListResponse; +import org.springframework.data.web.PagedModel; + +public interface ProductDiscountService { + + PagedModel getProductsByDailyDeal(final int page, final int size); + + void updateDailyDeals(); +} diff --git a/backend/JiShop/src/main/java/com/jishop/product/service/ProductService.java b/backend/JiShop/src/main/java/com/jishop/product/service/ProductService.java index 08906e73..65252033 100644 --- a/backend/JiShop/src/main/java/com/jishop/product/service/ProductService.java +++ b/backend/JiShop/src/main/java/com/jishop/product/service/ProductService.java @@ -4,18 +4,11 @@ import com.jishop.product.dto.request.ProductRequest; import com.jishop.product.dto.response.ProductDetailResponse; import com.jishop.product.dto.response.ProductResponse; -import com.jishop.product.dto.response.TodaySpecialListResponse; import org.springframework.data.web.PagedModel; -import java.util.List; - public interface ProductService { PagedModel getProductList(final ProductRequest productRequest, final int page, final int size); ProductDetailResponse getProduct(final User user, final Long productId); - - List getProductsByWishList(final int page, final int size); - - PagedModel getProductsByTodaySpecial(final int page, final int size); } diff --git a/backend/JiShop/src/main/java/com/jishop/product/service/ProductWishlistService.java b/backend/JiShop/src/main/java/com/jishop/product/service/ProductWishlistService.java new file mode 100644 index 00000000..ea66264b --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/service/ProductWishlistService.java @@ -0,0 +1,13 @@ +package com.jishop.product.service; + +import com.jishop.member.domain.User; +import com.jishop.product.dto.response.ProductResponse; + +import java.util.List; + +public interface ProductWishlistService { + + List getProductsByWishList(final int page, final int size); + + boolean isProductWishedByUser(final User user, final Long productId); +} diff --git a/backend/JiShop/src/main/java/com/jishop/product/service/impl/ProductCategoryServiceImpl.java b/backend/JiShop/src/main/java/com/jishop/product/service/impl/ProductCategoryServiceImpl.java new file mode 100644 index 00000000..38f3bdc5 --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/service/impl/ProductCategoryServiceImpl.java @@ -0,0 +1,54 @@ +package com.jishop.product.service.impl; + +import com.jishop.category.repository.CategoryRepository; +import com.jishop.product.domain.Product; +import com.jishop.product.dto.response.ProductResponse; +import com.jishop.product.service.ProductCategoryService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.*; +import org.springframework.data.web.PagedModel; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ProductCategoryServiceImpl implements ProductCategoryService { + + private final CategoryRepository categoryRepository; + + @Override + public PagedModel getProductsByCategory(final Long categoryId, final int page, final int size) { + + final Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "wishListCount")); + final Page productPage = categoryRepository.findProductsByCategoryWithAllDescendants(categoryId, pageable); + + final List productsResponse = productPage.getContent().stream() + .map(ProductResponse::from) + .toList(); + + return new PagedModel<>(new PageImpl<>( + productsResponse, + pageable, + productPage.getTotalElements() + )); + } + + @Override + public List getCategoryIdsWithSubcategories(final Long categoryId) { + if (categoryId == null) return List.of(); + + return categoryRepository.findById(categoryId) + .map(category -> { + final List subCategoryPKs = categoryRepository.findIdsByCurrentIds( + categoryRepository.findAllSubCategoryIds(categoryId) + ); + return subCategoryPKs.isEmpty() + ? List.of(category.getId()) + : subCategoryPKs; + }) + .orElse(List.of()); + } +} diff --git a/backend/JiShop/src/main/java/com/jishop/product/service/impl/ProductDiscountServiceImpl.java b/backend/JiShop/src/main/java/com/jishop/product/service/impl/ProductDiscountServiceImpl.java new file mode 100644 index 00000000..71d1183e --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/service/impl/ProductDiscountServiceImpl.java @@ -0,0 +1,50 @@ +package com.jishop.product.service.impl; + +import com.jishop.order.repository.OrderDetailRepository; +import com.jishop.product.domain.DiscountStatus; +import com.jishop.product.domain.Product; +import com.jishop.product.dto.response.TodaySpecialListResponse; +import com.jishop.product.repository.ProductRepository; +import com.jishop.product.service.ProductDiscountService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PagedModel; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ProductDiscountServiceImpl implements ProductDiscountService { + + private final ProductRepository productRepository; + private final OrderDetailRepository orderDetailRepository; + + @Override + @Transactional(readOnly = true) + public PagedModel getProductsByDailyDeal(final int page, final int size) { + final Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + final Page productPage = productRepository.findDailyDealProducts(DiscountStatus.DAILY_DEAL, pageable); + + final Page responsePage = productPage.map(product -> { + final long totalSales = orderDetailRepository.countTotalOrdersByProductId(product.getId()); + + return TodaySpecialListResponse.from(product, totalSales); + }); + + return new PagedModel<>(responsePage); + } + + @Transactional + public void updateDailyDeals() { + int resetCount = productRepository.resetPreviousDailyDeals(); + log.info(" {} 개의 상품이 리셋되었습니다. ", resetCount); + + int updatedCount = productRepository.updateNewDailyDeals(); + log.info(" {}개의 상품이 오늘의 특가로 뽑혔습니다.", updatedCount); + } +} diff --git a/backend/JiShop/src/main/java/com/jishop/product/service/ProductServiceImpl.java b/backend/JiShop/src/main/java/com/jishop/product/service/impl/ProductServiceImpl.java similarity index 56% rename from backend/JiShop/src/main/java/com/jishop/product/service/ProductServiceImpl.java rename to backend/JiShop/src/main/java/com/jishop/product/service/impl/ProductServiceImpl.java index 412e12b0..73ecc721 100644 --- a/backend/JiShop/src/main/java/com/jishop/product/service/ProductServiceImpl.java +++ b/backend/JiShop/src/main/java/com/jishop/product/service/impl/ProductServiceImpl.java @@ -1,53 +1,55 @@ -package com.jishop.product.service; +package com.jishop.product.service.impl; -import com.jishop.category.service.CategoryService; import com.jishop.common.exception.DomainException; import com.jishop.common.exception.ErrorType; import com.jishop.member.domain.User; import com.jishop.option.dto.FashionClothesOptionResponse; import com.jishop.option.dto.GeneralOptionResponse; -import com.jishop.order.repository.OrderDetailRepository; -import com.jishop.product.domain.DiscountStatus; +import com.jishop.option.dto.SizeOption; import com.jishop.product.domain.Product; import com.jishop.product.dto.request.ProductRequest; import com.jishop.product.dto.response.ProductDetailResponse; import com.jishop.product.dto.response.ProductResponse; -import com.jishop.product.dto.response.TodaySpecialListResponse; import com.jishop.product.repository.ProductRepository; -import com.jishop.productwishlist.repository.ProductWishListRepository; +import com.jishop.product.service.ProductCategoryService; +import com.jishop.product.service.ProductService; +import com.jishop.product.service.ProductWishlistService; import com.jishop.reviewproduct.domain.ReviewProduct; import com.jishop.reviewproduct.repository.ReviewProductRepository; import com.jishop.saleproduct.repository.SaleProductRepository; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.web.PagedModel; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.Map; @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class ProductServiceImpl implements ProductService { - private final CategoryService categoryService; - private final ProductRepository productRepository; private final ReviewProductRepository reviewProductRepository; - private final ProductWishListRepository productWishListRepository; private final SaleProductRepository saleProductRepository; - private final OrderDetailRepository orderDetailRepository; + + private final ProductWishlistService wishlistService; + private final ProductCategoryService productCategoryService; @Override public PagedModel getProductList(final ProductRequest productRequest, final int page, final int size) { - final List selectedProducts = productRepository.findProductsByCondition(productRequest, page, size); + final List categoryIds = productCategoryService.getCategoryIdsWithSubcategories(productRequest.categoryId()); + + final List selectedProducts = productRepository.findProductsByCondition(productRequest, page, size, categoryIds); final List productListResponse = selectedProducts.stream() .map(ProductResponse::from).toList(); - final long totalCount = productRepository.countProductsByCondition(productRequest); + final long totalCount = productRepository.countProductsByCondition(productRequest, categoryIds); final Pageable pageable = PageRequest.of(page, size); final Page pagedProductsResponse = new PageImpl<>(productListResponse, pageable, totalCount); @@ -59,26 +61,23 @@ public ProductDetailResponse getProduct(final User user, final Long productId) { final Product product = productRepository.findById(productId) .orElseThrow(() -> new DomainException(ErrorType.PRODUCT_NOT_FOUND)); - // 찜 상태 - final boolean isWished = (user != null) && productWishListRepository - .isProductWishedByUser(user.getId(), productId); + final boolean isWished = wishlistService.isProductWishedByUser(user, productId); + + final Long categoryType = product.getCategoryInfo().getLCatId(); - // 상품 옵션 - final Long categoryType = product.getLCatId(); final Object productsOptions; if (categoryType == 50000000L) { - final List> fashionClothesOptions = saleProductRepository + final List fashionClothesOptions = saleProductRepository .findFashionClothesOptionsByProductId(productId); - productsOptions = fashionClothesOptions.isEmpty() ? + productsOptions = fashionClothesOptions.isEmpty() ? List.of() : FashionClothesOptionResponse.from(fashionClothesOptions); } else { - final List> generalOptions = saleProductRepository + final List generalOptions = saleProductRepository .findGeneralOptionsByProductId(productId); - productsOptions = GeneralOptionResponse.from(generalOptions); + productsOptions = GeneralOptionResponse.from(generalOptions); } - // 상품 리뷰 final List productsReviews = reviewProductRepository.findAllByProduct(product); final int reviewCount = productsReviews.isEmpty() ? 0 : productsReviews.get(0).getReviewCount(); final double reviewRate = productsReviews.isEmpty() ? 0.0 : productsReviews.get(0).getAverageRating(); @@ -86,26 +85,4 @@ public ProductDetailResponse getProduct(final User user, final Long productId) { return ProductDetailResponse .from(ProductResponse.from(product), product, isWished, reviewCount, reviewRate, productsOptions); } - - @Override - public List getProductsByWishList(final int page, final int size) { - final Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); - final Page productPage = productWishListRepository.getPopularProductsByWishList(pageable); - - return productPage.stream().map(ProductResponse::from).toList(); - } - - @Override - public PagedModel getProductsByTodaySpecial(final int page, final int size) { - final Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); - final Page productPage = productRepository.findDailDealProducts(DiscountStatus.DAILY_DEAL, pageable); - - final Page responsePage = productPage.map(product -> { - final long totalSales = orderDetailRepository.countTotalOrdersByProductId(product.getId()); - - return TodaySpecialListResponse.from(product, totalSales); - }); - - return new PagedModel<>(responsePage); - } } diff --git a/backend/JiShop/src/main/java/com/jishop/product/service/impl/ProductWishlistServiceImpl.java b/backend/JiShop/src/main/java/com/jishop/product/service/impl/ProductWishlistServiceImpl.java new file mode 100644 index 00000000..99700960 --- /dev/null +++ b/backend/JiShop/src/main/java/com/jishop/product/service/impl/ProductWishlistServiceImpl.java @@ -0,0 +1,38 @@ +package com.jishop.product.service.impl; + +import com.jishop.member.domain.User; +import com.jishop.product.domain.Product; +import com.jishop.product.dto.response.ProductResponse; +import com.jishop.product.service.ProductWishlistService; +import com.jishop.productwishlist.repository.ProductWishListRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ProductWishlistServiceImpl implements ProductWishlistService { + + private final ProductWishListRepository productWishListRepository; + + @Override + public List getProductsByWishList(final int page, final int size) { + final Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + final Page productPage = productWishListRepository.getPopularProductsByWishList(pageable); + + return productPage.stream().map(ProductResponse::from).toList(); + } + + @Override + public boolean isProductWishedByUser(final User user, final Long productId) { + + return (user != null) && productWishListRepository.isProductWishedByUser(user.getId(), productId); + } +} diff --git a/backend/JiShop/src/main/java/com/jishop/productwishlist/dto/ProductWishProductResponse.java b/backend/JiShop/src/main/java/com/jishop/productwishlist/dto/ProductWishProductResponse.java index 6397b21b..abf0280f 100644 --- a/backend/JiShop/src/main/java/com/jishop/productwishlist/dto/ProductWishProductResponse.java +++ b/backend/JiShop/src/main/java/com/jishop/productwishlist/dto/ProductWishProductResponse.java @@ -8,7 +8,7 @@ public record ProductWishProductResponse( String mainImage ) { public ProductWishProductResponse(Product product){ - this(product.getId(), product.getName(), product.getMainImage()); + this(product.getId(), product.getProductInfo().getName(), product.getImage().getMainImage()); } } diff --git a/backend/JiShop/src/main/java/com/jishop/productwishlist/repository/ProductWishListRepository.java b/backend/JiShop/src/main/java/com/jishop/productwishlist/repository/ProductWishListRepository.java index 62dc3f60..1ff55ad9 100644 --- a/backend/JiShop/src/main/java/com/jishop/productwishlist/repository/ProductWishListRepository.java +++ b/backend/JiShop/src/main/java/com/jishop/productwishlist/repository/ProductWishListRepository.java @@ -26,15 +26,15 @@ public interface ProductWishListRepository extends JpaRepository getPopularProductsByWishList(Pageable pageable); @Query("SELECT CASE WHEN COUNT(pw) > 0 THEN true ELSE false END FROM ProductWishList pw " + diff --git a/backend/JiShop/src/main/java/com/jishop/review/dto/ProductSummary.java b/backend/JiShop/src/main/java/com/jishop/review/dto/ProductSummary.java index 37bdc94c..91da4194 100644 --- a/backend/JiShop/src/main/java/com/jishop/review/dto/ProductSummary.java +++ b/backend/JiShop/src/main/java/com/jishop/review/dto/ProductSummary.java @@ -16,13 +16,13 @@ public record ProductSummary( public static ProductSummary from(Product product) { return new ProductSummary( product.getId(), - product.getName(), - product.getLabels(), - product.getOriginPrice(), - product.getDiscountPrice(), - product.getIsDiscount(), - product.getBrand(), - product.getMainImage() + product.getProductInfo().getName(), + product.getStatus().getLabels(), + product.getProductInfo().getOriginPrice(), + product.getProductInfo().getDiscountPrice(), + product.getStatus().getIsDiscount(), + product.getProductInfo().getBrand(), + product.getImage().getMainImage() ); } } diff --git a/backend/JiShop/src/main/java/com/jishop/review/repository/ReviewRepository.java b/backend/JiShop/src/main/java/com/jishop/review/repository/ReviewRepository.java index 38c39dd9..122a9178 100644 --- a/backend/JiShop/src/main/java/com/jishop/review/repository/ReviewRepository.java +++ b/backend/JiShop/src/main/java/com/jishop/review/repository/ReviewRepository.java @@ -62,9 +62,9 @@ public interface ReviewRepository extends JpaRepository { @Query(value = """ SELECT new com.jishop.review.dto.ReviewWriteResponse ( - p.id, od.id ,p.name, p.labels, p.originPrice, - p.isDiscount, p.discountRate, p.discountPrice, - p.brand, od.createdAt, p.mainImage, od.quantity, ot.optionValue) + p.id, od.id ,p.productInfo.name, p.status.labels, p.productInfo.originPrice, + p.status.isDiscount, p.productInfo.discountRate, p.productInfo.discountPrice, + p.productInfo.brand, od.createdAt, p.image.mainImage, od.quantity, ot.optionValue) FROM OrderDetail od JOIN od.order o LEFT JOIN Review r ON r.orderDetail = od JOIN od.saleProduct sp JOIN sp.product p JOIN sp.option ot WHERE o.userId = :userId AND o.status = 'PURCHASED_CONFIRMED' AND r.id IS NULL diff --git a/backend/JiShop/src/main/java/com/jishop/saleproduct/repository/SaleProductRepository.java b/backend/JiShop/src/main/java/com/jishop/saleproduct/repository/SaleProductRepository.java index bc266dd7..f5419782 100644 --- a/backend/JiShop/src/main/java/com/jishop/saleproduct/repository/SaleProductRepository.java +++ b/backend/JiShop/src/main/java/com/jishop/saleproduct/repository/SaleProductRepository.java @@ -1,5 +1,6 @@ package com.jishop.saleproduct.repository; +import com.jishop.option.dto.SizeOption; import com.jishop.saleproduct.domain.SaleProduct; import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,7 +10,6 @@ import org.springframework.stereotype.Repository; import java.util.List; -import java.util.Map; @Repository public interface SaleProductRepository extends JpaRepository { @@ -25,15 +25,17 @@ public interface SaleProductRepository extends JpaRepository "WHERE sp.id IN :ids") List findAllByIdsForOrder(@Param("ids") List ids); - @Query("SELECT sp.id as saleProductId, o.optionValue as optionValue, o.optionExtra as optionExtra FROM SaleProduct sp " + - "LEFT JOIN sp.option o ON o.categoryType = 'FASHION_CLOTHES' " + - "WHERE sp.product.id = :productId AND o.id IS NOT NULL") - List> findFashionClothesOptionsByProductId(@Param("productId") Long productId); - - @Query("SELECT sp.id as saleProductId, o.optionValue as optionValue, o.optionExtra as optionExtra FROM SaleProduct sp " + - "LEFT JOIN sp.option o ON o.categoryType <> 'FASHION_CLOTHES' " + - "WHERE sp.product.id = :productId AND o.id IS NOT NULL") - List> findGeneralOptionsByProductId(@Param("productId") Long productId); + @Query("SELECT new com.jishop.option.dto.SizeOption(sp.id, o.optionValue, o.optionExtra) " + + "FROM SaleProduct sp " + + "LEFT JOIN sp.option o ON o.categoryType = 'FASHION_CLOTHES' " + + "WHERE sp.product.id = :productId AND o.id IS NOT NULL") + List findFashionClothesOptionsByProductId(@Param("productId") Long productId); + + @Query("SELECT new com.jishop.option.dto.SizeOption(sp.id, o.optionValue, o.optionExtra) " + + "FROM SaleProduct sp " + + "LEFT JOIN sp.option o ON o.categoryType <> 'FASHION_CLOTHES' " + + "WHERE sp.product.id = :productId AND o.id IS NOT NULL") + List findGeneralOptionsByProductId(@Param("productId") Long productId); @Query("SELECT sp FROM SaleProduct sp " +