From 1fa5e411d1e2b27be8914a9356504e85ea2636c5 Mon Sep 17 00:00:00 2001 From: Yujin1219 Date: Sat, 20 Sep 2025 12:49:05 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EC=A0=9C=ED=92=88=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94/=EC=B7=A8=EC=86=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ProductController.java | 8 +++ .../products/converter/ProductConverter.java | 10 ++++ .../dto/response/ProductLikeResponseDTO.java | 22 ++++++++ .../domain/products/entity/ProductLike.java | 34 +++++++++++++ .../repository/ProductLikeRepository.java | 14 +++++ .../products/service/ProductService.java | 51 ++++++++++++++----- 6 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/DecodEat/domain/products/dto/response/ProductLikeResponseDTO.java create mode 100644 src/main/java/com/DecodEat/domain/products/entity/ProductLike.java create mode 100644 src/main/java/com/DecodEat/domain/products/repository/ProductLikeRepository.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 a3d7469..074acd8 100644 --- a/src/main/java/com/DecodEat/domain/products/controller/ProductController.java +++ b/src/main/java/com/DecodEat/domain/products/controller/ProductController.java @@ -105,4 +105,12 @@ public ApiResponse> getRegisterHistor return ApiResponse.onSuccess(productService.getRegisterHistory(user, pageable)); } + @Operation(summary = "제품 좋아요 추가/취소", description = "좋아요를 누르면 추가, 다시 누르면 취소됩니다.") + @PostMapping("/{productId}/like") + public ApiResponse addOrUpdateLike( + @CurrentUser User user, + @Parameter(description = "제품 ID") @PathVariable Long productId + ) { + return ApiResponse.onSuccess(productService.addOrUpdateLike(user.getId(), productId)); + } } diff --git a/src/main/java/com/DecodEat/domain/products/converter/ProductConverter.java b/src/main/java/com/DecodEat/domain/products/converter/ProductConverter.java index 8a4de73..726df7b 100644 --- a/src/main/java/com/DecodEat/domain/products/converter/ProductConverter.java +++ b/src/main/java/com/DecodEat/domain/products/converter/ProductConverter.java @@ -3,6 +3,7 @@ import com.DecodEat.domain.products.dto.request.ProductRegisterRequestDto; import com.DecodEat.domain.products.dto.response.*; import com.DecodEat.domain.products.entity.Product; +import com.DecodEat.domain.products.entity.ProductInfoImage; import com.DecodEat.domain.products.entity.ProductNutrition; import com.DecodEat.domain.products.entity.RawMaterial.RawMaterialCategory; import org.springframework.data.domain.Slice; @@ -121,4 +122,13 @@ public static ProductResponseDTO.ProductListResultDTO toProductListResultDTO(Sli .nextCursorId(nextCursorId) .build(); } + + public static ProductLikeResponseDTO toProductLikeDTO(Long productId, boolean isLiked) { + + return ProductLikeResponseDTO.builder() + .productId(productId) + .isLiked(isLiked) + .build(); + + } } diff --git a/src/main/java/com/DecodEat/domain/products/dto/response/ProductLikeResponseDTO.java b/src/main/java/com/DecodEat/domain/products/dto/response/ProductLikeResponseDTO.java new file mode 100644 index 0000000..3ee76db --- /dev/null +++ b/src/main/java/com/DecodEat/domain/products/dto/response/ProductLikeResponseDTO.java @@ -0,0 +1,22 @@ +package com.DecodEat.domain.products.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "제품 좋아요 응답 정보") +public class ProductLikeResponseDTO { + + @Schema(description = "제품 ID", example = "1") + private Long productId; + + @Schema(description = "좋아요 여부", example = "true") + private boolean isLiked; + +} diff --git a/src/main/java/com/DecodEat/domain/products/entity/ProductLike.java b/src/main/java/com/DecodEat/domain/products/entity/ProductLike.java new file mode 100644 index 0000000..cbff1aa --- /dev/null +++ b/src/main/java/com/DecodEat/domain/products/entity/ProductLike.java @@ -0,0 +1,34 @@ +package com.DecodEat.domain.products.entity; + + +import com.DecodEat.domain.users.entity.User; +import com.DecodEat.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Table( + name = "product_like", + uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id", "product_id"}) + } +)// 한 유저는 하나의 제품에 대해 한번의 좋아요만 하도록 유니크 제약조건 설정 +public class ProductLike 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; + +} diff --git a/src/main/java/com/DecodEat/domain/products/repository/ProductLikeRepository.java b/src/main/java/com/DecodEat/domain/products/repository/ProductLikeRepository.java new file mode 100644 index 0000000..779ca71 --- /dev/null +++ b/src/main/java/com/DecodEat/domain/products/repository/ProductLikeRepository.java @@ -0,0 +1,14 @@ +package com.DecodEat.domain.products.repository; + +import com.DecodEat.domain.products.entity.Product; +import com.DecodEat.domain.products.entity.ProductLike; +import com.DecodEat.domain.users.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ProductLikeRepository extends JpaRepository { + + Optional findByUserAndProduct(User user, Product product); + +} 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 fee45f6..726902d 100644 --- a/src/main/java/com/DecodEat/domain/products/service/ProductService.java +++ b/src/main/java/com/DecodEat/domain/products/service/ProductService.java @@ -5,21 +5,13 @@ import com.DecodEat.domain.products.dto.request.AnalysisRequestDto; import com.DecodEat.domain.products.dto.request.ProductRegisterRequestDto; import com.DecodEat.domain.products.dto.response.*; -import com.DecodEat.domain.products.entity.DecodeStatus; -import com.DecodEat.domain.products.entity.Product; -import com.DecodEat.domain.products.entity.ProductInfoImage; -import com.DecodEat.domain.products.entity.ProductNutrition; -import com.DecodEat.domain.products.entity.ProductRawMaterial; +import com.DecodEat.domain.products.entity.*; import com.DecodEat.domain.products.entity.RawMaterial.RawMaterial; import com.DecodEat.domain.products.entity.RawMaterial.RawMaterialCategory; -import com.DecodEat.domain.products.repository.ProductImageRepository; -import com.DecodEat.domain.products.repository.ProductNutritionRepository; -import com.DecodEat.domain.products.repository.ProductRawMaterialRepository; -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.products.repository.*; import com.DecodEat.domain.users.entity.Behavior; 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.aws.s3.AmazonS3Manager; import com.DecodEat.global.dto.PageResponseDto; @@ -37,6 +29,7 @@ import javax.swing.*; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -55,9 +48,9 @@ public class ProductService { private final AmazonS3Manager amazonS3Manager; private final PythonAnalysisClient pythonAnalysisClient; private final UserBehaviorService userBehaviorService; - - private static final int PAGE_SIZE = 12; + private final UserRepository userRepository; + private final ProductLikeRepository productLikeRepository; public ProductDetailDto getDetail(Long id, User user) { Product product = productRepository.findById(id).orElseThrow(() -> new GeneralException(PRODUCT_NOT_EXISTED)); @@ -325,4 +318,36 @@ private Double parseDouble(String value) { return null; } } + + @Transactional + public ProductLikeResponseDTO addOrUpdateLike(Long userId, Long productId) { + + // 1. 유저 확인 + User user = userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(USER_NOT_EXISTED)); + + // 2. 제품 확인 + Product product = productRepository.findById(productId) + .orElseThrow(() -> new GeneralException(PRODUCT_NOT_EXISTED)); + + // 3. 기존 좋아요 여부 확인 + Optional existingLike = productLikeRepository.findByUserAndProduct(user, product); + + boolean isLiked; + + if (existingLike.isPresent()) { + // 이미 눌렀으면 → 좋아요 취소 + productLikeRepository.delete(existingLike.get()); + isLiked = false; + } else { + // 처음 누르면 → 좋아요 추가 + ProductLike productLike = ProductLike.builder() + .user(user) + .product(product) + .build(); + productLikeRepository.save(productLike); + isLiked = true; + } + return ProductConverter.toProductLikeDTO(productId, isLiked); + } } \ No newline at end of file