From e0237c5f8a94d4139cd9f42e781c49959c1a67e8 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 7 Aug 2024 22:16:32 +0900 Subject: [PATCH 1/7] :sparkles: add category and keyword field on recipe request --- .../net/pengcook/recipe/dto/PageRecipeRequest.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java b/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java index 79f76265..ea9ed4fd 100644 --- a/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java +++ b/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java @@ -1,6 +1,15 @@ package net.pengcook.recipe.dto; +import jakarta.annotation.Nullable; import jakarta.validation.constraints.Min; -public record PageRecipeRequest(@Min(0) int pageNumber, @Min(1) int pageSize) { +public record PageRecipeRequest( + @Nullable String category, + @Nullable String keyword, + @Min(0) int pageNumber, + @Min(1) int pageSize +) { + public PageRecipeRequest(@Min(0) int pageNumber, @Min(1) int pageSize) { + this(null, null, pageNumber, pageSize); + } } From 0286f5ab5cc4cded36f2f260c0dfcd294fb10bd0 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 8 Aug 2024 02:01:16 +0900 Subject: [PATCH 2/7] :sparkles: add dynamic query on view recipes --- .../net/pengcook/recipe/dto/PageRecipeRequest.java | 5 +++-- .../pengcook/recipe/repository/RecipeRepository.java | 12 +++++++++++- .../net/pengcook/recipe/service/RecipeService.java | 6 +++++- .../repository/CategoryRecipeRepositoryTest.java | 2 +- .../recipe/controller/RecipeControllerTest.java | 2 ++ .../recipe/repository/RecipeRepositoryTest.java | 4 ++-- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java b/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java index ea9ed4fd..84f3bdf3 100644 --- a/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java +++ b/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java @@ -2,10 +2,11 @@ import jakarta.annotation.Nullable; import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; public record PageRecipeRequest( - @Nullable String category, - @Nullable String keyword, + @Nullable @NotBlank String category, + @Nullable @NotBlank String keyword, @Min(0) int pageNumber, @Min(1) int pageSize ) { diff --git a/backend/src/main/java/net/pengcook/recipe/repository/RecipeRepository.java b/backend/src/main/java/net/pengcook/recipe/repository/RecipeRepository.java index bb8aca2c..94a91d50 100644 --- a/backend/src/main/java/net/pengcook/recipe/repository/RecipeRepository.java +++ b/backend/src/main/java/net/pengcook/recipe/repository/RecipeRepository.java @@ -1,20 +1,30 @@ package net.pengcook.recipe.repository; +import jakarta.annotation.Nullable; import java.util.List; import net.pengcook.recipe.domain.Recipe; import net.pengcook.recipe.dto.RecipeDataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface RecipeRepository extends JpaRepository { @Query(""" SELECT r.id FROM Recipe r + JOIN FETCH CategoryRecipe cr ON cr.recipe = r + JOIN FETCH Category c ON cr.category = c + WHERE (:category IS NULL OR c.name = :category) + AND (:keyword IS NULL OR CONCAT(r.title, r.description) LIKE CONCAT('%', :keyword, '%')) ORDER BY r.id DESC """) - List findRecipeIds(Pageable pageable); + List findRecipeIdsByCategoryAndKeyword( + @Param("category") @Nullable String category, + @Param("keyword") @Nullable String keyword, + Pageable pageable + ); @Query(""" SELECT new net.pengcook.recipe.dto.RecipeDataResponse( diff --git a/backend/src/main/java/net/pengcook/recipe/service/RecipeService.java b/backend/src/main/java/net/pengcook/recipe/service/RecipeService.java index 65777460..ab079a11 100644 --- a/backend/src/main/java/net/pengcook/recipe/service/RecipeService.java +++ b/backend/src/main/java/net/pengcook/recipe/service/RecipeService.java @@ -46,7 +46,11 @@ public class RecipeService { public List readRecipes(PageRecipeRequest pageRecipeRequest) { Pageable pageable = getValidatedPageable(pageRecipeRequest.pageNumber(), pageRecipeRequest.pageSize()); - List recipeIds = recipeRepository.findRecipeIds(pageable); + List recipeIds = recipeRepository.findRecipeIdsByCategoryAndKeyword( + pageRecipeRequest.category(), + pageRecipeRequest.keyword(), + pageable + ); List recipeDataResponses = recipeRepository.findRecipeData(recipeIds); return convertToMainRecipeResponses(recipeDataResponses); diff --git a/backend/src/test/java/net/pengcook/category/repository/CategoryRecipeRepositoryTest.java b/backend/src/test/java/net/pengcook/category/repository/CategoryRecipeRepositoryTest.java index 97bfbb6c..72534364 100644 --- a/backend/src/test/java/net/pengcook/category/repository/CategoryRecipeRepositoryTest.java +++ b/backend/src/test/java/net/pengcook/category/repository/CategoryRecipeRepositoryTest.java @@ -20,7 +20,7 @@ class CategoryRecipeRepositoryTest { @Test @DisplayName("요청한 카테고리와 페이지에 해당하는 레시피 id 목록을 반환한다.") - void findRecipeIds() { + void findRecipeIdsByCategoryAndKeyword() { Pageable pageable = PageRequest.of(0, 3); List recipeIds = repository.findRecipeIdsByCategoryName("한식", pageable); diff --git a/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java b/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java index 64df60ea..cad7b5ff 100644 --- a/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java +++ b/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java @@ -42,6 +42,8 @@ void readRecipes() { "특정 페이지의 레시피 목록을 조회합니다.", "레시피 조회 API", queryParameters( + parameterWithName("category").description("조회 카테고리").optional(), + parameterWithName("keyword").description("제목 또는 설명 검색 키워드").optional(), parameterWithName("pageNumber").description("페이지 번호"), parameterWithName("pageSize").description("페이지 크기") ), diff --git a/backend/src/test/java/net/pengcook/recipe/repository/RecipeRepositoryTest.java b/backend/src/test/java/net/pengcook/recipe/repository/RecipeRepositoryTest.java index b2acfaca..898c27d8 100644 --- a/backend/src/test/java/net/pengcook/recipe/repository/RecipeRepositoryTest.java +++ b/backend/src/test/java/net/pengcook/recipe/repository/RecipeRepositoryTest.java @@ -25,10 +25,10 @@ class RecipeRepositoryTest { @Test @DisplayName("요청한 페이지에 해당하는 레시피 id 목록을 반환한다.") - void findRecipeIds() { + void findRecipeIdsByCategoryAndKeyword() { Pageable pageable = PageRequest.of(0, 3); - List recipeIds = repository.findRecipeIds(pageable); + List recipeIds = repository.findRecipeIdsByCategoryAndKeyword(null, null, pageable); assertThat(recipeIds).containsExactly(15L, 14L, 13L); } From 2524667f0e9f1f425f67f79c251930d09b69f3d9 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 8 Aug 2024 15:14:25 +0900 Subject: [PATCH 3/7] :bug: add null field instead of adding required constructor to fix primary const problem --- .../net/pengcook/recipe/dto/PageRecipeRequest.java | 10 +++------- .../pengcook/recipe/repository/RecipeRepository.java | 6 +++--- .../net/pengcook/recipe/service/RecipeServiceTest.java | 4 ++-- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java b/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java index 84f3bdf3..ef5d600a 100644 --- a/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java +++ b/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java @@ -2,15 +2,11 @@ import jakarta.annotation.Nullable; import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotBlank; public record PageRecipeRequest( - @Nullable @NotBlank String category, - @Nullable @NotBlank String keyword, @Min(0) int pageNumber, - @Min(1) int pageSize + @Min(1) int pageSize, + @Nullable String category, + @Nullable String keyword ) { - public PageRecipeRequest(@Min(0) int pageNumber, @Min(1) int pageSize) { - this(null, null, pageNumber, pageSize); - } } diff --git a/backend/src/main/java/net/pengcook/recipe/repository/RecipeRepository.java b/backend/src/main/java/net/pengcook/recipe/repository/RecipeRepository.java index 94a91d50..47c48ea2 100644 --- a/backend/src/main/java/net/pengcook/recipe/repository/RecipeRepository.java +++ b/backend/src/main/java/net/pengcook/recipe/repository/RecipeRepository.java @@ -12,10 +12,10 @@ public interface RecipeRepository extends JpaRepository { @Query(""" - SELECT r.id + SELECT DISTINCT r.id FROM Recipe r - JOIN FETCH CategoryRecipe cr ON cr.recipe = r - JOIN FETCH Category c ON cr.category = c + LEFT JOIN CategoryRecipe cr ON cr.recipe = r + LEFT JOIN Category c ON cr.category = c WHERE (:category IS NULL OR c.name = :category) AND (:keyword IS NULL OR CONCAT(r.title, r.description) LIKE CONCAT('%', :keyword, '%')) ORDER BY r.id DESC diff --git a/backend/src/test/java/net/pengcook/recipe/service/RecipeServiceTest.java b/backend/src/test/java/net/pengcook/recipe/service/RecipeServiceTest.java index db33138f..2b4a0316 100644 --- a/backend/src/test/java/net/pengcook/recipe/service/RecipeServiceTest.java +++ b/backend/src/test/java/net/pengcook/recipe/service/RecipeServiceTest.java @@ -45,7 +45,7 @@ static Stream provideParameters() { @CsvSource(value = {"0,2,15", "1,2,13", "1,3,12"}) @DisplayName("요청받은 페이지의 레시피 개요 목록을 조회한다.") void readRecipes(int pageNumber, int pageSize, int expectedFirstRecipeId) { - PageRecipeRequest pageRecipeRequest = new PageRecipeRequest(pageNumber, pageSize); + PageRecipeRequest pageRecipeRequest = new PageRecipeRequest(pageNumber, pageSize, null, null); List mainRecipeResponses = recipeService.readRecipes(pageRecipeRequest); assertThat(mainRecipeResponses.getFirst().recipeId()).isEqualTo(expectedFirstRecipeId); @@ -56,7 +56,7 @@ void readRecipes(int pageNumber, int pageSize, int expectedFirstRecipeId) { void readRecipesWhenPageOffsetIsGreaterThanIntMaxValue() { int pageNumber = 1073741824; int pageSize = 2; - PageRecipeRequest pageRecipeRequest = new PageRecipeRequest(pageNumber, pageSize); + PageRecipeRequest pageRecipeRequest = new PageRecipeRequest(pageNumber, pageSize, null, null); assertThatThrownBy(() -> recipeService.readRecipes(pageRecipeRequest)) .isInstanceOf(InvalidParameterException.class); From 599ba67fedb08b60ada90b6fcb0dc11fd9d09715 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 8 Aug 2024 15:21:11 +0900 Subject: [PATCH 4/7] :sparkles: add userId dynamic query on read recipes --- .../java/net/pengcook/recipe/dto/PageRecipeRequest.java | 3 ++- .../net/pengcook/recipe/repository/RecipeRepository.java | 4 +++- .../java/net/pengcook/recipe/service/RecipeService.java | 3 ++- .../pengcook/recipe/controller/RecipeControllerTest.java | 5 +++-- .../pengcook/recipe/repository/RecipeRepositoryTest.java | 2 +- .../java/net/pengcook/recipe/service/RecipeServiceTest.java | 6 ++++-- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java b/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java index ef5d600a..9a180273 100644 --- a/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java +++ b/backend/src/main/java/net/pengcook/recipe/dto/PageRecipeRequest.java @@ -7,6 +7,7 @@ public record PageRecipeRequest( @Min(0) int pageNumber, @Min(1) int pageSize, @Nullable String category, - @Nullable String keyword + @Nullable String keyword, + @Nullable Long userId ) { } diff --git a/backend/src/main/java/net/pengcook/recipe/repository/RecipeRepository.java b/backend/src/main/java/net/pengcook/recipe/repository/RecipeRepository.java index 47c48ea2..f5029904 100644 --- a/backend/src/main/java/net/pengcook/recipe/repository/RecipeRepository.java +++ b/backend/src/main/java/net/pengcook/recipe/repository/RecipeRepository.java @@ -18,12 +18,14 @@ public interface RecipeRepository extends JpaRepository { LEFT JOIN Category c ON cr.category = c WHERE (:category IS NULL OR c.name = :category) AND (:keyword IS NULL OR CONCAT(r.title, r.description) LIKE CONCAT('%', :keyword, '%')) + AND (:userId IS NULL OR r.author.id = :userId) ORDER BY r.id DESC """) List findRecipeIdsByCategoryAndKeyword( + Pageable pageable, @Param("category") @Nullable String category, @Param("keyword") @Nullable String keyword, - Pageable pageable + @Param("userId") @Nullable Long userId ); @Query(""" diff --git a/backend/src/main/java/net/pengcook/recipe/service/RecipeService.java b/backend/src/main/java/net/pengcook/recipe/service/RecipeService.java index ab079a11..59a5ee6d 100644 --- a/backend/src/main/java/net/pengcook/recipe/service/RecipeService.java +++ b/backend/src/main/java/net/pengcook/recipe/service/RecipeService.java @@ -47,9 +47,10 @@ public class RecipeService { public List readRecipes(PageRecipeRequest pageRecipeRequest) { Pageable pageable = getValidatedPageable(pageRecipeRequest.pageNumber(), pageRecipeRequest.pageSize()); List recipeIds = recipeRepository.findRecipeIdsByCategoryAndKeyword( + pageable, pageRecipeRequest.category(), pageRecipeRequest.keyword(), - pageable + pageRecipeRequest.userId() ); List recipeDataResponses = recipeRepository.findRecipeData(recipeIds); diff --git a/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java b/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java index cad7b5ff..f029fe9c 100644 --- a/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java +++ b/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java @@ -42,10 +42,11 @@ void readRecipes() { "특정 페이지의 레시피 목록을 조회합니다.", "레시피 조회 API", queryParameters( + parameterWithName("pageNumber").description("페이지 번호"), + parameterWithName("pageSize").description("페이지 크기"), parameterWithName("category").description("조회 카테고리").optional(), parameterWithName("keyword").description("제목 또는 설명 검색 키워드").optional(), - parameterWithName("pageNumber").description("페이지 번호"), - parameterWithName("pageSize").description("페이지 크기") + parameterWithName("userId").description("작성자 아이디").optional() ), responseFields( fieldWithPath("[]").description("레시피 목록"), diff --git a/backend/src/test/java/net/pengcook/recipe/repository/RecipeRepositoryTest.java b/backend/src/test/java/net/pengcook/recipe/repository/RecipeRepositoryTest.java index 898c27d8..5295748f 100644 --- a/backend/src/test/java/net/pengcook/recipe/repository/RecipeRepositoryTest.java +++ b/backend/src/test/java/net/pengcook/recipe/repository/RecipeRepositoryTest.java @@ -28,7 +28,7 @@ class RecipeRepositoryTest { void findRecipeIdsByCategoryAndKeyword() { Pageable pageable = PageRequest.of(0, 3); - List recipeIds = repository.findRecipeIdsByCategoryAndKeyword(null, null, pageable); + List recipeIds = repository.findRecipeIdsByCategoryAndKeyword(pageable, null, null, null); assertThat(recipeIds).containsExactly(15L, 14L, 13L); } diff --git a/backend/src/test/java/net/pengcook/recipe/service/RecipeServiceTest.java b/backend/src/test/java/net/pengcook/recipe/service/RecipeServiceTest.java index 2b4a0316..da9b397d 100644 --- a/backend/src/test/java/net/pengcook/recipe/service/RecipeServiceTest.java +++ b/backend/src/test/java/net/pengcook/recipe/service/RecipeServiceTest.java @@ -45,7 +45,8 @@ static Stream provideParameters() { @CsvSource(value = {"0,2,15", "1,2,13", "1,3,12"}) @DisplayName("요청받은 페이지의 레시피 개요 목록을 조회한다.") void readRecipes(int pageNumber, int pageSize, int expectedFirstRecipeId) { - PageRecipeRequest pageRecipeRequest = new PageRecipeRequest(pageNumber, pageSize, null, null); + PageRecipeRequest pageRecipeRequest = new PageRecipeRequest( + pageNumber, pageSize, null, null, null); List mainRecipeResponses = recipeService.readRecipes(pageRecipeRequest); assertThat(mainRecipeResponses.getFirst().recipeId()).isEqualTo(expectedFirstRecipeId); @@ -56,7 +57,8 @@ void readRecipes(int pageNumber, int pageSize, int expectedFirstRecipeId) { void readRecipesWhenPageOffsetIsGreaterThanIntMaxValue() { int pageNumber = 1073741824; int pageSize = 2; - PageRecipeRequest pageRecipeRequest = new PageRecipeRequest(pageNumber, pageSize, null, null); + PageRecipeRequest pageRecipeRequest = new PageRecipeRequest( + pageNumber, pageSize, null, null, null); assertThatThrownBy(() -> recipeService.readRecipes(pageRecipeRequest)) .isInstanceOf(InvalidParameterException.class); From 0e75db03867eecb78f17c0cb50d851e4d278a0b3 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 8 Aug 2024 16:33:36 +0900 Subject: [PATCH 5/7] :white_check_mark: add keyword searching test --- .../controller/RecipeControllerTest.java | 94 ++++++------------- 1 file changed, 27 insertions(+), 67 deletions(-) diff --git a/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java b/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java index f029fe9c..b3ab1b96 100644 --- a/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java +++ b/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java @@ -379,85 +379,45 @@ void createRecipeStepWhenPreviousSequenceDoesNotExist() { @DisplayName("카테고리별 레시피 개요 목록을 조회한다.") void readRecipesOfCategory() { RestAssured.given(spec).log().all() - .filter(document(DEFAULT_RESTDOCS_PATH, - "특정 카테고리 페이지의 레시피 목록을 조회합니다.", - "카테고리별 레시피 조회 API", - queryParameters( - parameterWithName("category").description("카테고리"), - parameterWithName("pageNumber").description("페이지 번호"), - parameterWithName("pageSize").description("페이지 크기") - ), - responseFields( - fieldWithPath("[]").description("레시피 목록"), - fieldWithPath("[].recipeId").description("레시피 아이디"), - fieldWithPath("[].title").description("레시피 제목"), - fieldWithPath("[].author").description("작성자 정보"), - fieldWithPath("[].author.authorId").description("작성자 아이디"), - fieldWithPath("[].author.authorName").description("작성자 이름"), - fieldWithPath("[].author.authorImage").description("작성자 이미지"), - fieldWithPath("[].cookingTime").description("조리 시간"), - fieldWithPath("[].thumbnail").description("썸네일 이미지"), - fieldWithPath("[].difficulty").description("난이도"), - fieldWithPath("[].likeCount").description("좋아요 수"), - fieldWithPath("[].commentCount").description("댓글 수"), - fieldWithPath("[].description").description("레시피 설명"), - fieldWithPath("[].createdAt").description("레시피 생성일시"), - fieldWithPath("[].category").description("카테고리 목록"), - fieldWithPath("[].category[].categoryId").description("카테고리 아이디"), - fieldWithPath("[].category[].categoryName").description("카테고리 이름"), - fieldWithPath("[].ingredient").description("재료 목록"), - fieldWithPath("[].ingredient[].ingredientId").description("재료 아이디"), - fieldWithPath("[].ingredient[].ingredientName").description("재료 이름"), - fieldWithPath("[].ingredient[].requirement").description("재료 필수 여부") - ))) + .filter(document(DEFAULT_RESTDOCS_PATH)) + .queryParam("pageNumber", 0) + .queryParam("pageSize", 3) .queryParam("category", "한식") + .when().get("/recipes") + .then().log().all() + .body("size()", is(3)) + .body("[0].category[1].categoryName", is("한식")) + .body("[1].category[0].categoryName", is("한식")); + } + + @Test + @DisplayName("키워드로 레시피 개요 목록을 조회한다.") + void readRecipesOfKeyword() { + RestAssured.given(spec).log().all() + .filter(document(DEFAULT_RESTDOCS_PATH)) .queryParam("pageNumber", 0) .queryParam("pageSize", 3) - .when().get("/recipes/search") + .queryParam("keyword", "찌개") + .when().get("/recipes") .then().log().all() - .body("size()", is(3)); + .body("size()", is(2)) + .body("[0].title", is("된장찌개")) + .body("[1].title", is("김치찌개")); } @Test @DisplayName("사용자별 레시피 개요 목록을 조회한다.") void readRecipesOfUser() { RestAssured.given(spec).log().all() - .filter(document(DEFAULT_RESTDOCS_PATH, - "특정 사용자가 작성한 레시피 목록을 조회합니다.", - "사용자별 레시피 조회 API", - queryParameters( - parameterWithName("userId").description("사용자 아이디"), - parameterWithName("pageNumber").description("페이지 번호"), - parameterWithName("pageSize").description("페이지 크기") - ), - responseFields( - fieldWithPath("[]").description("레시피 목록"), - fieldWithPath("[].recipeId").description("레시피 아이디"), - fieldWithPath("[].title").description("레시피 제목"), - fieldWithPath("[].author").description("작성자 정보"), - fieldWithPath("[].author.authorId").description("작성자 아이디"), - fieldWithPath("[].author.authorName").description("작성자 이름"), - fieldWithPath("[].author.authorImage").description("작성자 이미지"), - fieldWithPath("[].cookingTime").description("조리 시간"), - fieldWithPath("[].thumbnail").description("썸네일 이미지"), - fieldWithPath("[].difficulty").description("난이도"), - fieldWithPath("[].likeCount").description("좋아요 수"), - fieldWithPath("[].commentCount").description("댓글 수"), - fieldWithPath("[].description").description("레시피 설명"), - fieldWithPath("[].createdAt").description("레시피 생성일시"), - fieldWithPath("[].category").description("카테고리 목록"), - fieldWithPath("[].category[].categoryId").description("카테고리 아이디"), - fieldWithPath("[].category[].categoryName").description("카테고리 이름"), - fieldWithPath("[].ingredient").description("재료 목록"), - fieldWithPath("[].ingredient[].ingredientId").description("재료 아이디"), - fieldWithPath("[].ingredient[].ingredientName").description("재료 이름"), - fieldWithPath("[].ingredient[].requirement").description("재료 필수 여부") - ))) - .queryParam("userId", 1L) + .filter(document(DEFAULT_RESTDOCS_PATH)) .queryParam("pageNumber", 1) .queryParam("pageSize", 3) - .when().get("/recipes/search/user") + .queryParam("userId", 1L) + .when().get("/recipes") .then().log().all() - .body("size()", is(3)); + .body("size()", is(3)) + .body("[0].author.authorId", is(1)) + .body("[1].author.authorId", is(1)) + .body("[2].author.authorId", is(1)); } } From 4d6034ace079de0c6cefcab58ca39b612626b4fa Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 8 Aug 2024 20:20:03 +0900 Subject: [PATCH 6/7] :sparkles: collect metric of host os with node-exporter --- .../main/resources/docker-compose-dev.yaml | 37 +++++++++++++------ .../main/resources/docker-compose-local.yaml | 35 ++++++++++++++---- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/backend/src/main/resources/docker-compose-dev.yaml b/backend/src/main/resources/docker-compose-dev.yaml index ae7e1efd..82ba2a01 100644 --- a/backend/src/main/resources/docker-compose-dev.yaml +++ b/backend/src/main/resources/docker-compose-dev.yaml @@ -1,7 +1,7 @@ volumes: db: grafana: - prometheus: + prom: services: db: @@ -37,11 +37,23 @@ services: restart: unless-stopped command: -config.file=/etc/loki/local-config.yaml - prometheus: + node: + image: prom/node-exporter + restart: unless-stopped + pid: host + command: + - '--path.rootfs=/host' + volumes: + - '/:/host:ro,rslave' + + prom: image: prom/prometheus restart: unless-stopped volumes: - - prometheus:/prometheus + - prom:/prometheus + links: + - app + - node entrypoint: - sh - -euc @@ -53,10 +65,13 @@ services: scrape_interval: 15s static_configs: - targets: ['app:8080'] + + - job_name: 'node' + scrape_interval: 15s + static_configs: + - targets: ['node:9100'] EOF /bin/prometheus - links: - - app grafana: image: grafana/grafana @@ -65,6 +80,11 @@ services: - grafana:/var/lib/grafana environment: - GF_SERVER_ROOT_URL=https://dev.mon.pengcook.net + ports: + - 3000:3000 + links: + - loki + - prom entrypoint: - sh - -euc @@ -87,14 +107,9 @@ services: type: prometheus access: proxy orgId: 1 - url: http://prometheus:9090 + url: http://prom:9090 basicAuth: false isDefault: false editable: false EOF /run.sh - ports: - - 3000:3000 - links: - - loki - - prometheus diff --git a/backend/src/main/resources/docker-compose-local.yaml b/backend/src/main/resources/docker-compose-local.yaml index 320ba885..b0331637 100644 --- a/backend/src/main/resources/docker-compose-local.yaml +++ b/backend/src/main/resources/docker-compose-local.yaml @@ -1,7 +1,7 @@ volumes: db: grafana: - prometheus: + prom: services: db: @@ -27,13 +27,26 @@ services: - 3100:3100 command: -config.file=/etc/loki/local-config.yaml - prometheus: + node: + image: prom/node-exporter + restart: unless-stopped + pid: host + ports: + - 9100:9100 + command: + - '--path.rootfs=/host' + volumes: + - '/:/host:ro' + + prom: image: prom/prometheus restart: unless-stopped volumes: - - prometheus:/prometheus + - prom:/prometheus ports: - 9090:9090 + links: + - node entrypoint: - sh - -euc @@ -45,6 +58,11 @@ services: scrape_interval: 15s static_configs: - targets: ['host.docker.internal:8080'] + + - job_name: 'node' + scrape_interval: 15s + static_configs: + - targets: ['node_exporter:9100'] EOF /bin/prometheus @@ -56,6 +74,11 @@ services: environment: - GF_AUTH_ANONYMOUS_ENABLED=true - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + ports: + - 3000:3000 + links: + - loki + - prom entrypoint: - sh - -euc @@ -78,13 +101,9 @@ services: type: prometheus access: proxy orgId: 1 - url: http://prometheus:9090 + url: http://prom:9090 basicAuth: false isDefault: false editable: false EOF /run.sh - ports: - - 3000:3000 - links: - - loki From 01082b1160f82688060ab80c73b8b119c6ae4b4c Mon Sep 17 00:00:00 2001 From: hyun Date: Wed, 14 Aug 2024 14:21:58 +0900 Subject: [PATCH 7/7] :recycle: register recipe with steps at once --- .../recipe/controller/RecipeController.java | 15 -- .../pengcook/recipe/dto/RecipeRequest.java | 3 +- .../recipe/dto/RecipeStepResponse.java | 3 +- .../recipe/service/RecipeService.java | 2 + .../recipe/service/RecipeStepService.java | 59 ++---- .../controller/RecipeControllerTest.java | 180 +++--------------- .../recipe/service/RecipeServiceTest.java | 8 +- .../recipe/service/RecipeStepServiceTest.java | 101 ++++------ 8 files changed, 84 insertions(+), 287 deletions(-) diff --git a/backend/src/main/java/net/pengcook/recipe/controller/RecipeController.java b/backend/src/main/java/net/pengcook/recipe/controller/RecipeController.java index 60cab6c6..e357a165 100644 --- a/backend/src/main/java/net/pengcook/recipe/controller/RecipeController.java +++ b/backend/src/main/java/net/pengcook/recipe/controller/RecipeController.java @@ -11,7 +11,6 @@ import net.pengcook.recipe.dto.RecipeOfUserRequest; import net.pengcook.recipe.dto.RecipeRequest; import net.pengcook.recipe.dto.RecipeResponse; -import net.pengcook.recipe.dto.RecipeStepRequest; import net.pengcook.recipe.dto.RecipeStepResponse; import net.pengcook.recipe.service.RecipeService; import net.pengcook.recipe.service.RecipeStepService; @@ -62,18 +61,4 @@ public List readRecipesOfUser( public List readRecipeSteps(@PathVariable long recipeId) { return recipeStepService.readRecipeSteps(recipeId); } - - @GetMapping("/{recipeId}/steps/{sequence}") - public RecipeStepResponse readRecipeStep(@PathVariable long recipeId, @PathVariable long sequence) { - return recipeStepService.readRecipeStep(recipeId, sequence); - } - - @PostMapping("/{recipeId}/steps") - @ResponseStatus(HttpStatus.CREATED) - public RecipeStepResponse createRecipeStep( - @PathVariable long recipeId, - @RequestBody @Valid RecipeStepRequest recipeStepRequest - ) { - return recipeStepService.createRecipeStep(recipeId, recipeStepRequest); - } } diff --git a/backend/src/main/java/net/pengcook/recipe/dto/RecipeRequest.java b/backend/src/main/java/net/pengcook/recipe/dto/RecipeRequest.java index b6a74a85..b219d1b2 100644 --- a/backend/src/main/java/net/pengcook/recipe/dto/RecipeRequest.java +++ b/backend/src/main/java/net/pengcook/recipe/dto/RecipeRequest.java @@ -14,6 +14,7 @@ public record RecipeRequest( @Min(0) @Max(10) int difficulty, @NotBlank String description, @NotEmpty List categories, - @NotEmpty List ingredients + @NotEmpty List ingredients, + List recipeSteps ) { } diff --git a/backend/src/main/java/net/pengcook/recipe/dto/RecipeStepResponse.java b/backend/src/main/java/net/pengcook/recipe/dto/RecipeStepResponse.java index c3a06f7d..0d4b9edf 100644 --- a/backend/src/main/java/net/pengcook/recipe/dto/RecipeStepResponse.java +++ b/backend/src/main/java/net/pengcook/recipe/dto/RecipeStepResponse.java @@ -9,7 +9,8 @@ public record RecipeStepResponse( String image, String description, int sequence, - LocalTime cookingTime) { + LocalTime cookingTime +) { public RecipeStepResponse(RecipeStep recipeStep) { this( diff --git a/backend/src/main/java/net/pengcook/recipe/service/RecipeService.java b/backend/src/main/java/net/pengcook/recipe/service/RecipeService.java index 59a5ee6d..c85a595c 100644 --- a/backend/src/main/java/net/pengcook/recipe/service/RecipeService.java +++ b/backend/src/main/java/net/pengcook/recipe/service/RecipeService.java @@ -43,6 +43,7 @@ public class RecipeService { private final CategoryService categoryService; private final IngredientService ingredientService; private final S3ClientService s3ClientService; + private final RecipeStepService recipeStepService; public List readRecipes(PageRecipeRequest pageRecipeRequest) { Pageable pageable = getValidatedPageable(pageRecipeRequest.pageNumber(), pageRecipeRequest.pageSize()); @@ -72,6 +73,7 @@ public RecipeResponse createRecipe(UserInfo userInfo, RecipeRequest recipeReques Recipe savedRecipe = recipeRepository.save(recipe); categoryService.saveCategories(savedRecipe, recipeRequest.categories()); ingredientService.register(recipeRequest.ingredients(), savedRecipe); + recipeStepService.saveRecipeSteps(savedRecipe.getId(), recipeRequest.recipeSteps()); return new RecipeResponse(savedRecipe); } diff --git a/backend/src/main/java/net/pengcook/recipe/service/RecipeStepService.java b/backend/src/main/java/net/pengcook/recipe/service/RecipeStepService.java index adf99e83..9c3d0eaf 100644 --- a/backend/src/main/java/net/pengcook/recipe/service/RecipeStepService.java +++ b/backend/src/main/java/net/pengcook/recipe/service/RecipeStepService.java @@ -31,30 +31,25 @@ public List readRecipeSteps(long recipeId) { return convertToRecipeStepResponses(recipeSteps); } - public RecipeStepResponse readRecipeStep(long recipeId, long sequence) { - RecipeStep recipeStep = recipeStepRepository.findByRecipeIdAndSequence(recipeId, sequence) - .orElseThrow(() -> new NotFoundException("해당되는 레시피 스텝 정보가 없습니다.")); - - return new RecipeStepResponse(recipeStep); + public void saveRecipeSteps(Long savedRecipeId, List recipeStepRequests) { + Recipe savedRecipe = recipeRepository.findById(savedRecipeId) + .orElseThrow(() -> new NotFoundException("해당되는 레시피가 없습니다.")); + recipeStepRequests.forEach(recipeStepRequest -> saveRecipeStep(savedRecipe, recipeStepRequest)); } - public RecipeStepResponse createRecipeStep(long recipeId, RecipeStepRequest recipeStepRequest) { - validateRecipeStepSequence(recipeId, recipeStepRequest.sequence()); - Recipe recipe = getRecipeByRecipeId(recipeId); + private void saveRecipeStep(Recipe savedRecipe, RecipeStepRequest recipeStepRequest) { String imageUrl = getValidatedImageUrl(recipeStepRequest.image()); - String description = recipeStepRequest.description(); LocalTime cookingTime = getValidatedCookingTime(recipeStepRequest.cookingTime()); - Optional existingRecipeStep = recipeStepRepository.findByRecipeIdAndSequence( - recipeId, - recipeStepRequest.sequence() + RecipeStep recipeStep = new RecipeStep( + savedRecipe, + imageUrl, + recipeStepRequest.description(), + recipeStepRequest.sequence(), + cookingTime ); - RecipeStep recipeStep = existingRecipeStep - .map(currentRecipeStep -> currentRecipeStep.update(imageUrl, description, cookingTime)) - .orElseGet(() -> saveRecipeStep(recipe, imageUrl, recipeStepRequest, cookingTime)); - - return new RecipeStepResponse(recipeStep); + recipeStepRepository.save(recipeStep); } private List convertToRecipeStepResponses(List recipeSteps) { @@ -63,19 +58,6 @@ private List convertToRecipeStepResponses(List r .toList(); } - private void validateRecipeStepSequence(long recipeId, int sequence) { - int previousSequence = sequence - 1; - if (previousSequence >= 1) { - recipeStepRepository.findByRecipeIdAndSequence(recipeId, previousSequence) - .orElseThrow(() -> new InvalidParameterException("이전 스텝이 등록되지 않았습니다.")); - } - } - - private Recipe getRecipeByRecipeId(long recipeId) { - return recipeRepository.findById(recipeId) - .orElseThrow(() -> new NotFoundException("해당되는 레시피가 없습니다.")); - } - private String getValidatedImageUrl(String image) { if (image == null) { return null; @@ -95,21 +77,4 @@ private LocalTime getValidatedCookingTime(String cookingTime) { throw new InvalidParameterException("적절하지 않은 조리시간입니다."); } } - - private RecipeStep saveRecipeStep( - Recipe recipe, - String imageUrl, - RecipeStepRequest recipeStepRequest, - LocalTime cookingTime - ) { - RecipeStep recipeStep = new RecipeStep( - recipe, - imageUrl, - recipeStepRequest.description(), - recipeStepRequest.sequence(), - cookingTime - ); - - return recipeStepRepository.save(recipeStep); - } } diff --git a/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java b/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java index b3ab1b96..47bbf4ed 100644 --- a/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java +++ b/backend/src/test/java/net/pengcook/recipe/controller/RecipeControllerTest.java @@ -1,9 +1,7 @@ package net.pengcook.recipe.controller; import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document; -import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertAll; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -21,7 +19,6 @@ import net.pengcook.ingredient.dto.IngredientCreateRequest; import net.pengcook.recipe.dto.RecipeRequest; import net.pengcook.recipe.dto.RecipeStepRequest; -import net.pengcook.recipe.dto.RecipeStepResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -34,6 +31,8 @@ @Sql(value = "/data/recipe.sql") class RecipeControllerTest extends RestDocsSetting { + private static final int INITIAL_RECIPE_COUNT = 15; + @Test @DisplayName("레시피 개요 목록을 조회한다.") void readRecipes() { @@ -127,6 +126,10 @@ void createRecipe() { new IngredientCreateRequest("Apple", Requirement.REQUIRED, substitutions), new IngredientCreateRequest("WaterMelon", Requirement.OPTIONAL, null) ); + List recipeStepRequests = List.of( + new RecipeStepRequest("스텝1 이미지.jpg", "스텝1 설명", 1, "00:10:00"), + new RecipeStepRequest(null, "스텝2 설명", 2, "00:20:00") + ); RecipeRequest recipeRequest = new RecipeRequest( "새로운 레시피 제목", "00:30:00", @@ -134,12 +137,13 @@ void createRecipe() { 4, "새로운 레시피 설명", categories, - ingredients + ingredients, + recipeStepRequests ); RestAssured.given(spec).log().all() .filter(document(DEFAULT_RESTDOCS_PATH, - "새로운 레시피 개요를 등록합니다.", + "새로운 레시피를 등록합니다.", "신규 레시피 생성 API", requestFields( fieldWithPath("title").description("레시피 제목"), @@ -151,7 +155,11 @@ void createRecipe() { fieldWithPath("ingredients[]").description("재료 목록"), fieldWithPath("ingredients[].name").description("재료 이름"), fieldWithPath("ingredients[].requirement").description("재료 필수 여부"), - fieldWithPath("ingredients[].substitutions").description("대체 재료 목록").optional() + fieldWithPath("ingredients[].substitutions").description("대체 재료 목록").optional(), + fieldWithPath("recipeSteps[].image").description("레시피 스텝 이미지").optional(), + fieldWithPath("recipeSteps[].description").description("레시피 스텝 설명"), + fieldWithPath("recipeSteps[].sequence").description("레시피 스텝 순서"), + fieldWithPath("recipeSteps[].cookingTime").description("레시피 스텝 소요시간") ), responseFields( fieldWithPath("recipeId").description("생성된 레시피 아이디") ))) @@ -160,7 +168,7 @@ void createRecipe() { .when().post("/recipes") .then().log().all() .statusCode(201) - .body("recipeId", is(16)); + .body("recipeId", is(INITIAL_RECIPE_COUNT + 1)); } @ParameterizedTest @@ -174,6 +182,10 @@ void createRecipeWhenInvalidValue(int difficulty) { new IngredientCreateRequest("Apple", Requirement.REQUIRED, substitutions), new IngredientCreateRequest("WaterMelon", Requirement.OPTIONAL, null) ); + List recipeStepRequests = List.of( + new RecipeStepRequest("스텝1 이미지.jpg", "스텝1 설명", 1, "00:10:00"), + new RecipeStepRequest(null, "스텝2 설명", 2, "00:20:00") + ); RecipeRequest recipeRequest = new RecipeRequest( "새로운 레시피 제목", "00:30:00", @@ -181,7 +193,8 @@ void createRecipeWhenInvalidValue(int difficulty) { difficulty, "새로운 레시피 설명", categories, - ingredients + ingredients, + recipeStepRequests ); RestAssured.given(spec).log().all() @@ -196,7 +209,11 @@ void createRecipeWhenInvalidValue(int difficulty) { fieldWithPath("ingredients[]").description("재료 목록"), fieldWithPath("ingredients[].name").description("재료 이름"), fieldWithPath("ingredients[].requirement").description("재료 필수 여부"), - fieldWithPath("ingredients[].substitutions").description("대체 재료 목록").optional() + fieldWithPath("ingredients[].substitutions").description("대체 재료 목록").optional(), + fieldWithPath("recipeSteps[].image").description("레시피 스텝 이미지").optional(), + fieldWithPath("recipeSteps[].description").description("레시피 스텝 설명"), + fieldWithPath("recipeSteps[].sequence").description("레시피 스텝 순서"), + fieldWithPath("recipeSteps[].cookingTime").description("레시피 스텝 소요시간") ))) .contentType(ContentType.JSON) .body(recipeRequest) @@ -230,151 +247,6 @@ void readRecipeSteps() { .body("size()", is(3)); } - @Test - @DisplayName("특정 레시피의 특정 스텝을 조회한다.") - void readRecipeStep() { - RestAssured.given(spec).log().all() - .filter(document(DEFAULT_RESTDOCS_PATH, - "특정 레시피의 특정 스텝을 조회합니다.", - "특정 레시피 특정 스텝 조회 API", - pathParameters( - parameterWithName("recipeId").description("조회할 레시피 아이디"), - parameterWithName("sequence").description("조회할 스텝 순서") - ), - responseFields( - fieldWithPath("id").description("레시피 스텝 아이디"), - fieldWithPath("recipeId").description("레시피 아이디"), - fieldWithPath("image").description("레시피 스텝 이미지"), - fieldWithPath("description").description("레시피 스텝 설명"), - fieldWithPath("sequence").description("레시피 스텝 순서"), - fieldWithPath("cookingTime").description("레시피 스텝 소요시간") - ))) - .when() - .get("/recipes/{recipeId}/steps/{sequence}", 1L, 1L) - .then().log().all() - .statusCode(200) - .body("description", is("레시피1 설명1")); - } - - @Test - @DisplayName("특정 레시피의 레시피 스텝을 생성한다.") - void createRecipeStep() { - RecipeStepRequest recipeStepRequest = new RecipeStepRequest("신규 스텝 이미지.jpg", "신규 스텝 설명", 4, "00:05:00"); - - RestAssured.given(spec).log().all() - .filter(document(DEFAULT_RESTDOCS_PATH, - "특정 레시피의 레시피 스텝을 생성합니다.", - "특정 레시피 레시피 스텝 생성 API", - pathParameters( - parameterWithName("recipeId").description("레시피 스텝을 추가할 레시피 아이디") - ), - requestFields( - fieldWithPath("image").description("레시피 스텝 이미지"), - fieldWithPath("description").description("레시피 스텝 설명"), - fieldWithPath("sequence").description("레시피 스텝 순서"), - fieldWithPath("cookingTime").description("레시피 스텝 소요시간") - ), - responseFields( - fieldWithPath("id").description("레시피 스텝 아이디"), - fieldWithPath("recipeId").description("레시피 아이디"), - fieldWithPath("image").description("레시피 스텝 이미지"), - fieldWithPath("description").description("레시피 스텝 설명"), - fieldWithPath("sequence").description("레시피 스텝 순서"), - fieldWithPath("cookingTime").description("레시피 스텝 소요시간") - ))) - .contentType(ContentType.JSON) - .body(recipeStepRequest) - .when() - .post("/recipes/{recipeId}/steps", 1L) - .then().log().all() - .statusCode(201); - } - - @Test - @DisplayName("특정 레시피의 레시피 스텝 생성 시 올바르지 않은 필드 값을 입력하면 예외가 발생한다.") - void createRecipeStepWhenInvalidValue() { - RecipeStepRequest recipeStepRequest = new RecipeStepRequest("신규 스텝 이미지.jpg", "", 4, "00:05:00"); - - RestAssured.given(spec).log().all() - .filter(document(DEFAULT_RESTDOCS_PATH, - pathParameters( - parameterWithName("recipeId").description("레시피 스텝을 추가할 레시피 아이디") - ), - requestFields( - fieldWithPath("image").description("레시피 스텝 이미지"), - fieldWithPath("description").description("레시피 스텝 설명"), - fieldWithPath("sequence").description("레시피 스텝 순서"), - fieldWithPath("cookingTime").description("레시피 스텝 소요시간") - ))) - .contentType(ContentType.JSON) - .body(recipeStepRequest) - .when() - .post("/recipes/{recipeId}/steps", 1L) - .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()); - } - - @Test - @DisplayName("레시피 스텝 등록 시 이미 존재하는 정보가 있으면 새로운 내용으로 수정한다.") - void createRecipeStepWithExistingRecipeStep() { - RecipeStepRequest recipeStepRequest = new RecipeStepRequest( - "changedImage.jpg", - "changedDescription", - 1, - "00:15:00" - ); - - RecipeStepResponse recipeStepResponse = RestAssured.given(spec).log().all() - .filter(document(DEFAULT_RESTDOCS_PATH, - pathParameters( - parameterWithName("recipeId").description("레시피 스텝을 추가할 레시피 아이디") - ), - requestFields( - fieldWithPath("image").description("레시피 스텝 이미지"), - fieldWithPath("description").description("레시피 스텝 설명"), - fieldWithPath("sequence").description("레시피 스텝 순서"), - fieldWithPath("cookingTime").description("레시피 스텝 소요시간") - ))) - .contentType(ContentType.JSON) - .body(recipeStepRequest) - .when() - .post("/recipes/{recipeId}/steps", 1L) - .then().log().all() - .statusCode(201) - .extract() - .as(RecipeStepResponse.class); - - assertAll( - () -> assertThat(recipeStepResponse.id()).isEqualTo(1L), - () -> assertThat(recipeStepResponse.description()).isEqualTo("changedDescription"), - () -> assertThat(recipeStepResponse.image()).endsWith("changedImage.jpg") - ); - } - - @Test - @DisplayName("레시피 스텝 등록 시 이전 sequence 정보가 없으면 예외가 발생한다.") - void createRecipeStepWhenPreviousSequenceDoesNotExist() { - RecipeStepRequest recipeStepRequest = new RecipeStepRequest("신규 스텝 이미지.jpg", "신규 스텝 설명", 5, "00:05:00"); - - RestAssured.given(spec).log().all() - .filter(document(DEFAULT_RESTDOCS_PATH, - pathParameters( - parameterWithName("recipeId").description("레시피 스텝을 추가할 레시피 아이디") - ), - requestFields( - fieldWithPath("image").description("레시피 스텝 이미지"), - fieldWithPath("description").description("레시피 스텝 설명"), - fieldWithPath("sequence").description("레시피 스텝 순서"), - fieldWithPath("cookingTime").description("레시피 스텝 소요시간") - ))) - .contentType(ContentType.JSON) - .body(recipeStepRequest) - .when() - .post("/recipes/{recipeId}/steps", 1L) - .then().log().all() - .statusCode(400); - } - @Test @DisplayName("카테고리별 레시피 개요 목록을 조회한다.") void readRecipesOfCategory() { diff --git a/backend/src/test/java/net/pengcook/recipe/service/RecipeServiceTest.java b/backend/src/test/java/net/pengcook/recipe/service/RecipeServiceTest.java index da9b397d..95a2677f 100644 --- a/backend/src/test/java/net/pengcook/recipe/service/RecipeServiceTest.java +++ b/backend/src/test/java/net/pengcook/recipe/service/RecipeServiceTest.java @@ -15,6 +15,7 @@ import net.pengcook.recipe.dto.RecipeOfUserRequest; import net.pengcook.recipe.dto.RecipeRequest; import net.pengcook.recipe.dto.RecipeResponse; +import net.pengcook.recipe.dto.RecipeStepRequest; import net.pengcook.recipe.exception.InvalidParameterException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -75,6 +76,10 @@ void createRecipe() { new IngredientCreateRequest("Apple", Requirement.REQUIRED, substitutions), new IngredientCreateRequest("WaterMelon", Requirement.OPTIONAL, null) ); + List recipeStepRequests = List.of( + new RecipeStepRequest(null, "스텝1 설명", 1, "00:20:00"), + new RecipeStepRequest(null, "스텝2 설명", 2, "00:30:00") + ); RecipeRequest recipeRequest = new RecipeRequest( "새로운 레시피 제목", "00:30:00", @@ -82,7 +87,8 @@ void createRecipe() { 4, "새로운 레시피 설명", categories, - ingredients + ingredients, + recipeStepRequests ); RecipeResponse recipe = recipeService.createRecipe(userInfo, recipeRequest); diff --git a/backend/src/test/java/net/pengcook/recipe/service/RecipeStepServiceTest.java b/backend/src/test/java/net/pengcook/recipe/service/RecipeStepServiceTest.java index a140d783..51de6bf9 100644 --- a/backend/src/test/java/net/pengcook/recipe/service/RecipeStepServiceTest.java +++ b/backend/src/test/java/net/pengcook/recipe/service/RecipeStepServiceTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; import java.time.LocalTime; import java.util.Arrays; @@ -10,6 +9,7 @@ import net.pengcook.recipe.dto.RecipeStepRequest; import net.pengcook.recipe.dto.RecipeStepResponse; import net.pengcook.recipe.exception.InvalidParameterException; +import net.pengcook.recipe.repository.RecipeStepRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -22,8 +22,12 @@ @Sql(value = "/data/recipe.sql") class RecipeStepServiceTest { + private static final int INITIAL_RECIPE_STEP_COUNT = 3; + @Autowired private RecipeStepService recipeStepService; + @Autowired + private RecipeStepRepository recipeStepRepository; @Test @DisplayName("특정 레시피의 스텝을 sequence 순서로 조회한다.") @@ -40,103 +44,64 @@ void readRecipeSteps() { assertThat(recipeStepResponses).isEqualTo(expectedRecipeStepResponses); } - @Test - @DisplayName("특정 레시피의 특정 레시피 스텝을 조회한다.") - void readRecipeStep() { - RecipeStepResponse recipeStepResponse = recipeStepService.readRecipeStep(1L, 1L); - - assertAll( - () -> assertThat(recipeStepResponse.recipeId()).isEqualTo(1L), - () -> assertThat(recipeStepResponse.description()).isEqualTo("레시피1 설명1") - ); - - } - @Test @DisplayName("특정 레시피의 레시피 스텝을 생성한다.") - void createRecipeStep() { - RecipeStepRequest recipeStepRequest = new RecipeStepRequest("새로운 스텝 이미지.jpg", "새로운 스텝 설명", 1, "00:05:00"); + void saveRecipeSteps() { + List recipeStepRequests = List.of( + new RecipeStepRequest("새로운 스텝 이미지1.jpg", "새로운 스텝 설명1", 1, "00:05:00"), + new RecipeStepRequest("새로운 스텝 이미지2.jpg", "새로운 스텝 설명2", 2, "00:05:00") + ); - RecipeStepResponse recipeStepResponse = recipeStepService.createRecipeStep(2L, recipeStepRequest); + recipeStepService.saveRecipeSteps(2L, recipeStepRequests); - assertAll( - () -> assertThat(recipeStepResponse.recipeId()).isEqualTo(2L), - () -> assertThat(recipeStepResponse.description()).isEqualTo("새로운 스텝 설명") - ); + assertThat(recipeStepRepository.count()).isEqualTo(INITIAL_RECIPE_STEP_COUNT + recipeStepRequests.size()); } @Test @DisplayName("레시피 스텝 생성 요청 이미지 값이 null이어도 정상적으로 생성하고, 이미지에 null을 저장한다.") - void createRecipeStepWithNullImage() { - RecipeStepRequest recipeStepRequest = new RecipeStepRequest(null, "새로운 스텝 설명", 1, "00:05:00"); + void saveRecipeStepsWithNullImage() { + List recipeStepRequests = List.of( + new RecipeStepRequest(null, "새로운 스텝 설명1", 1, "00:05:00") + ); - RecipeStepResponse recipeStepResponse = recipeStepService.createRecipeStep(2L, recipeStepRequest); + recipeStepService.saveRecipeSteps(2L, recipeStepRequests); - assertAll( - () -> assertThat(recipeStepResponse.recipeId()).isEqualTo(2L), - () -> assertThat(recipeStepResponse.image()).isNull(), - () -> assertThat(recipeStepResponse.description()).isEqualTo("새로운 스텝 설명") - ); + assertThat(recipeStepRepository.count()).isEqualTo(INITIAL_RECIPE_STEP_COUNT + recipeStepRequests.size()); } @ParameterizedTest @ValueSource(strings = {"", " ", " "}) @DisplayName("레시피 스텝 생성 요청 이미지 값이 빈 문자열이거나 공백만 있을 경우 예외가 발생한다.") - void createRecipeStepWhenBlankImage(String image) { - RecipeStepRequest recipeStepRequest = new RecipeStepRequest(image, "새로운 스텝 설명", 1, "00:05:00"); + void saveRecipeStepsWhenBlankImage(String image) { + List recipeStepRequests = List.of( + new RecipeStepRequest(image, "새로운 스텝 설명1", 1, "00:05:00") + ); - assertThatThrownBy(() -> recipeStepService.createRecipeStep(2L, recipeStepRequest)) + assertThatThrownBy(() -> recipeStepService.saveRecipeSteps(2L, recipeStepRequests)) .isInstanceOf(InvalidParameterException.class); } @Test @DisplayName("레시피 스텝 생성 요청 조리시간 값이 null이어도 정상적으로 생성하고, 조리시간에 null을 저장한다.") - void createRecipeStepWithNullCookingTime() { - RecipeStepRequest recipeStepRequest = new RecipeStepRequest("image.jpg", "스텝 설명", 1, null); + void saveRecipeStepsWithNullCookingTime() { + List recipeStepRequests = List.of( + new RecipeStepRequest("레시피1 설명1 이미지", "새로운 스텝 설명1", 1, null) + ); - RecipeStepResponse recipeStepResponse = recipeStepService.createRecipeStep(2L, recipeStepRequest); + recipeStepService.saveRecipeSteps(2L, recipeStepRequests); - assertAll( - () -> assertThat(recipeStepResponse.recipeId()).isEqualTo(2L), - () -> assertThat(recipeStepResponse.cookingTime()).isNull() - ); + assertThat(recipeStepRepository.count()).isEqualTo(INITIAL_RECIPE_STEP_COUNT + recipeStepRequests.size()); } @ParameterizedTest @ValueSource(strings = {"", " ", "aa:bb", "test"}) @DisplayName("레시피 스텝 생성 요청 조리시간의 형태가 HH:mm:ss가 아닌 경우 예외가 발생한다.") - void createRecipeStepWhenInappropriateCookingTime(String cookingTime) { - RecipeStepRequest recipeStepRequest = new RecipeStepRequest("image.jpg", "새로운 스텝 설명", 1, cookingTime); - - assertThatThrownBy(() -> recipeStepService.createRecipeStep(2L, recipeStepRequest)) - .isInstanceOf(InvalidParameterException.class); - } - - @Test - @DisplayName("레시피 스텝 등록 시 이미 존재하는 정보가 있으면 새로운 내용으로 수정한다.") - void createRecipeStepWithExistingRecipeStep() { - RecipeStepRequest recipeStepRequest = new RecipeStepRequest( - "changedImage.jpg", - "changedDescription", - 1, - "00:05:00" + void saveRecipeStepsWhenInappropriateCookingTime(String cookingTime) { + List recipeStepRequests = List.of( + new RecipeStepRequest("image.jpg", "새로운 스텝 설명1", 1, cookingTime) ); - RecipeStepResponse recipeStepResponse = recipeStepService.createRecipeStep(1L, recipeStepRequest); - - assertAll( - () -> assertThat(recipeStepResponse.id()).isEqualTo(1L), - () -> assertThat(recipeStepResponse.description()).isEqualTo("changedDescription"), - () -> assertThat(recipeStepResponse.image()).endsWith("changedImage.jpg") - ); - } - - @Test - @DisplayName("레시피 스텝 등록 시 이전 sequence 정보가 없으면 예외가 발생한다.") - void createRecipeWhenPreviousSequenceDoesNotExist() { - RecipeStepRequest recipeStepRequest = new RecipeStepRequest("새로운 스텝 이미지.jpg", "새로운 스텝 설명", 2, "00:05:00"); - - assertThatThrownBy(() -> recipeStepService.createRecipeStep(2L, recipeStepRequest)) + assertThatThrownBy(() -> recipeStepService.saveRecipeSteps(2L, recipeStepRequests)) .isInstanceOf(InvalidParameterException.class); } }