Skip to content

Commit

Permalink
feat: 저장한 꿀조합 목록 조회 기능 추가 (#70)
Browse files Browse the repository at this point in the history
* feat: 컨트롤러 메서드와 Swagger 문서 추가

* test: 저장한 레시피 조회 인수테스트 추가

* feat: 저장한 레시피 조회 API 요청에 필요한 기능 추가

인수 테스트만 정상적으로 실행될 수 있도록만 추가
서비스 메서드는 이후에 작성

* test: 인수 테스트에 부족한 조건 추가

* feat: 저장된 꿀조합 목록 조회 서비스 로직 및 테스트 추가

* test: 북마크한 레시피 목록 조회 리포지토리 테스트 추가

* style: 코드 포맷 수정
  • Loading branch information
70825 authored Jun 4, 2024
1 parent 8c7870f commit 30844a4
Show file tree
Hide file tree
Showing 15 changed files with 438 additions and 26 deletions.
41 changes: 41 additions & 0 deletions src/main/java/com/funeat/member/dto/MemberBookmarkRecipeDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.funeat.member.dto;

import com.funeat.product.domain.Product;
import com.funeat.recipe.domain.Recipe;
import com.funeat.recipe.domain.RecipeImage;

import java.time.LocalDateTime;
import java.util.List;

public record MemberBookmarkRecipeDto(
Long id,
String title,
String image,
String content,
Boolean favorite,
MemberResponse author,
MemberBookmarkRecipeProductsDto products,
LocalDateTime createdAt
) {

private MemberBookmarkRecipeDto(final Long id, final String title, final String content, final Boolean favorite,
final MemberResponse author, final MemberBookmarkRecipeProductsDto products,
final LocalDateTime createdAt) {
this(id, title, null, content, favorite, author, products, createdAt);
}

public static MemberBookmarkRecipeDto toDto(final Recipe recipe, final List<RecipeImage> findRecipeImages,
final List<Product> recipeInProducts, final Boolean isFavorite) {
final MemberResponse author = MemberResponse.toResponse(recipe.getMember());
final MemberBookmarkRecipeProductsDto products = MemberBookmarkRecipeProductsDto.toDto(recipeInProducts);

if (findRecipeImages.isEmpty()) {
return new MemberBookmarkRecipeDto(recipe.getId(), recipe.getTitle(), recipe.getContent(), isFavorite,
author, products, recipe.getCreatedAt()
);
}
return new MemberBookmarkRecipeDto(recipe.getId(), recipe.getTitle(), findRecipeImages.get(0).getImage(),
recipe.getContent(), isFavorite, author, products, recipe.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.funeat.member.dto;

import com.funeat.product.domain.Product;

public record MemberBookmarkRecipeProductDto(
Long id,
String name,
Long price,
String image,
Double averageRating
) {

public static MemberBookmarkRecipeProductDto toDto(final Product product) {
return new MemberBookmarkRecipeProductDto(product.getId(), product.getName(), product.getPrice(),
product.getImage(), product.getAverageRating());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.funeat.member.dto;

import com.funeat.product.domain.Product;

import java.util.List;

public record MemberBookmarkRecipeProductsDto(
List<MemberBookmarkRecipeProductDto> products
) {

public static MemberBookmarkRecipeProductsDto toDto(final List<Product> recipeInProducts) {
final List<MemberBookmarkRecipeProductDto> products = recipeInProducts.stream()
.map(MemberBookmarkRecipeProductDto::toDto)
.toList();

return new MemberBookmarkRecipeProductsDto(products);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.funeat.member.dto;

import com.funeat.common.dto.PageDto;

import java.util.List;

public record MemberBookmarkRecipesResponse(
PageDto page,
List<MemberBookmarkRecipeDto> recipes
) {

public static MemberBookmarkRecipesResponse toResponse(final PageDto page,
final List<MemberBookmarkRecipeDto> recipes) {
return new MemberBookmarkRecipesResponse(page, recipes);
}
}
26 changes: 0 additions & 26 deletions src/main/java/com/funeat/member/dto/MemberRecipeProductDto.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.funeat.auth.util.AuthenticationPrincipal;
import com.funeat.common.logging.Logging;
import com.funeat.member.application.MemberService;
import com.funeat.member.dto.MemberBookmarkRecipesResponse;
import com.funeat.member.dto.MemberProfileResponse;
import com.funeat.member.dto.MemberRecipesResponse;
import com.funeat.member.dto.MemberRequest;
Expand Down Expand Up @@ -80,4 +81,12 @@ public ResponseEntity<Void> deleteReview(@PathVariable final Long reviewId,

return ResponseEntity.noContent().build();
}

@GetMapping("/recipes/bookmark")
public ResponseEntity<MemberBookmarkRecipesResponse> getMemberBookmarkRecipe(@AuthenticationPrincipal final LoginInfo loginInfo,
@PageableDefault final Pageable pageable) {
final MemberBookmarkRecipesResponse response = recipeService.findBookmarkRecipeByMember(loginInfo.getId(), pageable);

return ResponseEntity.ok().body(response);
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/funeat/member/presentation/MemberController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.funeat.auth.dto.LoginInfo;
import com.funeat.auth.util.AuthenticationPrincipal;
import com.funeat.member.dto.MemberBookmarkRecipesResponse;
import com.funeat.member.dto.MemberProfileResponse;
import com.funeat.member.dto.MemberRecipesResponse;
import com.funeat.member.dto.MemberRequest;
Expand Down Expand Up @@ -66,4 +67,13 @@ ResponseEntity<MemberRecipesResponse> getMemberRecipe(@AuthenticationPrincipal f
@DeleteMapping
ResponseEntity<Void> deleteReview(@PathVariable final Long reviewId,
@AuthenticationPrincipal final LoginInfo loginInfo);

@Operation(summary = "사용자가 저장한 꿀조합 조회 (북마크)", description = "자신이 저장한 꿀조합을 조회한다.")
@ApiResponse(
responseCode = "200",
description = "저장한 꿀조합 조회 성공."
)
@GetMapping
ResponseEntity<MemberBookmarkRecipesResponse> getMemberBookmarkRecipe(@AuthenticationPrincipal final LoginInfo loginInfo,
@PageableDefault final Pageable pageable);
}
19 changes: 19 additions & 0 deletions src/main/java/com/funeat/recipe/application/RecipeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import com.funeat.member.domain.Member;
import com.funeat.member.domain.bookmark.RecipeBookmark;
import com.funeat.member.domain.favorite.RecipeFavorite;
import com.funeat.member.dto.MemberBookmarkRecipeDto;
import com.funeat.member.dto.MemberBookmarkRecipesResponse;
import com.funeat.member.dto.MemberRecipeDto;
import com.funeat.member.dto.MemberRecipesResponse;
import com.funeat.member.exception.MemberException.MemberDuplicateBookmarkException;
Expand Down Expand Up @@ -227,6 +229,23 @@ private RecipeBookmark createAndSaveRecipeBookmark(final Member member, final Re
}
}

public MemberBookmarkRecipesResponse findBookmarkRecipeByMember(final Long memberId, final Pageable pageable) {
final Member findMember = memberRepository.findById(memberId)
.orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId));

final Page<Recipe> sortedBookmarkRecipePages = recipeRepository.findBookmarkedRecipesByMember(findMember, pageable);

final PageDto page = PageDto.toDto(sortedBookmarkRecipePages);
final List<MemberBookmarkRecipeDto> dtos = sortedBookmarkRecipePages.stream()
.map(recipe -> MemberBookmarkRecipeDto.toDto(recipe,
recipeImageRepository.findByRecipe(recipe),
productRecipeRepository.findProductByRecipe(recipe),
recipeFavoriteRepository.existsByMemberAndRecipeAndFavoriteTrue(findMember, recipe)))
.toList();

return MemberBookmarkRecipesResponse.toResponse(page, dtos);
}

public SearchRecipeResultsResponse getSearchResults(final String query, final Long lastRecipeId) {
final List<Recipe> findRecipes = findAllByProductNameContaining(query, lastRecipeId);
final int resultSize = getResultSize(findRecipes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,7 @@ List<Recipe> findAllByProductNameContaining(@Param("name") final String name, fi
List<Recipe> findRecipesByFavoriteCountGreaterThanEqual(final Long favoriteCount);

Long countByMember(final Member member);

@Query("SELECT r FROM Recipe r JOIN RecipeBookmark rb ON r.id = rb.recipe.id WHERE rb.member = :member AND rb.bookmark = true")
Page<Recipe> findBookmarkedRecipesByMember(@Param("member") final Member member, final Pageable pageable);
}
129 changes: 129 additions & 0 deletions src/test/java/com/funeat/acceptance/member/MemberAcceptanceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
import static com.funeat.acceptance.member.MemberSteps.리뷰_삭제_요청;
import static com.funeat.acceptance.member.MemberSteps.사용자_꿀조합_조회_요청;
import static com.funeat.acceptance.member.MemberSteps.사용자_리뷰_조회_요청;
import static com.funeat.acceptance.member.MemberSteps.사용자_북마크한_꿀조합_조회_요청;
import static com.funeat.acceptance.member.MemberSteps.사용자_정보_수정_요청;
import static com.funeat.acceptance.member.MemberSteps.사용자_정보_조회_요청;
import static com.funeat.acceptance.recipe.RecipeSteps.레시피_북마크_요청;
import static com.funeat.acceptance.recipe.RecipeSteps.레시피_작성_요청;
import static com.funeat.acceptance.review.ReviewSteps.리뷰_작성_요청;
import static com.funeat.auth.exception.AuthErrorCode.LOGIN_MEMBER_NOT_FOUND;
Expand All @@ -40,7 +42,11 @@
import static com.funeat.fixture.RecipeFixture.레시피;
import static com.funeat.fixture.RecipeFixture.레시피1;
import static com.funeat.fixture.RecipeFixture.레시피2;
import static com.funeat.fixture.RecipeFixture.레시피3;
import static com.funeat.fixture.RecipeFixture.레시피북마크요청_생성;
import static com.funeat.fixture.RecipeFixture.레시피추요청_생성;
import static com.funeat.fixture.RecipeFixture.북마크O;
import static com.funeat.fixture.RecipeFixture.북마크X;
import static com.funeat.fixture.ReviewFixture.리뷰1;
import static com.funeat.fixture.ReviewFixture.리뷰2;
import static com.funeat.fixture.ReviewFixture.리뷰추요청_재구매O_생성;
Expand All @@ -56,6 +62,7 @@

import com.funeat.acceptance.common.AcceptanceTest;
import com.funeat.member.domain.Member;
import com.funeat.member.dto.MemberBookmarkRecipeDto;
import com.funeat.member.dto.MemberProfileResponse;
import com.funeat.member.dto.MemberRecipeDto;
import com.funeat.member.dto.MemberReviewDto;
Expand Down Expand Up @@ -394,6 +401,119 @@ class deleteReview_실패_테스트 {
}
}
@Nested
class getMemberBookmarkRecipe_성공_테스트 {
@Test
void 사용자가_저장한_꿀조합이_없을때_꿀조합은_빈상태로_조회된다() {
// given
final var 카테고리 = 카테고리_즉석조리_생성();
단일_카테고리_저장(카테고리);
final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점5점_생성(카테고리));
레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품));
final var 예상_응답_페이지 = 응답_페이지_생성(총_데이터_개수(0L), 총_페이지(0L), 첫페이지O, 마지막페이지O, FIRST_PAGE, PAGE_SIZE);
// when
final var 응답 = 사용자_북마크한_꿀조합_조회_요청(로그인_쿠키_획득(멤버1), FIRST_PAGE);
// then
STATUS_CODE를_검증한다(응답, 정상_처리);
페이지를_검증한다(응답, 예상_응답_페이지);
사용자_북마크한_꿀조합_조회_결과를_검증한다(응답, Collections.emptyList());
}
@Test
void 사용자가_저장한_꿀조합을_조회하다() {
// given
final var 카테고리 = 카테고리_즉석조리_생성();
단일_카테고리_저장(카테고리);
final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점5점_생성(카테고리));
레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품));
레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지2), 레시피추가요청_생성(상품));
레시피_작성_요청(로그인_쿠키_획득(멤버2), 여러개_사진_명세_요청(이미지3), 레시피추가요청_생성(상품));
레시피_북마크_요청(로그인_쿠키_획득(멤버1), 레시피1, 레시피북마크요청_생성(북마크O));
레시피_북마크_요청(로그인_쿠키_획득(멤버1), 레시피3, 레시피북마크요청_생성(북마크O));
final var 예상_응답_페이지 = 응답_페이지_생성(총_데이터_개수(2L), 총_페이지(1L), 첫페이지O, 마지막페이지O, FIRST_PAGE, PAGE_SIZE);
// when
final var 응답 = 사용자_북마크한_꿀조합_조회_요청(로그인_쿠키_획득(멤버1), FIRST_PAGE);
// then
STATUS_CODE를_검증한다(응답, 정상_처리);
페이지를_검증한다(응답, 예상_응답_페이지);
사용자_북마크한_꿀조합_조회_결과를_검증한다(응답, List.of(레시피3, 레시피1));
}
@Test
void 사용자가_꿀조합_저장을_취소했으면_해당_꿀조합은_보이지_않아야_한다() {
// given
final var 카테고리 = 카테고리_즉석조리_생성();
단일_카테고리_저장(카테고리);
final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점5점_생성(카테고리));
레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품));
레시피_작성_요청(로그인_쿠키_획득(멤버2), 여러개_사진_명세_요청(이미지2), 레시피추가요청_생성(상품));
레시피_북마크_요청(로그인_쿠키_획득(멤버1), 레시피1, 레시피북마크요청_생성(북마크O));
레시피_북마크_요청(로그인_쿠키_획득(멤버1), 레시피2, 레시피북마크요청_생성(북마크O));
레시피_북마크_요청(로그인_쿠키_획득(멤버1), 레시피1, 레시피북마크요청_생성(북마크X));
final var 예상_응답_페이지 = 응답_페이지_생성(총_데이터_개수(1L), 총_페이지(1L), 첫페이지O, 마지막페이지O, FIRST_PAGE, PAGE_SIZE);
// when
final var 응답 = 사용자_북마크한_꿀조합_조회_요청(로그인_쿠키_획득(멤버1), FIRST_PAGE);
// then
STATUS_CODE를_검증한다(응답, 정상_처리);
페이지를_검증한다(응답, 예상_응답_페이지);
사용자_북마크한_꿀조합_조회_결과를_검증한다(응답, List.of(레시피2));
}
@Test
void 사용자가_저장한_꿀조합에_이미지가_없을때_저장된_꿀조합은_이미지없이_조회된다() {
// given
final var 카테고리 = 카테고리_즉석조리_생성();
단일_카테고리_저장(카테고리);
final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점5점_생성(카테고리));
레시피_작성_요청(로그인_쿠키_획득(멤버1), null, 레시피추가요청_생성(상품));
레시피_북마크_요청(로그인_쿠키_획득(멤버1), 레시피1, 레시피북마크요청_생성(북마크O));
final var 예상_응답_페이지 = 응답_페이지_생성(총_데이터_개수(1L), 총_페이지(1L), 첫페이지O, 마지막페이지O, FIRST_PAGE, PAGE_SIZE);
// when
final var 응답 = 사용자_북마크한_꿀조합_조회_요청(로그인_쿠키_획득(멤버1), FIRST_PAGE);
// then
STATUS_CODE를_검증한다(응답, 정상_처리);
페이지를_검증한다(응답, 예상_응답_페이지);
사용자_북마크한_꿀조합_조회_결과를_검증한다(응답, List.of(레시피));
조회한_꿀조합의_이미지가_없는지_확인한다(응답);
}
}
@Nested
class getMemberBookmarkRecipe_실패_테스트 {
@ParameterizedTest
@NullAndEmptySource
void 로그인하지_않은_사용자가_저장한_꿀조합을_조회할때_예외가_발생한다(final String cookie) {
// given & when
final var 응답 = 사용자_북마크한_꿀조합_조회_요청(cookie, FIRST_PAGE);
// then
STATUS_CODE를_검증한다(응답, 인증되지_않음);
RESPONSE_CODE와_MESSAGE를_검증한다(응답, LOGIN_MEMBER_NOT_FOUND.getCode(),
LOGIN_MEMBER_NOT_FOUND.getMessage());
}
}
private void 사용자_리뷰_조회_결과를_검증한다(final ExtractableResponse<Response> response, final int expectedReviewSize) {
final var actual = response.jsonPath().getList("reviews", MemberReviewDto.class);
Expand Down Expand Up @@ -449,4 +569,13 @@ class deleteReview_실패_테스트 {

assertThat(actual).isNull();
}

private void 사용자_북마크한_꿀조합_조회_결과를_검증한다(final ExtractableResponse<Response> response,
final List<Long> recipeIds) {
final var actual = response.jsonPath()
.getList("recipes", MemberBookmarkRecipeDto.class);

assertThat(actual).extracting(MemberBookmarkRecipeDto::id)
.containsExactlyElementsOf(recipeIds);
}
}
10 changes: 10 additions & 0 deletions src/test/java/com/funeat/acceptance/member/MemberSteps.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,14 @@ public class MemberSteps {
.then()
.extract();
}

public static ExtractableResponse<Response> 사용자_북마크한_꿀조합_조회_요청(final String loginCookie, final Long page) {
return given()
.when()
.cookie("SESSION", loginCookie)
.queryParam("page", page)
.get("/api/members/recipes/bookmark")
.then()
.extract();
}
}
Loading

0 comments on commit 30844a4

Please sign in to comment.