From 722a9200bd016065594e3796fe05138d358d0bae Mon Sep 17 00:00:00 2001 From: beans3142 Date: Thu, 21 Aug 2025 18:42:16 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[REFACTOR]=20=EB=AA=A8=EB=8D=B8=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RecipeRecommendServiceImpl.java | 4 +- .../recipe/service/RecipeServiceImpl.java | 154 ++++++++++-------- .../com/mumuk/global/client/GeminiClient.java | 15 +- .../global/config/GeminiClientConfig.java | 6 +- src/main/resources/application.yml | 1 + 5 files changed, 110 insertions(+), 70 deletions(-) diff --git a/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java b/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java index f3b3a79..0772949 100644 --- a/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java +++ b/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java @@ -1040,9 +1040,9 @@ private String buildRecipePostPromptCommon() { "- 예시: 된장찌개 O, 미나리 된장찌개 O, 돼지고기 앞다리살 감자 상추 된장찌개 X\n" + "- 고기 요리, 생선 요리, 채식 요리, 면 요리 등 다양한 재료 활용\n" + "- 레시피 제목에는 사용 재료가 명확히 보이도록 작성 (토마토 바질 파스타)\n" + - "- 레시피 제목에 포함된 재료는 2개 이하로, 주식(면(파스타, 우동 등), 밥(덮밥, 볶음밥 등))의 경우 3개까지 가능 (토마토 바질 파스타 O, 고추장 돼지고기 볶음 O, 고추장 양파 돼지고기 볶음 X)\n" + + "- 레시피 제목에 재료 이름은 2개 이하로 구성, 주식(면(파스타, 우동 등), 밥(덮밥, 볶음밥 등))의 경우 3개까지 가능 (토마토 바질 파스타 O, 고추장 돼지고기 볶음 O, 고추장 양파 돼지고기 볶음 X)\n" + "※ 재료 포함 기준:\n" + - "- 실제 요리에 필요한 보편적인 모든 주요 재료를 포함해야 함 (최소 2개 재료)\n" + + "- 실제 요리에 필요한 보편적인 모든 주요 재료를 포함해야 함 (최소 4개 재료, 많을수록 좋음, 조미료 이름도 포함)\n" + "- 예시: 제육볶음 → 돼지고기, 양파, 당근, 고추장, 간장, 설탕, 식용유, 후추 (8개 재료)\n" + "- 레시피 제목과 재료, 설명 모두 순수 한글로만 구성 (스파게티 O, 양파 O, noodle X, 네기 X)\n" + "- 선택사항인 식재료는 제외 (연어 포케에 올리브도 들어갈 수 있지만 필수는 아님)n" + diff --git a/src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java b/src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java index 7fa0243..c6d1372 100644 --- a/src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java +++ b/src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java @@ -413,69 +413,95 @@ private String analyzeIngredientsWithAI(List userIngredients, List userIngredients, List recipeIngredients) { return String.format(""" - 사용자 재료: %s - 레시피 재료: %s - - 각 레시피 재료를 다음 3가지로 분류해주세요: - - 1. match: 사용자 재료와 정확히 일치하는 레시피 재료 - 2. mismatch: 사용자 재료에 없고 대체도 불가능한 레시피 재료 - 3. replaceable: 사용자 재료로 대체 가능한 레시피 재료 - - 분류 로직: - 각 레시피 재료에 대해: - 1) 사용자 재료 목록에서 정확히 일치하는 것이 있으면 → match - 2) 정확히 일치하지 않지만 아래 조건에 맞춰 확실한 대체 가능한 사용자 재료가 있으면 → replaceable - 3) 그 외의 경우 → mismatch - - 대체 가능한 경우는 3가지 기준 중 적어도 하나를 만족하고, - 레시피 재료를 기준으로 확실하게 조건을 만족하는 경우에만 대체가능: - - 1. 포함 관계 (상위 개념 ↔ 하위 개념): - - 돼지고기 ↔ 앞다리살, 목살, 삼겹살 (돼지고기가 상위, 구체적 부위가 하위) - - 소고기 ↔ 등심, 안심, 갈비 (소고기가 상위, 구체적 부위가 하위) - - 닭고기 ↔ 닭가슴살, 닭다리, 닭날개 (닭고기가 상위, 구체적 부위가 하위) - - 간장 ↔ 진간장, 국간장, 노추, 쯔유 (간장에 포함되는 개념) - - 치즈 ↔ 모짜렐라, 크림치즈, 브레드치즈 (치즈 종류) - - 파스타 ↔ 링귀니, 마카로니, 스파게티 (파스타 면 종류) - - 2. 같은 용도: - - 목살(구이용) ↔ 삼겹살(구이용) (같은 돼지고기, 같은 구이용도) - - 돼지고기(찌개용) ↔ 목살(찌개용) (같은 돼지고기, 같은 구이용도) - - 돼지고기(탕용) ↔ 앞다리살(탕용) (같은 돼지고기, 같은 탕용도) - - 3. 조미료 대체: - - 설탕 ↔ 올리고당, 알룰로스 (단맛 조미료) - - 소금 ↔ 천일염, 새우젓 (염분 조미료) - - 미원 ↔ 다시다, 혼다시, 연두 (감칠맛 조미료) - - 식용유 ↔ 올리브유, 포도씨유, 케네프리유 (식용유 종류) - - 대체 불가능한 경우: - - 실제 레시피를 고려했을 때 차이가 큰 경우 (식용유 알리오 올리오, 기름이 중요한 레시피인데 식용유는 적절X) - - 2칸 이상 차이나는 경우 (면 ↔ 파스타 ↔ 링귀니 : 면 링귀니는 대체불가, 돼지 등심(돈까스용) ↔ 돼지고기 ↔ 앞다리살(탕용)) - - 돼지 등심(돈까스용) ↔ 삼겹살(구이용),목살(구이용),앞다리살(탕용) (같은 돼지고기지만 용도가 완전히 다름) - - 소고기 ↔ 돼지고기 (완전히 다른 고기) - - 올리브유 ↔ 올리브, 고추장 ↔ 고추, 케찹 ↔ 토마토 (소스와 재료의 구분 및 원재료로 제작이 힘든 경우) - - 양파 ↔ 올리브, 파, 마늘 (전혀 다른 재료) - - 기타 대체로 보기 어려운 경우 - - 조금이라도 애매한 경우 대체 불가능으로 본다. - - 절대 규칙: - - 레시피 재료는 사용자 재료에 대해서 각각 match, mismatch, replaceable 중 오직 하나에만 속함 - - 최종적으로 응답에 레시피 재료가 모두 포함되어 있어야 함 - - JSON 형태로 응답: - { - "match": ["레시피재료1", "레시피재료2"], - "mismatch": ["레시피재료3", "레시피재료4"], - "replaceable": [ - { - "recipeIngredient": "레시피재료", - "userIngredient": "사용자재료" - } - ] - } - """, userIngredients, recipeIngredients); +너는 사용자의 재료와 레시피의 재료를 비교하여, 아래의 엄격한 규칙에 따라 분류하는 요리 재료 분석 AI다. 모든 판단은 추론을 최소화하고 주어진 규칙에 기반해야 한다. + +## 1단계: 대원칙 (Grand Principles) + +모든 판단은 아래 두 가지 대원칙을 반드시 따른다. + +**1. 카테고리 우선 원칙:** 모든 재료는 먼저 아래 **[카테고리 상세 정의]** 중 하나로 분류된다. 대체(replaceable) 여부는 **오직 동일 카테고리 내에서만** 검토될 수 있다. 카테고리가 다르면 즉시 `mismatch`다. + +**2. 판단 위계 원칙:** 각 레시피 재료는 아래 순서에 따라 단 한 번만 평가된다. + 1. **정확 일치(Match):** 사용자 재료에 정확히 같은 이름이 있는가? → `match` + 2. **대체 가능(Replaceable):** 일치하는 이름이 없다면, **[대체 가능 판단 기준]** 4가지(포함적 > 용도적 > 유사적 > 보편적 순)를 순서대로 검토한다. 하나라도 명백히 충족하고 **[절대 불가 규칙]**을 위반하지 않으면 → `replaceable` + 3. **불일치(Mismatch):** 위 모든 경우에 해당하지 않으면 → `mismatch` + +--- + +## 2단계: 카테고리 상세 정의 (Category Definitions) + +* **주재료:** 요리의 중심이 되는 육류, 해산물. (예: 소고기, 돼지고기, 닭고기, 오징어, 조개) +* **부재료:** 주재료를 보조하는 채소, 과일, 두부, 계란 등. (예: 양파, 당근, 파, 애호박, 주키니, 상추, 로메인, 두부) +* **향신료/허브:** 요리에 특정 향을 더하는 재료. (예: 바질, 타임, 후추, 계피, 시나몬) +* **기본 조미료:** 단맛, 짠맛, 신맛 등 기본적인 맛을 내는 재료. (예: 간장, 노추, 설탕, 소금, 식초, 식용유) +* **감칠맛 조미료:** 감칠맛(Umami)을 더하는 데 특화된 조미료. + * (예: 연두, 미원, 다시다, 굴소스, 치킨스톡) + * **카테고리 내 규칙:** 감칠맛 조미료끼리는 비교적 자유롭게 대체 가능하나, 요리의 국적(한식/중식/양식)을 고려해야 한다. +* **소스:** 완성된 형태의 복합 조미료. + * (예: 액젓, 케첩, 머스터드, 허니 머스터드, 마요네즈) + * **카테고리 내 규칙:** 대체는 주로 '포함적/유사적 대체'만 허용된다 (예: 머스터드 ↔ 허니 머스터드). 맛과 용도가 전혀 다른 소스 간의 대체는 절대 불가하다 (예: 케첩 ↔ 액젓). + +--- + +## 3단계: 대체 가능 판단 기준 상세 정의 (Replacement Criteria) + +아래 기준을 **1번부터 순서대로** 검토하여, 가장 먼저 충족하는 기준을 적용한다. + +**1. 포함적 대체 (Inclusion-based):** 일반적인 재료명에 대해 더 구체적인 재료로 대체. **가장 우선순위가 높은 대체 규칙.** + * `돼지고기` ↔ `앞다리살`, `삼겹살`, `항정살` + * `액젓` ↔ `멸치액젓`, `까나리액젓` + * `마늘` ↔ `흑마늘`, `깐마늘` + * `간장` ↔ `진간장`, `국간장` + +**2. 용도적 대체 (Purpose-based):** 재료의 부위나 형태는 다르지만, 요리에서의 '용도'가 명확히 같은 경우. + * `돼지고기 앞다리살 (수육용)` ↔ `삼겹살 (수육용)` + * `삼겹살 (구이용)` ↔ `목살 (구이용)` + * `소고기 (장조림용)` ↔ `홍두깨살 (장조림용)` + +**3. 유사적 대체 (Similarity-based):** 동일 소속 내에서 품종이나 가공 방식만 약간 다른 경우. + * `허니 머스터드` ↔ `스모키 머스터드` ↔ `홀그레인 머스터드` + * `모짜렐라 치즈` ↔ `체다 치즈` (요리 종류에 따라) + * `스파게티면` ↔ `링귀니면` + +**4. 보편적 대체 (Universal):** 요리계에서 일반적으로 통용되는 대체 재료. 위의 세 규칙에 해당하지 않지만 명백한 경우에만 한정적으로 적용. + * `애호박` ↔ `주키니` + * `상추` ↔ `로메인` + * `계피` ↔ `시나몬` + * `굴소스` ↔ `치킨스톡` (감칠맛 조미료 카테고리 내) + +--- + +## 4단계: 절대 불가 규칙 (Absolute Prohibitions) +g +아무리 대체 가능 기준에 부합해 보여도, 아래 규칙 중 하나라도 위반하면 즉시 `mismatch` 처리한다. + +* **카테고리 교차:** 다른 카테고리의 재료 간 대체는 절대 불가. +* **핵심 풍미/기능 훼손:** 요리의 정체성을 바꾸는 대체 불가. (예: 알리오 올리오의 `올리브유` ↔ `참기름`) +* **용도 불일치:** 동일 재료라도 용도가 다르면 불가. (예: `돼지고기 등심 (돈까스용)` ↔ `돼지고기 등심 (구이용)`) +* **원재료 ↔ 가공품:** (예: `삼겹살` ↔ `베이컨`, `밀가루` ↔ `빵가루`) +* **모호함:** 대체 가능한지 조금이라도 애매하거나 확신이 없으면 반드시 `mismatch`로 분류. (추정 금지) + +--- + +## 5단계: JSON 출력 형식 + +* 반드시 아래 JSON 구조를 따른다. +* 모든 레시피 재료는 세 그룹 중 하나에 반드시 포함되어야 한다. + +{ + "match": ["..."], + "mismatch": ["..."], + "replaceable": [ + { + "recipeIngredient": "...", + "userIngredient": "..." + } + ] +} + +**사용자 재료: %s** +**레시피 재료: %s** +""", userIngredients, recipeIngredients); } /** @@ -483,7 +509,7 @@ private String buildIngredientMatchingPrompt(List userIngredients, List< */ private String callAI(String prompt) { try { - return geminiClient.chat(prompt).block(java.time.Duration.ofSeconds(15)); + return geminiClient.chatAccurate(prompt).block(java.time.Duration.ofSeconds(15)); } catch (Exception e) { log.error("AI 호출 실패", e); // 스택트레이스 포함 로깅 throw new BusinessException(ErrorCode.OPENAI_API_ERROR); diff --git a/src/main/java/com/mumuk/global/client/GeminiClient.java b/src/main/java/com/mumuk/global/client/GeminiClient.java index 582494c..660613e 100644 --- a/src/main/java/com/mumuk/global/client/GeminiClient.java +++ b/src/main/java/com/mumuk/global/client/GeminiClient.java @@ -18,17 +18,28 @@ public class GeminiClient { private final WebClient webClient; private final String model; + private final String accurateModel; - public GeminiClient(WebClient webClient, @Value("${gemini.api.model}") String model) { + public GeminiClient(WebClient webClient, @Value("${gemini.api.model}") String model, + @Value("${gemini.api.model_accurate:${gemini.api.model}}") String accurateModel) { this.webClient = webClient; this.model = model; + this.accurateModel = accurateModel; } public Mono chat(String prompt) { + return chatWithModel(prompt, this.model); + } + + public Mono chatAccurate(String prompt) { + return chatWithModel(prompt, this.accurateModel); + } + + public Mono chatWithModel(String prompt, String modelName) { Map body = createRequestBody(prompt); return webClient.post() - .uri("/v1beta/models/" + model + ":generateContent") + .uri("/v1beta/models/" + modelName + ":generateContent") .bodyValue(body) .retrieve() .bodyToMono(new ParameterizedTypeReference>() {}) diff --git a/src/main/java/com/mumuk/global/config/GeminiClientConfig.java b/src/main/java/com/mumuk/global/config/GeminiClientConfig.java index c181b84..4f6b635 100644 --- a/src/main/java/com/mumuk/global/config/GeminiClientConfig.java +++ b/src/main/java/com/mumuk/global/config/GeminiClientConfig.java @@ -24,7 +24,9 @@ public WebClient geminiWebClient() { } @Bean - public GeminiClient geminiClient(WebClient geminiWebClient, @Value("${gemini.api.model}") String model) { - return new GeminiClient(geminiWebClient, model); + public GeminiClient geminiClient(WebClient geminiWebClient, + @Value("${gemini.api.model}") String model, + @Value("${gemini.api.model_accurate:${gemini.api.model}}") String accurateModel) { + return new GeminiClient(geminiWebClient, model, accurateModel); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 46bc82d..79ad471 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -95,4 +95,5 @@ gemini: key: ${GEMINI_API_KEY} url: https://generativelanguage.googleapis.com model: gemini-1.5-flash + model_accurate: gemini-1.5-pro From ab8e043a140486cc17e3564bad285cf07a0e8d30 Mon Sep 17 00:00:00 2001 From: beans3142 Date: Thu, 21 Aug 2025 19:01:55 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[REFACTOR]=20=EB=AA=A8=EB=8D=B8=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recipe/service/RecipeRecommendServiceImpl.java | 12 ++++++------ .../domain/recipe/service/RecipeServiceImpl.java | 1 - .../com/mumuk/global/config/GeminiClientConfig.java | 1 + 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java b/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java index 0772949..77265bc 100644 --- a/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java +++ b/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java @@ -1040,16 +1040,16 @@ private String buildRecipePostPromptCommon() { "- 예시: 된장찌개 O, 미나리 된장찌개 O, 돼지고기 앞다리살 감자 상추 된장찌개 X\n" + "- 고기 요리, 생선 요리, 채식 요리, 면 요리 등 다양한 재료 활용\n" + "- 레시피 제목에는 사용 재료가 명확히 보이도록 작성 (토마토 바질 파스타)\n" + - "- 레시피 제목에 재료 이름은 2개 이하로 구성, 주식(면(파스타, 우동 등), 밥(덮밥, 볶음밥 등))의 경우 3개까지 가능 (토마토 바질 파스타 O, 고추장 돼지고기 볶음 O, 고추장 양파 돼지고기 볶음 X)\n" + + "- 레시피 제목의 재료 노출은 2개 이하로 제한. 단, 주식(면: 파스타/우동 등, 밥: 덮밥/볶음밥 등)은 3개까지 허용 (예: 토마토 바질 파스타 O, 고추장 돼지고기 볶음 O, 고추장 양파 돼지고기 볶음 X)\n" + "※ 재료 포함 기준:\n" + - "- 실제 요리에 필요한 보편적인 모든 주요 재료를 포함해야 함 (최소 4개 재료, 많을수록 좋음, 조미료 이름도 포함)\n" + + "- 실제 요리에 필요한 주요 재료를 빠짐없이 포함 (최소 4개, 많을수록 좋음, 조미료도 명시)\n" + "- 예시: 제육볶음 → 돼지고기, 양파, 당근, 고추장, 간장, 설탕, 식용유, 후추 (8개 재료)\n" + "- 레시피 제목과 재료, 설명 모두 순수 한글로만 구성 (스파게티 O, 양파 O, noodle X, 네기 X)\n" + - "- 선택사항인 식재료는 제외 (연어 포케에 올리브도 들어갈 수 있지만 필수는 아님)n" + - "- 다양한 재료를 이용할 수 있는 경우 큰 틀로 작성 (앞다리살, 삼겹살, 목살 -> 돼지고기, 상추, 로메인, 샐러리 -> 샐러드 채소)"+ + "- 선택사항(옵션)인 식재료는 제외 (예: 연어 포케의 올리브는 필수 아님)\n" + + "- 다양한 재료를 이용할 수 있는 경우 큰 틀로 작성 (앞다리살/삼겹살/목살 → 돼지고기, 상추/로메인/샐러리 → 샐러드 채소)"+ "※ 카테고리 선택:\n" + - "- 각 요리의 특성에 맞는 카테고리를 적절하게 선택, 여러개 선택 가능, 애매하다 싶으면 추가\n" + - "- 마땅히 없다면 OTHER 선택 (OTHER은 유일해야 함함)\n\n" + + "- 각 요리의 특성에 맞는 카테고리를 적절하게 선택, 여러 개 선택 가능, 애매하면 추가\n" + + "- 마땅히 없다면 OTHER 선택 (OTHER은 유일해야 함)\n\n" + "※ 추천 결과는 다음 JSON 형식으로 출력해줘:\n" + "{\n" + " \"recommendations\": [\n" + diff --git a/src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java b/src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java index c6d1372..cb56533 100644 --- a/src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java +++ b/src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java @@ -472,7 +472,6 @@ private String buildIngredientMatchingPrompt(List userIngredients, List< --- ## 4단계: 절대 불가 규칙 (Absolute Prohibitions) -g 아무리 대체 가능 기준에 부합해 보여도, 아래 규칙 중 하나라도 위반하면 즉시 `mismatch` 처리한다. * **카테고리 교차:** 다른 카테고리의 재료 간 대체는 절대 불가. diff --git a/src/main/java/com/mumuk/global/config/GeminiClientConfig.java b/src/main/java/com/mumuk/global/config/GeminiClientConfig.java index 4f6b635..2f47a1f 100644 --- a/src/main/java/com/mumuk/global/config/GeminiClientConfig.java +++ b/src/main/java/com/mumuk/global/config/GeminiClientConfig.java @@ -24,6 +24,7 @@ public WebClient geminiWebClient() { } @Bean + @org.springframework.context.annotation.Primary public GeminiClient geminiClient(WebClient geminiWebClient, @Value("${gemini.api.model}") String model, @Value("${gemini.api.model_accurate:${gemini.api.model}}") String accurateModel) { From c3deb6406a9aecd155524d74dc28c291f6bf66e5 Mon Sep 17 00:00:00 2001 From: beans3142 Date: Thu, 21 Aug 2025 19:07:11 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[REFACTOR]=20=EB=AA=A8=EB=8D=B8=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/recipe/service/RecipeRecommendServiceImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java b/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java index 77265bc..7d51076 100644 --- a/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java +++ b/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java @@ -983,12 +983,12 @@ private List parseCategories(String category) { } } - // Gemini API를 사용하여 AI 호출 + // Gemini API를 사용하여 AI 호출 (Pro 모델 사용) private String callAI(String prompt) { try { - return geminiClient.chat(prompt).block(Duration.ofSeconds(30)); + return geminiClient.chatAccurate(prompt).block(Duration.ofSeconds(30)); } catch (Exception e) { - log.error("Gemini API 호출 실패: {}", e.getMessage()); + log.error("Gemini Pro API 호출 실패: {}", e.getMessage()); throw new BusinessException(ErrorCode.OPENAI_API_ERROR); } } From b96dea2b20864b2001bf94b25aaee41c12c818e7 Mon Sep 17 00:00:00 2001 From: beans3142 Date: Thu, 21 Aug 2025 19:23:08 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[REFACTOR]=20=EB=AA=A8=EB=8D=B8=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RecipeRecommendServiceImpl.java | 63 +++++++++++++------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java b/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java index 7d51076..9e346ca 100644 --- a/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java +++ b/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java @@ -1034,23 +1034,48 @@ private List getUserAllergies(Long userId) { * 공통 프롬프트 부분 생성 */ private String buildRecipePostPromptCommon() { - return "※ 레시피 선택 기준:\n" + - "- 실제 존재하는 보편적인 요리만 추천 (억지 조합 금지)\n" + - "- 레시피 제목은 검색으로 조리법을 찾을 수 있을 정도로 대중적이고 보편적\n" + - "- 예시: 된장찌개 O, 미나리 된장찌개 O, 돼지고기 앞다리살 감자 상추 된장찌개 X\n" + - "- 고기 요리, 생선 요리, 채식 요리, 면 요리 등 다양한 재료 활용\n" + - "- 레시피 제목에는 사용 재료가 명확히 보이도록 작성 (토마토 바질 파스타)\n" + - "- 레시피 제목의 재료 노출은 2개 이하로 제한. 단, 주식(면: 파스타/우동 등, 밥: 덮밥/볶음밥 등)은 3개까지 허용 (예: 토마토 바질 파스타 O, 고추장 돼지고기 볶음 O, 고추장 양파 돼지고기 볶음 X)\n" + - "※ 재료 포함 기준:\n" + - "- 실제 요리에 필요한 주요 재료를 빠짐없이 포함 (최소 4개, 많을수록 좋음, 조미료도 명시)\n" + - "- 예시: 제육볶음 → 돼지고기, 양파, 당근, 고추장, 간장, 설탕, 식용유, 후추 (8개 재료)\n" + - "- 레시피 제목과 재료, 설명 모두 순수 한글로만 구성 (스파게티 O, 양파 O, noodle X, 네기 X)\n" + - "- 선택사항(옵션)인 식재료는 제외 (예: 연어 포케의 올리브는 필수 아님)\n" + - "- 다양한 재료를 이용할 수 있는 경우 큰 틀로 작성 (앞다리살/삼겹살/목살 → 돼지고기, 상추/로메인/샐러리 → 샐러드 채소)"+ - "※ 카테고리 선택:\n" + - "- 각 요리의 특성에 맞는 카테고리를 적절하게 선택, 여러 개 선택 가능, 애매하면 추가\n" + - "- 마땅히 없다면 OTHER 선택 (OTHER은 유일해야 함)\n\n" + - "※ 추천 결과는 다음 JSON 형식으로 출력해줘:\n" + + return "너는 사용자가 제시한 재료를 바탕으로, 현실적이고 맛있는 요리 레시피를 추천하는 전문 레시피 큐레이터 AI다. 너의 임무는 주어진 규칙을 엄격히 준수하여, 사용자가 실제로 만들고 싶어지는 훌륭한 레시피를 JSON 형식으로 생성하는 것이다.\n\n" + + "## 1. 최상위 원칙: 억지 조합 절대 금지\n\n" + + "가장 중요한 원칙은 **'현실적인 요리'** 추천이다. 사용자가 제시한 재료 목록을 해석할 때, 아래의 지침을 반드시 따른다.\n\n" + + "* **'A 또는 B' 해석:** 입력에 '또는', '/' 등이 포함된 경우, 이는 **별개의 선택지**임을 의미한다. '코드 또는 햇살숭어'가 주어지면, '코드'를 사용한 요리 **혹은** '햇살숭어'를 사용한 요리를 각각 추천해야 한다. **두 재료를 한 요리에 섞는 것은 절대 금지된다.**\n" + + "* **'(선택)' 재료 해석:** '(선택)', '(옵션)' 등으로 표시된 재료는 필수가 아니다. 해당 재료가 없어도 완성되는 레시피를 우선적으로 고려해야 한다.\n" + + "* **재료의 본질 파악:** 사용자의 입력은 '만들 요리 재료 목록'이 아니라, '사용 가능한 재료 팔레트'이다. 이 팔레트에서 **가장 적절한 일부 재료를 선택**하여 만들 수 있는 최상의 요리를 추천해야 한다.\n\n" + + "---\n\n" + + "## 2. 작업 순서 (Workflow)\n\n" + + "너는 다음의 사고 과정에 따라 작업을 수행해야 한다.\n\n" + + "1. **입력 재료 분석 (Parse):** 사용자의 재료 목록을 보고 '또는', '(선택)' 등의 논리적/선택적 요소를 파악하여 재료 그룹을 나눈다.\n" + + "2. **핵심 재료 선정 (Select Core Ingredient):** 분석된 재료 그룹 중, 요리의 중심이 될 **하나의 핵심 재료**를 선택한다. (예: '코드'를 선택)\n" + + "3. **레시피 아이디어 생성 (Brainstorm):** 선택된 핵심 재료로 만들 수 있는, 대중적이고 상식적인 요리 아이디어를 떠올린다. (예: '대구 맑은탕', '대구 스테이크')\n" + + "4. **규칙 검증 (Validate):** 생성된 아이디어가 아래 **[3. 레시피 생성 상세 규칙]**을 모두 만족하는지 엄격하게 검증한다. 특히, 레시피 제목과 재료 구성이 규칙에 맞는지 확인한다.\n" + + "5. **JSON 형식화 (Format):** 검증을 통과한 레시피만 최종 JSON 형식으로 출력한다.\n\n" + + "---\n\n" + + "## 3. 레시피 생성 상세 규칙\n\n" + + "### ※ 레시피 제목 (Title)\n" + + "* **보편성:** 누구나 검색해서 조리법을 찾을 수 있는 대중적인 이름이어야 한다.\n" + + " * (O) 된장찌개, 토마토 바질 파스타\n" + + " * (X) 돼지고기 앞다리살 감자 상추 된장찌개\n" + + "* **재료 명시:** 요리의 정체성을 보여주는 **핵심 재료 1~2개**를 제목에 포함한다.\n" + + " * 단, 파스타, 덮밥, 볶음밥 등 주식(탄수화물) 기반 요리는 최대 3개까지 허용.\n" + + " * (O) 고추장 돼지고기 볶음\n" + + " * (X) 고추장 감자 양파 돼지고기 볶음\n\n" + + "### ※ 재료 구성 (Ingredients)\n" + + "* **필수 재료 포함:** 해당 요리를 만드는 데 필요한 **주요 재료를 최소 2개 이상** 빠짐없이 포함한다. (조미료도 명시)\n" + + " * (예) 제육볶음 → 돼지고기, 양파, 고추장, 간장, 설탕, 식용유 (6개 이상)\n" + + "* **선택 재료 제외:** 필수가 아닌 선택(옵션) 재료는 포함하지 않는다.\n" + + "* **일반화:** 대체 가능한 여러 재료는 포괄적인 상위 개념으로 작성한다.\n" + + " * (예) 앞다리살, 삼겹살, 목살 → `돼지고기`\n" + + " * (예) 상추, 로메인, 양상추 → `샐러드 채소`\n" + + " * (예) 숭어, 대구 → `흰살생선`\n" + + "* **언어:** 제목, 재료, 설명 모두 **순수 한글**로만 작성한다. (외래어 표기는 허용: 스파게티, 파스타)\n\n" + + "### ※ 카테고리 (Category)\n" + + "* **적절성:** 각 요리의 특성에 맞는 카테고리를 아래 목록에서 선택한다. (여러 개 선택 가능, 쉼표로 구분)\n" + + "* **기타:** 적절한 카테고리가 없을 경우 'OTHER'를 단독으로 사용한다.\n" + + "* **사용 가능 카테고리:** `BODY_WEIGHT_MANAGEMENT`, `HEALTH_MANAGEMENT`, `WEIGHT_LOSS`, `MUSCLE_GAIN`, `SUGAR_REDUCTION`, `BLOOD_PRESSURE`, `CHOLESTEROL`, `DIGESTION`, `OTHER`\n\n" + + "---\n\n" + + "## 4. JSON 출력 형식\n\n" + + "* 아래 명시된 JSON 구조와 필드명을 반드시 준수한다.\n" + + "* `description`은 UI 상단에 한 줄로 보여줄 예정이므로, 간결하고 매력적으로 작성한다. (예: '바삭한 계란전으로 든든한 한끼 완성!')\n" + + "* 영양 정보(칼로리, 단백질 등)는 일반적인 해당 요리의 평균값을 추정하여 기입한다.\n\n" + "{\n" + " \"recommendations\": [\n" + " {\n" + @@ -1065,9 +1090,7 @@ private String buildRecipePostPromptCommon() { " \"fat\": 10\n" + " }\n" + " ]\n" + - "}\n\n" + - "※ 사용 가능한 카테고리: BODY_WEIGHT_MANAGEMENT, HEALTH_MANAGEMENT, WEIGHT_LOSS, MUSCLE_GAIN, SUGAR_REDUCTION, BLOOD_PRESSURE, CHOLESTEROL, DIGESTION, OTHER\n\n" + - "※ summary는 UI 상단에 한 줄로 보여줄 예정이므로 간결하고 매력적으로 작성해줘. 예: '바삭한 계란전으로 든든한 한끼 완성!'"; + "}\n"; } /**