From d901310897e9677a6d3a5b27c37a6df8e18c3665 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 10 Apr 2026 12:03:03 +0200 Subject: [PATCH] feat(backend): add heroImageUrl and tags to RecipeSummaryResponse GET /v1/recipes was returning RecipeSummaryResponse with no tags and only heroImagePreview. The planner frontend needs protein tags to pick gradient backgrounds for tiles without a hero image. - Replace JPQL constructor projection with entity query + LEFT JOIN FETCH tags - Map Recipe entity to RecipeSummaryResponse in service (includes tags + heroImageUrl) - Drop heroImagePreview in favour of heroImageUrl on the summary DTO Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/recipeapp/recipe/RecipeRepository.java | 8 +++----- .../main/java/com/recipeapp/recipe/RecipeService.java | 10 +++++++++- .../recipeapp/recipe/dto/RecipeSummaryResponse.java | 4 +++- .../com/recipeapp/recipe/RecipeControllerTest.java | 6 +++++- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/com/recipeapp/recipe/RecipeRepository.java b/backend/src/main/java/com/recipeapp/recipe/RecipeRepository.java index 6b7a3d0..eb81386 100644 --- a/backend/src/main/java/com/recipeapp/recipe/RecipeRepository.java +++ b/backend/src/main/java/com/recipeapp/recipe/RecipeRepository.java @@ -1,6 +1,5 @@ package com.recipeapp.recipe; -import com.recipeapp.recipe.dto.RecipeSummaryResponse; import com.recipeapp.recipe.entity.Recipe; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -17,9 +16,8 @@ public interface RecipeRepository extends JpaRepository { List findByHouseholdIdAndDeletedAtIsNull(UUID householdId); @Query(""" - SELECT new com.recipeapp.recipe.dto.RecipeSummaryResponse( - r.id, r.name, r.serves, r.cookTimeMin, r.effort, r.heroImagePreview) - FROM Recipe r + SELECT r FROM Recipe r + LEFT JOIN FETCH r.tags WHERE r.household.id = :householdId AND r.deletedAt IS NULL AND (:search IS NULL OR LOWER(r.name) LIKE LOWER(CONCAT('%', CAST(:search AS string), '%'))) @@ -27,7 +25,7 @@ public interface RecipeRepository extends JpaRepository { AND (:cookTimeMaxMin IS NULL OR r.cookTimeMin <= :cookTimeMaxMin) ORDER BY r.createdAt DESC """) - List findFiltered( + List findFiltered( @Param("householdId") UUID householdId, @Param("search") String search, @Param("effort") String effort, diff --git a/backend/src/main/java/com/recipeapp/recipe/RecipeService.java b/backend/src/main/java/com/recipeapp/recipe/RecipeService.java index 6e3084b..28e4091 100644 --- a/backend/src/main/java/com/recipeapp/recipe/RecipeService.java +++ b/backend/src/main/java/com/recipeapp/recipe/RecipeService.java @@ -42,7 +42,15 @@ public class RecipeService { @Transactional(readOnly = true) public List listRecipes(UUID householdId, String search, String effort, Integer cookTimeMaxMin, String sort, int limit, int offset) { - return recipeRepository.findFiltered(householdId, search, effort, cookTimeMaxMin, sort, limit, offset); + return recipeRepository.findFiltered(householdId, search, effort, cookTimeMaxMin, sort, limit, offset) + .stream() + .map(r -> new RecipeSummaryResponse( + r.getId(), r.getName(), r.getServes(), r.getCookTimeMin(), r.getEffort(), + r.getHeroImageUrl(), + r.getTags().stream() + .map(t -> new TagResponse(t.getId(), t.getName(), t.getTagType())) + .toList())) + .toList(); } @Transactional(readOnly = true) diff --git a/backend/src/main/java/com/recipeapp/recipe/dto/RecipeSummaryResponse.java b/backend/src/main/java/com/recipeapp/recipe/dto/RecipeSummaryResponse.java index c128982..a3fd8c2 100644 --- a/backend/src/main/java/com/recipeapp/recipe/dto/RecipeSummaryResponse.java +++ b/backend/src/main/java/com/recipeapp/recipe/dto/RecipeSummaryResponse.java @@ -1,5 +1,6 @@ package com.recipeapp.recipe.dto; +import java.util.List; import java.util.UUID; public record RecipeSummaryResponse( @@ -8,5 +9,6 @@ public record RecipeSummaryResponse( short serves, short cookTimeMin, String effort, - String heroImagePreview + String heroImageUrl, + List tags ) {} diff --git a/backend/src/test/java/com/recipeapp/recipe/RecipeControllerTest.java b/backend/src/test/java/com/recipeapp/recipe/RecipeControllerTest.java index 0a1b94b..1e79817 100644 --- a/backend/src/test/java/com/recipeapp/recipe/RecipeControllerTest.java +++ b/backend/src/test/java/com/recipeapp/recipe/RecipeControllerTest.java @@ -46,8 +46,9 @@ class RecipeControllerTest { @Test void listRecipesShouldReturn200WithPagination() throws Exception { + var tag = new TagResponse(UUID.randomUUID(), "Rind", "protein"); var summary = new RecipeSummaryResponse(RECIPE_ID, "Spaghetti Bolognese", - (short) 4, (short) 45, "medium", null); + (short) 4, (short) 45, "medium", "https://example.com/img.jpg", List.of(tag)); when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); when(recipeService.listRecipes(eq(HOUSEHOLD_ID), isNull(), isNull(), isNull(), @@ -62,6 +63,9 @@ class RecipeControllerTest { .param("offset", "0")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data[0].name").value("Spaghetti Bolognese")) + .andExpect(jsonPath("$.data[0].heroImageUrl").value("https://example.com/img.jpg")) + .andExpect(jsonPath("$.data[0].tags[0].name").value("Rind")) + .andExpect(jsonPath("$.data[0].tags[0].tagType").value("protein")) .andExpect(jsonPath("$.meta.pagination.total").value(1)) .andExpect(jsonPath("$.meta.pagination.hasMore").value(false)); }