From 0d56ff143b77ba26ec196e9fac110a396929c456 Mon Sep 17 00:00:00 2001 From: 70825 Date: Thu, 30 May 2024 10:28:06 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EA=BF=80=EC=A1=B0=ED=95=A9=20?= =?UTF-8?q?=EB=B6=81=EB=A7=88=ED=81=AC=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/funeat/member/domain/Member.java | 8 +++ .../domain/bookmark/RecipeBookmark.java | 65 +++++++++++++++++ .../domain/bookmark/RecipeBookmarkTest.java | 71 +++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 src/main/java/com/funeat/member/domain/bookmark/RecipeBookmark.java create mode 100644 src/test/java/com/funeat/member/domain/bookmark/RecipeBookmarkTest.java diff --git a/src/main/java/com/funeat/member/domain/Member.java b/src/main/java/com/funeat/member/domain/Member.java index 63db2a8d..a876b84a 100644 --- a/src/main/java/com/funeat/member/domain/Member.java +++ b/src/main/java/com/funeat/member/domain/Member.java @@ -2,6 +2,7 @@ import static com.funeat.member.exception.MemberErrorCode.MEMBER_UPDATE_ERROR; +import com.funeat.member.domain.bookmark.RecipeBookmark; import com.funeat.member.domain.favorite.RecipeFavorite; import com.funeat.member.domain.favorite.ReviewFavorite; import com.funeat.member.exception.MemberException.MemberUpdateException; @@ -34,6 +35,9 @@ public class Member { @OneToMany(mappedBy = "member") private List recipeFavorites; + @OneToMany(mappedBy = "member") + private List recipeBookmarks; + protected Member() { } @@ -71,6 +75,10 @@ public List getRecipeFavorites() { return recipeFavorites; } + public List getRecipeBookmarks() { + return recipeBookmarks; + } + public void modifyProfile(final String nickname, final String profileImage) { if (!StringUtils.hasText(nickname) || Objects.isNull(profileImage)) { throw new MemberUpdateException(MEMBER_UPDATE_ERROR); diff --git a/src/main/java/com/funeat/member/domain/bookmark/RecipeBookmark.java b/src/main/java/com/funeat/member/domain/bookmark/RecipeBookmark.java new file mode 100644 index 00000000..82959952 --- /dev/null +++ b/src/main/java/com/funeat/member/domain/bookmark/RecipeBookmark.java @@ -0,0 +1,65 @@ +package com.funeat.member.domain.bookmark; + +import com.funeat.member.domain.Member; +import com.funeat.recipe.domain.Recipe; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + +@Entity +@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"member_id", "recipe_id"})) +public class RecipeBookmark { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipe_id") + private Recipe recipe; + + private Boolean bookmark; + + protected RecipeBookmark() { + } + + public RecipeBookmark(final Member member, final Recipe recipe, final Boolean bookmark) { + this.member = member; + this.recipe = recipe; + this.bookmark = bookmark; + } + + public static RecipeBookmark create(final Member member, final Recipe recipe, final Boolean bookmark) { + return new RecipeBookmark(member, recipe, bookmark); + } + + public void updateBookmark(final Boolean bookmark) { + this.bookmark = bookmark; + } + + public Long getId() { + return id; + } + + public Member getMember() { + return member; + } + + public Recipe getRecipe() { + return recipe; + } + + public Boolean getBookmark() { + return bookmark; + } +} diff --git a/src/test/java/com/funeat/member/domain/bookmark/RecipeBookmarkTest.java b/src/test/java/com/funeat/member/domain/bookmark/RecipeBookmarkTest.java new file mode 100644 index 00000000..5f518e4c --- /dev/null +++ b/src/test/java/com/funeat/member/domain/bookmark/RecipeBookmarkTest.java @@ -0,0 +1,71 @@ +package com.funeat.member.domain.bookmark; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static com.funeat.fixture.MemberFixture.멤버_멤버1_생성; +import static com.funeat.fixture.RecipeFixture.레시피_생성; +import static org.assertj.core.api.Assertions.assertThat; + +class RecipeBookmarkTest { + + @Nested + class updateBookmark_성공_테스트 { + + @Test + void 기존_false_신규_false인_경우_bookmark는_false_유지() { + // given + final var member = 멤버_멤버1_생성(); + final var recipe = 레시피_생성(member); + final var recipeBookmark = RecipeBookmark.create(member, recipe, false); + + // when + recipeBookmark.updateBookmark(false); + + // then + assertThat(recipeBookmark.getBookmark()).isFalse(); + } + + @Test + void 기존_false_신규_true인_경우_bookmark는_true로_변경() { + // given + final var member = 멤버_멤버1_생성(); + final var recipe = 레시피_생성(member); + final var recipeBookmark = RecipeBookmark.create(member, recipe, false); + + // when + recipeBookmark.updateBookmark(true); + + // then + assertThat(recipeBookmark.getBookmark()).isTrue(); + } + + @Test + void 기존_true_신규_true인_경우_bookmark는_true_유지() { + // given + final var member = 멤버_멤버1_생성(); + final var recipe = 레시피_생성(member); + final var recipeBookmark = RecipeBookmark.create(member, recipe, true); + + // when + recipeBookmark.updateBookmark(true); + + // then + assertThat(recipeBookmark.getBookmark()).isTrue(); + } + + @Test + void 기존_true_신규_false인_경우_bookmark는_false로_변경() { + // given + final var member = 멤버_멤버1_생성(); + final var recipe = 레시피_생성(member); + final var recipeBookmark = RecipeBookmark.create(member, recipe, true); + + // when + recipeBookmark.updateBookmark(false); + + // then + assertThat(recipeBookmark.getBookmark()).isFalse(); + } + } +} From cfc4d54d9eb31033ca576ef6c21c572c17793f20 Mon Sep 17 00:00:00 2001 From: 70825 Date: Thu, 30 May 2024 10:58:39 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test:=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EC=9D=B8=EC=88=98=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recipe/dto/RecipeBookmarkRequest.java | 9 ++ .../presentation/RecipeApiController.java | 13 +++ .../recipe/presentation/RecipeController.java | 14 ++- .../recipe/RecipeAcceptanceTest.java | 90 +++++++++++++++++++ .../funeat/acceptance/recipe/RecipeSteps.java | 22 ++++- .../com/funeat/fixture/RecipeFixture.java | 5 ++ 6 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/funeat/recipe/dto/RecipeBookmarkRequest.java diff --git a/src/main/java/com/funeat/recipe/dto/RecipeBookmarkRequest.java b/src/main/java/com/funeat/recipe/dto/RecipeBookmarkRequest.java new file mode 100644 index 00000000..9295d59e --- /dev/null +++ b/src/main/java/com/funeat/recipe/dto/RecipeBookmarkRequest.java @@ -0,0 +1,9 @@ +package com.funeat.recipe.dto; + +import jakarta.validation.constraints.NotNull; + +public record RecipeBookmarkRequest ( + @NotNull(message = "북마크를 확인해주세요") + Boolean bookmark +) { +} diff --git a/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java b/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java index 07a0540a..76199b40 100644 --- a/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java +++ b/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java @@ -5,6 +5,7 @@ import com.funeat.common.logging.Logging; import com.funeat.recipe.application.RecipeService; import com.funeat.recipe.dto.RankingRecipesResponse; +import com.funeat.recipe.dto.RecipeBookmarkRequest; import com.funeat.recipe.dto.RecipeCommentCondition; import com.funeat.recipe.dto.RecipeCommentCreateRequest; import com.funeat.recipe.dto.RecipeCommentsResponse; @@ -77,6 +78,18 @@ public ResponseEntity likeRecipe(@AuthenticationPrincipal final LoginInfo return ResponseEntity.noContent().build(); } + @Logging + @PatchMapping(value = "/api/recipes/{recipeId}/bookmark") + public ResponseEntity bookmarkRecipe(@AuthenticationPrincipal final LoginInfo loginInfo, + @PathVariable final Long recipeId, + @RequestBody @Valid final RecipeBookmarkRequest request) { + // TODO: 인수 테스트 추가 + // TODO: 서비스 테스트 추가 + // TODO: 서비스 로직에 북마크 기능 추가 + + return ResponseEntity.noContent().build(); + } + @GetMapping("/api/ranks/recipes") public ResponseEntity getRankingRecipes(@AuthenticationPrincipal final LoginInfo loginInfo) { final RankingRecipesResponse response = recipeService.getTop4Recipes(loginInfo.getId()); diff --git a/src/main/java/com/funeat/recipe/presentation/RecipeController.java b/src/main/java/com/funeat/recipe/presentation/RecipeController.java index 2ec5ca3e..eb30b133 100644 --- a/src/main/java/com/funeat/recipe/presentation/RecipeController.java +++ b/src/main/java/com/funeat/recipe/presentation/RecipeController.java @@ -3,6 +3,7 @@ import com.funeat.auth.dto.LoginInfo; import com.funeat.auth.util.AuthenticationPrincipal; import com.funeat.recipe.dto.RankingRecipesResponse; +import com.funeat.recipe.dto.RecipeBookmarkRequest; import com.funeat.recipe.dto.RecipeCommentCondition; import com.funeat.recipe.dto.RecipeCommentCreateRequest; import com.funeat.recipe.dto.RecipeCommentsResponse; @@ -14,7 +15,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.List; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; @@ -28,6 +28,8 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + @Tag(name = "07.Recipe", description = "꿀조합 관련 API 입니다.") public interface RecipeController { @@ -69,6 +71,16 @@ ResponseEntity likeRecipe(@AuthenticationPrincipal final LoginInfo loginIn @PathVariable final Long recipeId, @RequestBody final RecipeFavoriteRequest request); + @Operation(summary = "꿀조합 저장", description = "꿀조합을 저장 또는 취소를 한다.") + @ApiResponse( + responseCode = "204", + description = "꿀조합 저장(또는 저장 취소) 성공." + ) + @PatchMapping + ResponseEntity bookmarkRecipe(@AuthenticationPrincipal final LoginInfo loginInfo, + @PathVariable final Long recipeId, + @RequestBody final RecipeBookmarkRequest request); + @Operation(summary = "꿀조합 랭킹 조회", description = "전체 꿀조합들 중에서 랭킹 TOP4를 조회한다.") @ApiResponse( responseCode = "200", diff --git a/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java b/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java index 77713967..b3476440 100644 --- a/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java +++ b/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java @@ -15,6 +15,7 @@ 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.레시피_작성_요청; import static com.funeat.acceptance.recipe.RecipeSteps.레시피_좋아요_요청; @@ -58,6 +59,7 @@ import static com.funeat.fixture.RecipeFixture.레시피9; import static com.funeat.fixture.RecipeFixture.레시피_본문; import static com.funeat.fixture.RecipeFixture.레시피_제목; +import static com.funeat.fixture.RecipeFixture.레시피북마크요청_생성; import static com.funeat.fixture.RecipeFixture.레시피좋아요요청_생성; import static com.funeat.fixture.RecipeFixture.레시피추가요청_생성; import static com.funeat.fixture.RecipeFixture.존재하지_않는_레시피; @@ -373,6 +375,94 @@ class likeRecipe_실패_테스트 { } } + @Nested + class bookmarkRecipe_성공_테스트 { + + @Test + void 레시피에_북마크를_할_수_있다() { + // given + final var 카테고리 = 카테고리_간편식사_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점1점_생성(카테고리)); + + 레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품)); + + // when + final var 응답 = 레시피_북마크_요청(로그인_쿠키_획득(멤버1), 레시피, 레시피북마크요청_생성(좋아요O)); + + // then + STATUS_CODE를_검증한다(응답, 정상_처리_NO_CONTENT); + } + + @Test + void 레시피예_북마크를_취소할_수_있다() { + // given + final var 카테고리 = 카테고리_간편식사_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점1점_생성(카테고리)); + + 레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품)); + 레시피_북마크_요청(로그인_쿠키_획득(멤버1), 레시피, 레시피북마크요청_생성(좋아요O)); + + // when + final var 응답 = 레시피_북마크_요청(로그인_쿠키_획득(멤버1), 레시피, 레시피북마크요청_생성(좋아요X)); + + // then + STATUS_CODE를_검증한다(응답, 정상_처리_NO_CONTENT); + } + } + + @Nested + class bookmarkRecipe_실패_테스트 { + + @ParameterizedTest + @NullAndEmptySource + void 로그인_하지않은_사용자가_레시피를_저장할_때_예외가_발생한다(final String cookie) { + // given + final var 카테고리 = 카테고리_간편식사_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점1점_생성(카테고리)); + + 레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품)); + + // when + final var 응답 = 레시피_북마크_요청(cookie, 레시피, 레시피북마크요청_생성(좋아요O)); + + // then + STATUS_CODE를_검증한다(응답, 인증되지_않음); + RESPONSE_CODE와_MESSAGE를_검증한다(응답, LOGIN_MEMBER_NOT_FOUND.getCode(), + LOGIN_MEMBER_NOT_FOUND.getMessage()); + } + + @Test + void 사용자가_레시피를_저장할_때_저장값_미기입시_예외가_발생한다() { + // given + final var 카테고리 = 카테고리_간편식사_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점1점_생성(카테고리)); + + 레시피_작성_요청(로그인_쿠키_획득(멤버1), 여러개_사진_명세_요청(이미지1), 레시피추가요청_생성(상품)); + + // when + final var 응답 = 레시피_북마크_요청(로그인_쿠키_획득(멤버1), 레시피, 레시피북마크요청_생성(null)); + + // then + STATUS_CODE를_검증한다(응답, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(응답, REQUEST_VALID_ERROR_CODE.getCode(), + "북마크를 확인해주세요. " + REQUEST_VALID_ERROR_CODE.getMessage()); + } + + @Test + void 존재하지_않는_레시피에_사용자가_저장할_때_예외가_발생한다() { + // given & when + final var 응답 = 레시피_북마크_요청(로그인_쿠키_획득(멤버1), 존재하지_않는_레시피, 레시피북마크요청_생성(좋아요O)); + + // then + STATUS_CODE를_검증한다(응답, 찾을수_없음); + RESPONSE_CODE와_MESSAGE를_검증한다(응답, RECIPE_NOT_FOUND.getCode(), RECIPE_NOT_FOUND.getMessage()); + } + } + @Nested class getSearchResults_성공_테스트 { diff --git a/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java b/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java index b8bc49e0..d513ae97 100644 --- a/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java +++ b/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java @@ -1,9 +1,6 @@ package com.funeat.acceptance.recipe; -import static com.funeat.acceptance.auth.LoginSteps.로그인_쿠키_획득; -import static com.funeat.fixture.RecipeFixture.레시피좋아요요청_생성; -import static io.restassured.RestAssured.given; - +import com.funeat.recipe.dto.RecipeBookmarkRequest; import com.funeat.recipe.dto.RecipeCommentCondition; import com.funeat.recipe.dto.RecipeCommentCreateRequest; import com.funeat.recipe.dto.RecipeCreateRequest; @@ -11,9 +8,14 @@ import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import io.restassured.specification.MultiPartSpecification; + import java.util.List; import java.util.Objects; +import static com.funeat.acceptance.auth.LoginSteps.로그인_쿠키_획득; +import static com.funeat.fixture.RecipeFixture.레시피좋아요요청_생성; +import static io.restassured.RestAssured.given; + @SuppressWarnings("NonAsciiCharacters") public class RecipeSteps { @@ -86,6 +88,18 @@ public class RecipeSteps { } } + public static ExtractableResponse 레시피_북마크_요청(final String loginCookie, final Long recipeId, + final RecipeBookmarkRequest request) { + return given() + .cookie("SESSION", loginCookie) + .contentType("application/json") + .body(request) + .when() + .patch("/api/recipes/{recipeId}/bookmark", recipeId) + .then() + .extract(); + } + public static ExtractableResponse 레시피_랭킹_조회_요청() { return given() .when() diff --git a/src/test/java/com/funeat/fixture/RecipeFixture.java b/src/test/java/com/funeat/fixture/RecipeFixture.java index 69e90d8a..0db57b72 100644 --- a/src/test/java/com/funeat/fixture/RecipeFixture.java +++ b/src/test/java/com/funeat/fixture/RecipeFixture.java @@ -4,6 +4,7 @@ import com.funeat.member.domain.favorite.RecipeFavorite; import com.funeat.recipe.domain.Recipe; import com.funeat.recipe.domain.RecipeImage; +import com.funeat.recipe.dto.RecipeBookmarkRequest; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeFavoriteRequest; import java.time.LocalDateTime; @@ -66,6 +67,10 @@ public class RecipeFixture { return new RecipeFavoriteRequest(favorite); } + public static RecipeBookmarkRequest 레시피북마크요청_생성(final Boolean favorite) { + return new RecipeBookmarkRequest(favorite); + } + public static RecipeImage 레시피이미지_생성(final Recipe recipe) { return new RecipeImage("제일로 맛없는 사진", recipe); } From 253800d5f348e5202bcdbacf1fa54725368cd4c4 Mon Sep 17 00:00:00 2001 From: 70825 Date: Thu, 30 May 2024 11:11:39 +0900 Subject: [PATCH 3/4] =?UTF-8?q?test:=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/RecipeApiController.java | 1 - .../recipe/application/RecipeServiceTest.java | 119 ++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java b/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java index 76199b40..b87206ce 100644 --- a/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java +++ b/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java @@ -83,7 +83,6 @@ public ResponseEntity likeRecipe(@AuthenticationPrincipal final LoginInfo public ResponseEntity bookmarkRecipe(@AuthenticationPrincipal final LoginInfo loginInfo, @PathVariable final Long recipeId, @RequestBody @Valid final RecipeBookmarkRequest request) { - // TODO: 인수 테스트 추가 // TODO: 서비스 테스트 추가 // TODO: 서비스 로직에 북마크 기능 추가 diff --git a/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java b/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java index 9818ca52..e3b476a2 100644 --- a/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java +++ b/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java @@ -18,6 +18,7 @@ import static com.funeat.fixture.ProductFixture.상품_삼각김밥_가격3000원_평점4점_생성; import static com.funeat.fixture.RecipeFixture.레시피_생성; import static com.funeat.fixture.RecipeFixture.레시피_좋아요_생성; +import static com.funeat.fixture.RecipeFixture.레시피북마크요청_생성; import static com.funeat.fixture.RecipeFixture.레시피이미지_생성; import static com.funeat.fixture.RecipeFixture.레시피좋아요요청_생성; import static com.funeat.fixture.RecipeFixture.레시피추가요청_생성; @@ -669,6 +670,124 @@ class likeRecipe_실패_테스트 { } } + @Nested + class bookmarkRecipe_성공_테스트 { + + @Test + void 꿀조합에_북마크를_할_수_있다() { + // 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 productIds = 상품_아이디_변환(product1, product2, product3); + + final var author = 멤버_멤버1_생성(); + final var authorId = 단일_멤버_저장(author); + final var member = 멤버_멤버2_생성(); + final var memberId = 단일_멤버_저장(member); + + final var images = 여러_이미지_생성(3); + + final var createRequest = 레시피추가요청_생성(productIds); + final var recipeId = recipeService.create(authorId, images, createRequest); + + // when + final var bookmarkRequest = 레시피북마크요청_생성(true); + recipeService.bookmarkRecipe(memberId, recipeId, bookmarkRequest); + + final var actualRecipe = recipeRepository.findById(recipeId).get(); + final var actualRecipeBookmark = recipeBookmarkRepository.findByMemberAndRecipe(member, actualRecipe).get(); + + // then + assertThat(actualRecipeBookmark.getBookmark()).isTrue(); + } + + @Test + void 꿀조합에_북마크를_취소할_수_있다() { + // 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 productIds = 상품_아이디_변환(product1, product2, product3); + + final var author = 멤버_멤버1_생성(); + final var authorId = 단일_멤버_저장(author); + final var member = 멤버_멤버2_생성(); + final var memberId = 단일_멤버_저장(member); + + final var images = 여러_이미지_생성(3); + + final var createRequest = 레시피추가요청_생성(productIds); + final var recipeId = recipeService.create(authorId, images, createRequest); + + final var bookmarkRequest = 레시피북마크요청_생성(true); + recipeService.bookmarkRecipe(memberId, recipeId, bookmarkRequest); + + // when + final var cancelBookmarkRequest = 레시피북마크요청_생성(false); + recipeService.bookmarkRecipe(memberId, recipeId, cancelBookmarkRequest); + + final var actualRecipe = recipeRepository.findById(recipeId).get(); + final var actualRecipeBookmark = recipeBookmarkRepository.findByMemberAndRecipe(member, actualRecipe).get(); + + // then + assertThat(actualRecipeBookmark.getBookmark()).isFalse(); + } + } + + @Nested + class bookmarkRecipe_실패_테스트 { + + @Test + void 존재하지_않는_멤버가_레시피에_북마크를_하면_예외가_발생한다() { + // 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 productIds = 상품_아이디_변환(product1, product2, product3); + + final var author = 멤버_멤버1_생성(); + final var authorId = 단일_멤버_저장(author); + final var wrongMemberId = authorId + 1L; + + final var images = 여러_이미지_생성(3); + + final var createRequest = 레시피추가요청_생성(productIds); + final var recipeId = recipeService.create(authorId, images, createRequest); + + // when & then + final var bookmarkRequest = 레시피북마크요청_생성(true); + assertThatThrownBy(() -> recipeService.bookmarkRecipe(wrongMemberId, recipeId, bookmarkRequest)) + .isInstanceOf(MemberNotFoundException.class); + } + + @Test + void 멤버가_존재하지_않는_레시피에_북마크를_하면_예외가_발생한다() { + // given + final var member = 멤버_멤버1_생성(); + final var memberId = 단일_멤버_저장(member); + + final var wrongRecipeId = 999L; + + // when & then + final var bookmarkRequest = 레시피북마크요청_생성(true); + assertThatThrownBy(() -> recipeService.bookmarkRecipe(memberId, wrongRecipeId, bookmarkRequest)) + .isInstanceOf(RecipeNotFoundException.class); + } + } + @Nested class getTop4Recipes_성공_테스트 { From a3eef6279a89636404986a2c6a30de7479b442af Mon Sep 17 00:00:00 2001 From: 70825 Date: Thu, 30 May 2024 11:25:48 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/exception/MemberErrorCode.java | 1 + .../member/exception/MemberException.java | 6 ++++ .../persistence/RecipeBookmarkRepository.java | 13 ++++++++ .../recipe/application/RecipeService.java | 31 +++++++++++++++++++ .../presentation/RecipeApiController.java | 3 +- .../java/com/funeat/common/ServiceTest.java | 4 +++ 6 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/funeat/member/persistence/RecipeBookmarkRepository.java diff --git a/src/main/java/com/funeat/member/exception/MemberErrorCode.java b/src/main/java/com/funeat/member/exception/MemberErrorCode.java index 91b37d3c..9d70c4d3 100644 --- a/src/main/java/com/funeat/member/exception/MemberErrorCode.java +++ b/src/main/java/com/funeat/member/exception/MemberErrorCode.java @@ -7,6 +7,7 @@ public enum MemberErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다. 회원 id를 확인하세요.", "5001"), MEMBER_UPDATE_ERROR(HttpStatus.BAD_REQUEST, "닉네임 또는 이미지를 확인하세요.", "5002"), MEMBER_DUPLICATE_FAVORITE(HttpStatus.CONFLICT, "이미 좋아요를 누른 상태입니다.", "5003"), + MEMBER_DUPLICATE_BOOKMARK(HttpStatus.CONFLICT, "이미 북마크를 누른 상태입니다.", "5004"), ; private final HttpStatus status; diff --git a/src/main/java/com/funeat/member/exception/MemberException.java b/src/main/java/com/funeat/member/exception/MemberException.java index c0dab99e..1a6092ea 100644 --- a/src/main/java/com/funeat/member/exception/MemberException.java +++ b/src/main/java/com/funeat/member/exception/MemberException.java @@ -27,4 +27,10 @@ public MemberDuplicateFavoriteException(final MemberErrorCode errorCode, final L super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), memberId)); } } + + public static class MemberDuplicateBookmarkException extends MemberException { + public MemberDuplicateBookmarkException(final MemberErrorCode errorCode, final Long memberId) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), memberId)); + } + } } diff --git a/src/main/java/com/funeat/member/persistence/RecipeBookmarkRepository.java b/src/main/java/com/funeat/member/persistence/RecipeBookmarkRepository.java new file mode 100644 index 00000000..32e3e7f1 --- /dev/null +++ b/src/main/java/com/funeat/member/persistence/RecipeBookmarkRepository.java @@ -0,0 +1,13 @@ +package com.funeat.member.persistence; + +import com.funeat.member.domain.Member; +import com.funeat.member.domain.bookmark.RecipeBookmark; +import com.funeat.recipe.domain.Recipe; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface RecipeBookmarkRepository extends JpaRepository { + + Optional findByMemberAndRecipe(final Member member, final Recipe recipe); +} diff --git a/src/main/java/com/funeat/recipe/application/RecipeService.java b/src/main/java/com/funeat/recipe/application/RecipeService.java index 7a13851f..17b321c2 100644 --- a/src/main/java/com/funeat/recipe/application/RecipeService.java +++ b/src/main/java/com/funeat/recipe/application/RecipeService.java @@ -1,5 +1,6 @@ package com.funeat.recipe.application; +import static com.funeat.member.exception.MemberErrorCode.MEMBER_DUPLICATE_BOOKMARK; import static com.funeat.member.exception.MemberErrorCode.MEMBER_DUPLICATE_FAVORITE; import static com.funeat.member.exception.MemberErrorCode.MEMBER_NOT_FOUND; import static com.funeat.product.exception.ProductErrorCode.PRODUCT_NOT_FOUND; @@ -11,12 +12,15 @@ import com.funeat.common.ImageUploader; import com.funeat.common.dto.PageDto; 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.MemberRecipeDto; import com.funeat.member.dto.MemberRecipesResponse; +import com.funeat.member.exception.MemberException.MemberDuplicateBookmarkException; import com.funeat.member.exception.MemberException.MemberDuplicateFavoriteException; import com.funeat.member.exception.MemberException.MemberNotFoundException; import com.funeat.member.persistence.MemberRepository; +import com.funeat.member.persistence.RecipeBookmarkRepository; import com.funeat.member.persistence.RecipeFavoriteRepository; import com.funeat.product.domain.Product; import com.funeat.product.domain.ProductRecipe; @@ -28,6 +32,7 @@ import com.funeat.recipe.dto.RankingRecipeDto; import com.funeat.recipe.dto.RankingRecipesResponse; import com.funeat.recipe.dto.RecipeAuthorDto; +import com.funeat.recipe.dto.RecipeBookmarkRequest; import com.funeat.recipe.dto.RecipeCommentCondition; import com.funeat.recipe.dto.RecipeCommentCreateRequest; import com.funeat.recipe.dto.RecipeCommentResponse; @@ -74,6 +79,7 @@ public class RecipeService { private final RecipeRepository recipeRepository; private final RecipeImageRepository recipeImageRepository; private final RecipeFavoriteRepository recipeFavoriteRepository; + private final RecipeBookmarkRepository recipeBookmarkRepository; private final CommentRepository commentRepository; private final ImageUploader imageUploader; @@ -81,6 +87,7 @@ public RecipeService(final MemberRepository memberRepository, final ProductRepos final ProductRecipeRepository productRecipeRepository, final RecipeRepository recipeRepository, final RecipeImageRepository recipeImageRepository, final RecipeFavoriteRepository recipeFavoriteRepository, + final RecipeBookmarkRepository recipeBookmarkRepository, final CommentRepository commentRepository, final ImageUploader imageUploader) { this.memberRepository = memberRepository; this.productRepository = productRepository; @@ -88,6 +95,7 @@ public RecipeService(final MemberRepository memberRepository, final ProductRepos this.recipeRepository = recipeRepository; this.recipeImageRepository = recipeImageRepository; this.recipeFavoriteRepository = recipeFavoriteRepository; + this.recipeBookmarkRepository = recipeBookmarkRepository; this.commentRepository = commentRepository; this.imageUploader = imageUploader; } @@ -196,6 +204,29 @@ private RecipeFavorite createAndSaveRecipeFavorite(final Member member, final Re } } + @Transactional + public void bookmarkRecipe(final Long memberId, final Long recipeId, final RecipeBookmarkRequest request) { + final Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId)); + final Recipe recipe = recipeRepository.findByIdForUpdate(recipeId) + .orElseThrow(() -> new RecipeNotFoundException(RECIPE_NOT_FOUND, recipeId)); + + final RecipeBookmark recipeBookmark = recipeBookmarkRepository.findByMemberAndRecipe(member, recipe) + .orElseGet(() -> createAndSaveRecipeBookmark(member, recipe, request.bookmark())); + + recipeBookmark.updateBookmark(request.bookmark()); + } + + private RecipeBookmark createAndSaveRecipeBookmark(final Member member, final Recipe recipe, + final Boolean bookmark) { + try { + final RecipeBookmark recipeBookmark = RecipeBookmark.create(member, recipe, bookmark); + return recipeBookmarkRepository.save(recipeBookmark); + } catch (final DataIntegrityViolationException e) { + throw new MemberDuplicateBookmarkException(MEMBER_DUPLICATE_BOOKMARK, member.getId()); + } + } + public SearchRecipeResultsResponse getSearchResults(final String query, final Long lastRecipeId) { final List findRecipes = findAllByProductNameContaining(query, lastRecipeId); final int resultSize = getResultSize(findRecipes); diff --git a/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java b/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java index b87206ce..40af2b39 100644 --- a/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java +++ b/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java @@ -83,8 +83,7 @@ public ResponseEntity likeRecipe(@AuthenticationPrincipal final LoginInfo public ResponseEntity bookmarkRecipe(@AuthenticationPrincipal final LoginInfo loginInfo, @PathVariable final Long recipeId, @RequestBody @Valid final RecipeBookmarkRequest request) { - // TODO: 서비스 테스트 추가 - // TODO: 서비스 로직에 북마크 기능 추가 + recipeService.bookmarkRecipe(loginInfo.getId(), recipeId, request); return ResponseEntity.noContent().build(); } diff --git a/src/test/java/com/funeat/common/ServiceTest.java b/src/test/java/com/funeat/common/ServiceTest.java index 6544accb..e0dbe9e6 100644 --- a/src/test/java/com/funeat/common/ServiceTest.java +++ b/src/test/java/com/funeat/common/ServiceTest.java @@ -10,6 +10,7 @@ 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.RecipeBookmarkRepository; import com.funeat.member.persistence.RecipeFavoriteRepository; import com.funeat.member.persistence.ReviewFavoriteRepository; import com.funeat.product.application.CategoryService; @@ -57,6 +58,9 @@ public abstract class ServiceTest { @Autowired protected ReviewFavoriteRepository reviewFavoriteRepository; + @Autowired + protected RecipeBookmarkRepository recipeBookmarkRepository; + @Autowired protected CategoryRepository categoryRepository;