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 b9129a6..a3d7469 100644 --- a/src/main/java/com/DecodEat/domain/products/controller/ProductController.java +++ b/src/main/java/com/DecodEat/domain/products/controller/ProductController.java @@ -7,6 +7,7 @@ import com.DecodEat.domain.users.entity.User; import com.DecodEat.global.apiPayload.ApiResponse; import com.DecodEat.global.common.annotation.CurrentUser; +import com.DecodEat.global.common.annotation.OptionalUser; import com.DecodEat.global.dto.PageResponseDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -34,8 +35,9 @@ public class ProductController { summary = "제품 조회", description = "단일 제품 상세 정보 조회") @GetMapping("/{id}") - public ApiResponse getProduct(@PathVariable Long id) { - return ApiResponse.onSuccess(productService.getDetail(id)); + public ApiResponse getProduct(@PathVariable Long id, + @OptionalUser User user) { + return ApiResponse.onSuccess(productService.getDetail(id,user)); } @Operation( 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 248e5ed..53dfadf 100644 --- a/src/main/java/com/DecodEat/domain/products/service/ProductService.java +++ b/src/main/java/com/DecodEat/domain/products/service/ProductService.java @@ -18,7 +18,9 @@ import com.DecodEat.domain.products.repository.ProductRepository; import com.DecodEat.domain.products.repository.RawMaterialRepository; import com.DecodEat.domain.products.repository.ProductSpecification; +import com.DecodEat.domain.users.entity.Behavior; import com.DecodEat.domain.users.entity.User; +import com.DecodEat.domain.users.service.UserBehaviorService; import com.DecodEat.global.aws.s3.AmazonS3Manager; import com.DecodEat.global.dto.PageResponseDto; import com.DecodEat.global.exception.GeneralException; @@ -52,13 +54,17 @@ public class ProductService { private final ProductRawMaterialRepository productRawMaterialRepository; private final AmazonS3Manager amazonS3Manager; private final PythonAnalysisClient pythonAnalysisClient; + private final UserBehaviorService userBehaviorService; private static final int PAGE_SIZE = 12; - public ProductDetailDto getDetail(Long id) { + public ProductDetailDto getDetail(Long id, User user) { Product product = productRepository.findById(id).orElseThrow(() -> new GeneralException(PRODUCT_NOT_EXISTED)); + if(user != null) + userBehaviorService.saveUserBehavior(user,product, Behavior.VIEW); + List images = productImageRepository.findByProduct(product); List imageUrls = images.stream().map(ProductInfoImage::getImageUrl).toList(); @@ -106,6 +112,8 @@ public ProductRegisterResponseDto addProduct(User user, ProductRegisterRequestDt // 파이썬 서버에 비동기로 분석 요청 requestAnalysisAsync(savedProduct.getProductId(), productInfoImageUrls); + userBehaviorService.saveUserBehavior(user,savedProduct,Behavior.REGISTER); // todo: 만약에 분석 실패? + return ProductConverter.toProductRegisterDto(savedProduct, productInfoImageUrls); } @@ -117,6 +125,7 @@ public ProductResponseDTO.ProductListResultDTO getProducts(Long cursorId) { return ProductConverter.toProductListResultDTO(slice); } + // todo: 검색은 상품 엔티티와 1:1 매핑 불가능 -> userbehavior 어떻게? public List searchProducts(String productName) { Specification spec = Specification.where(ProductSpecification.isCompleted()); diff --git a/src/main/java/com/DecodEat/domain/users/entity/Behavior.java b/src/main/java/com/DecodEat/domain/users/entity/Behavior.java new file mode 100644 index 0000000..e7ee615 --- /dev/null +++ b/src/main/java/com/DecodEat/domain/users/entity/Behavior.java @@ -0,0 +1,5 @@ +package com.DecodEat.domain.users.entity; + +public enum Behavior { + VIEW,LIKE,REGISTER,SEARCH +} diff --git a/src/main/java/com/DecodEat/domain/users/entity/UserBehavior.java b/src/main/java/com/DecodEat/domain/users/entity/UserBehavior.java new file mode 100644 index 0000000..14e36c5 --- /dev/null +++ b/src/main/java/com/DecodEat/domain/users/entity/UserBehavior.java @@ -0,0 +1,32 @@ +package com.DecodEat.domain.users.entity; + +import com.DecodEat.domain.products.entity.Product; +import com.DecodEat.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table +@Builder +@AllArgsConstructor +public class UserBehavior extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id", nullable = false) + private Product product; + + @Enumerated(EnumType.STRING) + @Column(name = "behavior", nullable = false) + private Behavior behavior; + +} \ No newline at end of file diff --git a/src/main/java/com/DecodEat/domain/users/repository/UserBehaviorRepository.java b/src/main/java/com/DecodEat/domain/users/repository/UserBehaviorRepository.java new file mode 100644 index 0000000..8de1d52 --- /dev/null +++ b/src/main/java/com/DecodEat/domain/users/repository/UserBehaviorRepository.java @@ -0,0 +1,7 @@ +package com.DecodEat.domain.users.repository; + +import com.DecodEat.domain.users.entity.UserBehavior; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserBehaviorRepository extends JpaRepository { +} diff --git a/src/main/java/com/DecodEat/domain/users/service/UserBehaviorService.java b/src/main/java/com/DecodEat/domain/users/service/UserBehaviorService.java new file mode 100644 index 0000000..1ccd996 --- /dev/null +++ b/src/main/java/com/DecodEat/domain/users/service/UserBehaviorService.java @@ -0,0 +1,25 @@ +package com.DecodEat.domain.users.service; + +import com.DecodEat.domain.products.entity.Product; +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 lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserBehaviorService { + private final UserBehaviorRepository userBehaviorRepository; + @Transactional + public void saveUserBehavior(User user, Product product, Behavior behavior) { + UserBehavior userBehavior = UserBehavior.builder() + .user(user) + .product(product) + .behavior(behavior) + .build(); + userBehaviorRepository.save(userBehavior); + } +} diff --git a/src/main/java/com/DecodEat/global/config/CurrentUserArgumentResolver.java b/src/main/java/com/DecodEat/global/common/annotation/CurrentUserArgumentResolver.java similarity index 94% rename from src/main/java/com/DecodEat/global/config/CurrentUserArgumentResolver.java rename to src/main/java/com/DecodEat/global/common/annotation/CurrentUserArgumentResolver.java index b0f1ff7..e53eb6d 100644 --- a/src/main/java/com/DecodEat/global/config/CurrentUserArgumentResolver.java +++ b/src/main/java/com/DecodEat/global/common/annotation/CurrentUserArgumentResolver.java @@ -1,8 +1,7 @@ -package com.DecodEat.global.config; +package com.DecodEat.global.common.annotation; import com.DecodEat.domain.users.service.UserService; import com.DecodEat.global.apiPayload.code.status.ErrorStatus; -import com.DecodEat.global.common.annotation.CurrentUser; import com.DecodEat.global.exception.GeneralException; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; diff --git a/src/main/java/com/DecodEat/global/common/annotation/OptionalUser.java b/src/main/java/com/DecodEat/global/common/annotation/OptionalUser.java new file mode 100644 index 0000000..d98c6a2 --- /dev/null +++ b/src/main/java/com/DecodEat/global/common/annotation/OptionalUser.java @@ -0,0 +1,14 @@ +package com.DecodEat.global.common.annotation; + +import io.swagger.v3.oas.annotations.Parameter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Parameter(hidden = true) +public @interface OptionalUser { +} \ No newline at end of file diff --git a/src/main/java/com/DecodEat/global/common/annotation/OptionalUserArgumentResolver.java b/src/main/java/com/DecodEat/global/common/annotation/OptionalUserArgumentResolver.java new file mode 100644 index 0000000..bed3b8b --- /dev/null +++ b/src/main/java/com/DecodEat/global/common/annotation/OptionalUserArgumentResolver.java @@ -0,0 +1,39 @@ +package com.DecodEat.global.common.annotation; + +import com.DecodEat.domain.users.entity.User; +import com.DecodEat.domain.users.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class OptionalUserArgumentResolver implements HandlerMethodArgumentResolver { + private final UserService userService; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterAnnotation(OptionalUser.class) != null + && parameter.getParameterType().equals(User.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + if (principal instanceof org.springframework.security.core.userdetails.User springUser) { + Long userId = Long.valueOf(springUser.getUsername()); + return userService.findById(userId); + } + + // 예외를 던지는 대신 null을 반환! + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/DecodEat/global/config/WebConfig.java b/src/main/java/com/DecodEat/global/config/WebConfig.java index 24be7e0..9ce29c5 100644 --- a/src/main/java/com/DecodEat/global/config/WebConfig.java +++ b/src/main/java/com/DecodEat/global/config/WebConfig.java @@ -1,5 +1,6 @@ package com.DecodEat.global.config; +import com.DecodEat.global.common.annotation.CurrentUserArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver;