55import BE_Elixir .Elixir .domain .ingredient .repository .IngredientRepository ;
66import BE_Elixir .Elixir .domain .member .entity .Member ;
77import BE_Elixir .Elixir .domain .recipe .entity .Recipe ;
8- import BE_Elixir .Elixir .domain .recipe .entity .RecipeIngredient ;
98import BE_Elixir .Elixir .domain .recipe .repository .RecipeEventRepository ;
109import BE_Elixir .Elixir .domain .recipe .repository .RecipeRepository ;
1110import BE_Elixir .Elixir .domain .recommendation .dto .RecommendationResponseDTO ;
12- import BE_Elixir .Elixir .global . enums . CategoryType ;
11+ import BE_Elixir .Elixir .domain . recommendation . repository . RecommendationRepository ;
1312import BE_Elixir .Elixir .global .redis .RedisRecipeService ;
1413import org .springframework .transaction .annotation .Transactional ;
1514import lombok .RequiredArgsConstructor ;
@@ -28,61 +27,81 @@ public class RecommendationService {
2827 private final RedisRecipeService redisRecipeService ;
2928 private final RecipeEventRepository recipeEventRepository ;
3029 private final IngredientRepository ingredientRepository ;
30+ private final RecommendationRepository recommendationRepository ;
3131
3232
3333 // 사용자 맞춤형 레시피 추천
3434 @ Transactional (readOnly = true )
3535 public List <RecommendationResponseDTO > getRecommendationsForUser (Member member ) {
36+ // 동적 캐시 키 생성
37+ String cacheKey = createCacheKey (member );
38+
3639 // 캐시 확인
37- List <RecommendationResponseDTO > cached = redisRecipeService .getCachedRecommendations (member . getId () );
40+ List <RecommendationResponseDTO > cached = redisRecipeService .getCachedRecommendations (cacheKey );
3841 if (cached != null ) return cached ;
3942
40- List <Recipe > allRecipes = recipeRepository .findAll ();
43+ // 필터링 조건 추출
44+ List <String > mealStyles = member .getMealStyles ();
45+ List <String > recipeStyles = member .getRecipeStyles ();
46+ List <String > reasons = member .getReasons ();
47+ List <String > allergies = member .getAllergies ();
48+
49+ // Repository에서 필터된 레시피 조회 (필요에 따라 쿼리 수정 필요)
50+ List <Recipe > filteredRecipes = recommendationRepository .findFilteredRecipes (recipeStyles , reasons );
4151
42- // 필터링
43- List <Recipe > filtered = allRecipes .stream ()
44- .filter (recipe -> !hasAllergyConflict (member , recipe ))
45- .filter (recipe -> matchesMealStyle (member , recipe ))
46- .filter (recipe -> matchesRecipeStyle (member , recipe ))
47- .filter (recipe -> matchesReason (member , recipe ))
52+ // 알러지 필터링 + 식사 스타일, 이유 조건 필터링 추가 로직 (필요시)
53+ filteredRecipes = filteredRecipes .stream ()
54+ .filter (recipe -> !hasAllergyConflict (allergies , recipe ))
55+ .filter (recipe -> mealStyles .isEmpty () || mealStyles .contains (recipe .getCategoryType ().name ()))
4856 .limit (3 )
4957 .collect (Collectors .toList ());
5058
51- // 필터링된 레시피가 없을 경우 → 랜덤
52- if (filtered .isEmpty ()) {
53- Collections .shuffle (allRecipes ); // 리스트를 무작위로
54- filtered = allRecipes .stream ()
55- .limit (3 )
56- .collect (Collectors .toList ());
59+ // 필터링된 결과 없으면 랜덤 3개
60+ if (filteredRecipes .isEmpty ()) {
61+ filteredRecipes = recommendationRepository .findRandom3 ();
5762 }
5863
59- // 스크랩 여부 확인 및 DTO 변환
60- List <RecommendationResponseDTO > recommendations = filtered .stream ()
64+ // DTO 변환 및 스크랩 여부 체크
65+ List <RecommendationResponseDTO > recommendations = filteredRecipes .stream ()
6166 .map (recipe -> {
62- boolean scrappedByCurrentUser = recipeEventRepository .existsByRecipeIdAndMemberIdAndScrapFlagTrue (recipe .getId (), member .getId ());
63- return new RecommendationResponseDTO (recipe , scrappedByCurrentUser );
67+ boolean scrappedByUser = recipeEventRepository .existsByRecipeIdAndMemberIdAndScrapFlagTrue (recipe .getId (), member .getId ());
68+ return new RecommendationResponseDTO (recipe , scrappedByUser );
6469 })
6570 .collect (Collectors .toList ());
6671
67- // 캐싱 (1시간)
68- redisRecipeService .cacheRecommendations (member .getId (), recommendations , Duration .ofHours (1 ));
72+ // 캐싱
73+ redisRecipeService .cacheRecommendations (cacheKey , recommendations , Duration .ofMinutes (15 ));
74+
6975 return recommendations ;
7076 }
7177
72- // 알러지가 포함된 레시피는 제외
73- private boolean hasAllergyConflict (Member member , Recipe recipe ) {
74- List <String > userAllergies = member .getAllergies (); // 사용자의 알러지 리스트
75- List <String > recipeAllergies = recipe .getAllergyList (); // 레시피에 포함된 알러지 리스트
78+ private String createCacheKey (Member member ) {
79+ String mealKey = String .join ("," , member .getMealStyles ());
80+ String recipeKey = String .join ("," , member .getRecipeStyles ());
81+ String reasonKey = String .join ("," , member .getReasons ());
82+ String allergyKey = String .join ("," , member .getAllergies ());
83+
84+ return "recommendations:"
85+ + member .getId ()
86+ + ":mealStyles=" + mealKey
87+ + ":recipeStyles=" + recipeKey
88+ + ":reasons=" + reasonKey
89+ + ":allergies=" + allergyKey ;
90+ }
91+
7692
93+ // 알러지가 포함된 레시피는 제외
94+ private boolean hasAllergyConflict (List <String > userAllergies , Recipe recipe ) {
7795 for (String allergy : userAllergies ) {
78- if (recipeAllergies .contains (allergy )) {
79- return true ; // 겹치는 알러지가 있으면 true
96+ if (recipe . getAllergyList () .contains (allergy )) {
97+ return true ;
8098 }
8199 }
82- return false ; // 겹치는 알러지가 없으면 false
100+ return false ;
83101 }
84102
85103
104+
86105 // 식사 스타일(육류기반, 채식기반, 혼합식)에 따라 레시피 필터링
87106 public boolean matchesMealStyle (Member member , Recipe recipe ) {
88107 // 레시피의 식재료 카테고리 리스트 추출
@@ -178,7 +197,7 @@ private boolean matchesReason(Member member, Recipe recipe) {
178197 @ Transactional (readOnly = true )
179198 public List <String > getRecommendedKeywords (Member member ) {
180199 // 캐시된 추천 레시피 확인
181- List <RecommendationResponseDTO > cached = redisRecipeService .getCachedRecommendations (member .getId ());
200+ List <RecommendationResponseDTO > cached = redisRecipeService .getCachedRecommendations (String . valueOf ( member .getId () ));
182201 if (cached == null || cached .isEmpty ()) return List .of ();
183202
184203 Set <String > keywords = new LinkedHashSet <>(); // 중복 제거 + 순서 유지
0 commit comments