From 9fa839b4f8f36813a4323b86c77fdbec59101aa0 Mon Sep 17 00:00:00 2001 From: JHLEE0617 Date: Thu, 20 Nov 2025 08:04:20 +0900 Subject: [PATCH 1/4] add recipe import from FoodSafety API --- .../client/FoodSafetyApiClient.java | 71 ++++++++ .../config/RestTemplateConfig.java | 14 ++ .../controller/RecipeImportController.java | 23 +++ .../dto/recipe/CookRcpResponse.java | 166 ++++++++++++++++++ .../algorithmchef/model/Recipe.java | 16 +- .../repository/RecipeRepository.java | 13 ++ .../service/RecipeDataInitializer.java | 21 +++ .../service/RecipeImportService.java | 94 ++++++++++ 8 files changed, 412 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/webservice/algorithmchef/client/FoodSafetyApiClient.java create mode 100644 src/main/java/com/webservice/algorithmchef/config/RestTemplateConfig.java create mode 100644 src/main/java/com/webservice/algorithmchef/controller/RecipeImportController.java create mode 100644 src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java create mode 100644 src/main/java/com/webservice/algorithmchef/repository/RecipeRepository.java create mode 100644 src/main/java/com/webservice/algorithmchef/service/RecipeDataInitializer.java create mode 100644 src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java diff --git a/src/main/java/com/webservice/algorithmchef/client/FoodSafetyApiClient.java b/src/main/java/com/webservice/algorithmchef/client/FoodSafetyApiClient.java new file mode 100644 index 0000000..4efb67b --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/client/FoodSafetyApiClient.java @@ -0,0 +1,71 @@ +package com.webservice.algorithmchef.client; + +import com.webservice.algorithmchef.dto.recipe.CookRcpResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class FoodSafetyApiClient { + + private final RestTemplate restTemplate; + + public FoodSafetyApiClient(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @Value("${foodsafety.cookrcp.base-url}") + private String baseUrl; + + @Value("${foodsafety.cookrcp.service-key}") + private String serviceKey; + + @Value("${foodsafety.cookrcp.svc-no}") + private String svcNo; + + public int getTotalCount() { + String url = String.format("%s/%s/%s/json/1/1", + baseUrl, serviceKey, svcNo); + + CookRcpResponse res = restTemplate.getForObject(url, CookRcpResponse.class); + + if (res == null || res.getCOOKRCP01() == null) return 0; + + try { + return Integer.parseInt(res.getCOOKRCP01().getTotalCount()); + } catch (Exception e) { + return 0; + } + } + + public List fetchRange(int start, int end) { + String url = String.format("%s/%s/%s/json/%d/%d", + baseUrl, serviceKey, svcNo, start, end); + + CookRcpResponse res = restTemplate.getForObject(url, CookRcpResponse.class); + + return Optional.ofNullable(res) + .map(CookRcpResponse::getCOOKRCP01) + .map(CookRcpResponse.Body::getRow) + .orElse(List.of()); + } + + public List fetchAll() { + int total = getTotalCount(); + if (total <= 0) return List.of(); + + int pageSize = 200; + List all = new ArrayList<>(total); + + for (int start = 1; start <= total; start += pageSize) { + int end = Math.min(start + pageSize - 1, total); + all.addAll(fetchRange(start, end)); + } + + return all; + } +} diff --git a/src/main/java/com/webservice/algorithmchef/config/RestTemplateConfig.java b/src/main/java/com/webservice/algorithmchef/config/RestTemplateConfig.java new file mode 100644 index 0000000..d765585 --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/config/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package com.webservice.algorithmchef.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/com/webservice/algorithmchef/controller/RecipeImportController.java b/src/main/java/com/webservice/algorithmchef/controller/RecipeImportController.java new file mode 100644 index 0000000..461116b --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/controller/RecipeImportController.java @@ -0,0 +1,23 @@ +package com.webservice.algorithmchef.controller; + +import com.webservice.algorithmchef.service.RecipeImportService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin") +public class RecipeImportController { + + private final RecipeImportService recipeImportService; + + public RecipeImportController(RecipeImportService recipeImportService) { + this.recipeImportService = recipeImportService; + } + + @GetMapping("/import/foodsafety") + public String importFromFoodSafety() { + recipeImportService.importAllFromFoodSafety(); + return "import started"; + } +} diff --git a/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java b/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java new file mode 100644 index 0000000..6cf1e70 --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java @@ -0,0 +1,166 @@ +package com.webservice.algorithmchef.dto.recipe; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class CookRcpResponse { + + @JsonProperty("COOKRCP01") + private Body cookrcp01; + + public Body getCOOKRCP01() { + return cookrcp01; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Body { + + @JsonProperty("total_count") + private String totalCount; + + @JsonProperty("row") + private List row; + + public String getTotalCount() { + return totalCount; + } + + public List getRow() { + // NPE 방지용 + return row == null ? List.of() : row; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Item { + + @JsonProperty("RCP_NM") + private String name; + + @JsonProperty("RCP_SEQ") + private String recipeIdRaw; + + + @JsonProperty("RCP_PARTS_DTLS") + private String parts; + + @JsonProperty("ATT_FILE_NO_MAIN") + private String imageUrl; + + @JsonProperty("INFO_ENG") + private String infoEng; + + @JsonProperty("HASH_TAG") + private String hashTag; + + @JsonProperty("RCP_WAY2") + private String way; + + @JsonProperty("RCP_PAT2") + private String category; + + @JsonProperty("MANUAL01") private String manual01; + @JsonProperty("MANUAL02") private String manual02; + @JsonProperty("MANUAL03") private String manual03; + @JsonProperty("MANUAL04") private String manual04; + @JsonProperty("MANUAL05") private String manual05; + @JsonProperty("MANUAL06") private String manual06; + @JsonProperty("MANUAL07") private String manual07; + @JsonProperty("MANUAL08") private String manual08; + @JsonProperty("MANUAL09") private String manual09; + @JsonProperty("MANUAL10") private String manual10; + @JsonProperty("MANUAL11") private String manual11; + @JsonProperty("MANUAL12") private String manual12; + @JsonProperty("MANUAL13") private String manual13; + @JsonProperty("MANUAL14") private String manual14; + @JsonProperty("MANUAL15") private String manual15; + @JsonProperty("MANUAL16") private String manual16; + @JsonProperty("MANUAL17") private String manual17; + @JsonProperty("MANUAL18") private String manual18; + @JsonProperty("MANUAL19") private String manual19; + @JsonProperty("MANUAL20") private String manual20; + + + public String getName() { + return name; + } + + public String getParts() { + return parts; + } + + public String getDescription() { + return parts; + } + + public String getImageUrl() { + return imageUrl; + } + + public String getInfoEng() { + return infoEng; + } + + public Double getKcal() { + if (infoEng == null) return null; + try { + String n = infoEng.replaceAll("[^0-9.]", ""); + return n.isEmpty() ? null : Double.valueOf(n); + } catch (Exception e) { + return null; + } + } + + public Double getKcalOrNull() { + return getKcal(); + } + + public String getHashTag() { + return hashTag; + } + + public String getWay() { + return way; + } + + public String getCategory() { + return category; + } + + public String getType() { + if (category != null && !category.isBlank()) return category; + if (way != null && !way.isBlank()) return way; + return "기타"; + } + + public String getInstructions() { + List steps = new ArrayList<>(); + + for (String s : List.of( + manual01, manual02, manual03, manual04, manual05, + manual06, manual07, manual08, manual09, manual10, + manual11, manual12, manual13, manual14, manual15, + manual16, manual17, manual18, manual19, manual20 + )) { + if (s == null) continue; + + String t = s + .replace("\r\n", "\n") + .replace("\\n", "\n") + .replaceAll("[ \t]+", " ") + .trim(); + + if (!t.isEmpty()) { + steps.add(t); + } + } + + return String.join("\n", steps); + } + } +} diff --git a/src/main/java/com/webservice/algorithmchef/model/Recipe.java b/src/main/java/com/webservice/algorithmchef/model/Recipe.java index 930a2dd..7e0f3ad 100644 --- a/src/main/java/com/webservice/algorithmchef/model/Recipe.java +++ b/src/main/java/com/webservice/algorithmchef/model/Recipe.java @@ -35,12 +35,16 @@ public class Recipe { @Lob private String description; - - @Lob - @Column(nullable = false) - private String instructions; - - private String imageUrl; + + @Lob + @Column(nullable = false, columnDefinition = "LONGTEXT") + private String instructions; + + @Lob + @Column(columnDefinition = "TEXT") + private String neededIngredients; + + private String imageUrl; @Column(nullable = false) private String type; diff --git a/src/main/java/com/webservice/algorithmchef/repository/RecipeRepository.java b/src/main/java/com/webservice/algorithmchef/repository/RecipeRepository.java new file mode 100644 index 0000000..9d80018 --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/repository/RecipeRepository.java @@ -0,0 +1,13 @@ +package com.webservice.algorithmchef.repository; + +import com.webservice.algorithmchef.model.Recipe; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface RecipeRepository extends JpaRepository { + + Optional findByName(String name); + + boolean existsByName(String name); +} diff --git a/src/main/java/com/webservice/algorithmchef/service/RecipeDataInitializer.java b/src/main/java/com/webservice/algorithmchef/service/RecipeDataInitializer.java new file mode 100644 index 0000000..8a30743 --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/service/RecipeDataInitializer.java @@ -0,0 +1,21 @@ +package com.webservice.algorithmchef.service; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component("recipeDataInitializer") +public class RecipeDataInitializer implements CommandLineRunner { + + private final RecipeImportService importService; + + public RecipeDataInitializer(RecipeImportService importService) { + this.importService = importService; + } + + @Override + public void run(String... args) throws Exception { + System.out.println("[INIT] import start"); + importService.importAllFromFoodSafety(); + System.out.println("[INIT] import complete"); + } +} diff --git a/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java b/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java new file mode 100644 index 0000000..7b31bc4 --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java @@ -0,0 +1,94 @@ +package com.webservice.algorithmchef.service; + +import com.webservice.algorithmchef.client.FoodSafetyApiClient; +import com.webservice.algorithmchef.dto.recipe.CookRcpResponse; +import com.webservice.algorithmchef.model.Recipe; +import com.webservice.algorithmchef.repository.RecipeRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class RecipeImportService { + + private final FoodSafetyApiClient apiClient; + private final RecipeRepository recipeRepository; + + public RecipeImportService(FoodSafetyApiClient apiClient, + RecipeRepository recipeRepository) { + this.apiClient = apiClient; + this.recipeRepository = recipeRepository; + } + + @Transactional + public void importAllFromFoodSafety() { + + System.out.println("[IMPORT] foodsafetycountry total recipe api start"); + + final int BATCH_SIZE = 100; + int start = 1; + int totalSaved = 0; + + while (true) { + int end = start + BATCH_SIZE - 1; + + System.out.println("[IMPORT] fetchRange(" + start + ", " + end + ")"); + + List items; + try { + items = apiClient.fetchRange(start, end); + } catch (Exception e) { + System.out.println("[IMPORT] fetchRange exception, import stop"); + e.printStackTrace(); + break; + } + + if (items == null || items.isEmpty()) { + System.out.println("[IMPORT] " + start + " ~ " + end + " this panel No recipe. import stop"); + break; + } + + System.out.println("[IMPORT] received count = " + items.size()); + + for (CookRcpResponse.Item item : items) { + + String name = item.getName(); + System.out.println(" - API recipe name = " + name); + + if (name == null || name.isBlank()) { + System.out.println(" -> No name, skip"); + continue; + } + + // 이미 같은 이름이 있으면 스킵 + if (recipeRepository.existsByName(name)) { + System.out.println(" -> exist recipe, skip"); + continue; + } + + Recipe r = new Recipe(); + r.setName(name); + r.setDescription(null); + r.setNeededIngredients(item.getParts()); + r.setInstructions(item.getInstructions()); + r.setImageUrl(item.getImageUrl()); + r.setKcal(item.getKcal()); + String type = item.getType(); + if (type == null || type.isBlank()) { + type = "기타"; + } + r.setType(type); + + recipeRepository.save(r); + totalSaved++; + System.out.println(" -> DB save complete (current new save: " + totalSaved + ")"); + } + + start += BATCH_SIZE; + } + + System.out.println("[IMPORT] every import complete. A total" + totalSaved + " new saved"); + } + +} From b59bed059bc25191193c4a6a387bcfc6400dcd59 Mon Sep 17 00:00:00 2001 From: JHLEE0617 Date: Thu, 20 Nov 2025 23:51:55 +0900 Subject: [PATCH 2/4] First fixed Commit --- .../controller/RecipeImportController.java | 23 ------- .../dto/recipe/CookRcpResponse.java | 11 ++- .../algorithmchef/model/Recipe.java | 4 -- .../RecipeIngredientRepository.java | 7 ++ .../service/RecipeDataInitializer.java | 2 +- .../service/RecipeImportService.java | 69 +++++++++++++++++-- 6 files changed, 78 insertions(+), 38 deletions(-) delete mode 100644 src/main/java/com/webservice/algorithmchef/controller/RecipeImportController.java create mode 100644 src/main/java/com/webservice/algorithmchef/repository/RecipeIngredientRepository.java diff --git a/src/main/java/com/webservice/algorithmchef/controller/RecipeImportController.java b/src/main/java/com/webservice/algorithmchef/controller/RecipeImportController.java deleted file mode 100644 index 461116b..0000000 --- a/src/main/java/com/webservice/algorithmchef/controller/RecipeImportController.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.webservice.algorithmchef.controller; - -import com.webservice.algorithmchef.service.RecipeImportService; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/admin") -public class RecipeImportController { - - private final RecipeImportService recipeImportService; - - public RecipeImportController(RecipeImportService recipeImportService) { - this.recipeImportService = recipeImportService; - } - - @GetMapping("/import/foodsafety") - public String importFromFoodSafety() { - recipeImportService.importAllFromFoodSafety(); - return "import started"; - } -} diff --git a/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java b/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java index 6cf1e70..d043a80 100644 --- a/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java +++ b/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java @@ -90,12 +90,11 @@ public String getName() { return name; } - public String getParts() { - return parts; - } - - public String getDescription() { - return parts; + public String[] getParts() { + if (parts == null || parts.isBlank()) { + return new String[0]; // 빈 배열 반환 + } + return parts.split("[,\\n]+"); } public String getImageUrl() { diff --git a/src/main/java/com/webservice/algorithmchef/model/Recipe.java b/src/main/java/com/webservice/algorithmchef/model/Recipe.java index 7e0f3ad..cf237bc 100644 --- a/src/main/java/com/webservice/algorithmchef/model/Recipe.java +++ b/src/main/java/com/webservice/algorithmchef/model/Recipe.java @@ -40,10 +40,6 @@ public class Recipe { @Column(nullable = false, columnDefinition = "LONGTEXT") private String instructions; - @Lob - @Column(columnDefinition = "TEXT") - private String neededIngredients; - private String imageUrl; @Column(nullable = false) diff --git a/src/main/java/com/webservice/algorithmchef/repository/RecipeIngredientRepository.java b/src/main/java/com/webservice/algorithmchef/repository/RecipeIngredientRepository.java new file mode 100644 index 0000000..4ebf303 --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/repository/RecipeIngredientRepository.java @@ -0,0 +1,7 @@ +package com.webservice.algorithmchef.repository; + +import com.webservice.algorithmchef.model.RecipeIngredient; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RecipeIngredientRepository extends JpaRepository { +} diff --git a/src/main/java/com/webservice/algorithmchef/service/RecipeDataInitializer.java b/src/main/java/com/webservice/algorithmchef/service/RecipeDataInitializer.java index 8a30743..3aad28b 100644 --- a/src/main/java/com/webservice/algorithmchef/service/RecipeDataInitializer.java +++ b/src/main/java/com/webservice/algorithmchef/service/RecipeDataInitializer.java @@ -3,7 +3,7 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; -@Component("recipeDataInitializer") +@Component public class RecipeDataInitializer implements CommandLineRunner { private final RecipeImportService importService; diff --git a/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java b/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java index 7b31bc4..91fd966 100644 --- a/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java +++ b/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java @@ -3,22 +3,36 @@ import com.webservice.algorithmchef.client.FoodSafetyApiClient; import com.webservice.algorithmchef.dto.recipe.CookRcpResponse; import com.webservice.algorithmchef.model.Recipe; +import com.webservice.algorithmchef.model.RecipeIngredient; +import com.webservice.algorithmchef.model.Ingredient; import com.webservice.algorithmchef.repository.RecipeRepository; +import com.webservice.algorithmchef.repository.RecipeIngredientRepository; +import com.webservice.algorithmchef.repository.IngredientRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.ArrayList; @Service public class RecipeImportService { private final FoodSafetyApiClient apiClient; private final RecipeRepository recipeRepository; + private final RecipeIngredientRepository recipeIngredientRepository; + private final IngredientRepository ingredientRepository; public RecipeImportService(FoodSafetyApiClient apiClient, - RecipeRepository recipeRepository) { + RecipeRepository recipeRepository, + RecipeIngredientRepository recipeIngredientRepository, + IngredientRepository ingredientRepository) { this.apiClient = apiClient; this.recipeRepository = recipeRepository; + this.recipeIngredientRepository = recipeIngredientRepository; + this.ingredientRepository = ingredientRepository; } @Transactional @@ -61,16 +75,15 @@ public void importAllFromFoodSafety() { continue; } - // 이미 같은 이름이 있으면 스킵 if (recipeRepository.existsByName(name)) { System.out.println(" -> exist recipe, skip"); continue; } + Recipe r = new Recipe(); r.setName(name); r.setDescription(null); - r.setNeededIngredients(item.getParts()); r.setInstructions(item.getInstructions()); r.setImageUrl(item.getImageUrl()); r.setKcal(item.getKcal()); @@ -80,7 +93,33 @@ public void importAllFromFoodSafety() { } r.setType(type); + recipeRepository.save(r); + + + List recipeIngredients = new ArrayList<>(); + for (String ingredientWithUnit : item.getParts()) { + String ingredientName = extractIngredientName(ingredientWithUnit); + if (ingredientName == null || ingredientName.isBlank()) { + continue; + } + + + Ingredient ingredient = findMatchingIngredient(ingredientName.trim()); + if (ingredient == null) { + System.out.println("Ingredient not found for: " + ingredientName); + } + + RecipeIngredient recipeIngredient = new RecipeIngredient(); + recipeIngredient.setRecipe(r); + recipeIngredient.setNeededIngredients(ingredientWithUnit.trim()); + recipeIngredient.setIngredient(ingredient); + + recipeIngredients.add(recipeIngredient); + } + + // Save RecipeIngredients to DB + recipeIngredientRepository.saveAll(recipeIngredients); totalSaved++; System.out.println(" -> DB save complete (current new save: " + totalSaved + ")"); } @@ -88,7 +127,29 @@ public void importAllFromFoodSafety() { start += BATCH_SIZE; } - System.out.println("[IMPORT] every import complete. A total" + totalSaved + " new saved"); + System.out.println("[IMPORT] every import complete. A total " + totalSaved + " new saved"); + } + + private String extractIngredientName(String ingredientWithUnit) { + String ingredientName = ingredientWithUnit.replaceAll("\\d+\\s*[a-zA-Z가-힣]+", "").trim(); + return ingredientName.isEmpty() ? null : ingredientName; + } + + private Ingredient findMatchingIngredient(String ingredientName) { + ingredientName = ingredientName.trim(); + + Ingredient ingredient = ingredientRepository.findByName(ingredientName).orElse(null); + if (ingredient == null) { + Pageable pageable = PageRequest.of(0, 10); + + Page ingredientsPage = ingredientRepository.findByNameContaining(ingredientName, pageable); + + if (!ingredientsPage.isEmpty()) { + ingredient = ingredientsPage.getContent().get(0); + } + } + + return ingredient; } } From f527cde3993161a674c1543c6b51fa70385a0176 Mon Sep 17 00:00:00 2001 From: JHLEE0617 Date: Mon, 24 Nov 2025 03:04:54 +0900 Subject: [PATCH 3/4] update database --- .../algorithmchef/config/MailConfig.java | 16 ++++ .../controller/RecipeReviewController.java | 35 +++++++++ .../dto/recipe/CookRcpResponse.java | 7 +- .../dto/recipereview/RecipeReviewRequest.java | 13 ++++ .../recipereview/RecipeReviewResponse.java | 21 ++++++ .../algorithmchef/model/Ingredient.java | 3 - .../algorithmchef/model/Recipe.java | 66 +++++++++------- .../algorithmchef/model/RecipeIngredient.java | 45 ----------- .../algorithmchef/model/RecipeReview.java | 45 ++++++----- .../algorithmchef/model/RecipeTag.java | 38 ---------- .../webservice/algorithmchef/model/Tag.java | 38 ---------- .../RecipeIngredientRepository.java | 7 -- .../repository/RecipeRepository.java | 3 + .../repository/RecipeReviewRepository.java | 12 +++ .../service/RecipeImportService.java | 75 +++---------------- .../service/RecipeReviewService.java | 50 +++++++++++++ 16 files changed, 226 insertions(+), 248 deletions(-) create mode 100644 src/main/java/com/webservice/algorithmchef/config/MailConfig.java create mode 100644 src/main/java/com/webservice/algorithmchef/controller/RecipeReviewController.java create mode 100644 src/main/java/com/webservice/algorithmchef/dto/recipereview/RecipeReviewRequest.java create mode 100644 src/main/java/com/webservice/algorithmchef/dto/recipereview/RecipeReviewResponse.java delete mode 100644 src/main/java/com/webservice/algorithmchef/model/RecipeIngredient.java delete mode 100644 src/main/java/com/webservice/algorithmchef/model/RecipeTag.java delete mode 100644 src/main/java/com/webservice/algorithmchef/model/Tag.java delete mode 100644 src/main/java/com/webservice/algorithmchef/repository/RecipeIngredientRepository.java create mode 100644 src/main/java/com/webservice/algorithmchef/repository/RecipeReviewRepository.java create mode 100644 src/main/java/com/webservice/algorithmchef/service/RecipeReviewService.java diff --git a/src/main/java/com/webservice/algorithmchef/config/MailConfig.java b/src/main/java/com/webservice/algorithmchef/config/MailConfig.java new file mode 100644 index 0000000..ec09ab8 --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/config/MailConfig.java @@ -0,0 +1,16 @@ +package com.webservice.algorithmchef.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +@Configuration +public class MailConfig { + + @Bean + public JavaMailSender javaMailSender() { + // 실제 SMTP 설정 없이 빈만 등록하는 임시 설정 + return new JavaMailSenderImpl(); + } +} diff --git a/src/main/java/com/webservice/algorithmchef/controller/RecipeReviewController.java b/src/main/java/com/webservice/algorithmchef/controller/RecipeReviewController.java new file mode 100644 index 0000000..d7dd932 --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/controller/RecipeReviewController.java @@ -0,0 +1,35 @@ +package com.webservice.algorithmchef.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.webservice.algorithmchef.dto.recipereview.RecipeReviewRequest; +import com.webservice.algorithmchef.dto.recipereview.RecipeReviewResponse; +import com.webservice.algorithmchef.service.RecipeReviewService; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class RecipeReviewController { + + private final RecipeReviewService rService; + + @PostMapping("/recipe/review") + public ResponseEntity makeReview(@RequestBody RecipeReviewRequest request, + @AuthenticationPrincipal UserDetails userDetails){ + try { + String userId = userDetails.getUsername(); + RecipeReviewResponse response = rService.makeReview(request, userId); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + }catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java b/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java index d043a80..6d2069e 100644 --- a/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java +++ b/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java @@ -90,11 +90,8 @@ public String getName() { return name; } - public String[] getParts() { - if (parts == null || parts.isBlank()) { - return new String[0]; // 빈 배열 반환 - } - return parts.split("[,\\n]+"); + public String getParts() { + return parts; } public String getImageUrl() { diff --git a/src/main/java/com/webservice/algorithmchef/dto/recipereview/RecipeReviewRequest.java b/src/main/java/com/webservice/algorithmchef/dto/recipereview/RecipeReviewRequest.java new file mode 100644 index 0000000..d1ed40a --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/dto/recipereview/RecipeReviewRequest.java @@ -0,0 +1,13 @@ +package com.webservice.algorithmchef.dto.recipereview; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class RecipeReviewRequest { + + private String name; + private float rating; + private String content; +} \ No newline at end of file diff --git a/src/main/java/com/webservice/algorithmchef/dto/recipereview/RecipeReviewResponse.java b/src/main/java/com/webservice/algorithmchef/dto/recipereview/RecipeReviewResponse.java new file mode 100644 index 0000000..d029176 --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/dto/recipereview/RecipeReviewResponse.java @@ -0,0 +1,21 @@ +package com.webservice.algorithmchef.dto.recipereview; + +import com.webservice.algorithmchef.model.RecipeReview; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class RecipeReviewResponse { + + private String name; + private String userId; + private float rating; + + public RecipeReviewResponse(RecipeReview review) { + this.name = review.getRecipe().getName(); + this.userId = review.getUser().getUserId(); + this.rating = review.getRating(); + } +} \ No newline at end of file diff --git a/src/main/java/com/webservice/algorithmchef/model/Ingredient.java b/src/main/java/com/webservice/algorithmchef/model/Ingredient.java index d4a6264..008c20f 100644 --- a/src/main/java/com/webservice/algorithmchef/model/Ingredient.java +++ b/src/main/java/com/webservice/algorithmchef/model/Ingredient.java @@ -46,7 +46,4 @@ public class Ingredient { @OneToMany(mappedBy = "ingredient",orphanRemoval = true,cascade = CascadeType.ALL) private List ingredients; - @OneToMany(mappedBy = "ingredient",orphanRemoval = true,cascade = CascadeType.ALL) - private List recipeIngredients; - } diff --git a/src/main/java/com/webservice/algorithmchef/model/Recipe.java b/src/main/java/com/webservice/algorithmchef/model/Recipe.java index cf237bc..3dac415 100644 --- a/src/main/java/com/webservice/algorithmchef/model/Recipe.java +++ b/src/main/java/com/webservice/algorithmchef/model/Recipe.java @@ -25,36 +25,46 @@ @ToString public class Recipe { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name="recipe_id") - private Long id; - - @Column(nullable = false, unique = true) - private String name; - - @Lob - private String description; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="recipe_id") + private Long id; + + @Column(nullable = false) + private String name; + + @Lob + private String description; @Lob - @Column(nullable = false, columnDefinition = "LONGTEXT") + @Column(nullable = false) private String instructions; private String imageUrl; - - @Column(nullable = false) - private String type; - - @Column(nullable = false) - private double kcal; - - @OneToMany(mappedBy = "recipe", orphanRemoval = true) - private List recipeTags; - - @OneToMany(mappedBy = "recipe", orphanRemoval = true) - private List recipeIngredients; - - @OneToMany(mappedBy = "recipe", orphanRemoval = true) - private List recipeReviews; - -} + + @Column(nullable = false) + private String type; + + @Lob + @Column(name = "needed_ingredients", columnDefinition = "TEXT") + private String neededIngredients; + + @Column(nullable = false) + private double kcal; + + @Column + private String tag; + + @Column + private String portions; + + @Column + private String time; + + @Column + private String tip; + + @OneToMany(mappedBy = "recipe", orphanRemoval = true) + private List recipeReviews; + +} \ No newline at end of file diff --git a/src/main/java/com/webservice/algorithmchef/model/RecipeIngredient.java b/src/main/java/com/webservice/algorithmchef/model/RecipeIngredient.java deleted file mode 100644 index 45fbb18..0000000 --- a/src/main/java/com/webservice/algorithmchef/model/RecipeIngredient.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.webservice.algorithmchef.model; - -import jakarta.persistence.Column; -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.Lob; -import jakarta.persistence.ManyToOne; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Entity -@AllArgsConstructor -@NoArgsConstructor -@Builder -@Getter -@Setter -@ToString -public class RecipeIngredient { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name="recipe_ingredient_id") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name="recipe_id") - private Recipe recipe; - - @Column(nullable = false) - @Lob - private String neededIngredients; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name="ingredient_id") - private Ingredient ingredient; - -} diff --git a/src/main/java/com/webservice/algorithmchef/model/RecipeReview.java b/src/main/java/com/webservice/algorithmchef/model/RecipeReview.java index fc0c923..9ce7775 100644 --- a/src/main/java/com/webservice/algorithmchef/model/RecipeReview.java +++ b/src/main/java/com/webservice/algorithmchef/model/RecipeReview.java @@ -11,6 +11,7 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; import jakarta.persistence.ManyToOne; import lombok.AllArgsConstructor; import lombok.Builder; @@ -28,23 +29,27 @@ @ToString public class RecipeReview { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name="review_id") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name="user_id") - private User user; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name="recipe_id") - private Recipe recipe; - - @Column(nullable = false) - private float rating; - - @CreationTimestamp - private LocalDateTime createdAt; - -} + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="review_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="recipe_id") + private Recipe recipe; + + @Column(nullable = false) + private float rating; + + @Column + @Lob + private String content; + + @CreationTimestamp + private LocalDateTime createdAt; + +} \ No newline at end of file diff --git a/src/main/java/com/webservice/algorithmchef/model/RecipeTag.java b/src/main/java/com/webservice/algorithmchef/model/RecipeTag.java deleted file mode 100644 index cc94bfc..0000000 --- a/src/main/java/com/webservice/algorithmchef/model/RecipeTag.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.webservice.algorithmchef.model; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Entity -@AllArgsConstructor -@NoArgsConstructor -@Builder -@Getter -@Setter -@ToString -public class RecipeTag { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name="recipe_tag_id") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Recipe recipe; - - @ManyToOne(fetch = FetchType.LAZY) - private Tag tag; - - -} diff --git a/src/main/java/com/webservice/algorithmchef/model/Tag.java b/src/main/java/com/webservice/algorithmchef/model/Tag.java deleted file mode 100644 index 5a47ac2..0000000 --- a/src/main/java/com/webservice/algorithmchef/model/Tag.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.webservice.algorithmchef.model; - -import java.util.List; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Entity -@AllArgsConstructor -@NoArgsConstructor -@Builder -@Getter -@Setter -@ToString -public class Tag { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(nullable = false) - private Long id; - - @Column(nullable = false, unique = true) - private String name; - - @OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true) - private List recipeTags; -} diff --git a/src/main/java/com/webservice/algorithmchef/repository/RecipeIngredientRepository.java b/src/main/java/com/webservice/algorithmchef/repository/RecipeIngredientRepository.java deleted file mode 100644 index 4ebf303..0000000 --- a/src/main/java/com/webservice/algorithmchef/repository/RecipeIngredientRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.webservice.algorithmchef.repository; - -import com.webservice.algorithmchef.model.RecipeIngredient; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface RecipeIngredientRepository extends JpaRepository { -} diff --git a/src/main/java/com/webservice/algorithmchef/repository/RecipeRepository.java b/src/main/java/com/webservice/algorithmchef/repository/RecipeRepository.java index 9d80018..eac8210 100644 --- a/src/main/java/com/webservice/algorithmchef/repository/RecipeRepository.java +++ b/src/main/java/com/webservice/algorithmchef/repository/RecipeRepository.java @@ -4,10 +4,13 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; +import java.util.List; public interface RecipeRepository extends JpaRepository { Optional findByName(String name); boolean existsByName(String name); + + List findByNeededIngredientsContaining(String ingredient); } diff --git a/src/main/java/com/webservice/algorithmchef/repository/RecipeReviewRepository.java b/src/main/java/com/webservice/algorithmchef/repository/RecipeReviewRepository.java new file mode 100644 index 0000000..852a2f9 --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/repository/RecipeReviewRepository.java @@ -0,0 +1,12 @@ +package com.webservice.algorithmchef.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.webservice.algorithmchef.model.Recipe; +import com.webservice.algorithmchef.model.RecipeReview; +import com.webservice.algorithmchef.model.User; + +public interface RecipeReviewRepository extends JpaRepository { + + boolean existsByUserAndRecipe(User user, Recipe recipe); +} \ No newline at end of file diff --git a/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java b/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java index 91fd966..d936153 100644 --- a/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java +++ b/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java @@ -3,36 +3,22 @@ import com.webservice.algorithmchef.client.FoodSafetyApiClient; import com.webservice.algorithmchef.dto.recipe.CookRcpResponse; import com.webservice.algorithmchef.model.Recipe; -import com.webservice.algorithmchef.model.RecipeIngredient; -import com.webservice.algorithmchef.model.Ingredient; import com.webservice.algorithmchef.repository.RecipeRepository; -import com.webservice.algorithmchef.repository.RecipeIngredientRepository; -import com.webservice.algorithmchef.repository.IngredientRepository; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.ArrayList; @Service public class RecipeImportService { private final FoodSafetyApiClient apiClient; private final RecipeRepository recipeRepository; - private final RecipeIngredientRepository recipeIngredientRepository; - private final IngredientRepository ingredientRepository; public RecipeImportService(FoodSafetyApiClient apiClient, - RecipeRepository recipeRepository, - RecipeIngredientRepository recipeIngredientRepository, - IngredientRepository ingredientRepository) { + RecipeRepository recipeRepository) { this.apiClient = apiClient; this.recipeRepository = recipeRepository; - this.recipeIngredientRepository = recipeIngredientRepository; - this.ingredientRepository = ingredientRepository; } @Transactional @@ -80,46 +66,29 @@ public void importAllFromFoodSafety() { continue; } - Recipe r = new Recipe(); r.setName(name); + + // description은 당분간 null 유지 r.setDescription(null); + r.setInstructions(item.getInstructions()); r.setImageUrl(item.getImageUrl()); - r.setKcal(item.getKcal()); + + // kcal이 null일 수 있으니까 방어 코드 + Double kcal = item.getKcalOrNull(); + r.setKcal(kcal != null ? kcal : 0.0); + String type = item.getType(); if (type == null || type.isBlank()) { type = "기타"; } r.setType(type); + // ✅ 재료 전체 문자열 그대로 저장 + r.setNeededIngredients(item.getParts()); recipeRepository.save(r); - - - List recipeIngredients = new ArrayList<>(); - for (String ingredientWithUnit : item.getParts()) { - String ingredientName = extractIngredientName(ingredientWithUnit); - if (ingredientName == null || ingredientName.isBlank()) { - continue; - } - - - Ingredient ingredient = findMatchingIngredient(ingredientName.trim()); - if (ingredient == null) { - System.out.println("Ingredient not found for: " + ingredientName); - } - - RecipeIngredient recipeIngredient = new RecipeIngredient(); - recipeIngredient.setRecipe(r); - recipeIngredient.setNeededIngredients(ingredientWithUnit.trim()); - recipeIngredient.setIngredient(ingredient); - - recipeIngredients.add(recipeIngredient); - } - - // Save RecipeIngredients to DB - recipeIngredientRepository.saveAll(recipeIngredients); totalSaved++; System.out.println(" -> DB save complete (current new save: " + totalSaved + ")"); } @@ -130,26 +99,4 @@ public void importAllFromFoodSafety() { System.out.println("[IMPORT] every import complete. A total " + totalSaved + " new saved"); } - private String extractIngredientName(String ingredientWithUnit) { - String ingredientName = ingredientWithUnit.replaceAll("\\d+\\s*[a-zA-Z가-힣]+", "").trim(); - return ingredientName.isEmpty() ? null : ingredientName; - } - - private Ingredient findMatchingIngredient(String ingredientName) { - ingredientName = ingredientName.trim(); - - Ingredient ingredient = ingredientRepository.findByName(ingredientName).orElse(null); - if (ingredient == null) { - Pageable pageable = PageRequest.of(0, 10); - - Page ingredientsPage = ingredientRepository.findByNameContaining(ingredientName, pageable); - - if (!ingredientsPage.isEmpty()) { - ingredient = ingredientsPage.getContent().get(0); - } - } - - return ingredient; - } - } diff --git a/src/main/java/com/webservice/algorithmchef/service/RecipeReviewService.java b/src/main/java/com/webservice/algorithmchef/service/RecipeReviewService.java new file mode 100644 index 0000000..6f5559c --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/service/RecipeReviewService.java @@ -0,0 +1,50 @@ +package com.webservice.algorithmchef.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.webservice.algorithmchef.dto.recipereview.RecipeReviewRequest; +import com.webservice.algorithmchef.dto.recipereview.RecipeReviewResponse; +import com.webservice.algorithmchef.model.Recipe; +import com.webservice.algorithmchef.model.RecipeReview; +import com.webservice.algorithmchef.model.User; +import com.webservice.algorithmchef.repository.RecipeRepository; +import com.webservice.algorithmchef.repository.RecipeReviewRepository; +import com.webservice.algorithmchef.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class RecipeReviewService { + + private final RecipeRepository recipeRepository; + private final RecipeReviewRepository recipeReviewRepository; + private final UserRepository userRepository; + + + @Transactional + public RecipeReviewResponse makeReview(RecipeReviewRequest reviewRequest, String userId) { + User user = userRepository.findByUserId(userId) + .orElseThrow(()-> new IllegalArgumentException("해당하는 아이디의 사용작 없습니다.")); + String recipeName = reviewRequest.getName(); + float rating = reviewRequest.getRating(); + String content = reviewRequest.getContent(); + Recipe recipe = recipeRepository.findByName(recipeName) + .orElseThrow(()-> new IllegalArgumentException("해당하는 이름의 recipe가 존재하지 않습니다.")); + if (recipeReviewRepository.existsByUserAndRecipe(user, recipe)) { + throw new IllegalStateException("이미 리뷰를 작성한 레시피입니다."); + } + RecipeReview review = RecipeReview.builder() + .content(content) + .rating(rating) + .user(user) + .recipe(recipe) + .build(); + + RecipeReview recipeReview = recipeReviewRepository.save(review); + return new RecipeReviewResponse(recipeReview); + + } + +} \ No newline at end of file From a66c53bdaef200c3b59f721cad62573a99fcaa4f Mon Sep 17 00:00:00 2001 From: JHLEE0617 Date: Tue, 25 Nov 2025 04:24:52 +0900 Subject: [PATCH 4/4] recipe_domain include search --- .../config/JwtAuthenticationFilter.java | 12 ++--- .../algorithmchef/config/MailConfig.java | 16 ------ .../algorithmchef/config/SecurityConfig.java | 8 +-- .../controller/RecipeSearchController.java | 50 +++++++++++++++++ .../dto/recipe/CookRcpResponse.java | 6 +++ .../algorithmchef/model/Recipe.java | 2 +- .../repository/RecipeRepository.java | 22 +++++++- .../service/RecipeImportService.java | 20 +++---- .../algorithmchef/service/RecipeService.java | 54 +++++++++++++++++++ 9 files changed, 149 insertions(+), 41 deletions(-) delete mode 100644 src/main/java/com/webservice/algorithmchef/config/MailConfig.java create mode 100644 src/main/java/com/webservice/algorithmchef/controller/RecipeSearchController.java create mode 100644 src/main/java/com/webservice/algorithmchef/service/RecipeService.java diff --git a/src/main/java/com/webservice/algorithmchef/config/JwtAuthenticationFilter.java b/src/main/java/com/webservice/algorithmchef/config/JwtAuthenticationFilter.java index 53dadd0..9f5b5d9 100644 --- a/src/main/java/com/webservice/algorithmchef/config/JwtAuthenticationFilter.java +++ b/src/main/java/com/webservice/algorithmchef/config/JwtAuthenticationFilter.java @@ -13,7 +13,7 @@ import org.springframework.web.filter.OncePerRequestFilter; // (⭐수정) algorithmchef 프로젝트의 의존성으로 변경 -import com.webservice.algorithmchef.service.UserService; +import com.webservice.algorithmchef.service.UserService; import com.webservice.algorithmchef.util.JwtUtil; import io.jsonwebtoken.Claims; @@ -60,13 +60,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse claims = jwtUtil.getClaims(token); } catch (Exception e) { log.warn("유효하지 않은 JWT 토큰: {}", e.getMessage()); - // (참고) 여기서 401 Unauthorized를 바로 반환할 수도 있으나, + // (참고) 여기서 401 Unauthorized를 바로 반환할 수도 있으나, // SecurityConfig의 exceptionHandling에서 처리하도록 위임하는 것이 일반적입니다. } // 3. 토큰이 유효하고, 아직 SecurityContext에 인증 정보가 없는 경우 if (claims != null && SecurityContextHolder.getContext().getAuthentication() == null) { - + // claims.getSubject()는 User 엔티티의 "userLoginId" (로그인 ID)여야 합니다. String userLoginId = claims.getSubject(); String status = claims.get("status", String.class); // "status" 클레임 추출 @@ -84,11 +84,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; // 필터 체인 중단 } } - + // 6. (정상 사용자) 또는 (임시 사용자가 허용된 URL에 접근한 경우) // UserDetails를 DB에서 조회하여 인증 토큰 생성 UserDetails userDetails = userService.loadUserByUsername(userLoginId); - + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); @@ -97,7 +97,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse SecurityContextHolder.getContext().setAuthentication(authenticationToken); log.info("SecurityContext에 인증 정보 저장 완료: {}", userLoginId); } - + // 8. 다음 필터로 체인 넘김 filterChain.doFilter(request, response); } diff --git a/src/main/java/com/webservice/algorithmchef/config/MailConfig.java b/src/main/java/com/webservice/algorithmchef/config/MailConfig.java deleted file mode 100644 index ec09ab8..0000000 --- a/src/main/java/com/webservice/algorithmchef/config/MailConfig.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.webservice.algorithmchef.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.mail.javamail.JavaMailSenderImpl; - -@Configuration -public class MailConfig { - - @Bean - public JavaMailSender javaMailSender() { - // 실제 SMTP 설정 없이 빈만 등록하는 임시 설정 - return new JavaMailSenderImpl(); - } -} diff --git a/src/main/java/com/webservice/algorithmchef/config/SecurityConfig.java b/src/main/java/com/webservice/algorithmchef/config/SecurityConfig.java index a1bb528..40ae68f 100644 --- a/src/main/java/com/webservice/algorithmchef/config/SecurityConfig.java +++ b/src/main/java/com/webservice/algorithmchef/config/SecurityConfig.java @@ -17,8 +17,8 @@ @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { - - private final JwtAuthenticationFilter jwtAuthenticationFilter; + + private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean @@ -34,8 +34,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/auth/login", "/auth/signUp", - "/auth/findPassword", "/auth/findUserId").permitAll() + .requestMatchers("/auth/login", "/auth/signUp", + "/auth/findPassword", "/auth/findUserId").permitAll() .anyRequest().authenticated()); http diff --git a/src/main/java/com/webservice/algorithmchef/controller/RecipeSearchController.java b/src/main/java/com/webservice/algorithmchef/controller/RecipeSearchController.java new file mode 100644 index 0000000..c37816c --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/controller/RecipeSearchController.java @@ -0,0 +1,50 @@ +package com.webservice.algorithmchef.controller; + +import com.webservice.algorithmchef.model.Recipe; +import com.webservice.algorithmchef.service.RecipeService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("/api/recipes") +@CrossOrigin(origins = "http://localhost:3000") +public class RecipeSearchController { + + private final RecipeService recipeService; + + public RecipeSearchController(RecipeService recipeService) { + this.recipeService = recipeService; + } + + @GetMapping("/search") + public ResponseEntity getRandomRecipe(@RequestParam String ingredient) { + + Recipe recipe = recipeService.getRandomRecipeByIngredient(ingredient); + + if (recipe == null) { + return ResponseEntity.ok("검색된 레시피가 없습니다."); + } + + return ResponseEntity.ok(recipe); + } + + @GetMapping("/search-multi") + public ResponseEntity getRandomRecipeMultiple(@RequestParam String ingredients) { + + List ingList = + Arrays.stream(ingredients.split(",")) + .map(String::trim) + .toList(); + + Recipe recipe = recipeService.getRandomRecipeByIngredients(ingList); + + if (recipe == null) { + return ResponseEntity.ok("해당 재료들을 모두 포함한 레시피가 없습니다."); + } + + return ResponseEntity.ok(recipe); + } +} diff --git a/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java b/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java index 6d2069e..7ec4340 100644 --- a/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java +++ b/src/main/java/com/webservice/algorithmchef/dto/recipe/CookRcpResponse.java @@ -85,6 +85,9 @@ public static class Item { @JsonProperty("MANUAL19") private String manual19; @JsonProperty("MANUAL20") private String manual20; + @JsonProperty("RCP_NA_TIP") + private String naTip; + public String getName() { return name; @@ -158,5 +161,8 @@ public String getInstructions() { return String.join("\n", steps); } + public String getTip() { + return naTip; + } } } diff --git a/src/main/java/com/webservice/algorithmchef/model/Recipe.java b/src/main/java/com/webservice/algorithmchef/model/Recipe.java index 3dac415..007863f 100644 --- a/src/main/java/com/webservice/algorithmchef/model/Recipe.java +++ b/src/main/java/com/webservice/algorithmchef/model/Recipe.java @@ -37,7 +37,7 @@ public class Recipe { private String description; @Lob - @Column(nullable = false) + @Column(nullable = false, columnDefinition = "LONGTEXT") private String instructions; private String imageUrl; diff --git a/src/main/java/com/webservice/algorithmchef/repository/RecipeRepository.java b/src/main/java/com/webservice/algorithmchef/repository/RecipeRepository.java index eac8210..915f68f 100644 --- a/src/main/java/com/webservice/algorithmchef/repository/RecipeRepository.java +++ b/src/main/java/com/webservice/algorithmchef/repository/RecipeRepository.java @@ -2,6 +2,8 @@ import com.webservice.algorithmchef.model.Recipe; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; import java.util.List; @@ -12,5 +14,23 @@ public interface RecipeRepository extends JpaRepository { boolean existsByName(String name); - List findByNeededIngredientsContaining(String ingredient); + @Query(""" + SELECT r FROM Recipe r + WHERE (:i1 = '' OR r.neededIngredients LIKE %:i1%) + AND (:i2 = '' OR r.neededIngredients LIKE %:i2%) + AND (:i3 = '' OR r.neededIngredients LIKE %:i3%) + AND (:i4 = '' OR r.neededIngredients LIKE %:i4%) + AND (:i5 = '' OR r.neededIngredients LIKE %:i5%) + """) + List findByIngredientsMulti( + @Param("i1") String i1, + @Param("i2") String i2, + @Param("i3") String i3, + @Param("i4") String i4, + @Param("i5") String i5 + ); + + @Query("SELECT r FROM Recipe r " + + "WHERE r.neededIngredients LIKE %:ingredient%") + List findByIngredientKeyword(@Param("ingredient") String ingredient); } diff --git a/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java b/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java index d936153..761dea8 100644 --- a/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java +++ b/src/main/java/com/webservice/algorithmchef/service/RecipeImportService.java @@ -68,26 +68,21 @@ public void importAllFromFoodSafety() { Recipe r = new Recipe(); r.setName(name); - - // description은 당분간 null 유지 r.setDescription(null); - r.setInstructions(item.getInstructions()); r.setImageUrl(item.getImageUrl()); - - // kcal이 null일 수 있으니까 방어 코드 Double kcal = item.getKcalOrNull(); r.setKcal(kcal != null ? kcal : 0.0); - String type = item.getType(); - if (type == null || type.isBlank()) { - type = "기타"; + String originalType = item.getType(); + if (originalType == null || originalType.isBlank()) { + originalType = "기타"; } - r.setType(type); - - // ✅ 재료 전체 문자열 그대로 저장 + r.setTag(originalType); + r.setType("식품안전나라"); + r.setTime(null); + r.setTip(item.getTip()); r.setNeededIngredients(item.getParts()); - recipeRepository.save(r); totalSaved++; System.out.println(" -> DB save complete (current new save: " + totalSaved + ")"); @@ -98,5 +93,4 @@ public void importAllFromFoodSafety() { System.out.println("[IMPORT] every import complete. A total " + totalSaved + " new saved"); } - } diff --git a/src/main/java/com/webservice/algorithmchef/service/RecipeService.java b/src/main/java/com/webservice/algorithmchef/service/RecipeService.java new file mode 100644 index 0000000..abb8baa --- /dev/null +++ b/src/main/java/com/webservice/algorithmchef/service/RecipeService.java @@ -0,0 +1,54 @@ +package com.webservice.algorithmchef.service; + +import com.webservice.algorithmchef.model.Recipe; +import com.webservice.algorithmchef.repository.RecipeRepository; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Random; + +@Service +public class RecipeService { + + private final RecipeRepository recipeRepository; + private final Random random = new Random(); + + public RecipeService(RecipeRepository recipeRepository) { + this.recipeRepository = recipeRepository; + } + + public Recipe getRandomRecipeByIngredient(String ingredient) { + List list = recipeRepository.findByIngredientKeyword(ingredient); + + if (list.isEmpty()) { + return null; + } + + int idx = (int) (Math.random() * list.size()); + return list.get(idx); + } + + public Recipe getRandomRecipeByIngredients(List ingredients) { + + if (ingredients == null || ingredients.isEmpty()) { + throw new IllegalArgumentException("재료 리스트가 필요합니다."); + } + + String i1 = ingredients.size() > 0 ? ingredients.get(0) : ""; + String i2 = ingredients.size() > 1 ? ingredients.get(1) : ""; + String i3 = ingredients.size() > 2 ? ingredients.get(2) : ""; + String i4 = ingredients.size() > 3 ? ingredients.get(3) : ""; + String i5 = ingredients.size() > 4 ? ingredients.get(4) : ""; + + List list = + recipeRepository.findByIngredientsMulti(i1, i2, i3, i4, i5); + + if (list.isEmpty()) { + return null; + } + + return list.get(random.nextInt(list.size())); + } +}