이전 글: GPT-4: 재료->음식->레시피 기능 구현 (tistory.com)
결국 레시피는 GptRecipeResponseDto의 형태로 프론트에 전달된다.
public class GptRecipeResponseDto {
private List<String> ingredients;
private List<String> instructions;
}
재료와 조리과정이 하나하나 구분되어 전달되며, 음식 이름은 이미 재료 -> 음식 과정에서 저장되어 있다.
레시피를 한번에 저장하는 방법도 있었겠지만 위의 방식처럼 하나하나 저장한 이유는 update 기능 때문이다.
user가 레시피 형식부터 마음대로 저장한다면, 오히려 user 입장에서는 불편한 사항이 될 것이다.
잘못하다 레시피를 잘못 적거나 날릴 위험성이 크기 때문이다.
그리고 블로그에 글 적듯이 레시피라는 긴 글을 관리하기는 것은 user에게 더 큰 부담감이 될 것이다.
조금 번거롭더라도 레시피를 하나하나 수정하고 재료도 하나하나 수정 할 수 있게 하는 것이 더 좋은 방식이라고 생각해서 위와 같이 재료 하나하나, 조리과정 한과정 한과정을 따로따로 저장하게 된 것이다.
레시피를 저장하는 테이블은 총 3개의 테이블로 구성된다.
recipe 테이블은 음식의 이름을 저장하고 user와 1:N 관계이다.
recipe_info 테이블과 ingedients테이블은 recipe 테이블과 1:N관계이다.
recipe_info 에는 조리 과정이 저장되고, ingedients 재료들이 저장된다.
public class Recipe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "recipe_id")
private Long id;
private String menu;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Ingredients> ingredients = new ArrayList<>();
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true)
private List<RecipeInfo> recipeInfoList = new ArrayList<>();
}
위 클래스는 Recipe Entity 클래스이다.
ingredients, recipeInfo 모두 Recipe가 지워지면 같이 지워져야 하기 때문에 cascade를 지정해줬다.
고아 Entity에 대한 대비를 위해 orphanRemoval = true도 적용해 주었다.
private User getUserFromSession(HttpSession session) {
// 세션에서 사용자 정보 가져오기
User loginUser = (User) session.getAttribute(SessionConst.LOGIN_MEMBER);
// 만약 세션에 사용자 정보가 없다면 로그인하지 않은 상태이므로 적절히 처리
if (loginUser == null) {
throw new IllegalArgumentException("로그인이 필요합니다.");
}
return loginUser;
}
이전에 session login에서 설명했듯이 session으로 부터 user 정보를 가져오기 위한 메서드이다.
1. Save
@Transactional
public String saveRecipe(SaveRecipeRequestDto saveRecipeRequestDto, HttpSession session) {
// Recipe 엔티티 생성 및 저장
Recipe recipe = new Recipe();
recipe.setMenu(saveRecipeRequestDto.getMenu());
recipe.setUser(getUserFromSession(session));
recipe = recipeRepository.save(recipe);
// 재료(Ingredients) 저장
List<Ingredients> ingredientsList = new ArrayList<>();
for (String ingredient : saveRecipeRequestDto.getIngredients()) {
Ingredients ingredients = new Ingredients();
ingredients.setIngredient(ingredient);
ingredients.setRecipe(recipe);
ingredientsList.add(ingredients);
}
ingredientsRepository.saveAll(ingredientsList);
// 레시피 정보(RecipeInfo) 저장
List<RecipeInfo> recipeInfoList = new ArrayList<>();
for (String recipeInfo : saveRecipeRequestDto.getRecipeInfoList()) {
RecipeInfo info = new RecipeInfo();
info.setInformation(recipeInfo);
info.setRecipe(recipe);
recipeInfoList.add(info);
}
recipeInfoRepository.saveAll(recipeInfoList);
return recipe.getMenu();
}
menu, List<ingredient>, List<recipeInfo>가 저장되어 있는 Dto를 받아준다.
user의 정보는 session으로 부터 얻으며, 얻은 user 정보와 Dto에 있는 menu를 이용해 먼저 recipe table를 저장해준다.
이후 ingredients와 recipeInfoList는 jpaRepository에서 지원하는 saveAll메서드를 이용해 한번에 저장한다.
음식 이름을 반환해준다.
2. Read
public List<GetAllRecipesResponseDto> getAllRecipes(HttpSession session) {
List<Recipe> recipes = recipeRepository.findAllByUserId(getUserFromSession(session).getId());
List<GetAllRecipesResponseDto> responseDtoList = new ArrayList<>();
for (Recipe recipe : recipes) {
responseDtoList.add(new GetAllRecipesResponseDto(recipe.getMenu(), recipe.getId()));
}
return responseDtoList;
}
한 유저가 저장한 모든 레시피를 보여주기 위해 만들어진 메서드이다.
session으로 부터 user 정보를 얻으며, List<GetAllRecipesResponseDto>를 반환한다. 지금보니 작명이 영 구리다.
해당 Dto는 menu와 recipe_id 두개로 형성되어 있다.
모든 레시피를 보여줄 때, 내용까지 보여주기는 너무 많다.
고로 레시피 내용을 보여줄 recipe_id와 어떤 레시피인지 알려줄 menu 두개만 전달해 주기로 했다.
public GetRecipeResponseDto getRecipeDetails(Long recipeId) {
Recipe recipe = recipeRepository.findById(recipeId)
.orElseThrow(() -> new IllegalArgumentException("레시피를 찾을 수 없습니다. ID: " + recipeId));
GetRecipeResponseDto responseDto = new GetRecipeResponseDto();
responseDto.setMenu(recipe.getMenu());
responseDto.setIngredients(recipe.getIngredients().stream()
.map(Ingredients::getIngredient)
.collect(Collectors.toList()));
responseDto.setRecipeInfoList(recipe.getRecipeInfoList().stream()
.map(RecipeInfo::getInformation)
.collect(Collectors.toList()));
return responseDto;
}
만일 user가 해당 recipe를 클릭한다면, recipe_id를 위 메서드에 전달해 해당 레시피의 상세정보를 전달해 줄 것이다.
문법형식은 컬랙션을 채택했다. 아직 조금 익숙하지 않긴 하지만, 공간 복잡도가 상당히 줄어드는 것이 눈이 보인다.
public class GetRecipeResponseDto {
private String menu;
private List<String> Ingredients;
private List<String> RecipeInfoList;
}
이게 레시피 상세보기의 반환형으로, 프론트에서 편하게 데이터를 가공할 수 있다.
3. Update
@Transactional
public String updateRecipe(Long recipeId, SaveRecipeRequestDto saveRecipeRequestDto, HttpSession session) {
// 기존의 레시피를 찾습니다.
Recipe existingRecipe = recipeRepository.findById(recipeId)
.orElseThrow(() -> new IllegalArgumentException("레시피를 찾을 수 없습니다."));
// 현재 사용자의 ID를 가져옵니다.
Long currentUserId = getUserFromSession(session).getId();
// 기존 레시피의 소유자와 현재 사용자를 비교하여 권한이 있는지 확인합니다.
if (!Objects.equals(existingRecipe.getUser().getId(), currentUserId)) {
throw new IllegalArgumentException("권한이 없습니다.");
}
// 레시피 내용을 업데이트합니다.
existingRecipe.setMenu(saveRecipeRequestDto.getMenu());
// 재료(Ingredients)를 업데이트합니다.
List<String> ingredientsList = saveRecipeRequestDto.getIngredients();
List<Ingredients> existingIngredients = existingRecipe.getIngredients();
existingIngredients.clear(); // 기존 재료를 모두 제거합니다.
for (String ingredient : ingredientsList) {
Ingredients ingredients = new Ingredients();
ingredients.setIngredient(ingredient);
ingredients.setRecipe(existingRecipe);
existingIngredients.add(ingredients); // 새로운 재료를 추가합니다.
}
// 레시피 정보(RecipeInfo)를 업데이트합니다.
List<String> recipeInfoList = saveRecipeRequestDto.getRecipeInfoList();
List<RecipeInfo> existingRecipeInfoList = existingRecipe.getRecipeInfoList();
existingRecipeInfoList.clear(); // 기존 레시피 정보를 모두 제거합니다.
for (String recipeInfo : recipeInfoList) {
RecipeInfo info = new RecipeInfo();
info.setInformation(recipeInfo);
info.setRecipe(existingRecipe);
existingRecipeInfoList.add(info); // 새로운 레시피 정보를 추가합니다.
}
// 업데이트된 레시피를 저장합니다.
recipeRepository.save(existingRecipe);
return existingRecipe.getMenu();
}
먼저 로그인 된 유저의 수정 권한이 있는지 확인한다.
권한이 확인되면 받아준 SaveRecipeRequestDto 형태의 데이터를 Recipe 형태로 가공해서 다시 저장해준다.
@Transactional이 꼭 필요하며 @Transactional의 역할은 다음과 같다.
- @Transactional 어노테이션은 이 메서드가 트랜잭션 내에서 실행되어야 함을 나타냅니다. 메서드 내의 작업이 모두 성공적으로 완료되면 트랜잭션은 커밋되고, 만약 예외가 발생하면 롤백됩니다.
Update 방식에 대해서는 아직도 고민이 많고, 프론트 개발이 진행됨에 따라 기능이 변경될 수도 있을 듯 하다.
한번에 모든 데이터가 변경될 수도 있고, 레시피 한문장 한문장씩 변경 될수도 있다.
이전에 사용한 Redis를 적용시키기 좋은 대목인 것같다.
4. Delete
public ResponseEntity<?> deleteRecipe(Long recipeId, HttpSession session) {
Long currentUserId = getUserFromSession(session).getId();
Optional<Recipe> recipe = recipeRepository.findById(recipeId);
if (recipe.isEmpty()) {
throw new IllegalArgumentException("존재하지 않는 레시피입니다.");
}
Long ownerId = recipe.get().getUser().getId();
if (!Objects.equals(currentUserId, ownerId)) {
throw new IllegalArgumentException("권한이 없습니다.");
}
// 레시피 삭제 로직
recipeRepository.deleteById(recipeId);
// 삭제 후 응답
return ResponseEntity.ok("레시피가 성공적으로 삭제되었습니다.");
}
먼저 로그인 된 유저의 삭제 권한이 있는지 확인한다.
해당 레시피를 삭제하면 레시피에 대한 모든 정보가 삭제되는데, 위에서 설명한 cascadeType 덕분이다.
이렇게 레시피 CRUD를 1차적으로 완성했다.
update 기능은 프론트 작업을 하며 수정될 가능성이 다분한 것 같다.
다음에 손볼때는 성능개선까지 같이 할 수 있도록 해보자.
'프로젝트 > AI명종원' 카테고리의 다른 글
기능 수정: 채팅 -> 레시피 저장 (0) | 2024.05.20 |
---|---|
채팅 CRUD 기능 구현 (0) | 2024.05.18 |
GPT-4: 재료->음식->레시피 기능 구현 (1) | 2024.05.17 |
GPT-3.5: 재료->음식->레시피 기능 구현 (0) | 2024.05.16 |
Roboflow Java API (0) | 2024.05.10 |