diff --git a/.gitignore b/.gitignore index 2138ebc..f2a598f 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,7 @@ out/ .vscode/ ## yml -**/application.properties \ No newline at end of file +**/application.properties +/venv/ +/RecBole_Sys/ +/log/ diff --git a/src/main/java/BabAl/BabalServer/controller/RecipeController.java b/src/main/java/BabAl/BabalServer/controller/RecipeController.java new file mode 100644 index 0000000..00219f7 --- /dev/null +++ b/src/main/java/BabAl/BabalServer/controller/RecipeController.java @@ -0,0 +1,54 @@ +package BabAl.BabalServer.controller; + +import BabAl.BabalServer.apiPayload.ApiResponse; +import BabAl.BabalServer.dto.request.RecipeRecommendationsDto; +import BabAl.BabalServer.dto.response.RecipeIngredientsResponseDto; +import BabAl.BabalServer.dto.response.RecipeRecommendationsResponseDto; +import BabAl.BabalServer.jwt.JwtUtil; +import BabAl.BabalServer.service.RecipeService; +import com.fasterxml.jackson.core.JsonProcessingException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import org.w3c.dom.stylesheets.LinkStyle; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/recipe") +@Tag(name = "Recipe Recommendation Page", description = "Recipe API") +public class RecipeController { + + private final RecipeService recipeService; + + @GetMapping("/ingredients") // 레시피 재료 검색 + @Operation(summary = "레시피 재료 검색", description = "레시피 재료 검색할 때 사용하는 API") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "요청이 정상 처리되었습니다", content = @Content(mediaType = "application/json")), + }) + public ApiResponse getIngredients(@RequestParam String alpha) throws JsonProcessingException { + return ApiResponse.onSuccess(recipeService.getIngredients(alpha)); + } + + @PostMapping("/recommendation") // 재료를 입력하면 사용자 프로필과 재료로 레시피 추천 + @Operation(summary = "레시피 추천", description = "레시피 추천받을 때 사용하는 API") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "요청이 정상 처리되었습니다", content = @Content(mediaType = "application/json")), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "MEMBER4001", description = "사용자가 없습니다", content = @Content(mediaType = "application/json")) + }) + public ApiResponse> getRecommendations(@RequestHeader("Authorization") String token, + @Valid @RequestBody RecipeRecommendationsDto dto) throws JsonProcessingException { + return ApiResponse.onSuccess(recipeService.getRecommendations(extractUserEmail(token), dto)); + } + + public String extractUserEmail(String token) { + String jwtToken = token.substring(7); + String userEmail = JwtUtil.getEmail(jwtToken); + return userEmail; + } +} \ No newline at end of file diff --git a/src/main/java/BabAl/BabalServer/domain/User.java b/src/main/java/BabAl/BabalServer/domain/User.java index 0070e62..410007f 100644 --- a/src/main/java/BabAl/BabalServer/domain/User.java +++ b/src/main/java/BabAl/BabalServer/domain/User.java @@ -52,6 +52,15 @@ public class User extends BaseEntity { // 기초대사량 private int bmr; + // 원하는 음식 태그 + @ElementCollection + @CollectionTable( + name = "recipe_tag", // 별도의 테이블명 + joinColumns = @JoinColumn(name = "user_id") + ) + @Column(name = "tag") + private List tagList; + // 음식 카테고리 @ElementCollection @Enumerated(EnumType.STRING) diff --git a/src/main/java/BabAl/BabalServer/dto/flaskRequest/IngredientAlpha.java b/src/main/java/BabAl/BabalServer/dto/flaskRequest/IngredientAlpha.java new file mode 100644 index 0000000..28cf0ad --- /dev/null +++ b/src/main/java/BabAl/BabalServer/dto/flaskRequest/IngredientAlpha.java @@ -0,0 +1,18 @@ +package BabAl.BabalServer.dto.flaskRequest; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class IngredientAlpha { + private String alphabet; + + public static IngredientAlpha ingredientAlpha(String alpha) { + return IngredientAlpha.builder().alphabet(alpha).build(); + } +} diff --git a/src/main/java/BabAl/BabalServer/dto/flaskRequest/RecipeRecommendation.java b/src/main/java/BabAl/BabalServer/dto/flaskRequest/RecipeRecommendation.java new file mode 100644 index 0000000..6ea8a02 --- /dev/null +++ b/src/main/java/BabAl/BabalServer/dto/flaskRequest/RecipeRecommendation.java @@ -0,0 +1,30 @@ +package BabAl.BabalServer.dto.flaskRequest; + +import BabAl.BabalServer.domain.User; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Optional; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class RecipeRecommendation { + private List ingredients; + private List tags; + private String gender; + private int age; + + public static RecipeRecommendation recipeRecommendation(Optional user, List ingredients) { + return RecipeRecommendation.builder() + .ingredients(ingredients) + .tags(user.get().getTagList()) + .gender(String.valueOf(user.get().getGender())) + .age(user.get().getAge()) + .build(); + } +} diff --git a/src/main/java/BabAl/BabalServer/dto/flaskResponse/RecipeRecommendationResponse.java b/src/main/java/BabAl/BabalServer/dto/flaskResponse/RecipeRecommendationResponse.java new file mode 100644 index 0000000..d64c8b4 --- /dev/null +++ b/src/main/java/BabAl/BabalServer/dto/flaskResponse/RecipeRecommendationResponse.java @@ -0,0 +1,22 @@ +package BabAl.BabalServer.dto.flaskResponse; + +import lombok.*; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RecipeRecommendationResponse { + private String name; + private int minutes; + private float calories; + private float carbohydrate; + private float protein; + private float fat; + private int n_steps; + private String steps; + private int n_ingredients; + private List ingredients; +} diff --git a/src/main/java/BabAl/BabalServer/dto/request/RecipeRecommendationsDto.java b/src/main/java/BabAl/BabalServer/dto/request/RecipeRecommendationsDto.java new file mode 100644 index 0000000..d3fcce7 --- /dev/null +++ b/src/main/java/BabAl/BabalServer/dto/request/RecipeRecommendationsDto.java @@ -0,0 +1,14 @@ +package BabAl.BabalServer.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class RecipeRecommendationsDto { + private List ingredients; +} diff --git a/src/main/java/BabAl/BabalServer/dto/response/RecipeIngredientsResponseDto.java b/src/main/java/BabAl/BabalServer/dto/response/RecipeIngredientsResponseDto.java new file mode 100644 index 0000000..6c3f09a --- /dev/null +++ b/src/main/java/BabAl/BabalServer/dto/response/RecipeIngredientsResponseDto.java @@ -0,0 +1,15 @@ +package BabAl.BabalServer.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class RecipeIngredientsResponseDto { + private int count; + private List ingredients; +} diff --git a/src/main/java/BabAl/BabalServer/dto/response/RecipeRecommendationListResponseDto.java b/src/main/java/BabAl/BabalServer/dto/response/RecipeRecommendationListResponseDto.java new file mode 100644 index 0000000..3444e95 --- /dev/null +++ b/src/main/java/BabAl/BabalServer/dto/response/RecipeRecommendationListResponseDto.java @@ -0,0 +1,15 @@ +package BabAl.BabalServer.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RecipeRecommendationListResponseDto { + RecipeRecommendationsResponseDto mine; + RecipeRecommendationsResponseDto others; +} diff --git a/src/main/java/BabAl/BabalServer/dto/response/RecipeRecommendationsResponseDto.java b/src/main/java/BabAl/BabalServer/dto/response/RecipeRecommendationsResponseDto.java new file mode 100644 index 0000000..6a1c5ee --- /dev/null +++ b/src/main/java/BabAl/BabalServer/dto/response/RecipeRecommendationsResponseDto.java @@ -0,0 +1,38 @@ +package BabAl.BabalServer.dto.response; + +import BabAl.BabalServer.dto.flaskResponse.RecipeRecommendationResponse; +import lombok.*; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RecipeRecommendationsResponseDto { + private String name; + private int minutes; + private float calories; + private float carbohydrate; + private float protein; + private float fat; + private int n_steps; + private List steps; + private int n_ingredients; + private List ingredients; + + public static RecipeRecommendationsResponseDto recipeRecommendationsResponse(RecipeRecommendationResponse dto, List stepList) { + return RecipeRecommendationsResponseDto.builder() + .name(dto.getName()) + .minutes(dto.getMinutes()) + .calories(dto.getCalories()) + .carbohydrate(dto.getCarbohydrate()) + .protein(dto.getProtein()) + .fat(dto.getFat()) + .n_steps(dto.getN_steps()) + .steps(stepList) + .n_ingredients(dto.getN_ingredients()) + .ingredients(dto.getIngredients()) + .build(); + } +} diff --git a/src/main/java/BabAl/BabalServer/service/RecipeService.java b/src/main/java/BabAl/BabalServer/service/RecipeService.java new file mode 100644 index 0000000..0935104 --- /dev/null +++ b/src/main/java/BabAl/BabalServer/service/RecipeService.java @@ -0,0 +1,17 @@ +package BabAl.BabalServer.service; + +import BabAl.BabalServer.dto.request.RecipeRecommendationsDto; +import BabAl.BabalServer.dto.response.RecipeIngredientsResponseDto; +import BabAl.BabalServer.dto.response.RecipeRecommendationsResponseDto; +import com.fasterxml.jackson.core.JsonProcessingException; + +import java.util.List; + +public interface RecipeService { + + // 레시피 검색 + RecipeIngredientsResponseDto getIngredients(String alpha) throws JsonProcessingException; + + // 레시피 추천 + List getRecommendations(String userEmail, RecipeRecommendationsDto ingredients) throws JsonProcessingException; +} diff --git a/src/main/java/BabAl/BabalServer/service/RecipeServiceImpl.java b/src/main/java/BabAl/BabalServer/service/RecipeServiceImpl.java new file mode 100644 index 0000000..8aed8b7 --- /dev/null +++ b/src/main/java/BabAl/BabalServer/service/RecipeServiceImpl.java @@ -0,0 +1,153 @@ +package BabAl.BabalServer.service; + +import BabAl.BabalServer.apiPayload.code.status.ErrorStatus; +import BabAl.BabalServer.apiPayload.exception.GeneralException; +import BabAl.BabalServer.domain.User; +import BabAl.BabalServer.dto.flaskRequest.IngredientAlpha; +import BabAl.BabalServer.dto.flaskRequest.RecipeRecommendation; +import BabAl.BabalServer.dto.flaskResponse.RecipeRecommendationResponse; +import BabAl.BabalServer.dto.request.RecipeRecommendationsDto; +import BabAl.BabalServer.dto.response.MenuRecommendationResponseDto; +import BabAl.BabalServer.dto.response.RecipeIngredientsResponseDto; +import BabAl.BabalServer.dto.response.RecipeRecommendationsResponseDto; +import BabAl.BabalServer.repository.UserRepository; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@Transactional +@RequiredArgsConstructor +public class RecipeServiceImpl implements RecipeService { + private final ObjectMapper objectMapper; // 데이터를 JSON 객체로 변환 + private final UserRepository userRepository; + + @Value("${flask.url.search}") + private String flaskUrlIngredientSearch; + + @Value("${flask.url.recommendation}") + private String flaskUrlRecipeRecommendation; + + // 레시피 검색 + @Override + public RecipeIngredientsResponseDto getIngredients(String alpha) throws JsonProcessingException { + // 1. flask 로 알파벳 보내기 + IngredientAlpha alphabet = IngredientAlpha.ingredientAlpha(alpha); + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(objectMapper.writeValueAsString(alphabet), httpHeaders); + + // 2. flask 응답 받기 + String flaskResponse = restTemplate.postForObject(flaskUrlIngredientSearch, entity, String.class); + + // 3. 응답 JSON을 RecipeIngredientsResponseDto로 변환하여 반환 + return objectMapper.readValue(flaskResponse, RecipeIngredientsResponseDto.class); + } + + // 레시피 추천 + @Override + public List getRecommendations(String userEmail, RecipeRecommendationsDto ingredients) throws JsonProcessingException { + // 0. user 정보 불러오기 + Optional user = userRepository.findByEmail(userEmail); + if (user.isEmpty()) { + throw new GeneralException(ErrorStatus.MEMBER_NOT_FOUND); + } + + // 1. flask 로 보낼 dto 세팅 + RecipeRecommendation dto = RecipeRecommendation.recipeRecommendation(user, ingredients.getIngredients()); + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(objectMapper.writeValueAsString(dto), httpHeaders); + + // 2. flask 응답 받기 + String flaskResponse = restTemplate.postForObject(flaskUrlRecipeRecommendation, entity, String.class); + System.out.println("Flask Response: " + flaskResponse); // 응답 로그 출력 + + // 3. 응답 JSON을 RecipeRecommendationsResponseDto 변환 + // RecipeRecommendationResponse responseDto = objectMapper.readValue(flaskResponse, RecipeRecommendationResponse.class); + + List responseDtos = objectMapper.readValue( + flaskResponse, new TypeReference>() {} + ); + + // 여러 개의 응답 DTO를 담을 리스트 생성 + List answerDTOList = new ArrayList<>(); + + for (RecipeRecommendationResponse responseDto : responseDtos) { + + // 4. steps 문자열에서 불필요한 문자 제거 후 리스트로 변환 + String stepsString = responseDto.getSteps(); // steps는 하나의 문자열로 전달됨 + + // 4-1. 양 끝의 '['와 ']' 제거하고, 공백을 기준으로 나누기 + stepsString = stepsString.replace("[", "").replace("]", ""); + + // 4-2. 각 항목을 ' '로 나누고, 공백을 기준으로 결합된 단어들을 리스트로 변환 + String[] stepsArray = stepsString.split("'\\s*,\\s*'"); // 각 단계마다 ' 로 구분되어 있으므로 이를 기준으로 나눈다. + + // 4-3. 각 단계를 리스트로 변환하고 불필요한 공백 및 따옴표 제거 + List stepsList = new ArrayList<>(); + for (String step : stepsArray) { + stepsList.add(step.replace("'", "").trim()); // ' 제거하고 공백 제거 + } + + List result = new ArrayList<>(); + + for (String step : stepsArray) { + StringBuilder sb = new StringBuilder(); + boolean chk = false; + String last = "empty"; + + for (int i = 0; i < step.length(); i++) { + char c = step.charAt(i); + + if (Character.isAlphabetic(c)) { // 알파벳은 추가 + sb.append(c); + last = "alpha"; + chk = false; + } else if (c == ' ' && last.equals("alpha")) { + // 공백 등장, 앞 글자가 알파벳이었으면 공백 삭제 + last = "empty"; + } else if (chk) { + continue; + } else if (c == ' ' && last.equals("empty")) { + // 공백 등장, 앞 글자가 공백 + sb.append(' '); + chk = true; + } + } + + // 결과 리스트에 추가 + result.add(sb.toString()); + } + + // 앞뒤 공백을 제거한 새로운 리스트 생성 + List trimmedSteps = new ArrayList<>(); + for (String step : result) { + trimmedSteps.add(step.trim()); // trim()을 사용하여 공백을 제거 + } + + // 5. steps를 리스트로 변환한 후 응답에 설정 + RecipeRecommendationsResponseDto answerDTO = RecipeRecommendationsResponseDto.recipeRecommendationsResponse(responseDto, trimmedSteps); + answerDTOList.add(answerDTO); + } + + // 6. 반환 + return answerDTOList; + } +} diff --git a/src/main/java/BabAl/BabalServer/service/RestaurantServiceImpl.java b/src/main/java/BabAl/BabalServer/service/RestaurantServiceImpl.java index 7ab9697..561aa77 100644 --- a/src/main/java/BabAl/BabalServer/service/RestaurantServiceImpl.java +++ b/src/main/java/BabAl/BabalServer/service/RestaurantServiceImpl.java @@ -29,7 +29,7 @@ public class RestaurantServiceImpl implements RestaurantService { private final UserRepository userRepository; private final ObjectMapper objectMapper; // 데이터를 JSON 객체로 변환 - @Value("${flask.url}") + @Value("${flask.url.rec}") private String flaskUrl; @Override diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3d49e09..dfc1270 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -31,4 +31,7 @@ jwt: secret: ${JWT_SECRET} flask: - url: ${FLASK_URL} \ No newline at end of file + url: + rec: ${FLASK_URL_REC} + search: ${FLASK_URL_SEARCH} + recommendation: ${FLASK_URL_RECOMMENDATION} \ No newline at end of file