From a1d8f1fad0c5f177c6a980c206805aaf0fe99f57 Mon Sep 17 00:00:00 2001 From: junho <2171168@hansung.ac.kr> Date: Tue, 23 Sep 2025 18:39:57 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=96=89=EB=8F=99=EA=B8=B0=EB=B0=98=20=EC=B6=94=EC=B2=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ProductController.java | 6 +++ .../UserBasedRecommendationResponseDto.java | 18 +++++++++ .../repository/ProductRepository.java | 5 +++ .../products/service/ProductService.java | 38 +++++++++++++++++++ .../repository/UserBehaviorRepository.java | 3 ++ .../apiPayload/code/status/ErrorStatus.java | 2 +- 6 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/DecodEat/domain/products/dto/response/UserBasedRecommendationResponseDto.java diff --git a/src/main/java/com/DecodEat/domain/products/controller/ProductController.java b/src/main/java/com/DecodEat/domain/products/controller/ProductController.java index a5534c9..7a986be 100644 --- a/src/main/java/com/DecodEat/domain/products/controller/ProductController.java +++ b/src/main/java/com/DecodEat/domain/products/controller/ProductController.java @@ -121,4 +121,10 @@ public ApiResponse> getProductBase return ApiResponse.onSuccess(productService.getProductBasedRecommendation(productId, limit)); } + @GetMapping("/recommendation/user-behavior-based") + @Operation(summary = "상품 기반 추천", description = "사용자 행동 기반 추천") + public ApiResponse getUserBasedRecommendation(@CurrentUser User user) { + return ApiResponse.onSuccess(productService.getUserBasedRecommendation(user)); + } + } diff --git a/src/main/java/com/DecodEat/domain/products/dto/response/UserBasedRecommendationResponseDto.java b/src/main/java/com/DecodEat/domain/products/dto/response/UserBasedRecommendationResponseDto.java new file mode 100644 index 0000000..d1a1698 --- /dev/null +++ b/src/main/java/com/DecodEat/domain/products/dto/response/UserBasedRecommendationResponseDto.java @@ -0,0 +1,18 @@ +package com.DecodEat.domain.products.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserBasedRecommendationResponseDto { + private String message; + private ProductSearchResponseDto.SearchResultPrevDto standardProduct; + private List products; +} diff --git a/src/main/java/com/DecodEat/domain/products/repository/ProductRepository.java b/src/main/java/com/DecodEat/domain/products/repository/ProductRepository.java index 7916869..54ab1e8 100644 --- a/src/main/java/com/DecodEat/domain/products/repository/ProductRepository.java +++ b/src/main/java/com/DecodEat/domain/products/repository/ProductRepository.java @@ -9,9 +9,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; +import com.DecodEat.domain.users.entity.Behavior; import org.springframework.data.repository.query.Param; import java.util.List; +import java.util.Optional; public interface ProductRepository extends JpaRepository, JpaSpecificationExecutor { @@ -26,4 +28,7 @@ Slice findCompletedProductsByCursor(@Param("cursorId") Long cursorId, void deleteByDecodeStatusIn(List statuses); Page findByUserId(Long userId, Pageable pageable); + + @Query(value = "SELECT p.product_id FROM product p JOIN user_behavior ub ON p.product_id = ub.product_id WHERE ub.user_id = :userId AND ub.behavior = :behavior ORDER BY RAND() LIMIT 1", nativeQuery = true) + Optional findRandomProductIdByUserIdAndBehavior(@Param("userId") Long userId, @Param("behavior") Behavior behavior); } diff --git a/src/main/java/com/DecodEat/domain/products/service/ProductService.java b/src/main/java/com/DecodEat/domain/products/service/ProductService.java index e91950f..725bae0 100644 --- a/src/main/java/com/DecodEat/domain/products/service/ProductService.java +++ b/src/main/java/com/DecodEat/domain/products/service/ProductService.java @@ -12,6 +12,8 @@ import com.DecodEat.domain.products.repository.*; import com.DecodEat.domain.users.entity.Behavior; import com.DecodEat.domain.users.entity.User; +import com.DecodEat.domain.users.entity.UserBehavior; +import com.DecodEat.domain.users.repository.UserBehaviorRepository; import com.DecodEat.domain.users.repository.UserRepository; import com.DecodEat.domain.users.service.UserBehaviorService; import com.DecodEat.global.apiPayload.code.status.ErrorStatus; @@ -31,6 +33,7 @@ import javax.swing.*; import java.util.*; +import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; import static com.DecodEat.global.apiPayload.code.status.ErrorStatus.*; @@ -51,6 +54,7 @@ public class ProductService { private static final int PAGE_SIZE = 12; private final UserRepository userRepository; private final ProductLikeRepository productLikeRepository; + private final UserBehaviorRepository userBehaviorRepository; public ProductDetailDto getDetail(Long id, User user) { Product product = productRepository.findById(id).orElseThrow(() -> new GeneralException(PRODUCT_NOT_EXISTED)); @@ -210,6 +214,40 @@ public List getProductBasedRecommendati return productList.stream().map(ProductConverter::toProductPrevDto).toList(); } + public UserBasedRecommendationResponseDto getUserBasedRecommendation(User user) { + Long userId = user.getId(); + int randomCase = ThreadLocalRandom.current().nextInt(3); + Behavior selectedBehavior = null; + String message = ""; + switch (randomCase) { + case 0: + selectedBehavior = Behavior.LIKE; // 0번은 좋아요 기반 + message = "내가 최근 좋아요한 상품과 관련된 상품"; + break; + case 1: + selectedBehavior = Behavior.REGISTER; // 1번 등록 기반 + message = "내가 최근 등록한 상품과 관련된 상품"; + break; + case 2: + selectedBehavior = Behavior.VIEW; // 2번 조회 기반 + message = "내가 최근 조회한 상품과 관련된 상품"; + break; + + } + + Long standardProductId = productRepository.findRandomProductIdByUserIdAndBehavior(userId,selectedBehavior) + .orElseThrow(()-> new GeneralException(NO_USER_BEHAVIOR_EXISTED)); + Product standardProduct = productRepository.findById(standardProductId).orElseThrow(()->new GeneralException(NO_RESULT)); + + List products = getProductBasedRecommendation(standardProductId, 5); + + return UserBasedRecommendationResponseDto.builder() + .standardProduct(ProductConverter.toSearchResultPrevDto(standardProduct)) + .message(message) + .products(products) + .build(); + } + @Async public void requestAnalysisAsync(Long productId, List imageUrls) { diff --git a/src/main/java/com/DecodEat/domain/users/repository/UserBehaviorRepository.java b/src/main/java/com/DecodEat/domain/users/repository/UserBehaviorRepository.java index 4f9b9db..4955a18 100644 --- a/src/main/java/com/DecodEat/domain/users/repository/UserBehaviorRepository.java +++ b/src/main/java/com/DecodEat/domain/users/repository/UserBehaviorRepository.java @@ -6,8 +6,11 @@ import com.DecodEat.domain.users.entity.UserBehavior; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface UserBehaviorRepository extends JpaRepository { void deleteByUserAndProductAndBehavior(User user, Product product, Behavior behavior); + } diff --git a/src/main/java/com/DecodEat/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/DecodEat/global/apiPayload/code/status/ErrorStatus.java index d039f27..d52e68f 100644 --- a/src/main/java/com/DecodEat/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/DecodEat/global/apiPayload/code/status/ErrorStatus.java @@ -24,7 +24,7 @@ public enum ErrorStatus implements BaseErrorCode { // 추천 NO_RECOMMENDATION_PRODUCT_BASED(HttpStatus.NOT_FOUND,"RECOMMENDATION_400","유사한 상품이 존재하지 않습니다."), - + NO_USER_BEHAVIOR_EXISTED(HttpStatus.NOT_FOUND,"RECOMMENDATION_401","유저의 해당 메타 데이터가 존재하지 않습니다."), // 기본 에러 _BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON_400", "잘못된 요청입니다."), _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON_401", "인증이 필요합니다."),