Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.DecodEat.domain.products.client;

import com.DecodEat.domain.products.dto.request.AnalysisRequestDto;
import com.DecodEat.domain.products.dto.request.ProductBasedRecommendationRequestDto;
import com.DecodEat.domain.products.dto.response.AnalysisResponseDto;
import com.DecodEat.domain.products.dto.response.ProductBasedRecommendationResponseDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -33,4 +35,18 @@ public Mono<AnalysisResponseDto> analyzeProduct(AnalysisRequestDto request) {
.doOnSuccess(response -> log.info("Analysis completed with status: {}", response.getDecodeStatus()))
.doOnError(error -> log.error("Analysis request failed: {}", error.getMessage()));
}

public Mono<ProductBasedRecommendationResponseDto> getProductBasedRecommendation(
ProductBasedRecommendationRequestDto request){
log.info("Sending analysis request to Python server: {}", pythonServerUrl);

return webClient.post()
.uri(pythonServerUrl + "api/v1/recommend/product-based")
.bodyValue(request)
.retrieve()
.bodyToMono(ProductBasedRecommendationResponseDto.class)
.timeout(Duration.ofMinutes(2))
.doOnSuccess(response -> log.info("Success to get recommendation(product-based): {}",request.getProduct_id()))
.doOnError(error -> log.error("Recommendation request failed: {}", error.getMessage()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,12 @@ public ApiResponse<ProductLikeResponseDTO> addOrUpdateLike(
) {
return ApiResponse.onSuccess(productService.addOrUpdateLike(user.getId(), productId));
}

@GetMapping("/recommendation/product-based")
@Operation(summary = "상품 기반 추천", description = "상품 영양성분, 원재료명 기반 추천")
public ApiResponse<List<ProductSearchResponseDto.ProductPrevDto>> getProductBasedRecommendation(@RequestParam Long productId,
@RequestParam(defaultValue = "5") int limit) {
return ApiResponse.onSuccess(productService.getProductBasedRecommendation(productId, limit));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.DecodEat.domain.products.dto.request;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProductBasedRecommendationRequestDto {
private int product_id;
private int limit;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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 // JSON -> Java 객체 변환 시 Jackson 라이브러리가 사용하기 위함
public class ProductBasedRecommendationResponseDto {

private List<RecommendationDetailDto> recommendations;
private int totalCount;
private Long userId;
private Long referenceProductId;
private String recommendationType;
private String dataQuality;
private String message;

@Getter
@NoArgsConstructor
public static class RecommendationDetailDto {
private Long productId;
private double similarityScore;
private String recommendationReason;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.DecodEat.domain.products.client.PythonAnalysisClient;
import com.DecodEat.domain.products.converter.ProductConverter;
import com.DecodEat.domain.products.dto.request.AnalysisRequestDto;
import com.DecodEat.domain.products.dto.request.ProductBasedRecommendationRequestDto;
import com.DecodEat.domain.products.dto.request.ProductRegisterRequestDto;
import com.DecodEat.domain.products.dto.response.*;
import com.DecodEat.domain.products.entity.*;
Expand All @@ -13,9 +14,11 @@
import com.DecodEat.domain.users.entity.User;
import com.DecodEat.domain.users.repository.UserRepository;
import com.DecodEat.domain.users.service.UserBehaviorService;
import com.DecodEat.global.apiPayload.code.status.ErrorStatus;
import com.DecodEat.global.aws.s3.AmazonS3Manager;
import com.DecodEat.global.dto.PageResponseDto;
import com.DecodEat.global.exception.GeneralException;
import jdk.jfr.Frequency;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.*;
Expand Down Expand Up @@ -51,8 +54,8 @@ public class ProductService {

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);
if (user != null)
userBehaviorService.saveUserBehavior(user, product, Behavior.VIEW);

List<ProductInfoImage> images = productImageRepository.findByProduct(product);
List<String> imageUrls = images.stream().map(ProductInfoImage::getImageUrl).toList();
Expand All @@ -61,8 +64,8 @@ public ProductDetailDto getDetail(Long id, User user) {

// 좋아요 여부 확인
boolean isLiked = false;
if(user != null){
isLiked = productLikeRepository.existsByUserAndProduct(user,product);
if (user != null) {
isLiked = productLikeRepository.existsByUserAndProduct(user, product);
}
return ProductConverter.toProductDetailDto(product, imageUrls, productNutrition, isLiked);
}
Expand Down Expand Up @@ -106,7 +109,7 @@ public ProductRegisterResponseDto addProduct(User user, ProductRegisterRequestDt
// 파이썬 서버에 비동기로 분석 요청
requestAnalysisAsync(savedProduct.getProductId(), productInfoImageUrls);

userBehaviorService.saveUserBehavior(user,savedProduct,Behavior.REGISTER); // todo: 만약에 분석 실패?
userBehaviorService.saveUserBehavior(user, savedProduct, Behavior.REGISTER); // todo: 만약에 분석 실패?

return ProductConverter.toProductRegisterDto(savedProduct, productInfoImageUrls);
}
Expand Down Expand Up @@ -175,7 +178,7 @@ public PageResponseDto<ProductSearchResponseDto.ProductPrevDto> searchProducts(S
return new PageResponseDto<>(result);
}

public PageResponseDto<ProductRegisterHistoryDto> getRegisterHistory(User user, Pageable pageable){
public PageResponseDto<ProductRegisterHistoryDto> getRegisterHistory(User user, Pageable pageable) {

Long userId = user.getId();

Expand All @@ -185,10 +188,32 @@ public PageResponseDto<ProductRegisterHistoryDto> getRegisterHistory(User user,
return new PageResponseDto<>(result);
}

public List<ProductSearchResponseDto.ProductPrevDto> getProductBasedRecommendation(Long productId, int limit) {

ProductBasedRecommendationRequestDto request =
new ProductBasedRecommendationRequestDto(productId.intValue(), limit);

ProductBasedRecommendationResponseDto response =
pythonAnalysisClient.getProductBasedRecommendation(request).block();

if (response == null) {
log.warn("No recommendation response for product ID: {}", productId);
throw new GeneralException(NO_RECOMMENDATION_PRODUCT_BASED);
}

List<Product> productList = response.getRecommendations().stream()
.map(r -> productRepository.findById(r.getProductId())
.orElseThrow(() -> new GeneralException(PRODUCT_NOT_EXISTED)))
.toList();

return productList.stream().map(ProductConverter::toProductPrevDto).toList();
}


@Async
public void requestAnalysisAsync(Long productId, List<String> imageUrls) {
log.info("Starting async analysis for product ID: {}", productId);

if (imageUrls == null || imageUrls.isEmpty()) {
log.warn("No images to analyze for product ID: {}", productId);
updateProductStatus(productId, DecodeStatus.FAILED, "No images provided for analysis");
Expand Down Expand Up @@ -218,7 +243,7 @@ public void requestAnalysisAsync(Long productId, List<String> imageUrls) {
@Transactional
public void processAnalysisResult(Long productId, AnalysisResponseDto response) {
log.info("Processing analysis result for product ID: {} with status: {}", productId, response.getDecodeStatus());

try {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new GeneralException(PRODUCT_NOT_EXISTED));
Expand All @@ -244,10 +269,10 @@ public void updateProductStatus(Long productId, DecodeStatus status, String mess
try {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new GeneralException(PRODUCT_NOT_EXISTED));

product.setDecodeStatus(status);
productRepository.save(product);

log.info("Updated product ID: {} status to: {} - {}", productId, status, message);
} catch (Exception e) {
log.error("Failed to update product status for ID: {}", productId, e);
Expand All @@ -256,7 +281,7 @@ public void updateProductStatus(Long productId, DecodeStatus status, String mess

private void saveNutritionInfo(Long productId, AnalysisResponseDto response) {
log.info("Saving nutrition info for product ID: {}", productId);

try {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new GeneralException(PRODUCT_NOT_EXISTED));
Expand All @@ -277,7 +302,7 @@ private void saveNutritionInfo(Long productId, AnalysisResponseDto response) {
.sugar(parseDouble(response.getNutrition_info().getSugar()))
.transFat(parseDouble(response.getNutrition_info().getTrans_fat()))
.build();

productNutritionRepository.save(nutrition);
log.info("Saved nutrition info for product ID: {}", productId);
}
Expand Down Expand Up @@ -366,7 +391,7 @@ public ProductLikeResponseDTO addOrUpdateLike(Long userId, Long productId) {
// 이미 눌렀으면 → 좋아요 취소
productLikeRepository.delete(existingLike.get());
isLiked = false;
userBehaviorService.deleteUserBehavior(user,product, Behavior.LIKE);
userBehaviorService.deleteUserBehavior(user, product, Behavior.LIKE);

} else {
// 처음 누르면 → 좋아요 추가
Expand All @@ -376,7 +401,7 @@ public ProductLikeResponseDTO addOrUpdateLike(Long userId, Long productId) {
.build();
productLikeRepository.save(productLike);
isLiked = true;
userBehaviorService.saveUserBehavior(user,product, Behavior.LIKE);
userBehaviorService.saveUserBehavior(user, product, Behavior.LIKE);
}
return ProductConverter.toProductLikeDTO(productId, isLiked);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public enum ErrorStatus implements BaseErrorCode {
PAGE_OUT_OF_RANGE(HttpStatus.BAD_REQUEST,"SEARCH_400","요청한 페이지가 전체 페이지 수를 초과합니다."),
NO_RESULT(HttpStatus.NOT_FOUND,"SEARCH_401","검색 결과가 없습니다."),

// 추천
NO_RECOMMENDATION_PRODUCT_BASED(HttpStatus.NOT_FOUND,"RECOMMENDATION_400","유사한 상품이 존재하지 않습니다."),

// 기본 에러
_BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON_400", "잘못된 요청입니다."),
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON_401", "인증이 필요합니다."),
Expand Down
Loading