From dab066eb4d0336baba88db38073807c8bfa5a8c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9A=B0=EA=B0=80?= Date: Mon, 16 Oct 2023 13:56:28 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20feat:=20=EA=BF=80=EC=A1=B0=ED=95=A9=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=9E=91=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#747)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove: 북마크 관련 삭제 * feat: 꿀조합 댓글 작성 구현 * refactor: Comments 단방향으로 수정 * feat: 꿀조합 댓글 조회 기능 추가 * refactor: Specification private 기본생성자 추가 * refactor: 적용된 코드 SESSION ID 수정 * refactor: 생성자 정렬 수정 * refactor: 세션 쿠키 이름 SESSION 으로 수정 * refactor: 변수명 상세하게 specification 로 수정 * refactor: repeat 사용과 디버깅 출력 코드 삭제 * remove: 디버깅 출력 코드 삭제 * refactor: subList() 와 for each 사용으로 수정 * test: 꿀조합 댓글 관련 서비스 테스트 추가 * refactor: 응답 변수명 상세하게 수정 * refactor: toResponse 맞춰서 수정 * refactor: 메소드 순서에 맞게 수정 * refactor: 리뷰 반영 * refactor: 테스트 실패 수정 --- .../AdminProductSpecification.java | 3 + .../AdminReviewSpecification.java | 3 + .../com/funeat/comment/domain/Comment.java | 59 +++++ .../persistence/CommentRepository.java | 8 + .../specification/CommentSpecification.java | 56 +++++ .../domain/bookmark/ProductBookmark.java | 28 --- .../domain/bookmark/RecipeBookmark.java | 28 --- .../ProductBookmarkRepository.java | 7 - .../persistence/RecipeBookMarkRepository.java | 7 - .../com/funeat/product/domain/Product.java | 4 - .../recipe/application/RecipeService.java | 79 ++++++- .../recipe/dto/RecipeCommentCondition.java | 20 ++ .../dto/RecipeCommentCreateRequest.java | 20 ++ .../dto/RecipeCommentMemberResponse.java | 26 +++ .../recipe/dto/RecipeCommentResponse.java | 42 ++++ .../recipe/dto/RecipeCommentsResponse.java | 34 +++ .../presentation/RecipeApiController.java | 22 ++ .../recipe/presentation/RecipeController.java | 24 ++ .../acceptance/common/AcceptanceTest.java | 4 + .../recipe/RecipeAcceptanceTest.java | 215 +++++++++++++++++- .../funeat/acceptance/recipe/RecipeSteps.java | 28 +++ .../funeat/acceptance/review/ReviewSteps.java | 1 - .../com/funeat/common/RepositoryTest.java | 8 - .../java/com/funeat/common/ServiceTest.java | 12 +- .../recipe/application/RecipeServiceTest.java | 198 +++++++++++++++- .../persistence/RecipeRepositoryTest.java | 4 +- .../review/application/ReviewServiceTest.java | 4 +- 27 files changed, 844 insertions(+), 100 deletions(-) create mode 100644 backend/src/main/java/com/funeat/comment/domain/Comment.java create mode 100644 backend/src/main/java/com/funeat/comment/persistence/CommentRepository.java create mode 100644 backend/src/main/java/com/funeat/comment/specification/CommentSpecification.java delete mode 100644 backend/src/main/java/com/funeat/member/domain/bookmark/ProductBookmark.java delete mode 100644 backend/src/main/java/com/funeat/member/domain/bookmark/RecipeBookmark.java delete mode 100644 backend/src/main/java/com/funeat/member/persistence/ProductBookmarkRepository.java delete mode 100644 backend/src/main/java/com/funeat/member/persistence/RecipeBookMarkRepository.java create mode 100644 backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCondition.java create mode 100644 backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCreateRequest.java create mode 100644 backend/src/main/java/com/funeat/recipe/dto/RecipeCommentMemberResponse.java create mode 100644 backend/src/main/java/com/funeat/recipe/dto/RecipeCommentResponse.java create mode 100644 backend/src/main/java/com/funeat/recipe/dto/RecipeCommentsResponse.java diff --git a/backend/src/main/java/com/funeat/admin/specification/AdminProductSpecification.java b/backend/src/main/java/com/funeat/admin/specification/AdminProductSpecification.java index a8e63b748..c48ea0305 100644 --- a/backend/src/main/java/com/funeat/admin/specification/AdminProductSpecification.java +++ b/backend/src/main/java/com/funeat/admin/specification/AdminProductSpecification.java @@ -13,6 +13,9 @@ public class AdminProductSpecification { private static final List> COUNT_RESULT_TYPES = List.of(Long.class, long.class); + private AdminProductSpecification() { + } + public static Specification searchBy(final ProductSearchCondition condition) { return (root, query, criteriaBuilder) -> { if (!COUNT_RESULT_TYPES.contains(query.getResultType())) { diff --git a/backend/src/main/java/com/funeat/admin/specification/AdminReviewSpecification.java b/backend/src/main/java/com/funeat/admin/specification/AdminReviewSpecification.java index b7c345f14..045147de5 100644 --- a/backend/src/main/java/com/funeat/admin/specification/AdminReviewSpecification.java +++ b/backend/src/main/java/com/funeat/admin/specification/AdminReviewSpecification.java @@ -11,6 +11,9 @@ public class AdminReviewSpecification { + private AdminReviewSpecification() { + } + public static Specification searchBy(final ReviewSearchCondition condition) { return (root, query, criteriaBuilder) -> { if (query.getResultType() != Long.class && query.getResultType() != long.class) { diff --git a/backend/src/main/java/com/funeat/comment/domain/Comment.java b/backend/src/main/java/com/funeat/comment/domain/Comment.java new file mode 100644 index 000000000..4e6798b9d --- /dev/null +++ b/backend/src/main/java/com/funeat/comment/domain/Comment.java @@ -0,0 +1,59 @@ +package com.funeat.comment.domain; + +import com.funeat.member.domain.Member; +import com.funeat.recipe.domain.Recipe; +import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +@Entity +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String comment; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipe_id") + private Recipe recipe; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Column(nullable = false) + private LocalDateTime createdAt = LocalDateTime.now(); + + protected Comment() { + } + + public Comment(final Recipe recipe, final Member member, final String comment) { + this.recipe = recipe; + this.member = member; + this.comment = comment; + } + + public Long getId() { + return id; + } + + public String getComment() { + return comment; + } + + public Member getMember() { + return member; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } +} diff --git a/backend/src/main/java/com/funeat/comment/persistence/CommentRepository.java b/backend/src/main/java/com/funeat/comment/persistence/CommentRepository.java new file mode 100644 index 000000000..e40a47f67 --- /dev/null +++ b/backend/src/main/java/com/funeat/comment/persistence/CommentRepository.java @@ -0,0 +1,8 @@ +package com.funeat.comment.persistence; + +import com.funeat.comment.domain.Comment; +import com.funeat.common.repository.BaseRepository; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository, BaseRepository { +} diff --git a/backend/src/main/java/com/funeat/comment/specification/CommentSpecification.java b/backend/src/main/java/com/funeat/comment/specification/CommentSpecification.java new file mode 100644 index 000000000..db6c734bb --- /dev/null +++ b/backend/src/main/java/com/funeat/comment/specification/CommentSpecification.java @@ -0,0 +1,56 @@ +package com.funeat.comment.specification; + +import com.funeat.comment.domain.Comment; +import com.funeat.recipe.domain.Recipe; +import java.util.List; +import java.util.Objects; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.Path; +import org.springframework.data.jpa.domain.Specification; + +public class CommentSpecification { + + private CommentSpecification() { + } + + private static final List> COUNT_RESULT_TYPES = List.of(Long.class, long.class); + + public static Specification findAllByRecipe(final Recipe recipe, final Long lastCommentId) { + return (root, query, criteriaBuilder) -> { + if (!COUNT_RESULT_TYPES.contains(query.getResultType())) { + root.fetch("member", JoinType.LEFT); + } + + criteriaBuilder.desc(root.get("id")); + + return Specification + .where(lessThan(lastCommentId)) + .and(equalToRecipe(recipe)) + .toPredicate(root, query, criteriaBuilder); + }; + } + + private static Specification lessThan(final Long commentId) { + return (root, query, criteriaBuilder) -> { + if (Objects.isNull(commentId)) { + return null; + } + + final Path commentIdPath = root.get("id"); + + return criteriaBuilder.lessThan(commentIdPath, commentId); + }; + } + + private static Specification equalToRecipe(final Recipe recipe) { + return (root, query, criteriaBuilder) -> { + if (Objects.isNull(recipe)) { + return null; + } + + final Path recipePath = root.get("recipe"); + + return criteriaBuilder.equal(recipePath, recipe); + }; + } +} diff --git a/backend/src/main/java/com/funeat/member/domain/bookmark/ProductBookmark.java b/backend/src/main/java/com/funeat/member/domain/bookmark/ProductBookmark.java deleted file mode 100644 index c18c84b59..000000000 --- a/backend/src/main/java/com/funeat/member/domain/bookmark/ProductBookmark.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.funeat.member.domain.bookmark; - -import com.funeat.member.domain.Member; -import com.funeat.product.domain.Product; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; - -@Entity -public class ProductBookmark { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "member_id") - private Member member; - - @ManyToOne - @JoinColumn(name = "product_id") - private Product product; - - private Boolean checked; -} diff --git a/backend/src/main/java/com/funeat/member/domain/bookmark/RecipeBookmark.java b/backend/src/main/java/com/funeat/member/domain/bookmark/RecipeBookmark.java deleted file mode 100644 index 9dc0b75ad..000000000 --- a/backend/src/main/java/com/funeat/member/domain/bookmark/RecipeBookmark.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.funeat.member.domain.bookmark; - -import com.funeat.member.domain.Member; -import com.funeat.recipe.domain.Recipe; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; - -@Entity -public class RecipeBookmark { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "member_id") - private Member member; - - @ManyToOne - @JoinColumn(name = "recipe_id") - private Recipe recipe; - - private Boolean checked; -} diff --git a/backend/src/main/java/com/funeat/member/persistence/ProductBookmarkRepository.java b/backend/src/main/java/com/funeat/member/persistence/ProductBookmarkRepository.java deleted file mode 100644 index c7651b592..000000000 --- a/backend/src/main/java/com/funeat/member/persistence/ProductBookmarkRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.funeat.member.persistence; - -import com.funeat.member.domain.bookmark.ProductBookmark; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ProductBookmarkRepository extends JpaRepository { -} diff --git a/backend/src/main/java/com/funeat/member/persistence/RecipeBookMarkRepository.java b/backend/src/main/java/com/funeat/member/persistence/RecipeBookMarkRepository.java deleted file mode 100644 index 4ed5cce46..000000000 --- a/backend/src/main/java/com/funeat/member/persistence/RecipeBookMarkRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.funeat.member.persistence; - -import com.funeat.member.domain.bookmark.RecipeBookmark; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface RecipeBookMarkRepository extends JpaRepository { -} diff --git a/backend/src/main/java/com/funeat/product/domain/Product.java b/backend/src/main/java/com/funeat/product/domain/Product.java index a485eaf55..512f77f8f 100644 --- a/backend/src/main/java/com/funeat/product/domain/Product.java +++ b/backend/src/main/java/com/funeat/product/domain/Product.java @@ -1,6 +1,5 @@ package com.funeat.product.domain; -import com.funeat.member.domain.bookmark.ProductBookmark; import com.funeat.review.domain.Review; import java.util.List; import javax.persistence.Entity; @@ -39,9 +38,6 @@ public class Product { @OneToMany(mappedBy = "product") private List productRecipes; - @OneToMany(mappedBy = "product") - private List productBookmarks; - private Long reviewCount = 0L; protected Product() { diff --git a/backend/src/main/java/com/funeat/recipe/application/RecipeService.java b/backend/src/main/java/com/funeat/recipe/application/RecipeService.java index d67fd7b79..d9ede67c3 100644 --- a/backend/src/main/java/com/funeat/recipe/application/RecipeService.java +++ b/backend/src/main/java/com/funeat/recipe/application/RecipeService.java @@ -5,6 +5,9 @@ import static com.funeat.product.exception.ProductErrorCode.PRODUCT_NOT_FOUND; import static com.funeat.recipe.exception.RecipeErrorCode.RECIPE_NOT_FOUND; +import com.funeat.comment.domain.Comment; +import com.funeat.comment.persistence.CommentRepository; +import com.funeat.comment.specification.CommentSpecification; import com.funeat.common.ImageUploader; import com.funeat.common.dto.PageDto; import com.funeat.member.domain.Member; @@ -26,6 +29,10 @@ import com.funeat.recipe.dto.RankingRecipeDto; import com.funeat.recipe.dto.RankingRecipesResponse; import com.funeat.recipe.dto.RecipeAuthorDto; +import com.funeat.recipe.dto.RecipeCommentCondition; +import com.funeat.recipe.dto.RecipeCommentCreateRequest; +import com.funeat.recipe.dto.RecipeCommentResponse; +import com.funeat.recipe.dto.RecipeCommentsResponse; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeDetailResponse; import com.funeat.recipe.dto.RecipeDto; @@ -36,6 +43,7 @@ import com.funeat.recipe.exception.RecipeException.RecipeNotFoundException; import com.funeat.recipe.persistence.RecipeImageRepository; import com.funeat.recipe.persistence.RecipeRepository; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -43,6 +51,8 @@ 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.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -53,6 +63,8 @@ public class RecipeService { private static final int THREE = 3; private static final int TOP = 0; + private static final int RECIPE_COMMENT_PAGE_SIZE = 10; + private static final int DEFAULT_CURSOR_PAGINATION_SIZE = 11; private final MemberRepository memberRepository; private final ProductRepository productRepository; @@ -60,18 +72,21 @@ public class RecipeService { private final RecipeRepository recipeRepository; private final RecipeImageRepository recipeImageRepository; private final RecipeFavoriteRepository recipeFavoriteRepository; + private final CommentRepository commentRepository; private final ImageUploader imageUploader; public RecipeService(final MemberRepository memberRepository, final ProductRepository productRepository, final ProductRecipeRepository productRecipeRepository, final RecipeRepository recipeRepository, final RecipeImageRepository recipeImageRepository, - final RecipeFavoriteRepository recipeFavoriteRepository, final ImageUploader imageUploader) { + final RecipeFavoriteRepository recipeFavoriteRepository, + final CommentRepository commentRepository, final ImageUploader imageUploader) { this.memberRepository = memberRepository; this.productRepository = productRepository; this.productRecipeRepository = productRecipeRepository; this.recipeRepository = recipeRepository; this.recipeImageRepository = recipeImageRepository; this.recipeFavoriteRepository = recipeFavoriteRepository; + this.commentRepository = commentRepository; this.imageUploader = imageUploader; } @@ -166,7 +181,8 @@ public void likeRecipe(final Long memberId, final Long recipeId, final RecipeFav recipeFavorite.updateFavorite(request.getFavorite()); } - private RecipeFavorite createAndSaveRecipeFavorite(final Member member, final Recipe recipe, final Boolean favorite) { + private RecipeFavorite createAndSaveRecipeFavorite(final Member member, final Recipe recipe, + final Boolean favorite) { try { final RecipeFavorite recipeFavorite = RecipeFavorite.create(member, recipe, favorite); return recipeFavoriteRepository.save(recipeFavorite); @@ -201,4 +217,63 @@ public RankingRecipesResponse getTop3Recipes() { .collect(Collectors.toList()); return RankingRecipesResponse.toResponse(dtos); } + + @Transactional + public Long writeCommentOfRecipe(final Long memberId, final Long recipeId, + final RecipeCommentCreateRequest request) { + final Member findMember = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId)); + + final Recipe findRecipe = recipeRepository.findById(recipeId) + .orElseThrow(() -> new RecipeNotFoundException(RECIPE_NOT_FOUND, recipeId)); + + final Comment comment = new Comment(findRecipe, findMember, request.getComment()); + + final Comment savedComment = commentRepository.save(comment); + return savedComment.getId(); + } + + public RecipeCommentsResponse getCommentsOfRecipe(final Long recipeId, final RecipeCommentCondition condition) { + final Recipe findRecipe = recipeRepository.findById(recipeId) + .orElseThrow(() -> new RecipeNotFoundException(RECIPE_NOT_FOUND, recipeId)); + + final Specification specification = CommentSpecification.findAllByRecipe(findRecipe, + condition.getLastId()); + + final PageRequest pageable = PageRequest.of(0, DEFAULT_CURSOR_PAGINATION_SIZE, Sort.by("id").descending()); + + final Page commentPaginationResult = commentRepository.findAllForPagination(specification, pageable, + condition.getTotalElements()); + + final List recipeCommentResponses = getRecipeCommentResponses( + commentPaginationResult.getContent()); + + final Boolean hasNext = hasNextPage(commentPaginationResult); + + return RecipeCommentsResponse.toResponse(recipeCommentResponses, hasNext, + commentPaginationResult.getTotalElements()); + } + + private List getRecipeCommentResponses(final List findComments) { + final List recipeCommentResponses = new ArrayList<>(); + final int resultSize = getResultSize(findComments); + final List comments = findComments.subList(0, resultSize); + + for (final Comment comment : comments) { + final RecipeCommentResponse recipeCommentResponse = RecipeCommentResponse.toResponse(comment); + recipeCommentResponses.add(recipeCommentResponse); + } + return recipeCommentResponses; + } + + private int getResultSize(final List findComments) { + if (findComments.size() < DEFAULT_CURSOR_PAGINATION_SIZE) { + return findComments.size(); + } + return RECIPE_COMMENT_PAGE_SIZE; + } + + private Boolean hasNextPage(final Page findComments) { + return findComments.getContent().size() > RECIPE_COMMENT_PAGE_SIZE; + } } diff --git a/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCondition.java b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCondition.java new file mode 100644 index 000000000..dcb3cf2d1 --- /dev/null +++ b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCondition.java @@ -0,0 +1,20 @@ +package com.funeat.recipe.dto; + +public class RecipeCommentCondition { + + private final Long lastId; + private final Long totalElements; + + public RecipeCommentCondition(final Long lastId, final Long totalElements) { + this.lastId = lastId; + this.totalElements = totalElements; + } + + public Long getLastId() { + return lastId; + } + + public Long getTotalElements() { + return totalElements; + } +} diff --git a/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCreateRequest.java b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCreateRequest.java new file mode 100644 index 000000000..2b24e9207 --- /dev/null +++ b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCreateRequest.java @@ -0,0 +1,20 @@ +package com.funeat.recipe.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +public class RecipeCommentCreateRequest { + + @NotBlank(message = "꿀조합 댓글을 확인해 주세요") + @Size(max = 200, message = "꿀조합 댓글은 최대 200자까지 입력 가능합니다") + private final String comment; + + public RecipeCommentCreateRequest(@JsonProperty("comment") final String comment) { + this.comment = comment; + } + + public String getComment() { + return comment; + } +} diff --git a/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentMemberResponse.java b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentMemberResponse.java new file mode 100644 index 000000000..ad66d7811 --- /dev/null +++ b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentMemberResponse.java @@ -0,0 +1,26 @@ +package com.funeat.recipe.dto; + +import com.funeat.member.domain.Member; + +public class RecipeCommentMemberResponse { + + private final String nickname; + private final String profileImage; + + private RecipeCommentMemberResponse(final String nickname, final String profileImage) { + this.nickname = nickname; + this.profileImage = profileImage; + } + + public static RecipeCommentMemberResponse toResponse(final Member member) { + return new RecipeCommentMemberResponse(member.getNickname(), member.getProfileImage()); + } + + public String getNickname() { + return nickname; + } + + public String getProfileImage() { + return profileImage; + } +} diff --git a/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentResponse.java b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentResponse.java new file mode 100644 index 000000000..989e52bd5 --- /dev/null +++ b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentResponse.java @@ -0,0 +1,42 @@ +package com.funeat.recipe.dto; + +import com.funeat.comment.domain.Comment; +import java.time.LocalDateTime; + +public class RecipeCommentResponse { + + private final Long id; + private final String comment; + private final LocalDateTime createdAt; + private final RecipeCommentMemberResponse author; + + private RecipeCommentResponse(final Long id, final String comment, final LocalDateTime createdAt, + final RecipeCommentMemberResponse author) { + this.id = id; + this.comment = comment; + this.createdAt = createdAt; + this.author = author; + } + + public static RecipeCommentResponse toResponse(final Comment comment) { + final RecipeCommentMemberResponse author = RecipeCommentMemberResponse.toResponse(comment.getMember()); + + return new RecipeCommentResponse(comment.getId(), comment.getComment(), comment.getCreatedAt(), author); + } + + public Long getId() { + return id; + } + + public String getComment() { + return comment; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public RecipeCommentMemberResponse getAuthor() { + return author; + } +} diff --git a/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentsResponse.java b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentsResponse.java new file mode 100644 index 000000000..7e7d6dc19 --- /dev/null +++ b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentsResponse.java @@ -0,0 +1,34 @@ +package com.funeat.recipe.dto; + +import java.util.List; + +public class RecipeCommentsResponse { + + private final List comments; + private final boolean hasNext; + private final Long totalElements; + + private RecipeCommentsResponse(final List comments, final boolean hasNext, + final Long totalElements) { + this.comments = comments; + this.hasNext = hasNext; + this.totalElements = totalElements; + } + + public static RecipeCommentsResponse toResponse(final List comments, final boolean hasNext, + final Long totalElements) { + return new RecipeCommentsResponse(comments, hasNext, totalElements); + } + + public List getComments() { + return comments; + } + + public boolean getHasNext() { + return hasNext; + } + + public Long getTotalElements() { + return totalElements; + } +} diff --git a/backend/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java b/backend/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java index 8406c1645..17eb1f1d6 100644 --- a/backend/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java +++ b/backend/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java @@ -5,6 +5,9 @@ import com.funeat.common.logging.Logging; import com.funeat.recipe.application.RecipeService; import com.funeat.recipe.dto.RankingRecipesResponse; +import com.funeat.recipe.dto.RecipeCommentCondition; +import com.funeat.recipe.dto.RecipeCommentCreateRequest; +import com.funeat.recipe.dto.RecipeCommentsResponse; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeDetailResponse; import com.funeat.recipe.dto.RecipeFavoriteRequest; @@ -19,6 +22,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -88,4 +92,22 @@ public ResponseEntity getSearchResults(@RequestPara return ResponseEntity.ok(response); } + + @PostMapping("/api/recipes/{recipeId}/comments") + public ResponseEntity writeComment(@AuthenticationPrincipal final LoginInfo loginInfo, + @PathVariable final Long recipeId, + @RequestBody @Valid final RecipeCommentCreateRequest request) { + final Long savedCommentId = recipeService.writeCommentOfRecipe(loginInfo.getId(), recipeId, request); + + return ResponseEntity.created(URI.create("/api/recipes/" + recipeId + "/" + savedCommentId)).build(); + } + + @GetMapping("/api/recipes/{recipeId}/comments") + public ResponseEntity getCommentsOfRecipe( + @AuthenticationPrincipal final LoginInfo loginInfo, @PathVariable final Long recipeId, + @ModelAttribute final RecipeCommentCondition condition) { + final RecipeCommentsResponse response = recipeService.getCommentsOfRecipe(recipeId, condition); + + return ResponseEntity.ok(response); + } } diff --git a/backend/src/main/java/com/funeat/recipe/presentation/RecipeController.java b/backend/src/main/java/com/funeat/recipe/presentation/RecipeController.java index 013c559cd..05602cc7f 100644 --- a/backend/src/main/java/com/funeat/recipe/presentation/RecipeController.java +++ b/backend/src/main/java/com/funeat/recipe/presentation/RecipeController.java @@ -3,6 +3,9 @@ import com.funeat.auth.dto.LoginInfo; import com.funeat.auth.util.AuthenticationPrincipal; import com.funeat.recipe.dto.RankingRecipesResponse; +import com.funeat.recipe.dto.RecipeCommentCondition; +import com.funeat.recipe.dto.RecipeCommentCreateRequest; +import com.funeat.recipe.dto.RecipeCommentsResponse; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeDetailResponse; import com.funeat.recipe.dto.RecipeFavoriteRequest; @@ -16,6 +19,7 @@ import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -80,4 +84,24 @@ ResponseEntity likeRecipe(@AuthenticationPrincipal final LoginInfo loginIn @GetMapping ResponseEntity getSearchResults(@RequestParam final String query, @PageableDefault final Pageable pageable); + + @Operation(summary = "꿀조합 댓글 작성", description = "꿀조합 상세에서 댓글을 작성한다.") + @ApiResponse( + responseCode = "201", + description = "꿀조합 댓글 작성 성공." + ) + @PostMapping("/api/recipes/{recipeId}/comments") + ResponseEntity writeComment(@AuthenticationPrincipal final LoginInfo loginInfo, + @PathVariable final Long recipeId, + @RequestBody final RecipeCommentCreateRequest request); + + @Operation(summary = "꿀조합 댓글 조회", description = "꿀조합 상세에서 댓글을 조회한다.") + @ApiResponse( + responseCode = "200", + description = "꿀조합 댓글 조회 성공." + ) + @GetMapping("/api/recipes/{recipeId}/comments") + ResponseEntity getCommentsOfRecipe(@AuthenticationPrincipal final LoginInfo loginInfo, + @PathVariable final Long recipeId, + @ModelAttribute final RecipeCommentCondition condition); } diff --git a/backend/src/test/java/com/funeat/acceptance/common/AcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/common/AcceptanceTest.java index 17c6d725b..128490663 100644 --- a/backend/src/test/java/com/funeat/acceptance/common/AcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/common/AcceptanceTest.java @@ -1,5 +1,6 @@ package com.funeat.acceptance.common; +import com.funeat.comment.persistence.CommentRepository; import com.funeat.common.DataClearExtension; import com.funeat.member.domain.Member; import com.funeat.member.persistence.MemberRepository; @@ -68,6 +69,9 @@ public abstract class AcceptanceTest { @Autowired public RecipeFavoriteRepository recipeFavoriteRepository; + @Autowired + protected CommentRepository commentRepository; + @BeforeEach void setUp() { RestAssured.port = port; diff --git a/backend/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java index af0b74801..bb388ea78 100644 --- a/backend/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java @@ -11,6 +11,8 @@ import static com.funeat.acceptance.common.CommonSteps.찾을수_없음; import static com.funeat.acceptance.common.CommonSteps.페이지를_검증한다; import static com.funeat.acceptance.recipe.RecipeSteps.레시피_검색_결과_조회_요청; +import static com.funeat.acceptance.recipe.RecipeSteps.레시피_댓글_작성_요청; +import static com.funeat.acceptance.recipe.RecipeSteps.레시피_댓글_조회_요청; import static com.funeat.acceptance.recipe.RecipeSteps.레시피_랭킹_조회_요청; import static com.funeat.acceptance.recipe.RecipeSteps.레시피_목록_요청; import static com.funeat.acceptance.recipe.RecipeSteps.레시피_상세_정보_요청; @@ -59,12 +61,14 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import com.funeat.acceptance.common.AcceptanceTest; -import com.funeat.common.dto.PageDto; import com.funeat.member.domain.Member; import com.funeat.recipe.domain.Recipe; import com.funeat.recipe.dto.ProductRecipeDto; import com.funeat.recipe.dto.RankingRecipeDto; import com.funeat.recipe.dto.RecipeAuthorDto; +import com.funeat.recipe.dto.RecipeCommentCondition; +import com.funeat.recipe.dto.RecipeCommentCreateRequest; +import com.funeat.recipe.dto.RecipeCommentResponse; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeDetailResponse; import com.funeat.recipe.dto.RecipeDto; @@ -532,6 +536,189 @@ class getRankingRecipes_성공_테스트 { } } + @Nested + class writeRecipeComment_성공_테스트 { + + @Test + void 꿀조합에_댓글을_작성할_수_있다() { + // given + final var 카테고리 = 카테고리_간편식사_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점5점_생성(카테고리)); + final var 꿀조합_작성_응답 = 레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품)); + + // when + final var 작성된_꿀조합_아이디 = 작성된_꿀조합_아이디_추출(꿀조합_작성_응답); + final var 댓글작성자_로그인_쿠키_획득 = 로그인_쿠키_획득(멤버2); + final var 꿀조합_댓글 = new RecipeCommentCreateRequest("테스트 코멘트 1"); + + final var 응답 = 레시피_댓글_작성_요청(댓글작성자_로그인_쿠키_획득, 작성된_꿀조합_아이디, 꿀조합_댓글); + + // then + STATUS_CODE를_검증한다(응답, 정상_생성); + 꿀조합_댓글_작성_결과를_검증한다(응답, 멤버2, 꿀조합_댓글); + } + } + + @Nested + class writeRecipeComment_실패_테스트 { + + @ParameterizedTest + @NullAndEmptySource + void 꿀조합에_댓글을_작성할때_댓글이_비어있을시_예외가_발생한다(final String comment) { + // given + final var 카테고리 = 카테고리_간편식사_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점5점_생성(카테고리)); + final var 꿀조합_작성_응답 = 레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품)); + + // when + final var 작성된_꿀조합_아이디 = 작성된_꿀조합_아이디_추출(꿀조합_작성_응답); + final var 댓글작성자_로그인_쿠키_획득 = 로그인_쿠키_획득(멤버2); + final var 꿀조합_댓글 = new RecipeCommentCreateRequest(comment); + + final var 레시피_댓글_작성_요청 = 레시피_댓글_작성_요청(댓글작성자_로그인_쿠키_획득, 작성된_꿀조합_아이디, 꿀조합_댓글); + + // then + STATUS_CODE를_검증한다(레시피_댓글_작성_요청, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(레시피_댓글_작성_요청, REQUEST_VALID_ERROR_CODE.getCode(), + "꿀조합 댓글을 확인해 주세요. " + REQUEST_VALID_ERROR_CODE.getMessage()); + } + + @Test + void 꿀조합에_댓글을_작성할때_댓글이_200자_초과시_예외가_발생한다() { + // given + final var 카테고리 = 카테고리_간편식사_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점5점_생성(카테고리)); + final var 꿀조합_작성_응답 = 레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품)); + + // when + final var 작성된_꿀조합_아이디 = 작성된_꿀조합_아이디_추출(꿀조합_작성_응답); + final var 댓글작성자_로그인_쿠키_획득 = 로그인_쿠키_획득(멤버2); + final var 꿀조합_댓글 = new RecipeCommentCreateRequest("1" + "댓글입니다".repeat(40)); + + final var 응답 = 레시피_댓글_작성_요청(댓글작성자_로그인_쿠키_획득, 작성된_꿀조합_아이디, 꿀조합_댓글); + + // then + STATUS_CODE를_검증한다(응답, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(응답, REQUEST_VALID_ERROR_CODE.getCode(), + "꿀조합 댓글은 최대 200자까지 입력 가능합니다. " + REQUEST_VALID_ERROR_CODE.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void 로그인_하지않은_사용자가_꿀조합_댓글_작성시_예외가_발생한다(final String cookie) { + // given + final var 카테고리 = 카테고리_간편식사_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점5점_생성(카테고리)); + final var 꿀조합_작성_응답 = 레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품)); + + // when + final var 작성된_꿀조합_아이디 = 작성된_꿀조합_아이디_추출(꿀조합_작성_응답); + final var 꿀조합_댓글 = new RecipeCommentCreateRequest("테스트 코멘트 1"); + + final var 응답 = 레시피_댓글_작성_요청(cookie, 작성된_꿀조합_아이디, 꿀조합_댓글); + + // then + STATUS_CODE를_검증한다(응답, 인증되지_않음); + RESPONSE_CODE와_MESSAGE를_검증한다(응답, LOGIN_MEMBER_NOT_FOUND.getCode(), + LOGIN_MEMBER_NOT_FOUND.getMessage()); + } + } + + @Nested + class getRecipeComment_성공_테스트 { + + @Test + void 꿀조합에_댓글을_조회할_수_있다() { + // given + final var 카테고리 = 카테고리_간편식사_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점5점_생성(카테고리)); + final var 꿀조합_작성_응답 = 레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품)); + + final var 작성된_꿀조합_아이디 = 작성된_꿀조합_아이디_추출(꿀조합_작성_응답); + final var 댓글작성자_로그인_쿠키_획득 = 로그인_쿠키_획득(멤버2); + + for (int i = 1; i <= 15; i++) { + 레시피_댓글_작성_요청(댓글작성자_로그인_쿠키_획득, 작성된_꿀조합_아이디, + new RecipeCommentCreateRequest("테스트 코멘트" + i)); + } + + // when + final var 응답 = 레시피_댓글_조회_요청(로그인_쿠키_획득(멤버1), 작성된_꿀조합_아이디, + new RecipeCommentCondition(null, null)); + + // then + final var expectedSize = 10; + final var expectedHasNext = true; + + STATUS_CODE를_검증한다(응답, 정상_처리); + 레시피_댓글_조회_결과를_검증한다(응답, expectedSize, expectedHasNext); + } + + @Test + void 꿀조합에_댓글을_마지막_페이지를_조회할_수_있다() { + // given + final var 카테고리 = 카테고리_간편식사_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점5점_생성(카테고리)); + final var 꿀조합_작성_응답 = 레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품)); + + final var 작성된_꿀조합_아이디 = 작성된_꿀조합_아이디_추출(꿀조합_작성_응답); + final var 댓글작성자_로그인_쿠키_획득 = 로그인_쿠키_획득(멤버2); + + final var totalElements = 15L; + final var lastId = 6L; + + for (int i = 1; i <= totalElements; i++) { + 레시피_댓글_작성_요청(댓글작성자_로그인_쿠키_획득, 작성된_꿀조합_아이디, + new RecipeCommentCreateRequest("테스트 코멘트" + i)); + } + + // when + final var 응답 = 레시피_댓글_조회_요청(로그인_쿠키_획득(멤버1), 작성된_꿀조합_아이디, new RecipeCommentCondition(lastId, totalElements)); + + // then + final var expectedSize = 5; + final var expectedHasNext = false; + + STATUS_CODE를_검증한다(응답, 정상_처리); + 레시피_댓글_조회_결과를_검증한다(응답, expectedSize, expectedHasNext); + } + } + + @Nested + class getRecipeComment_실패_테스트 { + + @ParameterizedTest + @NullAndEmptySource + void 로그인_하지않은_사용자가_꿀조합_댓글_조회시_예외가_발생한다(final String cookie) { + // given + final var 카테고리 = 카테고리_간편식사_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점5점_생성(카테고리)); + final var 꿀조합_작성_응답 = 레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품)); + + final var 작성된_꿀조합_아이디 = 작성된_꿀조합_아이디_추출(꿀조합_작성_응답); + final var 댓글작성자_로그인_쿠키_획득 = 로그인_쿠키_획득(멤버2); + final var 꿀조합_댓글 = new RecipeCommentCreateRequest("테스트 코멘트"); + + 레시피_댓글_작성_요청(댓글작성자_로그인_쿠키_획득, 작성된_꿀조합_아이디, 꿀조합_댓글); + + // when + final var 응답 = 레시피_댓글_조회_요청(cookie, 작성된_꿀조합_아이디, + new RecipeCommentCondition(6L, 15L)); + + // then + STATUS_CODE를_검증한다(응답, 인증되지_않음); + RESPONSE_CODE와_MESSAGE를_검증한다(응답, LOGIN_MEMBER_NOT_FOUND.getCode(), + LOGIN_MEMBER_NOT_FOUND.getMessage()); + } + } + private void 레시피_목록_조회_결과를_검증한다(final ExtractableResponse response, final List recipeIds) { final var actual = response.jsonPath().getList("recipes", RecipeDto.class); @@ -590,4 +777,30 @@ class getRankingRecipes_성공_테스트 { assertThat(actual).extracting(SearchRecipeResultDto::getId) .containsExactlyElementsOf(recipeIds); } + + private Long 작성된_꿀조합_아이디_추출(final ExtractableResponse response) { + return Long.parseLong(response.header("Location").split("/")[3]); + } + + private void 꿀조합_댓글_작성_결과를_검증한다(final ExtractableResponse response, final Long memberId, + final RecipeCommentCreateRequest request) { + final var savedCommentId = Long.parseLong(response.header("Location").split("/")[4]); + + final var findComments = commentRepository.findAll(); + + assertSoftly(soft -> { + soft.assertThat(savedCommentId).isEqualTo(findComments.get(0).getId()); + soft.assertThat(memberId).isEqualTo(findComments.get(0).getMember().getId()); + soft.assertThat(request.getComment()).isEqualTo(findComments.get(0).getComment()); + }); + } + + private void 레시피_댓글_조회_결과를_검증한다(final ExtractableResponse response, final int expectedSize, + final boolean expectedHasNext) { + final var actualComments = response.jsonPath().getList("comments", RecipeCommentResponse.class); + final var actualHasNext = response.jsonPath().getBoolean("hasNext"); + + assertThat(actualComments).hasSize(expectedSize); + assertThat(actualHasNext).isEqualTo(expectedHasNext); + } } diff --git a/backend/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java b/backend/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java index 4ff18dc2e..6ebc08ba5 100644 --- a/backend/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java @@ -4,6 +4,8 @@ import static com.funeat.fixture.RecipeFixture.레시피좋아요요청_생성; import static io.restassured.RestAssured.given; +import com.funeat.recipe.dto.RecipeCommentCondition; +import com.funeat.recipe.dto.RecipeCommentCreateRequest; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeFavoriteRequest; import io.restassured.response.ExtractableResponse; @@ -90,4 +92,30 @@ public class RecipeSteps { .then() .extract(); } + + public static ExtractableResponse 레시피_댓글_작성_요청(final String loginCookie, + final Long recipeId, + final RecipeCommentCreateRequest request) { + return given() + .cookie("JSESSIONID", loginCookie) + .contentType("application/json") + .body(request) + .when() + .post("/api/recipes/" + recipeId + "/comments") + .then() + .extract(); + } + + public static ExtractableResponse 레시피_댓글_조회_요청(final String loginCookie, final Long recipeId, + final RecipeCommentCondition condition) { + return given() + .cookie("JSESSIONID", loginCookie) + .contentType("application/json") + .param("lastId", condition.getLastId()) + .param("totalElements", condition.getTotalElements()) + .when() + .get("/api/recipes/" + recipeId + "/comments") + .then() + .extract(); + } } diff --git a/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java b/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java index 0d8ce8fd7..93dfd6292 100644 --- a/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java @@ -1,7 +1,6 @@ package com.funeat.acceptance.review; import static com.funeat.acceptance.auth.LoginSteps.로그인_쿠키_획득; -import static com.funeat.acceptance.common.CommonSteps.LOCATION_헤더에서_ID_추출; import static com.funeat.fixture.ReviewFixture.리뷰좋아요요청_생성; import static io.restassured.RestAssured.given; diff --git a/backend/src/test/java/com/funeat/common/RepositoryTest.java b/backend/src/test/java/com/funeat/common/RepositoryTest.java index b438ca41b..6fdb2307b 100644 --- a/backend/src/test/java/com/funeat/common/RepositoryTest.java +++ b/backend/src/test/java/com/funeat/common/RepositoryTest.java @@ -4,8 +4,6 @@ import com.funeat.member.domain.favorite.RecipeFavorite; import com.funeat.member.domain.favorite.ReviewFavorite; import com.funeat.member.persistence.MemberRepository; -import com.funeat.member.persistence.ProductBookmarkRepository; -import com.funeat.member.persistence.RecipeBookMarkRepository; import com.funeat.member.persistence.RecipeFavoriteRepository; import com.funeat.member.persistence.ReviewFavoriteRepository; import com.funeat.product.domain.Category; @@ -42,12 +40,6 @@ public abstract class RepositoryTest { @Autowired protected MemberRepository memberRepository; - @Autowired - protected ProductBookmarkRepository productBookmarkRepository; - - @Autowired - protected RecipeBookMarkRepository recipeBookMarkRepository; - @Autowired protected RecipeFavoriteRepository recipeFavoriteRepository; diff --git a/backend/src/test/java/com/funeat/common/ServiceTest.java b/backend/src/test/java/com/funeat/common/ServiceTest.java index f8b58e5a2..6612223aa 100644 --- a/backend/src/test/java/com/funeat/common/ServiceTest.java +++ b/backend/src/test/java/com/funeat/common/ServiceTest.java @@ -1,12 +1,11 @@ package com.funeat.common; import com.funeat.auth.application.AuthService; +import com.funeat.comment.persistence.CommentRepository; import com.funeat.member.application.TestMemberService; import com.funeat.member.domain.Member; import com.funeat.member.domain.favorite.ReviewFavorite; import com.funeat.member.persistence.MemberRepository; -import com.funeat.member.persistence.ProductBookmarkRepository; -import com.funeat.member.persistence.RecipeBookMarkRepository; import com.funeat.member.persistence.RecipeFavoriteRepository; import com.funeat.member.persistence.ReviewFavoriteRepository; import com.funeat.product.application.CategoryService; @@ -48,12 +47,6 @@ public abstract class ServiceTest { @Autowired protected MemberRepository memberRepository; - @Autowired - protected ProductBookmarkRepository productBookmarkRepository; - - @Autowired - protected RecipeBookMarkRepository recipeBookMarkRepository; - @Autowired protected RecipeFavoriteRepository recipeFavoriteRepository; @@ -84,6 +77,9 @@ public abstract class ServiceTest { @Autowired protected TagRepository tagRepository; + @Autowired + protected CommentRepository commentRepository; + @Autowired protected AuthService authService; diff --git a/backend/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java b/backend/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java index fcd4e2f40..3fecf28bb 100644 --- a/backend/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java +++ b/backend/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java @@ -24,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import com.funeat.comment.domain.Comment; import com.funeat.common.ServiceTest; import com.funeat.common.dto.PageDto; import com.funeat.member.domain.Member; @@ -35,17 +36,20 @@ import com.funeat.product.domain.CategoryType; import com.funeat.product.domain.Product; import com.funeat.product.exception.ProductException.ProductNotFoundException; +import com.funeat.recipe.dto.RecipeCommentCondition; +import com.funeat.recipe.dto.RecipeCommentCreateRequest; +import com.funeat.recipe.dto.RecipeCommentResponse; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeDetailResponse; import com.funeat.recipe.dto.RecipeDto; import com.funeat.recipe.exception.RecipeException.RecipeNotFoundException; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; @SuppressWarnings("NonAsciiCharacters") class RecipeServiceTest extends ServiceTest { @@ -317,7 +321,7 @@ class getSortingRecipes_성공_테스트 { } @Test - void 꿀조합을_최신순으로_정렬할_수_있다() { + void 꿀조합을_최신순으로_정렬할_수_있다() throws InterruptedException { // given final var member1 = 멤버_멤버1_생성(); final var member2 = 멤버_멤버2_생성(); @@ -333,7 +337,9 @@ class getSortingRecipes_성공_테스트 { 복수_상품_저장(product1, product2, product3); final var recipe1_1 = 레시피_생성(member1, 1L); + Thread.sleep(1000); final var recipe1_2 = 레시피_생성(member1, 3L); + Thread.sleep(1000); final var recipe1_3 = 레시피_생성(member1, 2L); 복수_꿀조합_저장(recipe1_1, recipe1_2, recipe1_3); @@ -545,6 +551,190 @@ class likeRecipe_실패_테스트 { } } + @Nested + class writeCommentOfRecipe_성공_테스트 { + + @Test + void 꿀조합에_댓글을_작성할_수_있다() { + // given + final var category = 카테고리_간편식사_생성(); + final var product1 = 상품_삼각김밥_가격1000원_평점2점_생성(category); + final var product2 = 상품_삼각김밥_가격3000원_평점4점_생성(category); + final var product3 = 상품_삼각김밥_가격2000원_평점3점_생성(category); + 복수_상품_저장(product1, product2, product3); + final var author = 멤버_멤버1_생성(); + 단일_멤버_저장(author); + final var authorId = author.getId(); + + final var images = 여러_이미지_생성(3); + + final var productIds = List.of(product1.getId(), product2.getId(), product3.getId()); + final var recipeCreateRequest = new RecipeCreateRequest("제일로 맛있는 레시피", productIds, + "우선 밥을 넣어요. 그리고 밥을 또 넣어요. 그리고 밥을 또 넣으면.. 끝!!"); + + final var savedMemberId = 단일_멤버_저장(멤버_멤버1_생성()); + final var savedRecipeId = recipeService.create(authorId, images, recipeCreateRequest); + + // when + final var request = new RecipeCommentCreateRequest("꿀조합 댓글이에요"); + final var savedCommentId = recipeService.writeCommentOfRecipe(savedMemberId, savedRecipeId, request); + + // then + final var result = commentRepository.findById(savedCommentId).get(); + final var savedRecipe = recipeRepository.findById(savedRecipeId).get(); + final var savedMember = memberRepository.findById(savedMemberId).get(); + + assertThat(result).usingRecursiveComparison() + .ignoringFields("id", "createdAt") + .isEqualTo(new Comment(savedRecipe, savedMember, request.getComment())); + } + } + + @Nested + class writeCommentOfRecipe_실패_테스트 { + + @Test + void 존재하지_않은_멤버가_꿀조합에_댓글을_작성하면_예외가_발생한다() { + // given + final var category = 카테고리_간편식사_생성(); + final var product1 = 상품_삼각김밥_가격1000원_평점2점_생성(category); + final var product2 = 상품_삼각김밥_가격3000원_평점4점_생성(category); + final var product3 = 상품_삼각김밥_가격2000원_평점3점_생성(category); + 복수_상품_저장(product1, product2, product3); + final var author = new Member("author", "image.png", "1"); + 단일_멤버_저장(author); + final var authorId = author.getId(); + + final var images = 여러_이미지_생성(3); + + final var productIds = List.of(product1.getId(), product2.getId(), product3.getId()); + final var recipeCreateRequest = new RecipeCreateRequest("제일로 맛있는 레시피", productIds, + "우선 밥을 넣어요. 그리고 밥을 또 넣어요. 그리고 밥을 또 넣으면.. 끝!!"); + + final var notExistMemberId = 999999999L; + final var savedRecipeId = recipeService.create(authorId, images, recipeCreateRequest); + final var request = new RecipeCommentCreateRequest("꿀조합 댓글이에요"); + + // when then + assertThatThrownBy(() -> recipeService.writeCommentOfRecipe(notExistMemberId, savedRecipeId, request)) + .isInstanceOf(MemberNotFoundException.class); + } + + @Test + void 존재하지_않은_꿀조합에_댓글을_작성하면_예외가_발생한다() { + // given + final var memberId = 단일_멤버_저장(멤버_멤버1_생성()); + final var request = new RecipeCommentCreateRequest("꿀조합 댓글이에요"); + final var notExistRecipeId = 999999999L; + + // when then + assertThatThrownBy(() -> recipeService.writeCommentOfRecipe(memberId, notExistRecipeId, request)) + .isInstanceOf(RecipeNotFoundException.class); + } + } + + @Nested + class getCommentsOfRecipe_성공_테스트 { + + @Test + void 꿀조합에_달린_댓글들을_커서페이징을_통해_조회할_수_있다_총_댓글_15개_중_첫페이지_댓글_10개조회() { + // given + final var category = 카테고리_간편식사_생성(); + 단일_카테고리_저장(category); + final var product1 = 상품_삼각김밥_가격1000원_평점2점_생성(category); + final var product2 = 상품_삼각김밥_가격3000원_평점4점_생성(category); + final var product3 = 상품_삼각김밥_가격2000원_평점3점_생성(category); + 복수_상품_저장(product1, product2, product3); + final var author = 멤버_멤버1_생성(); + 단일_멤버_저장(author); + final var authorId = author.getId(); + + final var images = 여러_이미지_생성(3); + + final var productIds = List.of(product1.getId(), product2.getId(), product3.getId()); + final var recipeCreateRequest = new RecipeCreateRequest("제일로 맛있는 레시피", productIds, + "우선 밥을 넣어요. 그리고 밥을 또 넣어요. 그리고 밥을 또 넣으면.. 끝!!"); + + final var savedMemberId = 단일_멤버_저장(멤버_멤버1_생성()); + final var savedRecipeId = recipeService.create(authorId, images, recipeCreateRequest); + + for (int i = 1; i <= 15; i++) { + final var request = new RecipeCommentCreateRequest("꿀조합 댓글이에요" + i); + recipeService.writeCommentOfRecipe(savedMemberId, savedRecipeId, request); + } + + // when + final var result = recipeService.getCommentsOfRecipe(savedRecipeId, + new RecipeCommentCondition(null, null)); + + // + final var savedRecipe = recipeRepository.findById(savedRecipeId).get(); + final var savedMember = memberRepository.findById(savedMemberId).get(); + + final var expectedCommentResponses = new ArrayList<>(); + for (int i = 0; i < result.getComments().size(); i++) { + expectedCommentResponses.add(RecipeCommentResponse.toResponse( + new Comment(savedRecipe, savedMember, "꿀조합 댓글이에요" + (15 - i)))); + } + + assertThat(result.getHasNext()).isTrue(); + assertThat(result.getTotalElements()).isEqualTo(15); + assertThat(result.getComments()).hasSize(10); + assertThat(result.getComments()).usingRecursiveComparison() + .ignoringFields("id", "createdAt") + .isEqualTo(expectedCommentResponses); + } + + @Test + void 꿀조합에_달린_댓글들을_커서페이징을_통해_조회할_수_있다_총_댓글_15개_중_마지막페이지_댓글_5개조회() { + // given + final var category = 카테고리_간편식사_생성(); + 단일_카테고리_저장(category); + final var product1 = 상품_삼각김밥_가격1000원_평점2점_생성(category); + final var product2 = 상품_삼각김밥_가격3000원_평점4점_생성(category); + final var product3 = 상품_삼각김밥_가격2000원_평점3점_생성(category); + 복수_상품_저장(product1, product2, product3); + final var author = 멤버_멤버1_생성(); + 단일_멤버_저장(author); + final var authorId = author.getId(); + + final var images = 여러_이미지_생성(3); + + final var productIds = List.of(product1.getId(), product2.getId(), product3.getId()); + final var recipeCreateRequest = new RecipeCreateRequest("제일로 맛있는 레시피", productIds, + "우선 밥을 넣어요. 그리고 밥을 또 넣어요. 그리고 밥을 또 넣으면.. 끝!!"); + + final var savedMemberId = 단일_멤버_저장(멤버_멤버1_생성()); + final var savedRecipeId = recipeService.create(authorId, images, recipeCreateRequest); + + for (int i = 1; i <= 15; i++) { + final var request = new RecipeCommentCreateRequest("꿀조합 댓글이에요" + i); + recipeService.writeCommentOfRecipe(savedMemberId, savedRecipeId, request); + } + + // when + final var result = recipeService.getCommentsOfRecipe(savedRecipeId, + new RecipeCommentCondition(6L, 15L)); + + // + final var savedRecipe = recipeRepository.findById(savedRecipeId).get(); + final var savedMember = memberRepository.findById(savedMemberId).get(); + + final var expectedCommentResponses = new ArrayList<>(); + for (int i = 0; i < result.getComments().size(); i++) { + expectedCommentResponses.add(RecipeCommentResponse.toResponse( + new Comment(savedRecipe, savedMember, "꿀조합 댓글이에요" + (5 - i)))); + } + + assertThat(result.getHasNext()).isFalse(); + assertThat(result.getTotalElements()).isEqualTo(15); + assertThat(result.getComments()).hasSize(5); + assertThat(result.getComments()).usingRecursiveComparison() + .ignoringFields("id", "createdAt") + .isEqualTo(expectedCommentResponses); + } + } + private void 해당멤버의_꿀조합과_페이징_결과를_검증한다(final MemberRecipesResponse actual, final List expectedRecipesDtos, final PageDto expectedPage) { assertSoftly(soft -> { diff --git a/backend/src/test/java/com/funeat/recipe/persistence/RecipeRepositoryTest.java b/backend/src/test/java/com/funeat/recipe/persistence/RecipeRepositoryTest.java index c7130d2ce..f52eae169 100644 --- a/backend/src/test/java/com/funeat/recipe/persistence/RecipeRepositoryTest.java +++ b/backend/src/test/java/com/funeat/recipe/persistence/RecipeRepositoryTest.java @@ -119,7 +119,7 @@ class findAllRecipes_성공_테스트 { } @Test - void 꿀조합을_최신순으로_정렬한다() { + void 꿀조합을_최신순으로_정렬한다() throws InterruptedException { // given final var member1 = 멤버_멤버1_생성(); final var member2 = 멤버_멤버2_생성(); @@ -135,7 +135,9 @@ class findAllRecipes_성공_테스트 { 복수_상품_저장(product1, product2, product3); final var recipe1_1 = 레시피_생성(member1, 1L); + Thread.sleep(1000); final var recipe1_2 = 레시피_생성(member1, 3L); + Thread.sleep(1000); final var recipe1_3 = 레시피_생성(member1, 2L); 복수_꿀조합_저장(recipe1_1, recipe1_2, recipe1_3); diff --git a/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java b/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java index e6ca43b22..597033063 100644 --- a/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java +++ b/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java @@ -441,7 +441,7 @@ class sortingReviews_성공_테스트 { } @Test - void 최신순으로_정렬을_할_수_있다() { + void 최신순으로_정렬을_할_수_있다() throws InterruptedException { // given final var member1 = 멤버_멤버1_생성(); final var member2 = 멤버_멤버2_생성(); @@ -455,7 +455,9 @@ class sortingReviews_성공_테스트 { final var productId = 단일_상품_저장(product); final var review1 = 리뷰_이미지test3_평점3점_재구매O_생성(member1, product, 351L); + Thread.sleep(1000); final var review2 = 리뷰_이미지test4_평점4점_재구매O_생성(member2, product, 24L); + Thread.sleep(1000); final var review3 = 리뷰_이미지test3_평점3점_재구매X_생성(member3, product, 130L); 복수_리뷰_저장(review1, review2, review3);