From 9fa839b4f8f36813a4323b86c77fdbec59101aa0 Mon Sep 17 00:00:00 2001 From: JHLEE0617 Date: Thu, 20 Nov 2025 08:04:20 +0900 Subject: [PATCH] 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"); + } + +}