From 26c9bc5f6d5d5e882f616676535963214b44b72b Mon Sep 17 00:00:00 2001 From: mjkang4416 Date: Wed, 12 Feb 2025 14:56:19 +0900 Subject: [PATCH 01/26] =?UTF-8?q?[Feat]=20=EA=B8=B0=EB=B3=B8=EC=84=B8?= =?UTF-8?q?=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기본 Db 세팅 --- .../blendish/domain/comments/entity/.gitkeep | 0 .../domain/comments/entity/Comment.java | 45 +++++++++++++++++++ .../blendish/domain/recipe/entity/.gitkeep | 0 .../blendish/domain/recipe/entity/Recipe.java | 4 ++ .../blendish/domain/user/entity/User.java | 4 ++ 5 files changed, 53 insertions(+) delete mode 100644 src/main/java/com/example/blendish/domain/comments/entity/.gitkeep create mode 100644 src/main/java/com/example/blendish/domain/comments/entity/Comment.java delete mode 100644 src/main/java/com/example/blendish/domain/recipe/entity/.gitkeep diff --git a/src/main/java/com/example/blendish/domain/comments/entity/.gitkeep b/src/main/java/com/example/blendish/domain/comments/entity/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/blendish/domain/comments/entity/Comment.java b/src/main/java/com/example/blendish/domain/comments/entity/Comment.java new file mode 100644 index 0000000..478d3d7 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/comments/entity/Comment.java @@ -0,0 +1,45 @@ +package com.example.blendish.domain.comments.entity; + +import com.example.blendish.domain.recipe.entity.Recipe; +import com.example.blendish.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Date; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long commentId; + + @Column(nullable = false, length = 100) + private String content; + + @Temporal(TemporalType.DATE) + private Date createdAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; + + @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL, orphanRemoval = true) + private List replies; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parentCommentId", nullable = true, insertable = false, updatable = false) + private Comment parentComment; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipeId", nullable = false) + private Recipe recipe; + + @PrePersist + public void prePersist() { + this.createdAt = new Date(); + } +} diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/.gitkeep b/src/main/java/com/example/blendish/domain/recipe/entity/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java index 66b8492..a2f2a4e 100644 --- a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java +++ b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java @@ -1,5 +1,6 @@ package com.example.blendish.domain.recipe.entity; +import com.example.blendish.domain.comments.entity.Comment; import com.example.blendish.domain.user.entity.User; import jakarta.persistence.*; import lombok.Getter; @@ -47,4 +48,7 @@ public class Recipe { @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) private List steps; + @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) + private List comment ; + } diff --git a/src/main/java/com/example/blendish/domain/user/entity/User.java b/src/main/java/com/example/blendish/domain/user/entity/User.java index 6518a88..14b554d 100644 --- a/src/main/java/com/example/blendish/domain/user/entity/User.java +++ b/src/main/java/com/example/blendish/domain/user/entity/User.java @@ -1,6 +1,7 @@ package com.example.blendish.domain.user.entity; +import com.example.blendish.domain.comments.entity.Comment; import jakarta.persistence.*; import lombok.Data; @@ -24,4 +25,7 @@ public class User { @OneToMany(mappedBy = "user",cascade = CascadeType.ALL,orphanRemoval = true) private List tastePreferences; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments ; } From d8789e7ac8138633e0a1669b667c8c7e13af3b8d Mon Sep 17 00:00:00 2001 From: mjkang4416 Date: Thu, 13 Feb 2025 12:14:38 +0900 Subject: [PATCH 02/26] =?UTF-8?q?[Feat]=20=EC=9D=B8=EA=B8=B0=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=ED=94=BC=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 좋아요 순으로 인기레시피 4개 띄우는 api Related to: #17 --- .../controller/CommunityController.java | 33 ++++++++++++ .../domain/comments/repository/.gitkeep | 0 .../repository/CommentsRepository.java | 12 +++++ .../blendish/domain/recipe/dto/.gitkeep | 0 .../recipe/dto/CommunityHotRecipyDTO.java | 17 +++++++ .../blendish/domain/recipe/entity/Likes.java | 26 ++++++++++ .../blendish/domain/recipe/entity/Recipe.java | 8 +++ .../blendish/domain/recipe/entity/Scrap.java | 25 +++++++++ .../domain/recipe/repository/.gitkeep | 0 .../recipe/repository/LikeRepository.java | 16 ++++++ .../recipe/repository/RecipeRepository.java | 9 ++++ .../blendish/domain/recipe/service/.gitkeep | 0 .../recipe/service/CommunityService.java | 51 +++++++++++++++++++ .../blendish/domain/user/entity/User.java | 10 ++++ 14 files changed, 207 insertions(+) create mode 100644 src/main/java/com/example/blendish/controller/CommunityController.java delete mode 100644 src/main/java/com/example/blendish/domain/comments/repository/.gitkeep create mode 100644 src/main/java/com/example/blendish/domain/comments/repository/CommentsRepository.java delete mode 100644 src/main/java/com/example/blendish/domain/recipe/dto/.gitkeep create mode 100644 src/main/java/com/example/blendish/domain/recipe/dto/CommunityHotRecipyDTO.java create mode 100644 src/main/java/com/example/blendish/domain/recipe/entity/Likes.java create mode 100644 src/main/java/com/example/blendish/domain/recipe/entity/Scrap.java delete mode 100644 src/main/java/com/example/blendish/domain/recipe/repository/.gitkeep create mode 100644 src/main/java/com/example/blendish/domain/recipe/repository/LikeRepository.java create mode 100644 src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java delete mode 100644 src/main/java/com/example/blendish/domain/recipe/service/.gitkeep create mode 100644 src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java diff --git a/src/main/java/com/example/blendish/controller/CommunityController.java b/src/main/java/com/example/blendish/controller/CommunityController.java new file mode 100644 index 0000000..cb2b478 --- /dev/null +++ b/src/main/java/com/example/blendish/controller/CommunityController.java @@ -0,0 +1,33 @@ +package com.example.blendish.controller; + +import com.example.blendish.domain.recipe.dto.CommunityHotRecipyDTO; +import com.example.blendish.domain.recipe.service.CommunityService; +import com.example.blendish.global.dto.ApiResponseTemplate; +import com.example.blendish.global.response.SuccessCode; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Tag(name = "Community Controller", description = "커뮤니티 관련 API") +@RestController +@RequestMapping("/api/community") +@AllArgsConstructor +public class CommunityController { + + private final CommunityService communityService; + + + @GetMapping("/main") + public ResponseEntity>> getmain() { + + List hotRecipyDTOS = communityService.getTopLikedRecipes(); + + return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, hotRecipyDTOS)); + } +} diff --git a/src/main/java/com/example/blendish/domain/comments/repository/.gitkeep b/src/main/java/com/example/blendish/domain/comments/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/blendish/domain/comments/repository/CommentsRepository.java b/src/main/java/com/example/blendish/domain/comments/repository/CommentsRepository.java new file mode 100644 index 0000000..a3347e4 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/comments/repository/CommentsRepository.java @@ -0,0 +1,12 @@ +package com.example.blendish.domain.comments.repository; + +import com.example.blendish.domain.comments.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface CommentsRepository extends JpaRepository { + // 해당 recipeId에 대한 댓글 개수 카운트 + @Query("SELECT COUNT(c) FROM Comment c WHERE c.recipe.recipeId = :recipeId") + int countByRecipeRecipeId(@Param("recipeId") Long recipeId); +} diff --git a/src/main/java/com/example/blendish/domain/recipe/dto/.gitkeep b/src/main/java/com/example/blendish/domain/recipe/dto/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/blendish/domain/recipe/dto/CommunityHotRecipyDTO.java b/src/main/java/com/example/blendish/domain/recipe/dto/CommunityHotRecipyDTO.java new file mode 100644 index 0000000..63c6c1d --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipe/dto/CommunityHotRecipyDTO.java @@ -0,0 +1,17 @@ +package com.example.blendish.domain.recipe.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class CommunityHotRecipyDTO { + private Long recipeId; + private String foodImage; + private int likeCount; + private int commentCount; +} diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/Likes.java b/src/main/java/com/example/blendish/domain/recipe/entity/Likes.java new file mode 100644 index 0000000..88e1fba --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipe/entity/Likes.java @@ -0,0 +1,26 @@ +package com.example.blendish.domain.recipe.entity; + +import com.example.blendish.domain.recipe.entity.Recipe; +import com.example.blendish.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Entity +public class Likes { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long likeId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipeId", nullable = false) + private Recipe recipe; + +} diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java index a2f2a4e..ca6128a 100644 --- a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java +++ b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java @@ -29,6 +29,8 @@ public class Recipe { private int likeCount; + private int scrapCount; + @Column(length = 200) private String information; @@ -51,4 +53,10 @@ public class Recipe { @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) private List comment ; + @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) + private List likes ; + + @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) + private List scraps ; + } diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/Scrap.java b/src/main/java/com/example/blendish/domain/recipe/entity/Scrap.java new file mode 100644 index 0000000..fee2507 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipe/entity/Scrap.java @@ -0,0 +1,25 @@ +package com.example.blendish.domain.recipe.entity; + +import com.example.blendish.domain.recipe.entity.Recipe; +import com.example.blendish.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Entity +public class Scrap { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long scrapId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipeId", nullable = false) + private Recipe recipe; +} diff --git a/src/main/java/com/example/blendish/domain/recipe/repository/.gitkeep b/src/main/java/com/example/blendish/domain/recipe/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/blendish/domain/recipe/repository/LikeRepository.java b/src/main/java/com/example/blendish/domain/recipe/repository/LikeRepository.java new file mode 100644 index 0000000..8e64263 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipe/repository/LikeRepository.java @@ -0,0 +1,16 @@ +package com.example.blendish.domain.recipe.repository; + +import com.example.blendish.domain.recipe.entity.Likes; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface LikeRepository extends JpaRepository { + @Query("SELECT l.recipe.recipeId FROM Likes l GROUP BY l.recipe.recipeId ORDER BY COUNT(l) DESC") + List findTopLikedRecipeIds(); + + + +} diff --git a/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java b/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java new file mode 100644 index 0000000..a95ad8c --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java @@ -0,0 +1,9 @@ +package com.example.blendish.domain.recipe.repository; + +import com.example.blendish.domain.recipe.dto.CommunityHotRecipyDTO; +import com.example.blendish.domain.recipe.entity.Recipe; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RecipeRepository extends JpaRepository { + Recipe findByRecipeId(Long recipiId); +} diff --git a/src/main/java/com/example/blendish/domain/recipe/service/.gitkeep b/src/main/java/com/example/blendish/domain/recipe/service/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java b/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java new file mode 100644 index 0000000..238b91d --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java @@ -0,0 +1,51 @@ +package com.example.blendish.domain.recipe.service; + +import com.example.blendish.domain.comments.repository.CommentsRepository; +import com.example.blendish.domain.recipe.dto.CommunityHotRecipyDTO; +import com.example.blendish.domain.recipe.entity.Recipe; +import com.example.blendish.domain.recipe.repository.LikeRepository; +import com.example.blendish.domain.recipe.repository.RecipeRepository; +import io.jsonwebtoken.lang.Collections; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +@AllArgsConstructor +public class CommunityService { + private final RecipeRepository recipeRepository; + private final LikeRepository likeRepository; + private final CommentsRepository commentsRepository; + + // 인기 레시피 가져오는 서비스 + public List getTopLikedRecipes() { + + List topLikedRecipi = new ArrayList<>(); + + // 좋아요 많은 id 가져오기 + List topLikedRecipeIds = likeRepository.findTopLikedRecipeIds(); + + + if (topLikedRecipeIds.isEmpty()){ + return Collections.emptyList(); + } + + // 가져온거 4번 돌리면서 dto 에 집어넣기 + for (int i = 0; i < Math.min(topLikedRecipeIds.size(), 4); i++) { + Long recipeId = topLikedRecipeIds.get(i); + // 레시피 객체 가져오기 + Recipe recipe = recipeRepository.findByRecipeId(recipeId); + // 코멘트 개수 가져오기 + int CommentNum = commentsRepository.countByRecipeRecipeId(recipeId); + + CommunityHotRecipyDTO topRecipes = new CommunityHotRecipyDTO(recipeId,recipe.getFoodImage(),recipe.getLikeCount(),CommentNum); + + + topLikedRecipi.add(topRecipes); + } + + return topLikedRecipi; + } +} diff --git a/src/main/java/com/example/blendish/domain/user/entity/User.java b/src/main/java/com/example/blendish/domain/user/entity/User.java index 14b554d..b1aaf0c 100644 --- a/src/main/java/com/example/blendish/domain/user/entity/User.java +++ b/src/main/java/com/example/blendish/domain/user/entity/User.java @@ -2,6 +2,8 @@ import com.example.blendish.domain.comments.entity.Comment; +import com.example.blendish.domain.recipe.entity.Likes; +import com.example.blendish.domain.recipe.entity.Scrap; import jakarta.persistence.*; import lombok.Data; @@ -28,4 +30,12 @@ public class User { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List comments ; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List likes ; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List scraps ; + } + From e0e39127755c72a055943803e71407d3b771cf41 Mon Sep 17 00:00:00 2001 From: mjkang4416 Date: Thu, 13 Feb 2025 12:46:19 +0900 Subject: [PATCH 03/26] =?UTF-8?q?[Feat]=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=EB=9E=98=EC=8B=9C=ED=94=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 오늘의 래시피 랜덤으로 띄우기 Related to: #17 --- .../controller/CommunityController.java | 20 ++++++++++++---- ...ipyDTO.java => CommunityHotRecipeDTO.java} | 3 ++- .../recipe/dto/CommunityTodayRecipeDTO.java | 16 +++++++++++++ .../recipe/repository/RecipeRepository.java | 7 +++++- .../recipe/service/CommunityService.java | 23 +++++++++++++++---- 5 files changed, 58 insertions(+), 11 deletions(-) rename src/main/java/com/example/blendish/domain/recipe/dto/{CommunityHotRecipyDTO.java => CommunityHotRecipeDTO.java} (84%) create mode 100644 src/main/java/com/example/blendish/domain/recipe/dto/CommunityTodayRecipeDTO.java diff --git a/src/main/java/com/example/blendish/controller/CommunityController.java b/src/main/java/com/example/blendish/controller/CommunityController.java index cb2b478..0194a6d 100644 --- a/src/main/java/com/example/blendish/controller/CommunityController.java +++ b/src/main/java/com/example/blendish/controller/CommunityController.java @@ -1,10 +1,10 @@ package com.example.blendish.controller; -import com.example.blendish.domain.recipe.dto.CommunityHotRecipyDTO; +import com.example.blendish.domain.recipe.dto.CommunityHotRecipeDTO; +import com.example.blendish.domain.recipe.dto.CommunityTodayRecipeDTO; import com.example.blendish.domain.recipe.service.CommunityService; import com.example.blendish.global.dto.ApiResponseTemplate; import com.example.blendish.global.response.SuccessCode; -import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; import org.springframework.http.ResponseEntity; @@ -23,11 +23,21 @@ public class CommunityController { private final CommunityService communityService; - @GetMapping("/main") - public ResponseEntity>> getmain() { + @GetMapping("/HotRecipe") + public ResponseEntity>> getHot() { - List hotRecipyDTOS = communityService.getTopLikedRecipes(); + List hotRecipyDTOS = communityService.getTopLikedRecipes(); return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, hotRecipyDTOS)); } + + @GetMapping("/TodayRecipe") + public ResponseEntity>> getToday() { + + List todayRecipe = communityService.getTodayRecipe(); + + return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, todayRecipe)); + } + + } diff --git a/src/main/java/com/example/blendish/domain/recipe/dto/CommunityHotRecipyDTO.java b/src/main/java/com/example/blendish/domain/recipe/dto/CommunityHotRecipeDTO.java similarity index 84% rename from src/main/java/com/example/blendish/domain/recipe/dto/CommunityHotRecipyDTO.java rename to src/main/java/com/example/blendish/domain/recipe/dto/CommunityHotRecipeDTO.java index 63c6c1d..e11352c 100644 --- a/src/main/java/com/example/blendish/domain/recipe/dto/CommunityHotRecipyDTO.java +++ b/src/main/java/com/example/blendish/domain/recipe/dto/CommunityHotRecipeDTO.java @@ -9,9 +9,10 @@ @Setter @NoArgsConstructor @AllArgsConstructor -public class CommunityHotRecipyDTO { +public class CommunityHotRecipeDTO { private Long recipeId; private String foodImage; private int likeCount; private int commentCount; + private String name; } diff --git a/src/main/java/com/example/blendish/domain/recipe/dto/CommunityTodayRecipeDTO.java b/src/main/java/com/example/blendish/domain/recipe/dto/CommunityTodayRecipeDTO.java new file mode 100644 index 0000000..c5a36f5 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipe/dto/CommunityTodayRecipeDTO.java @@ -0,0 +1,16 @@ +package com.example.blendish.domain.recipe.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class CommunityTodayRecipeDTO { + private Long recipeId; + private String foodImage; + private String name; +} diff --git a/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java b/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java index a95ad8c..83bc234 100644 --- a/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java +++ b/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java @@ -1,9 +1,14 @@ package com.example.blendish.domain.recipe.repository; -import com.example.blendish.domain.recipe.dto.CommunityHotRecipyDTO; import com.example.blendish.domain.recipe.entity.Recipe; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; public interface RecipeRepository extends JpaRepository { Recipe findByRecipeId(Long recipiId); + + @Query("SELECT r FROM Recipe r ORDER BY FUNCTION('RAND')") // 랜덤으로 레시피 정렬 + List findRandomRecipes(); } diff --git a/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java b/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java index 238b91d..8a441c3 100644 --- a/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java +++ b/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java @@ -1,7 +1,8 @@ package com.example.blendish.domain.recipe.service; import com.example.blendish.domain.comments.repository.CommentsRepository; -import com.example.blendish.domain.recipe.dto.CommunityHotRecipyDTO; +import com.example.blendish.domain.recipe.dto.CommunityHotRecipeDTO; +import com.example.blendish.domain.recipe.dto.CommunityTodayRecipeDTO; import com.example.blendish.domain.recipe.entity.Recipe; import com.example.blendish.domain.recipe.repository.LikeRepository; import com.example.blendish.domain.recipe.repository.RecipeRepository; @@ -20,9 +21,9 @@ public class CommunityService { private final CommentsRepository commentsRepository; // 인기 레시피 가져오는 서비스 - public List getTopLikedRecipes() { + public List getTopLikedRecipes() { - List topLikedRecipi = new ArrayList<>(); + List topLikedRecipi = new ArrayList<>(); // 좋아요 많은 id 가져오기 List topLikedRecipeIds = likeRepository.findTopLikedRecipeIds(); @@ -40,7 +41,7 @@ public List getTopLikedRecipes() { // 코멘트 개수 가져오기 int CommentNum = commentsRepository.countByRecipeRecipeId(recipeId); - CommunityHotRecipyDTO topRecipes = new CommunityHotRecipyDTO(recipeId,recipe.getFoodImage(),recipe.getLikeCount(),CommentNum); + CommunityHotRecipeDTO topRecipes = new CommunityHotRecipeDTO(recipeId,recipe.getFoodImage(),recipe.getLikeCount(),CommentNum,recipe.getName()); topLikedRecipi.add(topRecipes); @@ -48,4 +49,18 @@ public List getTopLikedRecipes() { return topLikedRecipi; } + + // 오늘의 레시피 랜덤 띄우기 + public List getTodayRecipe(){ + List RandomRecipe = new ArrayList<>(); + List rendomRecipeRepo = recipeRepository.findRandomRecipes(); + + for(int i=0; i Date: Thu, 13 Feb 2025 20:36:48 +0900 Subject: [PATCH 04/26] =?UTF-8?q?[Chore]=20dto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 레시피 관련 dto 및 맛 테이블 생성 Related to: #17 --- .../blendish/domain/comments/dto/.gitkeep | 0 .../domain/comments/dto/CommnetDTO.java | 18 ++++++++++ .../domain/foodflavor/repository/.gitkeep | 0 .../foodflavor/repository/FoodFlavor.java | 24 +++++++++++++ .../domain/recipe/dto/CommunityDetailDTO.java | 36 +++++++++++++++++++ .../blendish/domain/recipe/entity/Recipe.java | 9 +++++ 6 files changed, 87 insertions(+) delete mode 100644 src/main/java/com/example/blendish/domain/comments/dto/.gitkeep create mode 100644 src/main/java/com/example/blendish/domain/comments/dto/CommnetDTO.java delete mode 100644 src/main/java/com/example/blendish/domain/foodflavor/repository/.gitkeep create mode 100644 src/main/java/com/example/blendish/domain/foodflavor/repository/FoodFlavor.java create mode 100644 src/main/java/com/example/blendish/domain/recipe/dto/CommunityDetailDTO.java diff --git a/src/main/java/com/example/blendish/domain/comments/dto/.gitkeep b/src/main/java/com/example/blendish/domain/comments/dto/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/blendish/domain/comments/dto/CommnetDTO.java b/src/main/java/com/example/blendish/domain/comments/dto/CommnetDTO.java new file mode 100644 index 0000000..6404a68 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/comments/dto/CommnetDTO.java @@ -0,0 +1,18 @@ +package com.example.blendish.domain.comments.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +@Getter +@Setter +@AllArgsConstructor +public class CommnetDTO { + private String userId; + private String profilePic; + private String content; + private Date createdAt; + private int numOfReply; +} diff --git a/src/main/java/com/example/blendish/domain/foodflavor/repository/.gitkeep b/src/main/java/com/example/blendish/domain/foodflavor/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/blendish/domain/foodflavor/repository/FoodFlavor.java b/src/main/java/com/example/blendish/domain/foodflavor/repository/FoodFlavor.java new file mode 100644 index 0000000..1c8b97a --- /dev/null +++ b/src/main/java/com/example/blendish/domain/foodflavor/repository/FoodFlavor.java @@ -0,0 +1,24 @@ +package com.example.blendish.domain.foodflavor.repository; + +import com.example.blendish.domain.recipe.entity.Recipe; +import com.example.blendish.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class FoodFlavor { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long tasteId; + + @Column(nullable = false) + private String taste; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipeId", nullable = false) + private Recipe recipe; + +} diff --git a/src/main/java/com/example/blendish/domain/recipe/dto/CommunityDetailDTO.java b/src/main/java/com/example/blendish/domain/recipe/dto/CommunityDetailDTO.java new file mode 100644 index 0000000..216a4e4 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipe/dto/CommunityDetailDTO.java @@ -0,0 +1,36 @@ +package com.example.blendish.domain.recipe.dto; + +import com.example.blendish.domain.comments.dto.CommnetDTO; +import com.example.blendish.domain.comments.entity.Comment; +import com.example.blendish.domain.user.entity.User; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Date; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class CommunityDetailDTO { + private Long recipeId; + private String foodImage; + private String level; + private String time; + private Date postDate; + private String userId; + private String profilePic; + private String name; + private int spicyLevel; + private boolean isHart; + private boolean isScrap; + private int crapCount; + private int likeCount; + private int commentCount; + private String information; + private List commentDTOList; + private List flavor; +} diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java index ca6128a..10f71bb 100644 --- a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java +++ b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java @@ -1,6 +1,7 @@ package com.example.blendish.domain.recipe.entity; import com.example.blendish.domain.comments.entity.Comment; +import com.example.blendish.domain.foodflavor.repository.FoodFlavor; import com.example.blendish.domain.user.entity.User; import jakarta.persistence.*; import lombok.Getter; @@ -29,8 +30,13 @@ public class Recipe { private int likeCount; + @Column(nullable = false, length = 100) + private String time; + private int scrapCount; + private int spicyLevel; + @Column(length = 200) private String information; @@ -59,4 +65,7 @@ public class Recipe { @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) private List scraps ; + @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) + private List foodFlavors ; + } From c1437f536023f983dc84fab052440d0e4cf566f3 Mon Sep 17 00:00:00 2001 From: dlqja Date: Fri, 14 Feb 2025 00:06:45 +0900 Subject: [PATCH 05/26] [Feat] S3 init #21 --- .../example/blendish/global/s3/S3Config.java | 31 ++++++++++++++ .../blendish/global/s3/S3UploadService.java | 40 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/main/java/com/example/blendish/global/s3/S3Config.java create mode 100644 src/main/java/com/example/blendish/global/s3/S3UploadService.java diff --git a/src/main/java/com/example/blendish/global/s3/S3Config.java b/src/main/java/com/example/blendish/global/s3/S3Config.java new file mode 100644 index 0000000..192431e --- /dev/null +++ b/src/main/java/com/example/blendish/global/s3/S3Config.java @@ -0,0 +1,31 @@ +package com.example.blendish.global.s3; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String accessSecret; + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 s3Client() { + AWSCredentials credentials = new BasicAWSCredentials(accessKey, accessSecret); + return AmazonS3ClientBuilder.standard() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(region).build(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/blendish/global/s3/S3UploadService.java b/src/main/java/com/example/blendish/global/s3/S3UploadService.java new file mode 100644 index 0000000..4448c4a --- /dev/null +++ b/src/main/java/com/example/blendish/global/s3/S3UploadService.java @@ -0,0 +1,40 @@ +package com.example.blendish.global.s3; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; + +@Service +@RequiredArgsConstructor +public class S3UploadService { + + private final AmazonS3 amazonS3; + @Value("${cloud.aws.s3.bucketName}") + private String bucket; + + public String saveFile(MultipartFile multipartFile) throws IOException { + String originalFilename = multipartFile.getOriginalFilename(); + + if (originalFilename == null || originalFilename.isEmpty()) { + throw new IllegalArgumentException("파일 이름이 유효하지 않습니다."); + } + + try (InputStream inputStream = multipartFile.getInputStream()) { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(multipartFile.getSize()); + metadata.setContentType(multipartFile.getContentType()); + + amazonS3.putObject(bucket, originalFilename, inputStream, metadata); + } catch (IOException e) { + throw new IOException("파일 업로드 중 오류가 발생했습니다.", e); + } + + return amazonS3.getUrl(bucket, originalFilename).toString(); + } +} \ No newline at end of file From 0baefe921bcad2ad8b0418c038509e3e5693c28d Mon Sep 17 00:00:00 2001 From: dlqja Date: Fri, 14 Feb 2025 01:57:52 +0900 Subject: [PATCH 06/26] [Feat] Update user information #23 --- .../blendish/controller/UserController.java | 35 +++++++++++++++- .../domain/user/config/UserMapper.java | 28 +++++++++++++ .../user/controller/JoinController.java | 2 +- .../user/dto/{ => check}/CheckUserIdDTO.java | 2 +- .../domain/user/dto/check/CheckUserPwDTO.java | 8 ++++ .../domain/user/service/UserService.java | 42 ++++++++++++++++++- 6 files changed, 112 insertions(+), 5 deletions(-) rename src/main/java/com/example/blendish/domain/user/dto/{ => check}/CheckUserIdDTO.java (62%) create mode 100644 src/main/java/com/example/blendish/domain/user/dto/check/CheckUserPwDTO.java diff --git a/src/main/java/com/example/blendish/controller/UserController.java b/src/main/java/com/example/blendish/controller/UserController.java index 64690bd..3b272cc 100644 --- a/src/main/java/com/example/blendish/controller/UserController.java +++ b/src/main/java/com/example/blendish/controller/UserController.java @@ -1,16 +1,20 @@ package com.example.blendish.controller; import com.example.blendish.domain.user.dto.UserDTO; +import com.example.blendish.domain.user.dto.check.CheckUserPwDTO; import com.example.blendish.domain.user.service.UserService; import com.example.blendish.global.dto.ApiResponseTemplate; import com.example.blendish.global.response.SuccessCode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.List; @Tag(name = "User Controller", description = "사용자 관리 관련 API") @Controller @@ -32,4 +36,31 @@ public ResponseEntity>> getAllUsers() { List users = userService.getAllUsers(); return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, users)); } + + @Operation( + summary = "사용자 정보 업데이트", + description = "전체 사용자 정보를 전달받아 유저 정보를 업데이트 한다." + ) + @PutMapping(value = "/update", consumes = {"multipart/form-data"}) + public ResponseEntity> updateUser( + @RequestPart("user") UserDTO userDTO, + @RequestPart(value = "profilePic", required = false) MultipartFile profilePic + ) throws IOException { + UserDTO updatedUser = userService.updateUser(userDTO, profilePic); + return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, updatedUser)); + } + + @Operation( + summary = "비밀번호 확인", + description = "입력받은 비밀번호가 저장된 비밀번호와 동일한지 확인한다." + ) + @PostMapping("/check") + public ResponseEntity> checkPassword(@RequestBody CheckUserPwDTO dto) { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String userId = authentication.getName(); + + boolean result = userService.checkPassword(userId, dto.getPassword()); + return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, result)); + } } diff --git a/src/main/java/com/example/blendish/domain/user/config/UserMapper.java b/src/main/java/com/example/blendish/domain/user/config/UserMapper.java index 3f413be..50ade98 100644 --- a/src/main/java/com/example/blendish/domain/user/config/UserMapper.java +++ b/src/main/java/com/example/blendish/domain/user/config/UserMapper.java @@ -32,6 +32,34 @@ public static UserDTO toDTO(User user) { return dto; } + public static User toEntity(UserDTO dto) { + if (dto == null) return null; + + User user = new User(); + user.setUserId(dto.getUserId()); + user.setUserPw(dto.getUserPw()); + user.setEmail(dto.getEmail()); + user.setHometown(dto.getHometown()); + user.setCountry(dto.getCountry()); + user.setProfilePic(dto.getProfilePic()); + user.setRole(dto.getRole()); + + if (dto.getTastePreference() != null) { + List tastePreferences = dto.getTastePreference().stream() + .map(tpDTO -> { + TastePreference tastePreference = new TastePreference(); + tastePreference.setTaste(tpDTO.getTaste()); + tastePreference.setSpicyLevel(tpDTO.getSpicyLevel()); + tastePreference.setUser(user); + return tastePreference; + }) + .collect(Collectors.toList()); + user.setTastePreferences(tastePreferences); + } + + return user; + } + private static TastePreferenceDTO mapTastePreference(TastePreference tastePreference) { TastePreferenceDTO dto = new TastePreferenceDTO(); dto.setTaste(tastePreference.getTaste()); diff --git a/src/main/java/com/example/blendish/domain/user/controller/JoinController.java b/src/main/java/com/example/blendish/domain/user/controller/JoinController.java index ca16f16..7fbb8ba 100644 --- a/src/main/java/com/example/blendish/domain/user/controller/JoinController.java +++ b/src/main/java/com/example/blendish/domain/user/controller/JoinController.java @@ -1,6 +1,6 @@ package com.example.blendish.domain.user.controller; -import com.example.blendish.domain.user.dto.CheckUserIdDTO; +import com.example.blendish.domain.user.dto.check.CheckUserIdDTO; import com.example.blendish.domain.user.dto.JoinDTO; import com.example.blendish.domain.user.dto.UserDTO; import com.example.blendish.domain.user.dto.preference.UserPreferencesDTO; diff --git a/src/main/java/com/example/blendish/domain/user/dto/CheckUserIdDTO.java b/src/main/java/com/example/blendish/domain/user/dto/check/CheckUserIdDTO.java similarity index 62% rename from src/main/java/com/example/blendish/domain/user/dto/CheckUserIdDTO.java rename to src/main/java/com/example/blendish/domain/user/dto/check/CheckUserIdDTO.java index e877769..42b2210 100644 --- a/src/main/java/com/example/blendish/domain/user/dto/CheckUserIdDTO.java +++ b/src/main/java/com/example/blendish/domain/user/dto/check/CheckUserIdDTO.java @@ -1,4 +1,4 @@ -package com.example.blendish.domain.user.dto; +package com.example.blendish.domain.user.dto.check; import lombok.Data; diff --git a/src/main/java/com/example/blendish/domain/user/dto/check/CheckUserPwDTO.java b/src/main/java/com/example/blendish/domain/user/dto/check/CheckUserPwDTO.java new file mode 100644 index 0000000..ae98325 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/user/dto/check/CheckUserPwDTO.java @@ -0,0 +1,8 @@ +package com.example.blendish.domain.user.dto.check; + +import lombok.Data; + +@Data +public class CheckUserPwDTO { + private String password; +} \ No newline at end of file diff --git a/src/main/java/com/example/blendish/domain/user/service/UserService.java b/src/main/java/com/example/blendish/domain/user/service/UserService.java index d5aa343..a907629 100644 --- a/src/main/java/com/example/blendish/domain/user/service/UserService.java +++ b/src/main/java/com/example/blendish/domain/user/service/UserService.java @@ -5,10 +5,14 @@ import com.example.blendish.domain.user.entity.User; import com.example.blendish.domain.user.repository.UserRepository; import com.example.blendish.global.dto.ApiResponseTemplate; +import com.example.blendish.global.s3.S3UploadService; import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.List; import java.util.stream.Collectors; @@ -16,9 +20,13 @@ @Transactional public class UserService { private final UserRepository userRepository; + private final S3UploadService s3UploadService; + private final BCryptPasswordEncoder bCryptPasswordEncoder; - public UserService(UserRepository userRepository) { + public UserService(UserRepository userRepository, S3UploadService s3UploadService, BCryptPasswordEncoder bCryptPasswordEncoder) { this.userRepository = userRepository; + this.s3UploadService = s3UploadService; + this.bCryptPasswordEncoder = bCryptPasswordEncoder; } public List getAllUsers() { @@ -27,4 +35,36 @@ public List getAllUsers() { .map(UserMapper::toDTO) .collect(Collectors.toList()); } + + public UserDTO updateUser(UserDTO userDTO, MultipartFile profilePic) throws IOException { + + if (profilePic != null && !profilePic.isEmpty()) { + String uploadedUrl = s3UploadService.saveFile(profilePic); + userDTO.setProfilePic(uploadedUrl); + } + + userDTO.setRole("ROLE_ADMIN"); + + if (userDTO.getUserPw() != null && !userDTO.getUserPw().isEmpty()) { + userDTO.setUserPw(bCryptPasswordEncoder.encode(userDTO.getUserPw())); + } + + User existingUser = userRepository.findByUserId(userDTO.getUserId()); + if (existingUser != null) { + userRepository.delete(existingUser); + userRepository.flush(); + } + + User newUser = UserMapper.toEntity(userDTO); // 기존 UserMapper를 그대로 사용 + newUser = userRepository.save(newUser); + return UserMapper.toDTO(newUser); + } + + public boolean checkPassword(String userId, String rawPassword) { + User user = userRepository.findByUserId(userId); + if (user == null) { + throw new IllegalArgumentException("해당 유저를 찾을 수 없습니다."); + } + return bCryptPasswordEncoder.matches(rawPassword, user.getUserPw()); + } } From 30fc518e0be7e3aab224063bec828d98e1b307cf Mon Sep 17 00:00:00 2001 From: dlqja Date: Fri, 14 Feb 2025 04:48:18 +0900 Subject: [PATCH 07/26] [Chore] Add application.yml to .gitignore #23 --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 69fa948..0abe6e2 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,6 @@ out/ ### VS Code ### .vscode/ application.properties -application-*.yml \ No newline at end of file +application-*.yml +application.yml + From 204435762864032d67db7aa4a25f6a10e2c00cc3 Mon Sep 17 00:00:00 2001 From: dlqja Date: Fri, 14 Feb 2025 04:50:03 +0900 Subject: [PATCH 08/26] [Fix] Fix property key #23 --- src/main/java/com/example/blendish/global/s3/S3Config.java | 2 ++ .../java/com/example/blendish/global/s3/S3UploadService.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/blendish/global/s3/S3Config.java b/src/main/java/com/example/blendish/global/s3/S3Config.java index 192431e..b22ad3c 100644 --- a/src/main/java/com/example/blendish/global/s3/S3Config.java +++ b/src/main/java/com/example/blendish/global/s3/S3Config.java @@ -12,11 +12,13 @@ @Configuration public class S3Config { + @Value("${cloud.aws.credentials.access-key}") private String accessKey; @Value("${cloud.aws.credentials.secret-key}") private String accessSecret; + @Value("${cloud.aws.region.static}") private String region; diff --git a/src/main/java/com/example/blendish/global/s3/S3UploadService.java b/src/main/java/com/example/blendish/global/s3/S3UploadService.java index 4448c4a..214b755 100644 --- a/src/main/java/com/example/blendish/global/s3/S3UploadService.java +++ b/src/main/java/com/example/blendish/global/s3/S3UploadService.java @@ -15,7 +15,7 @@ public class S3UploadService { private final AmazonS3 amazonS3; - @Value("${cloud.aws.s3.bucketName}") + @Value("${cloud.aws.s3.bucket-name}") private String bucket; public String saveFile(MultipartFile multipartFile) throws IOException { From 3250cb4c0536556cb9e1b7e5c1f77e5e09740f03 Mon Sep 17 00:00:00 2001 From: mjkang4416 Date: Fri, 14 Feb 2025 11:19:29 +0900 Subject: [PATCH 09/26] =?UTF-8?q?[char]=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EC=A0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 인증전 주석처리 Related to: #17 --- .../recipe/repository/RecipeRepository.java | 34 ++-- .../recipe/repository/ScrapRepository.java | 2 +- .../recipe/service/CommunityService.java | 160 +++++++++--------- src/main/resources/application.yml | 22 --- 4 files changed, 98 insertions(+), 120 deletions(-) delete mode 100644 src/main/resources/application.yml diff --git a/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java b/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java index 8eda61b..877d437 100644 --- a/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java +++ b/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java @@ -18,22 +18,22 @@ public interface RecipeRepository extends JpaRepository { List findByUser(User user); // like 증가 - @Modifying - @Query("UPDATE Recipe r SET r.likeCount = r.likeCount + 1 WHERE r.recipeId = :recipeId") - void incrementLikeCount(Long recipeId); - - // like 감소 - @Modifying - @Query("UPDATE Recipe r SET r.likeCount = r.likeCount - 1 WHERE r.recipeId = :recipeId") - void decrementLikeCount(Long recipeId); - - // 스크랩 증가 - @Modifying - @Query("UPDATE Recipe r SET r.scrapCount = r.scrapCount + 1 WHERE r.recipeId = :recipeId") - void incrementScrapCount(Long recipeId); - - @Modifying - @Query("UPDATE Recipe r SET r.scrapCount = r.likeCount - 1 WHERE r.recipeId = :recipeId") - void decrementScrapCount(Long recipeId); +// @Modifying +// @Query("UPDATE Recipe r SET r.likeCount = r.likeCount + 1 WHERE r.recipeId = :recipeId") +// void incrementLikeCount(Long recipeId); +// +// // like 감소 +// @Modifying +// @Query("UPDATE Recipe r SET r.likeCount = r.likeCount - 1 WHERE r.recipeId = :recipeId") +// void decrementLikeCount(Long recipeId); +// +// // 스크랩 증가 +// @Modifying +// @Query("UPDATE Recipe r SET r.scrapCount = r.scrapCount + 1 WHERE r.recipeId = :recipeId") +// void incrementScrapCount(Long recipeId); +// +// @Modifying +// @Query("UPDATE Recipe r SET r.scrapCount = r.likeCount - 1 WHERE r.recipeId = :recipeId") +// void decrementScrapCount(Long recipeId); } diff --git a/src/main/java/com/example/blendish/domain/recipe/repository/ScrapRepository.java b/src/main/java/com/example/blendish/domain/recipe/repository/ScrapRepository.java index 2b47f37..252d2a4 100644 --- a/src/main/java/com/example/blendish/domain/recipe/repository/ScrapRepository.java +++ b/src/main/java/com/example/blendish/domain/recipe/repository/ScrapRepository.java @@ -5,5 +5,5 @@ public interface ScrapRepository extends JpaRepository { - void deleteByRecipeRecipeIdAndUserId(Long recipeId,Long userId); +// void deleteByRecipeRecipeIdAndUserId(Long recipeId,Long userId); } diff --git a/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java b/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java index c83f9c8..8050da1 100644 --- a/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java +++ b/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java @@ -128,84 +128,84 @@ public CommunityDetailDTO getDetail(Long recipeId){ } // 좋아요 클릭시 - public void insertLike(@RequestBody Long recipeId){ - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication != null && authentication.getPrincipal() instanceof User) { - User user = (User) authentication.getPrincipal(); - - log.info(user.getUserId()); - log.info(user.getEmail()); - - // Recipe 조회 - Recipe recipe = recipeRepository.findByRecipeId(recipeId); - - // Like 객체 생성 - Likes like = new Likes(); - like.setUser(user); - like.setRecipe(recipe); - - // Likes 저장 - likeRepository.save(like); - - // 레시피의 likecount 증가 - recipeRepository.incrementLikeCount(recipeId); - } - } - - // 좋아요 삭제시 - public void removeLike(Long recipeId){ - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication != null && authentication.getPrincipal() instanceof User) { - User user = (User) authentication.getPrincipal(); - - // Likes 삭제 - likeRepository.deleteByRecipeRecipeIdAndUserId(recipeId,user.getId()); - - // 레시피의 likecount 감소 - recipeRepository.decrementLikeCount(recipeId); - } - } - - // 스크랩 등록시 - public void insertScrap(Long recipeId){ - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication != null && authentication.getPrincipal() instanceof User) { - User user = (User) authentication.getPrincipal(); - - // Recipe 조회 - Recipe recipe = recipeRepository.findByRecipeId(recipeId); - - // 스크랩 객체 생성 - Scrap scrap = new Scrap(); - scrap.setUser(user); - scrap.setRecipe(recipe); - - // Likes 저장 - scrapRepository.save(scrap); - - // 레시피의 likecount 증가 - recipeRepository.incrementScrapCount(recipeId); - } - } - - // 스크랩 삭제시 - public void removeScrap(Long recipeId){ - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication != null && authentication.getPrincipal() instanceof User) { - User user = (User) authentication.getPrincipal(); - - // Likes 삭제 - scrapRepository.deleteByRecipeRecipeIdAndUserId(recipeId,user.getId()); - - // 레시피의 likecount 감소 - recipeRepository.decrementScrapCount(recipeId); - } - } +// public void insertLike(@RequestBody Long recipeId){ +// +// Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); +// +// if (authentication != null && authentication.getPrincipal() instanceof User) { +// User user = (User) authentication.getPrincipal(); +// +// log.info(user.getUserId()); +// log.info(user.getEmail()); +// +// // Recipe 조회 +// Recipe recipe = recipeRepository.findByRecipeId(recipeId); +// +// // Like 객체 생성 +// Likes like = new Likes(); +// like.setUser(user); +// like.setRecipe(recipe); +// +// // Likes 저장 +// likeRepository.save(like); +// +// // 레시피의 likecount 증가 +// recipeRepository.incrementLikeCount(recipeId); +// } +// } +// +// // 좋아요 삭제시 +// public void removeLike(Long recipeId){ +// +// Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); +// +// if (authentication != null && authentication.getPrincipal() instanceof User) { +// User user = (User) authentication.getPrincipal(); +// +// // Likes 삭제 +// likeRepository.deleteByRecipeRecipeIdAndUserId(recipeId,user.getId()); +// +// // 레시피의 likecount 감소 +// recipeRepository.decrementLikeCount(recipeId); +// } +// } +// +// // 스크랩 등록시 +// public void insertScrap(Long recipeId){ +// Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); +// +// if (authentication != null && authentication.getPrincipal() instanceof User) { +// User user = (User) authentication.getPrincipal(); +// +// // Recipe 조회 +// Recipe recipe = recipeRepository.findByRecipeId(recipeId); +// +// // 스크랩 객체 생성 +// Scrap scrap = new Scrap(); +// scrap.setUser(user); +// scrap.setRecipe(recipe); +// +// // Likes 저장 +// scrapRepository.save(scrap); +// +// // 레시피의 likecount 증가 +// recipeRepository.incrementScrapCount(recipeId); +// } +// } +// +// // 스크랩 삭제시 +// public void removeScrap(Long recipeId){ +// +// Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); +// +// if (authentication != null && authentication.getPrincipal() instanceof User) { +// User user = (User) authentication.getPrincipal(); +// +// // Likes 삭제 +// scrapRepository.deleteByRecipeRecipeIdAndUserId(recipeId,user.getId()); +// +// // 레시피의 likecount 감소 +// recipeRepository.decrementScrapCount(recipeId); +// } +// } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index 68373f0..0000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,22 +0,0 @@ -spring: - profiles: - active: prod - - datasource: - url: ${spring.datasource.url} - username: ${spring.datasource.username} - password: ${spring.datasource.password} - driver-class-name: ${spring.datasource.driver-class-name} - - jpa: - database: mysql - database-platform: org.hibernate.dialect.MySQLDialect - hibernate: - ddl-auto: update - show-sql: true - properties: - hibernate: - format_sql: false - use_sql_comments: true - jwt: - secret: ${JWT_SECRET} \ No newline at end of file From 918e6e6c1fd386e0a0d8c7b10efc4c7ce9c948c5 Mon Sep 17 00:00:00 2001 From: mjkang4416 Date: Fri, 14 Feb 2025 11:39:54 +0900 Subject: [PATCH 10/26] =?UTF-8?q?[char]=20=EB=B3=91=ED=95=A9=EC=A0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 병합전 기타수정 Related to: #17 --- .../java/com/example/blendish/domain/comments/service/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/java/com/example/blendish/domain/comments/service/.gitkeep diff --git a/src/main/java/com/example/blendish/domain/comments/service/.gitkeep b/src/main/java/com/example/blendish/domain/comments/service/.gitkeep deleted file mode 100644 index e69de29..0000000 From b760144f60676d2ecfb6bd68bc9b6f39b501a31b Mon Sep 17 00:00:00 2001 From: mjkang4416 Date: Fri, 14 Feb 2025 11:54:25 +0900 Subject: [PATCH 11/26] =?UTF-8?q?[Feat]=20=EB=B6=80=EB=AA=A8=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=A0=84=EC=B2=B4=20=EB=9D=84=EC=9A=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 부모댓글 띄우기 Related to: #17 --- .../controller/CommentController.java | 32 ++++++++++++ .../controller/CommunityController.java | 50 +++++++++---------- .../comments/service/CommentService.java | 18 +++++++ 3 files changed, 75 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/example/blendish/controller/CommentController.java create mode 100644 src/main/java/com/example/blendish/domain/comments/service/CommentService.java diff --git a/src/main/java/com/example/blendish/controller/CommentController.java b/src/main/java/com/example/blendish/controller/CommentController.java new file mode 100644 index 0000000..95f22ab --- /dev/null +++ b/src/main/java/com/example/blendish/controller/CommentController.java @@ -0,0 +1,32 @@ +package com.example.blendish.controller; + +import com.example.blendish.domain.comments.dto.CommentDTO; +import com.example.blendish.domain.comments.service.CommentService; +import com.example.blendish.domain.recipe.dto.CommunityHotRecipeDTO; +import com.example.blendish.global.dto.ApiResponseTemplate; +import com.example.blendish.global.response.SuccessCode; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Tag(name = "Comment Controller", description = "댓글 API") +@RestController +@RequestMapping("/api/Comment") +@AllArgsConstructor +public class CommentController { + private final CommentService commentService; + + //부모댓글 전체 띄우기 + @GetMapping("/ParentsComment") + public ResponseEntity>> getParentsComment(@RequestParam(name = "recipeId") Long recipeId) { + + List commentDTOList = commentService.getParentCommnet(recipeId); + return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, commentDTOList)); + } +} diff --git a/src/main/java/com/example/blendish/controller/CommunityController.java b/src/main/java/com/example/blendish/controller/CommunityController.java index 8ae2673..4ce446c 100644 --- a/src/main/java/com/example/blendish/controller/CommunityController.java +++ b/src/main/java/com/example/blendish/controller/CommunityController.java @@ -47,31 +47,31 @@ public ResponseEntity> getDetail(@Reques } // 좋아요 클릭시 - @PostMapping("/updateLike") - public ResponseEntity> updateLike(@RequestBody Long recipeId) { - - communityService.insertLike(recipeId); - - return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, null )); - } - - // 좋아요 삭제시 - @PostMapping("/deleteLike") - public ResponseEntity> deleteLike(@RequestBody Long recipeId) { - - communityService.removeLike(recipeId); - - return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, null )); - } - - // 스크랩 클릭시 - @PostMapping("/updateScrap") - public ResponseEntity> updatScrap(@RequestBody Long recipeId) { - - communityService.insertScrap(recipeId); - - return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, null )); - } +// @PostMapping("/updateLike") +// public ResponseEntity> updateLike(@RequestBody Long recipeId) { +// +// communityService.insertLike(recipeId); +// +// return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, null )); +// } +// +// // 좋아요 삭제시 +// @PostMapping("/deleteLike") +// public ResponseEntity> deleteLike(@RequestBody Long recipeId) { +// +// communityService.removeLike(recipeId); +// +// return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, null )); +// } +// +// // 스크랩 클릭시 +// @PostMapping("/updateScrap") +// public ResponseEntity> updatScrap(@RequestBody Long recipeId) { +// +// communityService.insertScrap(recipeId); +// +// return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, null )); +// } diff --git a/src/main/java/com/example/blendish/domain/comments/service/CommentService.java b/src/main/java/com/example/blendish/domain/comments/service/CommentService.java new file mode 100644 index 0000000..147badb --- /dev/null +++ b/src/main/java/com/example/blendish/domain/comments/service/CommentService.java @@ -0,0 +1,18 @@ +package com.example.blendish.domain.comments.service; + +import com.example.blendish.domain.comments.dto.CommentDTO; +import com.example.blendish.domain.comments.repository.CommentsRepository; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@AllArgsConstructor +public class CommentService { + private final CommentsRepository commentsRepository; + + public List getParentCommnet(Long recipeId){ + return commentsRepository.getCommentLately(recipeId); + } +} From e7e8c25cd3fa2c3bcdbfd0d95ff2d00a4505bfa1 Mon Sep 17 00:00:00 2001 From: mjkang4416 Date: Fri, 14 Feb 2025 12:51:27 +0900 Subject: [PATCH 12/26] =?UTF-8?q?[Feat]=20=EC=A0=84=EC=B2=B4=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EB=9D=84=EC=9A=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 대댓글 표함 전체 댓글 띄우기 Related to: #17 --- .../controller/CommentController.java | 9 ++++ .../domain/comments/dto/CommentAllDTO.java | 20 ++++++++ .../repository/CommentsRepository.java | 4 ++ .../comments/service/CommentService.java | 46 +++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 src/main/java/com/example/blendish/domain/comments/dto/CommentAllDTO.java diff --git a/src/main/java/com/example/blendish/controller/CommentController.java b/src/main/java/com/example/blendish/controller/CommentController.java index 95f22ab..abd8e04 100644 --- a/src/main/java/com/example/blendish/controller/CommentController.java +++ b/src/main/java/com/example/blendish/controller/CommentController.java @@ -1,5 +1,6 @@ package com.example.blendish.controller; +import com.example.blendish.domain.comments.dto.CommentAllDTO; import com.example.blendish.domain.comments.dto.CommentDTO; import com.example.blendish.domain.comments.service.CommentService; import com.example.blendish.domain.recipe.dto.CommunityHotRecipeDTO; @@ -29,4 +30,12 @@ public ResponseEntity>> getParentsComment(@ List commentDTOList = commentService.getParentCommnet(recipeId); return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, commentDTOList)); } + + // 전체 댓글 띄우기 + @GetMapping("/AllComment") + public ResponseEntity>> getAllComment(@RequestParam(name = "recipeId") Long recipeId) { + + List commentAllDTOList = commentService.getAllComment(recipeId); + return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, commentAllDTOList)); + } } diff --git a/src/main/java/com/example/blendish/domain/comments/dto/CommentAllDTO.java b/src/main/java/com/example/blendish/domain/comments/dto/CommentAllDTO.java new file mode 100644 index 0000000..84a750e --- /dev/null +++ b/src/main/java/com/example/blendish/domain/comments/dto/CommentAllDTO.java @@ -0,0 +1,20 @@ +package com.example.blendish.domain.comments.dto; + +import lombok.*; + +import java.util.Date; +import java.util.List; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CommentAllDTO { + private Long commentId; + private String userId; + private String profilePic; + private String content; + private Date createdAt; + private List ReplyList; +} diff --git a/src/main/java/com/example/blendish/domain/comments/repository/CommentsRepository.java b/src/main/java/com/example/blendish/domain/comments/repository/CommentsRepository.java index d7be716..3a3e070 100644 --- a/src/main/java/com/example/blendish/domain/comments/repository/CommentsRepository.java +++ b/src/main/java/com/example/blendish/domain/comments/repository/CommentsRepository.java @@ -22,6 +22,10 @@ public interface CommentsRepository extends JpaRepository { "ORDER BY c.createdAt DESC ") List getCommentLately(@Param("recipeId") Long recipeId); + // 전체 댓글 띄우기 + @Query("SELECT c FROM Comment c LEFT JOIN FETCH c.replies WHERE c.recipe.recipeId = :recipeId AND c.parentComment IS NULL") + List getCommentsByRecipeRecipeId(@Param("recipeId") Long recipeId); + } diff --git a/src/main/java/com/example/blendish/domain/comments/service/CommentService.java b/src/main/java/com/example/blendish/domain/comments/service/CommentService.java index 147badb..43dc0de 100644 --- a/src/main/java/com/example/blendish/domain/comments/service/CommentService.java +++ b/src/main/java/com/example/blendish/domain/comments/service/CommentService.java @@ -1,10 +1,13 @@ package com.example.blendish.domain.comments.service; +import com.example.blendish.domain.comments.dto.CommentAllDTO; import com.example.blendish.domain.comments.dto.CommentDTO; +import com.example.blendish.domain.comments.entity.Comment; import com.example.blendish.domain.comments.repository.CommentsRepository; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; @Service @@ -12,7 +15,50 @@ public class CommentService { private final CommentsRepository commentsRepository; + // 부모 댓글띄우기 public List getParentCommnet(Long recipeId){ return commentsRepository.getCommentLately(recipeId); } + + public List getAllComment(Long recipeId) { + List comments = commentsRepository.getCommentsByRecipeRecipeId(recipeId); + + List commentDTOList = new ArrayList<>(); + + for (Comment comment : comments) { + // 부모 댓글을 DTO로 변환 + CommentAllDTO commentDTO = CommentAllDTO.builder() + .commentId(comment.getCommentId()) + .createdAt(comment.getCreatedAt()) + .content(comment.getContent()) + .profilePic(comment.getUser().getProfilePic()) + .userId(comment.getUser().getUserId()) + .ReplyList(getChildComments(comment.getReplies())) + .build(); + + commentDTOList.add(commentDTO); + } + + return commentDTOList; + } + + // 대댓글을 재귀적으로 가져오는 메서드 + private List getChildComments(List childs) { + List childDTOList = new ArrayList<>(); + + for (int i=0; i Date: Fri, 14 Feb 2025 13:37:26 +0900 Subject: [PATCH 13/26] =?UTF-8?q?[Feat]=20=EC=A0=84=EC=B2=B4=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EC=84=B8=EB=B6=80=EB=82=B4=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 더보기 눌렀을때 게시글 세부내용 띄우기 Related to: #17 --- .../controller/CommunityController.java | 10 +++++ .../domain/recipe/dto/RecipeDetailDTO.java | 24 ++++++++++++ .../recipe/repository/RecipeRepository.java | 2 + .../recipe/service/CommunityService.java | 38 +++++++++++++------ .../blendish/domain/recipesteps/dto/.gitkeep | 0 .../recipesteps/dto/RecipeStepsDTO.java | 17 +++++++++ .../domain/recipesteps/repository/.gitkeep | 0 .../repository/RecipestepsRepository.java | 9 +++++ 8 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/example/blendish/domain/recipe/dto/RecipeDetailDTO.java delete mode 100644 src/main/java/com/example/blendish/domain/recipesteps/dto/.gitkeep create mode 100644 src/main/java/com/example/blendish/domain/recipesteps/dto/RecipeStepsDTO.java delete mode 100644 src/main/java/com/example/blendish/domain/recipesteps/repository/.gitkeep create mode 100644 src/main/java/com/example/blendish/domain/recipesteps/repository/RecipestepsRepository.java diff --git a/src/main/java/com/example/blendish/controller/CommunityController.java b/src/main/java/com/example/blendish/controller/CommunityController.java index 4ce446c..1b1f713 100644 --- a/src/main/java/com/example/blendish/controller/CommunityController.java +++ b/src/main/java/com/example/blendish/controller/CommunityController.java @@ -3,6 +3,7 @@ import com.example.blendish.domain.recipe.dto.CommunityDetailDTO; import com.example.blendish.domain.recipe.dto.CommunityHotRecipeDTO; import com.example.blendish.domain.recipe.dto.CommunityTodayRecipeDTO; +import com.example.blendish.domain.recipe.dto.RecipeDetailDTO; import com.example.blendish.domain.recipe.service.CommunityService; import com.example.blendish.global.dto.ApiResponseTemplate; import com.example.blendish.global.response.SuccessCode; @@ -73,6 +74,15 @@ public ResponseEntity> getDetail(@Reques // return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, null )); // } + //레시피 전체 디테일 + @GetMapping("/AllDetailRecipe") + public ResponseEntity> getAllDetail(@RequestParam(name = "recipeId") Long recipeId) { + + RecipeDetailDTO recipeDetailDTO = communityService.getAllDetail(recipeId); + + return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, recipeDetailDTO)); + } + diff --git a/src/main/java/com/example/blendish/domain/recipe/dto/RecipeDetailDTO.java b/src/main/java/com/example/blendish/domain/recipe/dto/RecipeDetailDTO.java new file mode 100644 index 0000000..9907d0e --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipe/dto/RecipeDetailDTO.java @@ -0,0 +1,24 @@ +package com.example.blendish.domain.recipe.dto; + +import com.example.blendish.domain.comments.dto.CommentDTO; +import com.example.blendish.domain.recipe.entity.Ingredient; +import com.example.blendish.domain.recipe.entity.RecipeSteps; +import com.example.blendish.domain.recipesteps.dto.RecipeStepsDTO; +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RecipeDetailDTO { + private Long recipeId; + private String level; + private String time; + private String name; + private List ingredients; + private RecipeStepsDTO recipeSteps; + +} diff --git a/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java b/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java index 877d437..3337331 100644 --- a/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java +++ b/src/main/java/com/example/blendish/domain/recipe/repository/RecipeRepository.java @@ -1,5 +1,6 @@ package com.example.blendish.domain.recipe.repository; +import com.example.blendish.domain.recipe.entity.Ingredient; import com.example.blendish.domain.recipe.entity.Recipe; import com.example.blendish.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; @@ -36,4 +37,5 @@ public interface RecipeRepository extends JpaRepository { // @Query("UPDATE Recipe r SET r.scrapCount = r.likeCount - 1 WHERE r.recipeId = :recipeId") // void decrementScrapCount(Long recipeId); + } diff --git a/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java b/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java index 8050da1..bf7c1c3 100644 --- a/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java +++ b/src/main/java/com/example/blendish/domain/recipe/service/CommunityService.java @@ -1,30 +1,23 @@ package com.example.blendish.domain.recipe.service; import com.example.blendish.domain.comments.dto.CommentDTO; -import com.example.blendish.domain.comments.entity.Comment; import com.example.blendish.domain.comments.repository.CommentsRepository; import com.example.blendish.domain.recipe.dto.CommunityDetailDTO; import com.example.blendish.domain.recipe.dto.CommunityHotRecipeDTO; import com.example.blendish.domain.recipe.dto.CommunityTodayRecipeDTO; -import com.example.blendish.domain.recipe.entity.Likes; +import com.example.blendish.domain.recipe.dto.RecipeDetailDTO; import com.example.blendish.domain.recipe.entity.Recipe; -import com.example.blendish.domain.recipe.entity.Scrap; +import com.example.blendish.domain.recipe.entity.RecipeSteps; import com.example.blendish.domain.recipe.repository.LikeRepository; import com.example.blendish.domain.recipe.repository.RecipeRepository; import com.example.blendish.domain.recipe.repository.ScrapRepository; -import com.example.blendish.domain.user.entity.User; +import com.example.blendish.domain.recipesteps.dto.RecipeStepsDTO; +import com.example.blendish.domain.recipesteps.repository.RecipestepsRepository; import io.jsonwebtoken.lang.Collections; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.relational.core.sql.Like; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; -import org.springframework.web.bind.annotation.RequestBody; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -37,6 +30,7 @@ public class CommunityService { private final LikeRepository likeRepository; private final CommentsRepository commentsRepository; private final ScrapRepository scrapRepository; + private final RecipestepsRepository recipestepsRepository; // 인기 레시피 가져오는 서비스 public List getTopLikedRecipes() { @@ -208,4 +202,26 @@ public CommunityDetailDTO getDetail(Long recipeId){ // recipeRepository.decrementScrapCount(recipeId); // } // } + + // 레시피 디테일 띄우기 + public RecipeDetailDTO getAllDetail(Long recipeId){ + + Recipe recipe = recipeRepository.findByRecipeId(recipeId); + RecipeSteps recipeSteps =recipestepsRepository.findByRecipeRecipeId(recipeId); + + RecipeStepsDTO recipeStepsDTO = new RecipeStepsDTO(recipeSteps.getDetails(),recipeSteps.getStepImage(), + recipeSteps.getStepNum()); + + + return RecipeDetailDTO.builder() + .recipeId(recipeId) + .time(recipe.getTime()) + .level(recipe.getLevel()) + .name(recipe.getName()) + .ingredients(recipe.getIngredients()) + .recipeSteps(recipeStepsDTO) + .build(); + + } + } diff --git a/src/main/java/com/example/blendish/domain/recipesteps/dto/.gitkeep b/src/main/java/com/example/blendish/domain/recipesteps/dto/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/blendish/domain/recipesteps/dto/RecipeStepsDTO.java b/src/main/java/com/example/blendish/domain/recipesteps/dto/RecipeStepsDTO.java new file mode 100644 index 0000000..e8dcffa --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipesteps/dto/RecipeStepsDTO.java @@ -0,0 +1,17 @@ +package com.example.blendish.domain.recipesteps.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.stereotype.Service; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class RecipeStepsDTO { + private String details; + private String stepImage; + private int stepNum; +} diff --git a/src/main/java/com/example/blendish/domain/recipesteps/repository/.gitkeep b/src/main/java/com/example/blendish/domain/recipesteps/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/blendish/domain/recipesteps/repository/RecipestepsRepository.java b/src/main/java/com/example/blendish/domain/recipesteps/repository/RecipestepsRepository.java new file mode 100644 index 0000000..b2a467b --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipesteps/repository/RecipestepsRepository.java @@ -0,0 +1,9 @@ +package com.example.blendish.domain.recipesteps.repository; + +import com.example.blendish.domain.recipe.dto.RecipeStepDTO; +import com.example.blendish.domain.recipe.entity.RecipeSteps; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RecipestepsRepository extends JpaRepository { + RecipeSteps findByRecipeRecipeId(Long recipeId); +} From 4fc72b6651e7aeca023482bbbbc58907ce20e604 Mon Sep 17 00:00:00 2001 From: dlqja Date: Fri, 14 Feb 2025 17:05:02 +0900 Subject: [PATCH 14/26] [Feat] Find user by Id --- .../blendish/controller/UserController.java | 16 ++++++++++++++++ .../domain/user/dto/CustomUserDetails.java | 3 +++ .../blendish/domain/user/jwt/LoginFilter.java | 3 +-- .../domain/user/service/UserService.java | 9 +++++++++ .../blendish/global/config/CorsMvcConfig.java | 16 ---------------- .../blendish/global/config/SwaggerConfig.java | 19 ++++++++++++++++--- 6 files changed, 45 insertions(+), 21 deletions(-) delete mode 100644 src/main/java/com/example/blendish/global/config/CorsMvcConfig.java diff --git a/src/main/java/com/example/blendish/controller/UserController.java b/src/main/java/com/example/blendish/controller/UserController.java index 3b272cc..4d76e16 100644 --- a/src/main/java/com/example/blendish/controller/UserController.java +++ b/src/main/java/com/example/blendish/controller/UserController.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.util.List; + @Tag(name = "User Controller", description = "사용자 관리 관련 API") @Controller @RequestMapping("/api/user") @@ -59,8 +60,23 @@ public ResponseEntity> checkPassword(@RequestBody C Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String userId = authentication.getName(); + System.out.println(userId); boolean result = userService.checkPassword(userId, dto.getPassword()); return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, result)); } + + @Operation( + summary = "현재 로그인된 사용자 조회", + description = "토큰에 포함된 사용자 ID를 기반으로 전체 사용자 데이터를 반환한다." + ) + @GetMapping("/me") + public ResponseEntity> getUserById() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String userId = authentication.getName(); + System.out.println(userId); + UserDTO userDTO = userService.getUserById(userId); + return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, userDTO)); + } + } diff --git a/src/main/java/com/example/blendish/domain/user/dto/CustomUserDetails.java b/src/main/java/com/example/blendish/domain/user/dto/CustomUserDetails.java index 7a9469c..bfe0830 100644 --- a/src/main/java/com/example/blendish/domain/user/dto/CustomUserDetails.java +++ b/src/main/java/com/example/blendish/domain/user/dto/CustomUserDetails.java @@ -17,6 +17,9 @@ public CustomUserDetails(User user) { this.user = user; } + public User getUser() { + return user; + } //롤값 반환 @Override public Collection getAuthorities() { diff --git a/src/main/java/com/example/blendish/domain/user/jwt/LoginFilter.java b/src/main/java/com/example/blendish/domain/user/jwt/LoginFilter.java index 6d13743..1429819 100644 --- a/src/main/java/com/example/blendish/domain/user/jwt/LoginFilter.java +++ b/src/main/java/com/example/blendish/domain/user/jwt/LoginFilter.java @@ -13,7 +13,6 @@ import java.util.Collection; import java.util.Iterator; - public class LoginFilter extends UsernamePasswordAuthenticationFilter { private final AuthenticationManager authenticationManager; @@ -49,7 +48,7 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR String role = auth.getAuthority(); //토큰 시간!!! - String token = jwtUtil.createJwt(username, role, 60 * 60 * 1000L); + String token = jwtUtil.createJwt(username, role, 48 * 60 * 60 * 1000L); response.addHeader("Authorization", "Bearer " + token); } diff --git a/src/main/java/com/example/blendish/domain/user/service/UserService.java b/src/main/java/com/example/blendish/domain/user/service/UserService.java index a907629..753500d 100644 --- a/src/main/java/com/example/blendish/domain/user/service/UserService.java +++ b/src/main/java/com/example/blendish/domain/user/service/UserService.java @@ -67,4 +67,13 @@ public boolean checkPassword(String userId, String rawPassword) { } return bCryptPasswordEncoder.matches(rawPassword, user.getUserPw()); } + + public UserDTO getUserById(String userId) { + User user = userRepository.findByUserId(userId); + if (user == null) { + throw new IllegalArgumentException("해당 유저를 찾을 수 없습니다."); + } + return UserMapper.toDTO(user); + } + } diff --git a/src/main/java/com/example/blendish/global/config/CorsMvcConfig.java b/src/main/java/com/example/blendish/global/config/CorsMvcConfig.java deleted file mode 100644 index f3a55a8..0000000 --- a/src/main/java/com/example/blendish/global/config/CorsMvcConfig.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.blendish.global.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class CorsMvcConfig implements WebMvcConfigurer { - - @Override - public void addCorsMappings(CorsRegistry corsRegistry) { - - corsRegistry.addMapping("/**") - .allowedOrigins("http://localhost:3000"); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/blendish/global/config/SwaggerConfig.java b/src/main/java/com/example/blendish/global/config/SwaggerConfig.java index 0d04a54..c289a9f 100644 --- a/src/main/java/com/example/blendish/global/config/SwaggerConfig.java +++ b/src/main/java/com/example/blendish/global/config/SwaggerConfig.java @@ -3,6 +3,8 @@ import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -10,8 +12,19 @@ public class SwaggerConfig { @Bean public OpenAPI openAPI() { + final String securitySchemeName = "bearerAuth"; + return new OpenAPI() - .components(new Components()) + .components(new Components() + .addSecuritySchemes(securitySchemeName, + new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + ) + ) + .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) .info(apiInfo()); } @@ -19,6 +32,6 @@ private Info apiInfo() { return new Info() .title("Blendish") .description("나만의 레시피 만들기") - .version("1.0.0"); // API의 버전 + .version("1.0.0"); } -} +} \ No newline at end of file From cf1fcaa4e07fdcceef7219373167386aad64855d Mon Sep 17 00:00:00 2001 From: Junyeong-An <2117ab@gmail.com> Date: Sat, 15 Feb 2025 02:20:57 +0900 Subject: [PATCH 15/26] =?UTF-8?q?[Refactor]=20s3=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- .../blendish/controller/RecipeController.java | 12 ++++-- .../blendish/controller/RecipeSwagger.java | 6 ++- .../domain/recipe/dto/AddRecipeDTO.java | 1 - .../domain/recipe/service/RecipeService.java | 5 ++- src/main/resources/application.yml | 38 ++++++++++++++++++- 6 files changed, 54 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 69fa948..f723d23 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,4 @@ out/ ### VS Code ### .vscode/ application.properties -application-*.yml \ No newline at end of file +application*.yml \ No newline at end of file diff --git a/src/main/java/com/example/blendish/controller/RecipeController.java b/src/main/java/com/example/blendish/controller/RecipeController.java index 23183b9..c0ac684 100644 --- a/src/main/java/com/example/blendish/controller/RecipeController.java +++ b/src/main/java/com/example/blendish/controller/RecipeController.java @@ -5,11 +5,14 @@ import com.example.blendish.domain.recipe.service.RecipeService; import com.example.blendish.global.dto.ApiResponseTemplate; import com.example.blendish.global.response.SuccessCode; +import com.example.blendish.global.s3.S3UploadService; +import java.io.IOException; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor @@ -17,11 +20,14 @@ public class RecipeController implements RecipeSwagger{ private final RecipeService recipeService; + private final S3UploadService s3UploadService; - @PostMapping + @PostMapping(consumes = {"multipart/form-data"}) public ResponseEntity> addRecipe(@RequestBody AddRecipeDTO addRecipeDTO, - @AuthenticationPrincipal UserDetails userDetails) { - recipeService.createRecipe(addRecipeDTO, userDetails.getUsername()); + @RequestPart(value = "image", required = false) MultipartFile image, + @AuthenticationPrincipal UserDetails userDetails) throws IOException { + String imageUrl = (image != null && !image.isEmpty()) ? s3UploadService.saveFile(image) : null; + recipeService.createRecipe(addRecipeDTO, userDetails.getUsername(), imageUrl); return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, "레시피가 등록되었습니다.")); } } diff --git a/src/main/java/com/example/blendish/controller/RecipeSwagger.java b/src/main/java/com/example/blendish/controller/RecipeSwagger.java index c50c359..87db2e9 100644 --- a/src/main/java/com/example/blendish/controller/RecipeSwagger.java +++ b/src/main/java/com/example/blendish/controller/RecipeSwagger.java @@ -8,11 +8,14 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import java.io.IOException; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; public interface RecipeSwagger { @@ -29,5 +32,6 @@ public interface RecipeSwagger { ) @PostMapping("/api/recipe") ResponseEntity> addRecipe(@RequestBody AddRecipeDTO addRecipeDTO, - @AuthenticationPrincipal UserDetails userDetails); + @RequestPart(value = "image", required = false) MultipartFile image, + @AuthenticationPrincipal UserDetails userDetails) throws IOException; } diff --git a/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java b/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java index d5619b4..7ee41dc 100644 --- a/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java +++ b/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java @@ -8,6 +8,5 @@ public record AddRecipeDTO(String name, String level, List ingredients, String information, - String foodImage, List steps) { } diff --git a/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java b/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java index 33739f5..b800668 100644 --- a/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java +++ b/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java @@ -7,6 +7,7 @@ import com.example.blendish.domain.recipe.repository.RecipeRepository; import com.example.blendish.domain.user.entity.User; import com.example.blendish.domain.user.repository.UserRepository; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -21,7 +22,7 @@ public class RecipeService { private final RecipeRepository recipeRepository; private final UserRepository userRepository; - public void createRecipe(AddRecipeDTO addRecipeDTO, String userId) { + public void createRecipe(AddRecipeDTO addRecipeDTO, String userId, String imageUrl) { User user = userRepository.findByUserId(userId); if (user == null) { throw new IllegalArgumentException("유저를 찾을 수 없습니다."); @@ -41,7 +42,7 @@ public void createRecipe(AddRecipeDTO addRecipeDTO, String userId) { .level(addRecipeDTO.level()) .ingredients(ingredients) .information(addRecipeDTO.information()) - .foodImage(addRecipeDTO.foodImage()) + .foodImage(imageUrl) .user(user) .steps(steps) .build(); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 68373f0..7562b06 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,11 +12,45 @@ spring: database: mysql database-platform: org.hibernate.dialect.MySQLDialect hibernate: - ddl-auto: update + ddl-auto: create-drop show-sql: true properties: hibernate: format_sql: false use_sql_comments: true + jwt: - secret: ${JWT_SECRET} \ No newline at end of file + secret: ${JWT_SECRET} + +openai: + api: + key: ${OPEN_AI_API_KEY} + +security: + oauth2: + client: + registration: + google: + client-name: google + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + redirect-uri: ${GOOGLE_REDIRECT_URI} + authorization-grant-type: authorization_code + scope: profile, email + +cloud: + aws: + s3: + bucketName: ${AWS_S3_BUCKET_NAME} + credentials: + access-key: ${AWS_ACCESS_KEY_ID} + secret-key: ${AWS_SECRET_ACCESS_KEY} + region: + static: ${AWS_REGION} + stack: + auto: false + +servlet: + multipart: + max-file-size: 50MB + max-request-size: 50MB From d4af87b294790227518d7e2fa7b5848c9795f843 Mon Sep 17 00:00:00 2001 From: Junyeong-An <2117ab@gmail.com> Date: Sat, 15 Feb 2025 02:38:19 +0900 Subject: [PATCH 16/26] =?UTF-8?q?[Refactor]=20s3=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- src/main/resources/application.yml | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index f723d23..fd837ac 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ out/ ### VS Code ### .vscode/ application.properties -application*.yml \ No newline at end of file +application*.yml +application.yml \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7562b06..12d70cf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -22,26 +22,26 @@ spring: jwt: secret: ${JWT_SECRET} + security: + oauth2: + client: + registration: + google: + client-name: google + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + redirect-uri: ${GOOGLE_REDIRECT_URI} + authorization-grant-type: authorization_code + scope: profile, email + openai: api: key: ${OPEN_AI_API_KEY} -security: - oauth2: - client: - registration: - google: - client-name: google - client-id: ${GOOGLE_CLIENT_ID} - client-secret: ${GOOGLE_CLIENT_SECRET} - redirect-uri: ${GOOGLE_REDIRECT_URI} - authorization-grant-type: authorization_code - scope: profile, email - cloud: aws: s3: - bucketName: ${AWS_S3_BUCKET_NAME} + bucket-name: ${AWS_S3_BUCKET_NAME} credentials: access-key: ${AWS_ACCESS_KEY_ID} secret-key: ${AWS_SECRET_ACCESS_KEY} From 2f160672a533ea4c295c147c6a80630742495cd4 Mon Sep 17 00:00:00 2001 From: Junyeong-An <2117ab@gmail.com> Date: Sat, 15 Feb 2025 02:47:08 +0900 Subject: [PATCH 17/26] =?UTF-8?q?[Refactor]=20AI=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/recipe/entity/AiIngredient.java | 27 ++++++ .../domain/recipe/entity/AiRecipe.java | 84 +++++++++++++++++++ .../domain/recipe/entity/Ingredient.java | 26 +++++- .../blendish/domain/recipe/entity/Recipe.java | 14 ++-- 4 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/example/blendish/domain/recipe/entity/AiIngredient.java create mode 100644 src/main/java/com/example/blendish/domain/recipe/entity/AiRecipe.java diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/AiIngredient.java b/src/main/java/com/example/blendish/domain/recipe/entity/AiIngredient.java new file mode 100644 index 0000000..6913a11 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipe/entity/AiIngredient.java @@ -0,0 +1,27 @@ +package com.example.blendish.domain.recipe.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Table(name = "ai_ingredient") +public class AiIngredient { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 100) + private String name; + + @Column(nullable = false, length = 50) + private String amount; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ai_recipe_id", nullable = false) + private AiRecipe aiRecipe; +} diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/AiRecipe.java b/src/main/java/com/example/blendish/domain/recipe/entity/AiRecipe.java new file mode 100644 index 0000000..974e1ef --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipe/entity/AiRecipe.java @@ -0,0 +1,84 @@ +package com.example.blendish.domain.recipe.entity; + +import com.example.blendish.domain.comments.entity.Comment; +import com.example.blendish.domain.foodflavor.entity.FoodFlavor; +import com.example.blendish.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +import java.util.Date; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Table(name = "ai_recipe") +public class AiRecipe { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long aiRecipeId; + + @Column(nullable = false, length = 100) + private String name; + + @Column(nullable = false, length = 1) + private String level; + + @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) + private List ingredients; + + private int likeCount; + + @Column(nullable = false, length = 100) + private String time; + + private int scrapCount; + + private int spicyLevel; + + @Column(length = 200) + private String information; + + @Temporal(TemporalType.DATE) + private Date postDate; + + @Temporal(TemporalType.DATE) + private Date updatedDate; + + @Column(length = 255) + private String foodImage; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; + + @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) + private List steps; + + @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) + private List comment; + + @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) + private List likes; + + @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) + private List scraps; + + @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) + private List foodFlavors; + + @PrePersist + protected void onCreate() { + this.postDate = new Date(); + this.updatedDate = this.postDate; + } + + @PreUpdate + protected void onUpdate() { + this.updatedDate = new Date(); + } + +} diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/Ingredient.java b/src/main/java/com/example/blendish/domain/recipe/entity/Ingredient.java index 8acc197..30d99a7 100644 --- a/src/main/java/com/example/blendish/domain/recipe/entity/Ingredient.java +++ b/src/main/java/com/example/blendish/domain/recipe/entity/Ingredient.java @@ -1,7 +1,27 @@ package com.example.blendish.domain.recipe.entity; -import jakarta.persistence.Embeddable; +import jakarta.persistence.*; +import lombok.*; -@Embeddable -public record Ingredient(String name, String amount) { +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Table(name = "ingredient") +public class Ingredient { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 100) + private String name; + + @Column(nullable = false, length = 50) + private String amount; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipe_id", nullable = false) + private Recipe recipe; } diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java index 57621a2..f60909c 100644 --- a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java +++ b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java @@ -14,7 +14,7 @@ @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -@Setter +@Table(name = "recipe") public class Recipe { @Id @@ -27,8 +27,7 @@ public class Recipe { @Column(nullable = false, length = 1) private String level; - @ElementCollection - @CollectionTable(name = "recipe_ingredients", joinColumns = @JoinColumn(name = "recipe_id")) + @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) private List ingredients; private int likeCount; @@ -60,16 +59,16 @@ public class Recipe { private List steps; @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List comment ; + private List comment; @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List likes ; + private List likes; @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List scraps ; + private List scraps; @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List foodFlavors ; + private List foodFlavors; @PrePersist protected void onCreate() { @@ -81,5 +80,4 @@ protected void onCreate() { protected void onUpdate() { this.updatedDate = new Date(); } - } From 5ed9b2cee0d48a296ddb5e4a3dd02932a92cb369 Mon Sep 17 00:00:00 2001 From: Junyeong-An <2117ab@gmail.com> Date: Sat, 15 Feb 2025 19:57:14 +0900 Subject: [PATCH 18/26] =?UTF-8?q?[Feat]=20AI=20=EB=A0=88=EC=8B=9C=ED=94=BC?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #14 --- build.gradle | 3 + .../blendish/controller/GPTController.java | 37 ++++++ .../blendish/controller/RecipeController.java | 2 +- .../domain/gpt/config/OpenAIConfig.java | 57 +++++++++ .../domain/gpt/dto/CustomRecipeReqDTO.java | 12 ++ .../domain/gpt/dto/GPTRequestDTO.java | 6 + .../domain/gpt/service/GPTRecipeService.java | 108 ++++++++++++++++++ .../domain/gpt/service/OpenAIService.java | 40 +++++++ .../domain/recipe/dto/AddRecipeDTO.java | 1 + .../domain/recipe/entity/AiIngredient.java | 4 +- .../domain/recipe/entity/AiRecipe.java | 84 -------------- .../blendish/domain/recipe/entity/Recipe.java | 6 + .../domain/recipe/service/RecipeService.java | 34 +++++- .../blendish/global/config/JacksonConfig.java | 20 ++++ .../global/dto/ApiResponseTemplate.java | 4 + 15 files changed, 330 insertions(+), 88 deletions(-) create mode 100644 src/main/java/com/example/blendish/controller/GPTController.java create mode 100644 src/main/java/com/example/blendish/domain/gpt/config/OpenAIConfig.java create mode 100644 src/main/java/com/example/blendish/domain/gpt/dto/CustomRecipeReqDTO.java create mode 100644 src/main/java/com/example/blendish/domain/gpt/dto/GPTRequestDTO.java create mode 100644 src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java create mode 100644 src/main/java/com/example/blendish/domain/gpt/service/OpenAIService.java delete mode 100644 src/main/java/com/example/blendish/domain/recipe/entity/AiRecipe.java create mode 100644 src/main/java/com/example/blendish/global/config/JacksonConfig.java diff --git a/build.gradle b/build.gradle index e8e3f95..6540514 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,9 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' implementation group: 'ch.simas.qlrm', name: 'qlrm', version: '1.7.1' + + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' } tasks.named('test') { diff --git a/src/main/java/com/example/blendish/controller/GPTController.java b/src/main/java/com/example/blendish/controller/GPTController.java new file mode 100644 index 0000000..b2ff491 --- /dev/null +++ b/src/main/java/com/example/blendish/controller/GPTController.java @@ -0,0 +1,37 @@ +package com.example.blendish.controller; + +import com.example.blendish.domain.gpt.dto.CustomRecipeReqDTO; +import com.example.blendish.domain.gpt.service.GPTRecipeService; +import com.example.blendish.domain.gpt.service.OpenAIService; +import com.example.blendish.global.dto.ApiResponseTemplate; +import com.example.blendish.global.response.SuccessCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import reactor.core.publisher.Mono; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/gpt") +public class GPTController { + + private final OpenAIService openAIService; + private final GPTRecipeService gptRecipeService; + + @GetMapping("/chat") + public Mono chatWithGpt(@RequestParam String message) { + return openAIService.getGptResponse(message); + } + + @PostMapping("/recipe") + public Mono>> generateCustomRecipe( + @RequestBody CustomRecipeReqDTO request, + @AuthenticationPrincipal UserDetails userDetails) { + return gptRecipeService.getAiGeneratedRecipe(request, userDetails) + .map(result -> ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.CREATED, result))); + } + +} diff --git a/src/main/java/com/example/blendish/controller/RecipeController.java b/src/main/java/com/example/blendish/controller/RecipeController.java index c0ac684..63c640a 100644 --- a/src/main/java/com/example/blendish/controller/RecipeController.java +++ b/src/main/java/com/example/blendish/controller/RecipeController.java @@ -27,7 +27,7 @@ public ResponseEntity> addRecipe(@RequestBody AddRec @RequestPart(value = "image", required = false) MultipartFile image, @AuthenticationPrincipal UserDetails userDetails) throws IOException { String imageUrl = (image != null && !image.isEmpty()) ? s3UploadService.saveFile(image) : null; - recipeService.createRecipe(addRecipeDTO, userDetails.getUsername(), imageUrl); + recipeService.createUserRecipe(addRecipeDTO, userDetails.getUsername(), imageUrl); return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, "레시피가 등록되었습니다.")); } } diff --git a/src/main/java/com/example/blendish/domain/gpt/config/OpenAIConfig.java b/src/main/java/com/example/blendish/domain/gpt/config/OpenAIConfig.java new file mode 100644 index 0000000..05b9bc4 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/gpt/config/OpenAIConfig.java @@ -0,0 +1,57 @@ +package com.example.blendish.domain.gpt.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +@Configuration +public class OpenAIConfig { + + @Value("${openai.api.key}") + private String openAiKey; + + @Bean + public WebClient webClient() { + HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofSeconds(15)) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .doOnConnected(conn -> + conn.addHandlerLast(new ReadTimeoutHandler(10, TimeUnit.SECONDS)) + .addHandlerLast(new WriteTimeoutHandler(10, TimeUnit.SECONDS)) + ); + + return WebClient.builder() + .baseUrl("https://api.openai.com/v1") // baseUrl을 명확히 지정 + .defaultHeader("Content-Type", "application/json") + .filter(addAuthorizationHeader()) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); + } + + + private ExchangeFilterFunction addAuthorizationHeader() { + return (request, next) -> { + return next.exchange( + ClientRequest.from(request) + .header("Authorization", "Bearer " + openAiKey) + .build() + ); + }; + } + + @Bean(name = "openAiObjectMapper") + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } +} diff --git a/src/main/java/com/example/blendish/domain/gpt/dto/CustomRecipeReqDTO.java b/src/main/java/com/example/blendish/domain/gpt/dto/CustomRecipeReqDTO.java new file mode 100644 index 0000000..911cf50 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/gpt/dto/CustomRecipeReqDTO.java @@ -0,0 +1,12 @@ +package com.example.blendish.domain.gpt.dto; + +import java.util.List; + +public record CustomRecipeReqDTO( + String category, + int cookingTime, + String difficulty, + List tastes, + int spiceLevel +) { +} diff --git a/src/main/java/com/example/blendish/domain/gpt/dto/GPTRequestDTO.java b/src/main/java/com/example/blendish/domain/gpt/dto/GPTRequestDTO.java new file mode 100644 index 0000000..d39a0a9 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/gpt/dto/GPTRequestDTO.java @@ -0,0 +1,6 @@ +package com.example.blendish.domain.gpt.dto; + +import java.util.List; + +public record GPTRequestDTO(String model, List messages) { +} diff --git a/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java b/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java new file mode 100644 index 0000000..0a6f34c --- /dev/null +++ b/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java @@ -0,0 +1,108 @@ +package com.example.blendish.domain.gpt.service; + +import com.example.blendish.domain.gpt.dto.CustomRecipeReqDTO; +import com.example.blendish.domain.user.entity.User; +import com.example.blendish.domain.user.repository.UserRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +@Service +@RequiredArgsConstructor +public class GPTRecipeService { + + private static final Logger logger = LoggerFactory.getLogger(GPTRecipeService.class); + private final WebClient webClient; + private final UserRepository userRepository; + private final ObjectMapper objectMapper; + + @Value("${openai.api.key}") + private String openAiApiKey; + + public Mono getAiGeneratedRecipe(CustomRecipeReqDTO request, @AuthenticationPrincipal UserDetails userDetails) { + String userId = userDetails.getUsername(); + User user = userRepository.findByUserId(userId); + + if (user == null) { + throw new IllegalArgumentException("유저를 찾을 수 없습니다."); + } + + String prompt = generateRecipePrompt(request, user.getCountry()); + + + Map requestBody = Map.of( + "model", "gpt-4", + "messages", List.of( + Map.of("role", "system", "content", "너는 요리 레시피 추천 AI야. 요청에 따라 요리를 추천해줘."), + Map.of("role", "user", "content", prompt.replaceAll("\\[|\\]", "")) + ), + "temperature", 0.7 + ); + + + try { + String requestJson = objectMapper.writeValueAsString(requestBody); + logger.info("Sending request to OpenAI: {}", requestJson); + } catch (Exception e) { + logger.error("Error converting request to JSON", e); + } + return Mono.delay(Duration.ofSeconds(3)) // 3초 딜레이 추가 + .then(webClient.post() + .uri("/chat/completions") + .header("Authorization", "Bearer " + openAiApiKey) + .header("Content-Type", "application/json") + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)") + .bodyValue(requestBody) + .retrieve() + .bodyToMono(Map.class) + .doOnNext(response -> logger.info("Response from OpenAI: {}", response)) + .onErrorResume(WebClientResponseException.class, e -> { + logger.error("OpenAI API Error: StatusCode = {}, ResponseBody = {}", + e.getStatusCode(), e.getResponseBodyAsString()); + return Mono.just(Map.of("error", "OpenAI API 요청 중 오류가 발생했습니다.")); + }) + .retryWhen(Retry.backoff(3, Duration.ofSeconds(3)) + .maxBackoff(Duration.ofSeconds(10)) + .filter(throwable -> !(throwable instanceof WebClientResponseException && + ((WebClientResponseException) throwable).getStatusCode().value() == 421))) + .map(response -> { + List> choices = (List>) response.get("choices"); + if (choices != null && !choices.isEmpty()) { + Map firstChoice = choices.get(0); + Map message = (Map) firstChoice.get("message"); + return (String) message.get("content"); + } + return "레시피를 생성하는 데 실패했습니다."; + })); + + + } + + private String generateRecipePrompt(CustomRecipeReqDTO request, String country) { + return String.format(""" + %s 국가에서 쉽게 구할 수 있는 재료를 활용하여 %s, %s 맛의 %d분 내 조리 가능한 %s 요리를 추천해줘. + 맵기 레벨 %d의 4가지 레시피를 제공해줘. + 각 레시피는 다음 형식으로: + 1. [레시피 이름] + - 조리 시간: [시간]분 + - 난이도: [쉬움/보통/어려움] + - 특징: [레시피의 특징] + - 재료 팁: [%s에서 재료를 구할 수 있는 팁] + - 조리 순서 (4단계): + """, + country, request.tastes(), request.difficulty(), request.cookingTime(), request.category(), request.spiceLevel(), country + ); + } +} diff --git a/src/main/java/com/example/blendish/domain/gpt/service/OpenAIService.java b/src/main/java/com/example/blendish/domain/gpt/service/OpenAIService.java new file mode 100644 index 0000000..d7c2791 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/gpt/service/OpenAIService.java @@ -0,0 +1,40 @@ +package com.example.blendish.domain.gpt.service; + +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.Map; + +@Service +public class OpenAIService { + + private final WebClient webClient; + + public OpenAIService(WebClient webClient) { + this.webClient = webClient; + } + + public Mono getGptResponse(String userMessage) { + return webClient.post() + .uri("/chat/completions") + .header("Content-Type", "application/json") + .bodyValue(Map.of( + "model", "gpt-4", + "messages", new Object[]{ + Map.of("role", "system", "content", "You are a helpful assistant."), + Map.of("role", "user", "content", userMessage) + }, + "max_tokens", 100 + )) + .retrieve() + .bodyToMono(Map.class) + .map(response -> { + var choices = (java.util.List>) response.get("choices"); + if (choices != null && !choices.isEmpty()) { + return choices.get(0).get("message").toString(); + } + return "No response from GPT."; + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java b/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java index 7ee41dc..50026f8 100644 --- a/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java +++ b/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java @@ -8,5 +8,6 @@ public record AddRecipeDTO(String name, String level, List ingredients, String information, + boolean isAiGenerated, List steps) { } diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/AiIngredient.java b/src/main/java/com/example/blendish/domain/recipe/entity/AiIngredient.java index 6913a11..740fd62 100644 --- a/src/main/java/com/example/blendish/domain/recipe/entity/AiIngredient.java +++ b/src/main/java/com/example/blendish/domain/recipe/entity/AiIngredient.java @@ -22,6 +22,6 @@ public class AiIngredient { private String amount; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "ai_recipe_id", nullable = false) - private AiRecipe aiRecipe; + @JoinColumn(name = "recipe_id", nullable = false) + private Recipe recipe; } diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/AiRecipe.java b/src/main/java/com/example/blendish/domain/recipe/entity/AiRecipe.java deleted file mode 100644 index 974e1ef..0000000 --- a/src/main/java/com/example/blendish/domain/recipe/entity/AiRecipe.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.blendish.domain.recipe.entity; - -import com.example.blendish.domain.comments.entity.Comment; -import com.example.blendish.domain.foodflavor.entity.FoodFlavor; -import com.example.blendish.domain.user.entity.User; -import jakarta.persistence.*; -import lombok.*; - -import java.util.Date; -import java.util.List; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Table(name = "ai_recipe") -public class AiRecipe { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long aiRecipeId; - - @Column(nullable = false, length = 100) - private String name; - - @Column(nullable = false, length = 1) - private String level; - - @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List ingredients; - - private int likeCount; - - @Column(nullable = false, length = 100) - private String time; - - private int scrapCount; - - private int spicyLevel; - - @Column(length = 200) - private String information; - - @Temporal(TemporalType.DATE) - private Date postDate; - - @Temporal(TemporalType.DATE) - private Date updatedDate; - - @Column(length = 255) - private String foodImage; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; - - @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List steps; - - @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List comment; - - @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List likes; - - @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List scraps; - - @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List foodFlavors; - - @PrePersist - protected void onCreate() { - this.postDate = new Date(); - this.updatedDate = this.postDate; - } - - @PreUpdate - protected void onUpdate() { - this.updatedDate = new Date(); - } - -} diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java index f60909c..b287f36 100644 --- a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java +++ b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java @@ -70,6 +70,12 @@ public class Recipe { @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) private List foodFlavors; + @Column(nullable = false) + private boolean isAiGenerated; + + @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) + private List aiIngredients; + @PrePersist protected void onCreate() { this.postDate = new Date(); diff --git a/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java b/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java index b800668..cdd0fd9 100644 --- a/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java +++ b/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java @@ -22,7 +22,7 @@ public class RecipeService { private final RecipeRepository recipeRepository; private final UserRepository userRepository; - public void createRecipe(AddRecipeDTO addRecipeDTO, String userId, String imageUrl) { + public void createUserRecipe(AddRecipeDTO addRecipeDTO, String userId, String imageUrl) { User user = userRepository.findByUserId(userId); if (user == null) { throw new IllegalArgumentException("유저를 찾을 수 없습니다."); @@ -44,6 +44,38 @@ public void createRecipe(AddRecipeDTO addRecipeDTO, String userId, String imageU .information(addRecipeDTO.information()) .foodImage(imageUrl) .user(user) + .isAiGenerated(false) + .steps(steps) + .build(); + + steps.forEach(step -> step.updateRecipe(recipe)); + + recipeRepository.save(recipe); + } + + public void createAiRecipe(AddRecipeDTO addRecipeDTO,String userId) { + User user = userRepository.findByUserId(userId); + if (user == null) { + throw new IllegalArgumentException("유저를 찾을 수 없습니다."); + } + + List steps = addRecipeDTO.steps().stream() + .map(stepDTO -> RecipeSteps.builder() + .stepNum(stepDTO.stepNumber()) + .details(stepDTO.details()) + .build()) + .collect(Collectors.toList()); + + List ingredients = addRecipeDTO.ingredients(); + + Recipe recipe = Recipe.builder() + .name(addRecipeDTO.name()) + .level(addRecipeDTO.level()) + .ingredients(ingredients) + .information(addRecipeDTO.information()) + .foodImage(null) + .user(null) + .isAiGenerated(true) .steps(steps) .build(); diff --git a/src/main/java/com/example/blendish/global/config/JacksonConfig.java b/src/main/java/com/example/blendish/global/config/JacksonConfig.java new file mode 100644 index 0000000..af6cf05 --- /dev/null +++ b/src/main/java/com/example/blendish/global/config/JacksonConfig.java @@ -0,0 +1,20 @@ +package com.example.blendish.global.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +@Configuration +public class JacksonConfig { + + @Primary + @Bean + public ObjectMapper objectMapper() { + return Jackson2ObjectMapperBuilder.json() + .modules(new JavaTimeModule()) + .build(); + } +} diff --git a/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java b/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java index 497c3a0..4a56895 100644 --- a/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java +++ b/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java @@ -2,6 +2,7 @@ import com.example.blendish.global.response.ErrorCode; import com.example.blendish.global.response.SuccessCode; +import com.fasterxml.jackson.annotation.JsonFormat; import lombok.*; import java.time.LocalDateTime; @@ -14,7 +15,10 @@ public class ApiResponseTemplate { private final int status; private final String message; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") private final LocalDateTime timestamp; // 추가된 날짜/시간 필드 + private T data; public static ApiResponseTemplate success(SuccessCode successCode, T data) { From 373aa916dcaaf4b1fb2f2843a892cc37b6f7146e Mon Sep 17 00:00:00 2001 From: Junyeong-An <2117ab@gmail.com> Date: Sat, 15 Feb 2025 19:57:14 +0900 Subject: [PATCH 19/26] =?UTF-8?q?[Chore]=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=86=8D=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../blendish/controller/GPTController.java | 37 ++++++ .../blendish/controller/RecipeController.java | 2 +- .../domain/gpt/config/OpenAIConfig.java | 57 +++++++++ .../domain/gpt/dto/CustomRecipeReqDTO.java | 12 ++ .../domain/gpt/dto/GPTRequestDTO.java | 6 + .../domain/gpt/service/GPTRecipeService.java | 108 ++++++++++++++++++ .../domain/gpt/service/OpenAIService.java | 40 +++++++ .../domain/recipe/dto/AddRecipeDTO.java | 1 + .../domain/recipe/entity/AiIngredient.java | 4 +- .../domain/recipe/entity/AiRecipe.java | 84 -------------- .../blendish/domain/recipe/entity/Recipe.java | 6 + .../domain/recipe/service/RecipeService.java | 34 +++++- .../blendish/global/config/JacksonConfig.java | 20 ++++ 14 files changed, 326 insertions(+), 88 deletions(-) create mode 100644 src/main/java/com/example/blendish/controller/GPTController.java create mode 100644 src/main/java/com/example/blendish/domain/gpt/config/OpenAIConfig.java create mode 100644 src/main/java/com/example/blendish/domain/gpt/dto/CustomRecipeReqDTO.java create mode 100644 src/main/java/com/example/blendish/domain/gpt/dto/GPTRequestDTO.java create mode 100644 src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java create mode 100644 src/main/java/com/example/blendish/domain/gpt/service/OpenAIService.java delete mode 100644 src/main/java/com/example/blendish/domain/recipe/entity/AiRecipe.java create mode 100644 src/main/java/com/example/blendish/global/config/JacksonConfig.java diff --git a/build.gradle b/build.gradle index e8e3f95..6540514 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,9 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' implementation group: 'ch.simas.qlrm', name: 'qlrm', version: '1.7.1' + + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' } tasks.named('test') { diff --git a/src/main/java/com/example/blendish/controller/GPTController.java b/src/main/java/com/example/blendish/controller/GPTController.java new file mode 100644 index 0000000..b2ff491 --- /dev/null +++ b/src/main/java/com/example/blendish/controller/GPTController.java @@ -0,0 +1,37 @@ +package com.example.blendish.controller; + +import com.example.blendish.domain.gpt.dto.CustomRecipeReqDTO; +import com.example.blendish.domain.gpt.service.GPTRecipeService; +import com.example.blendish.domain.gpt.service.OpenAIService; +import com.example.blendish.global.dto.ApiResponseTemplate; +import com.example.blendish.global.response.SuccessCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import reactor.core.publisher.Mono; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/gpt") +public class GPTController { + + private final OpenAIService openAIService; + private final GPTRecipeService gptRecipeService; + + @GetMapping("/chat") + public Mono chatWithGpt(@RequestParam String message) { + return openAIService.getGptResponse(message); + } + + @PostMapping("/recipe") + public Mono>> generateCustomRecipe( + @RequestBody CustomRecipeReqDTO request, + @AuthenticationPrincipal UserDetails userDetails) { + return gptRecipeService.getAiGeneratedRecipe(request, userDetails) + .map(result -> ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.CREATED, result))); + } + +} diff --git a/src/main/java/com/example/blendish/controller/RecipeController.java b/src/main/java/com/example/blendish/controller/RecipeController.java index c0ac684..63c640a 100644 --- a/src/main/java/com/example/blendish/controller/RecipeController.java +++ b/src/main/java/com/example/blendish/controller/RecipeController.java @@ -27,7 +27,7 @@ public ResponseEntity> addRecipe(@RequestBody AddRec @RequestPart(value = "image", required = false) MultipartFile image, @AuthenticationPrincipal UserDetails userDetails) throws IOException { String imageUrl = (image != null && !image.isEmpty()) ? s3UploadService.saveFile(image) : null; - recipeService.createRecipe(addRecipeDTO, userDetails.getUsername(), imageUrl); + recipeService.createUserRecipe(addRecipeDTO, userDetails.getUsername(), imageUrl); return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, "레시피가 등록되었습니다.")); } } diff --git a/src/main/java/com/example/blendish/domain/gpt/config/OpenAIConfig.java b/src/main/java/com/example/blendish/domain/gpt/config/OpenAIConfig.java new file mode 100644 index 0000000..05b9bc4 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/gpt/config/OpenAIConfig.java @@ -0,0 +1,57 @@ +package com.example.blendish.domain.gpt.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +@Configuration +public class OpenAIConfig { + + @Value("${openai.api.key}") + private String openAiKey; + + @Bean + public WebClient webClient() { + HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofSeconds(15)) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .doOnConnected(conn -> + conn.addHandlerLast(new ReadTimeoutHandler(10, TimeUnit.SECONDS)) + .addHandlerLast(new WriteTimeoutHandler(10, TimeUnit.SECONDS)) + ); + + return WebClient.builder() + .baseUrl("https://api.openai.com/v1") // baseUrl을 명확히 지정 + .defaultHeader("Content-Type", "application/json") + .filter(addAuthorizationHeader()) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); + } + + + private ExchangeFilterFunction addAuthorizationHeader() { + return (request, next) -> { + return next.exchange( + ClientRequest.from(request) + .header("Authorization", "Bearer " + openAiKey) + .build() + ); + }; + } + + @Bean(name = "openAiObjectMapper") + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } +} diff --git a/src/main/java/com/example/blendish/domain/gpt/dto/CustomRecipeReqDTO.java b/src/main/java/com/example/blendish/domain/gpt/dto/CustomRecipeReqDTO.java new file mode 100644 index 0000000..911cf50 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/gpt/dto/CustomRecipeReqDTO.java @@ -0,0 +1,12 @@ +package com.example.blendish.domain.gpt.dto; + +import java.util.List; + +public record CustomRecipeReqDTO( + String category, + int cookingTime, + String difficulty, + List tastes, + int spiceLevel +) { +} diff --git a/src/main/java/com/example/blendish/domain/gpt/dto/GPTRequestDTO.java b/src/main/java/com/example/blendish/domain/gpt/dto/GPTRequestDTO.java new file mode 100644 index 0000000..d39a0a9 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/gpt/dto/GPTRequestDTO.java @@ -0,0 +1,6 @@ +package com.example.blendish.domain.gpt.dto; + +import java.util.List; + +public record GPTRequestDTO(String model, List messages) { +} diff --git a/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java b/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java new file mode 100644 index 0000000..0a6f34c --- /dev/null +++ b/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java @@ -0,0 +1,108 @@ +package com.example.blendish.domain.gpt.service; + +import com.example.blendish.domain.gpt.dto.CustomRecipeReqDTO; +import com.example.blendish.domain.user.entity.User; +import com.example.blendish.domain.user.repository.UserRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +@Service +@RequiredArgsConstructor +public class GPTRecipeService { + + private static final Logger logger = LoggerFactory.getLogger(GPTRecipeService.class); + private final WebClient webClient; + private final UserRepository userRepository; + private final ObjectMapper objectMapper; + + @Value("${openai.api.key}") + private String openAiApiKey; + + public Mono getAiGeneratedRecipe(CustomRecipeReqDTO request, @AuthenticationPrincipal UserDetails userDetails) { + String userId = userDetails.getUsername(); + User user = userRepository.findByUserId(userId); + + if (user == null) { + throw new IllegalArgumentException("유저를 찾을 수 없습니다."); + } + + String prompt = generateRecipePrompt(request, user.getCountry()); + + + Map requestBody = Map.of( + "model", "gpt-4", + "messages", List.of( + Map.of("role", "system", "content", "너는 요리 레시피 추천 AI야. 요청에 따라 요리를 추천해줘."), + Map.of("role", "user", "content", prompt.replaceAll("\\[|\\]", "")) + ), + "temperature", 0.7 + ); + + + try { + String requestJson = objectMapper.writeValueAsString(requestBody); + logger.info("Sending request to OpenAI: {}", requestJson); + } catch (Exception e) { + logger.error("Error converting request to JSON", e); + } + return Mono.delay(Duration.ofSeconds(3)) // 3초 딜레이 추가 + .then(webClient.post() + .uri("/chat/completions") + .header("Authorization", "Bearer " + openAiApiKey) + .header("Content-Type", "application/json") + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)") + .bodyValue(requestBody) + .retrieve() + .bodyToMono(Map.class) + .doOnNext(response -> logger.info("Response from OpenAI: {}", response)) + .onErrorResume(WebClientResponseException.class, e -> { + logger.error("OpenAI API Error: StatusCode = {}, ResponseBody = {}", + e.getStatusCode(), e.getResponseBodyAsString()); + return Mono.just(Map.of("error", "OpenAI API 요청 중 오류가 발생했습니다.")); + }) + .retryWhen(Retry.backoff(3, Duration.ofSeconds(3)) + .maxBackoff(Duration.ofSeconds(10)) + .filter(throwable -> !(throwable instanceof WebClientResponseException && + ((WebClientResponseException) throwable).getStatusCode().value() == 421))) + .map(response -> { + List> choices = (List>) response.get("choices"); + if (choices != null && !choices.isEmpty()) { + Map firstChoice = choices.get(0); + Map message = (Map) firstChoice.get("message"); + return (String) message.get("content"); + } + return "레시피를 생성하는 데 실패했습니다."; + })); + + + } + + private String generateRecipePrompt(CustomRecipeReqDTO request, String country) { + return String.format(""" + %s 국가에서 쉽게 구할 수 있는 재료를 활용하여 %s, %s 맛의 %d분 내 조리 가능한 %s 요리를 추천해줘. + 맵기 레벨 %d의 4가지 레시피를 제공해줘. + 각 레시피는 다음 형식으로: + 1. [레시피 이름] + - 조리 시간: [시간]분 + - 난이도: [쉬움/보통/어려움] + - 특징: [레시피의 특징] + - 재료 팁: [%s에서 재료를 구할 수 있는 팁] + - 조리 순서 (4단계): + """, + country, request.tastes(), request.difficulty(), request.cookingTime(), request.category(), request.spiceLevel(), country + ); + } +} diff --git a/src/main/java/com/example/blendish/domain/gpt/service/OpenAIService.java b/src/main/java/com/example/blendish/domain/gpt/service/OpenAIService.java new file mode 100644 index 0000000..d7c2791 --- /dev/null +++ b/src/main/java/com/example/blendish/domain/gpt/service/OpenAIService.java @@ -0,0 +1,40 @@ +package com.example.blendish.domain.gpt.service; + +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.Map; + +@Service +public class OpenAIService { + + private final WebClient webClient; + + public OpenAIService(WebClient webClient) { + this.webClient = webClient; + } + + public Mono getGptResponse(String userMessage) { + return webClient.post() + .uri("/chat/completions") + .header("Content-Type", "application/json") + .bodyValue(Map.of( + "model", "gpt-4", + "messages", new Object[]{ + Map.of("role", "system", "content", "You are a helpful assistant."), + Map.of("role", "user", "content", userMessage) + }, + "max_tokens", 100 + )) + .retrieve() + .bodyToMono(Map.class) + .map(response -> { + var choices = (java.util.List>) response.get("choices"); + if (choices != null && !choices.isEmpty()) { + return choices.get(0).get("message").toString(); + } + return "No response from GPT."; + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java b/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java index 7ee41dc..50026f8 100644 --- a/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java +++ b/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java @@ -8,5 +8,6 @@ public record AddRecipeDTO(String name, String level, List ingredients, String information, + boolean isAiGenerated, List steps) { } diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/AiIngredient.java b/src/main/java/com/example/blendish/domain/recipe/entity/AiIngredient.java index 6913a11..740fd62 100644 --- a/src/main/java/com/example/blendish/domain/recipe/entity/AiIngredient.java +++ b/src/main/java/com/example/blendish/domain/recipe/entity/AiIngredient.java @@ -22,6 +22,6 @@ public class AiIngredient { private String amount; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "ai_recipe_id", nullable = false) - private AiRecipe aiRecipe; + @JoinColumn(name = "recipe_id", nullable = false) + private Recipe recipe; } diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/AiRecipe.java b/src/main/java/com/example/blendish/domain/recipe/entity/AiRecipe.java deleted file mode 100644 index 974e1ef..0000000 --- a/src/main/java/com/example/blendish/domain/recipe/entity/AiRecipe.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.blendish.domain.recipe.entity; - -import com.example.blendish.domain.comments.entity.Comment; -import com.example.blendish.domain.foodflavor.entity.FoodFlavor; -import com.example.blendish.domain.user.entity.User; -import jakarta.persistence.*; -import lombok.*; - -import java.util.Date; -import java.util.List; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Table(name = "ai_recipe") -public class AiRecipe { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long aiRecipeId; - - @Column(nullable = false, length = 100) - private String name; - - @Column(nullable = false, length = 1) - private String level; - - @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List ingredients; - - private int likeCount; - - @Column(nullable = false, length = 100) - private String time; - - private int scrapCount; - - private int spicyLevel; - - @Column(length = 200) - private String information; - - @Temporal(TemporalType.DATE) - private Date postDate; - - @Temporal(TemporalType.DATE) - private Date updatedDate; - - @Column(length = 255) - private String foodImage; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; - - @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List steps; - - @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List comment; - - @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List likes; - - @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List scraps; - - @OneToMany(mappedBy = "aiRecipe", cascade = CascadeType.ALL, orphanRemoval = true) - private List foodFlavors; - - @PrePersist - protected void onCreate() { - this.postDate = new Date(); - this.updatedDate = this.postDate; - } - - @PreUpdate - protected void onUpdate() { - this.updatedDate = new Date(); - } - -} diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java index f60909c..b287f36 100644 --- a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java +++ b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java @@ -70,6 +70,12 @@ public class Recipe { @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) private List foodFlavors; + @Column(nullable = false) + private boolean isAiGenerated; + + @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) + private List aiIngredients; + @PrePersist protected void onCreate() { this.postDate = new Date(); diff --git a/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java b/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java index b800668..cdd0fd9 100644 --- a/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java +++ b/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java @@ -22,7 +22,7 @@ public class RecipeService { private final RecipeRepository recipeRepository; private final UserRepository userRepository; - public void createRecipe(AddRecipeDTO addRecipeDTO, String userId, String imageUrl) { + public void createUserRecipe(AddRecipeDTO addRecipeDTO, String userId, String imageUrl) { User user = userRepository.findByUserId(userId); if (user == null) { throw new IllegalArgumentException("유저를 찾을 수 없습니다."); @@ -44,6 +44,38 @@ public void createRecipe(AddRecipeDTO addRecipeDTO, String userId, String imageU .information(addRecipeDTO.information()) .foodImage(imageUrl) .user(user) + .isAiGenerated(false) + .steps(steps) + .build(); + + steps.forEach(step -> step.updateRecipe(recipe)); + + recipeRepository.save(recipe); + } + + public void createAiRecipe(AddRecipeDTO addRecipeDTO,String userId) { + User user = userRepository.findByUserId(userId); + if (user == null) { + throw new IllegalArgumentException("유저를 찾을 수 없습니다."); + } + + List steps = addRecipeDTO.steps().stream() + .map(stepDTO -> RecipeSteps.builder() + .stepNum(stepDTO.stepNumber()) + .details(stepDTO.details()) + .build()) + .collect(Collectors.toList()); + + List ingredients = addRecipeDTO.ingredients(); + + Recipe recipe = Recipe.builder() + .name(addRecipeDTO.name()) + .level(addRecipeDTO.level()) + .ingredients(ingredients) + .information(addRecipeDTO.information()) + .foodImage(null) + .user(null) + .isAiGenerated(true) .steps(steps) .build(); diff --git a/src/main/java/com/example/blendish/global/config/JacksonConfig.java b/src/main/java/com/example/blendish/global/config/JacksonConfig.java new file mode 100644 index 0000000..af6cf05 --- /dev/null +++ b/src/main/java/com/example/blendish/global/config/JacksonConfig.java @@ -0,0 +1,20 @@ +package com.example.blendish.global.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +@Configuration +public class JacksonConfig { + + @Primary + @Bean + public ObjectMapper objectMapper() { + return Jackson2ObjectMapperBuilder.json() + .modules(new JavaTimeModule()) + .build(); + } +} From 6fd894065729c50e137b52a128c3d2da979543fc Mon Sep 17 00:00:00 2001 From: Junyeong-An <2117ab@gmail.com> Date: Sat, 15 Feb 2025 21:25:30 +0900 Subject: [PATCH 20/26] =?UTF-8?q?[Chore]=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=86=8D=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/blendish/global/dto/ApiResponseTemplate.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java b/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java index 4a56895..86dcf0f 100644 --- a/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java +++ b/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java @@ -2,7 +2,6 @@ import com.example.blendish.global.response.ErrorCode; import com.example.blendish.global.response.SuccessCode; -import com.fasterxml.jackson.annotation.JsonFormat; import lombok.*; import java.time.LocalDateTime; @@ -16,7 +15,6 @@ public class ApiResponseTemplate { private final int status; private final String message; - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") private final LocalDateTime timestamp; // 추가된 날짜/시간 필드 private T data; From 3b64f6d2b9cae57cb1c9c72b53e51088b1df408d Mon Sep 17 00:00:00 2001 From: Junyeong-An <2117ab@gmail.com> Date: Sat, 15 Feb 2025 22:16:12 +0900 Subject: [PATCH 21/26] =?UTF-8?q?[Feat]=20Ai=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=A0=88=EC=8B=9C=ED=94=BC=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 - .../blendish/controller/GPTController.java | 26 ++++-- .../blendish/controller/GPTSwagger.java | 30 +++++++ .../domain/gpt/config/OpenAIConfig.java | 57 ------------- .../domain/gpt/config/OpenAiConfig.java | 22 +++++ .../domain/gpt/service/GPTRecipeService.java | 83 ++++++++++--------- .../domain/gpt/service/OpenAIService.java | 74 +++++++++++------ .../domain/recipe/service/RecipeService.java | 2 +- .../blendish/global/config/JacksonConfig.java | 20 ----- .../global/config/RestTemplateConfig.java | 14 ++++ .../global/dto/ApiResponseTemplate.java | 4 + 11 files changed, 179 insertions(+), 154 deletions(-) create mode 100644 src/main/java/com/example/blendish/controller/GPTSwagger.java delete mode 100644 src/main/java/com/example/blendish/domain/gpt/config/OpenAIConfig.java create mode 100644 src/main/java/com/example/blendish/domain/gpt/config/OpenAiConfig.java delete mode 100644 src/main/java/com/example/blendish/global/config/JacksonConfig.java create mode 100644 src/main/java/com/example/blendish/global/config/RestTemplateConfig.java diff --git a/build.gradle b/build.gradle index 6540514..2dcabb1 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,6 @@ dependencies { implementation group: 'ch.simas.qlrm', name: 'qlrm', version: '1.7.1' - implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' } diff --git a/src/main/java/com/example/blendish/controller/GPTController.java b/src/main/java/com/example/blendish/controller/GPTController.java index b2ff491..5c042eb 100644 --- a/src/main/java/com/example/blendish/controller/GPTController.java +++ b/src/main/java/com/example/blendish/controller/GPTController.java @@ -3,6 +3,8 @@ import com.example.blendish.domain.gpt.dto.CustomRecipeReqDTO; import com.example.blendish.domain.gpt.service.GPTRecipeService; import com.example.blendish.domain.gpt.service.OpenAIService; +import com.example.blendish.domain.recipe.dto.AddRecipeDTO; +import com.example.blendish.domain.recipe.service.RecipeService; import com.example.blendish.global.dto.ApiResponseTemplate; import com.example.blendish.global.response.SuccessCode; import lombok.RequiredArgsConstructor; @@ -11,27 +13,37 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; -import reactor.core.publisher.Mono; @RestController @RequiredArgsConstructor @RequestMapping("/api/gpt") -public class GPTController { +public class GPTController implements GPTSwagger{ private final OpenAIService openAIService; private final GPTRecipeService gptRecipeService; + private final RecipeService recipeService; @GetMapping("/chat") - public Mono chatWithGpt(@RequestParam String message) { - return openAIService.getGptResponse(message); + public ResponseEntity chatWithGpt(@RequestParam String message) { + return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.OK, openAIService.getGptResponse(message))); + } @PostMapping("/recipe") - public Mono>> generateCustomRecipe( + public ResponseEntity> generateCustomRecipe( @RequestBody CustomRecipeReqDTO request, @AuthenticationPrincipal UserDetails userDetails) { - return gptRecipeService.getAiGeneratedRecipe(request, userDetails) - .map(result -> ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.CREATED, result))); + + String result = gptRecipeService.getAiGeneratedRecipe(request, userDetails); + + return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.CREATED, result)); + } + + @PostMapping("/recipe/save") + public ResponseEntity> saveRecipe(AddRecipeDTO addRecipeDTO, UserDetails userDetails) { + recipeService.createAiRecipe(addRecipeDTO, userDetails.getUsername()); + + return ResponseEntity.ok(ApiResponseTemplate.success(SuccessCode.CREATED, "레시피가 등록되었습니다.")); } } diff --git a/src/main/java/com/example/blendish/controller/GPTSwagger.java b/src/main/java/com/example/blendish/controller/GPTSwagger.java new file mode 100644 index 0000000..6baf51a --- /dev/null +++ b/src/main/java/com/example/blendish/controller/GPTSwagger.java @@ -0,0 +1,30 @@ +package com.example.blendish.controller; + +import com.example.blendish.domain.gpt.dto.CustomRecipeReqDTO; +import com.example.blendish.domain.recipe.dto.AddRecipeDTO; +import com.example.blendish.global.dto.ApiResponseTemplate; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +@Tag(name = "GPT API", description = "GPT 관련 API") +public interface GPTSwagger { + + @Operation(summary = "GPT 채팅", description = "GPT에게 메시지를 보내고 응답을 받습니다.") + ResponseEntity chatWithGpt(@RequestParam String message); + + @Operation(summary = "사용자 맞춤 GPT 레시피 생성", description = "사용자 입력을 기반으로 GPT가 맞춤형 레시피를 생성합니다.") + ResponseEntity> generateCustomRecipe( + @RequestBody CustomRecipeReqDTO request, + UserDetails userDetails + ); + + @Operation(summary = "AI 레시피 저장", description = "AI가 생성한 레시피를 데이터베이스에 저장합니다.") + ResponseEntity> saveRecipe( + @RequestBody AddRecipeDTO addRecipeDTO, + UserDetails userDetails + ); +} diff --git a/src/main/java/com/example/blendish/domain/gpt/config/OpenAIConfig.java b/src/main/java/com/example/blendish/domain/gpt/config/OpenAIConfig.java deleted file mode 100644 index 05b9bc4..0000000 --- a/src/main/java/com/example/blendish/domain/gpt/config/OpenAIConfig.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.blendish.domain.gpt.config; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.netty.channel.ChannelOption; -import io.netty.handler.timeout.ReadTimeoutHandler; -import io.netty.handler.timeout.WriteTimeoutHandler; -import java.time.Duration; -import java.util.concurrent.TimeUnit; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.ClientRequest; -import org.springframework.web.reactive.function.client.ExchangeFilterFunction; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.netty.http.client.HttpClient; - -@Configuration -public class OpenAIConfig { - - @Value("${openai.api.key}") - private String openAiKey; - - @Bean - public WebClient webClient() { - HttpClient httpClient = HttpClient.create() - .responseTimeout(Duration.ofSeconds(15)) - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) - .doOnConnected(conn -> - conn.addHandlerLast(new ReadTimeoutHandler(10, TimeUnit.SECONDS)) - .addHandlerLast(new WriteTimeoutHandler(10, TimeUnit.SECONDS)) - ); - - return WebClient.builder() - .baseUrl("https://api.openai.com/v1") // baseUrl을 명확히 지정 - .defaultHeader("Content-Type", "application/json") - .filter(addAuthorizationHeader()) - .clientConnector(new ReactorClientHttpConnector(httpClient)) - .build(); - } - - - private ExchangeFilterFunction addAuthorizationHeader() { - return (request, next) -> { - return next.exchange( - ClientRequest.from(request) - .header("Authorization", "Bearer " + openAiKey) - .build() - ); - }; - } - - @Bean(name = "openAiObjectMapper") - public ObjectMapper objectMapper() { - return new ObjectMapper(); - } -} diff --git a/src/main/java/com/example/blendish/domain/gpt/config/OpenAiConfig.java b/src/main/java/com/example/blendish/domain/gpt/config/OpenAiConfig.java new file mode 100644 index 0000000..df987cb --- /dev/null +++ b/src/main/java/com/example/blendish/domain/gpt/config/OpenAiConfig.java @@ -0,0 +1,22 @@ +package com.example.blendish.domain.gpt.config; + + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class OpenAiConfig { + @Value("${openai.api.key}") + private String openAiKey; + @Bean + public RestTemplate template(){ + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add((request, body, execution) -> { + request.getHeaders().add("Authorization", "Bearer " + openAiKey); + return execution.execute(request, body); + }); + return restTemplate; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java b/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java index 0a6f34c..1ef9564 100644 --- a/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java +++ b/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java @@ -4,34 +4,34 @@ import com.example.blendish.domain.user.entity.User; import com.example.blendish.domain.user.repository.UserRepository; import com.fasterxml.jackson.databind.ObjectMapper; -import java.time.Duration; -import java.util.List; -import java.util.Map; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.client.WebClientResponseException; -import reactor.core.publisher.Mono; -import reactor.util.retry.Retry; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.Map; @Service @RequiredArgsConstructor public class GPTRecipeService { private static final Logger logger = LoggerFactory.getLogger(GPTRecipeService.class); - private final WebClient webClient; + + private final RestTemplate restTemplate; private final UserRepository userRepository; private final ObjectMapper objectMapper; @Value("${openai.api.key}") private String openAiApiKey; - public Mono getAiGeneratedRecipe(CustomRecipeReqDTO request, @AuthenticationPrincipal UserDetails userDetails) { + public String getAiGeneratedRecipe(CustomRecipeReqDTO request, @AuthenticationPrincipal UserDetails userDetails) { String userId = userDetails.getUsername(); User user = userRepository.findByUserId(userId); @@ -41,9 +41,8 @@ public Mono getAiGeneratedRecipe(CustomRecipeReqDTO request, @Authentica String prompt = generateRecipePrompt(request, user.getCountry()); - Map requestBody = Map.of( - "model", "gpt-4", + "model", "gpt-4o", "messages", List.of( Map.of("role", "system", "content", "너는 요리 레시피 추천 AI야. 요청에 따라 요리를 추천해줘."), Map.of("role", "user", "content", prompt.replaceAll("\\[|\\]", "")) @@ -51,43 +50,44 @@ public Mono getAiGeneratedRecipe(CustomRecipeReqDTO request, @Authentica "temperature", 0.7 ); - try { String requestJson = objectMapper.writeValueAsString(requestBody); logger.info("Sending request to OpenAI: {}", requestJson); } catch (Exception e) { logger.error("Error converting request to JSON", e); } - return Mono.delay(Duration.ofSeconds(3)) // 3초 딜레이 추가 - .then(webClient.post() - .uri("/chat/completions") - .header("Authorization", "Bearer " + openAiApiKey) - .header("Content-Type", "application/json") - .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)") - .bodyValue(requestBody) - .retrieve() - .bodyToMono(Map.class) - .doOnNext(response -> logger.info("Response from OpenAI: {}", response)) - .onErrorResume(WebClientResponseException.class, e -> { - logger.error("OpenAI API Error: StatusCode = {}, ResponseBody = {}", - e.getStatusCode(), e.getResponseBodyAsString()); - return Mono.just(Map.of("error", "OpenAI API 요청 중 오류가 발생했습니다.")); - }) - .retryWhen(Retry.backoff(3, Duration.ofSeconds(3)) - .maxBackoff(Duration.ofSeconds(10)) - .filter(throwable -> !(throwable instanceof WebClientResponseException && - ((WebClientResponseException) throwable).getStatusCode().value() == 421))) - .map(response -> { - List> choices = (List>) response.get("choices"); - if (choices != null && !choices.isEmpty()) { - Map firstChoice = choices.get(0); - Map message = (Map) firstChoice.get("message"); - return (String) message.get("content"); - } - return "레시피를 생성하는 데 실패했습니다."; - })); + String url = "https://api.openai.com/v1/chat/completions"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(openAiApiKey); + + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + try { + ResponseEntity responseEntity = restTemplate.exchange( + url, HttpMethod.POST, requestEntity, Map.class + ); + + Map responseBody = responseEntity.getBody(); + if (responseBody != null && responseBody.containsKey("choices")) { + List> choices = (List>) responseBody.get("choices"); + if (!choices.isEmpty()) { + Map firstChoice = choices.get(0); + Map message = (Map) firstChoice.get("message"); + return message.getOrDefault("content", "레시피를 생성하는 데 실패했습니다."); + } + } + return "레시피를 생성하는 데 실패했습니다."; + + } catch (HttpClientErrorException e) { + logger.error("OpenAI API Error: StatusCode = {}, ResponseBody = {}", e.getStatusCode(), e.getResponseBodyAsString()); + return "OpenAI API 요청 중 오류가 발생했습니다."; + } catch (Exception e) { + logger.error("Unexpected error calling OpenAI API", e); + return "레시피를 생성하는 중 오류가 발생했습니다."; + } } private String generateRecipePrompt(CustomRecipeReqDTO request, String country) { @@ -99,8 +99,9 @@ private String generateRecipePrompt(CustomRecipeReqDTO request, String country) - 조리 시간: [시간]분 - 난이도: [쉬움/보통/어려움] - 특징: [레시피의 특징] + - 재료: [재료 목록] - 재료 팁: [%s에서 재료를 구할 수 있는 팁] - - 조리 순서 (4단계): + - 조리 순서 (6단계): """, country, request.tastes(), request.difficulty(), request.cookingTime(), request.category(), request.spiceLevel(), country ); diff --git a/src/main/java/com/example/blendish/domain/gpt/service/OpenAIService.java b/src/main/java/com/example/blendish/domain/gpt/service/OpenAIService.java index d7c2791..a1295a4 100644 --- a/src/main/java/com/example/blendish/domain/gpt/service/OpenAIService.java +++ b/src/main/java/com/example/blendish/domain/gpt/service/OpenAIService.java @@ -1,40 +1,60 @@ package com.example.blendish.domain.gpt.service; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.*; import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; +import org.springframework.web.client.RestTemplate; +import java.util.List; import java.util.Map; @Service public class OpenAIService { - private final WebClient webClient; + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; // JSON 파싱을 위한 ObjectMapper - public OpenAIService(WebClient webClient) { - this.webClient = webClient; + public OpenAIService(RestTemplate restTemplate, ObjectMapper objectMapper) { + this.restTemplate = restTemplate; + this.objectMapper = objectMapper; } - public Mono getGptResponse(String userMessage) { - return webClient.post() - .uri("/chat/completions") - .header("Content-Type", "application/json") - .bodyValue(Map.of( - "model", "gpt-4", - "messages", new Object[]{ - Map.of("role", "system", "content", "You are a helpful assistant."), - Map.of("role", "user", "content", userMessage) - }, - "max_tokens", 100 - )) - .retrieve() - .bodyToMono(Map.class) - .map(response -> { - var choices = (java.util.List>) response.get("choices"); - if (choices != null && !choices.isEmpty()) { - return choices.get(0).get("message").toString(); - } - return "No response from GPT."; - }); + public String getGptResponse(String userMessage) { + String url = "https://api.openai.com/v1/chat/completions"; // OpenAI API 엔드포인트 + + // 요청 본문 (JSON) + Map requestBody = Map.of( + "model", "gpt-4", + "messages", List.of( + Map.of("role", "system", "content", "You are a helpful assistant."), + Map.of("role", "user", "content", userMessage) + ), + "max_tokens", 100 + ); + + // HTTP 헤더 설정 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth("YOUR_OPENAI_API_KEY"); // 🔴 OpenAI API 키 설정 + + // HTTP 요청 객체 생성 + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + // API 호출 및 응답 처리 + ResponseEntity responseEntity = restTemplate.exchange( + url, HttpMethod.POST, requestEntity, Map.class + ); + + // 응답에서 메시지 추출 + Map responseBody = responseEntity.getBody(); + if (responseBody != null && responseBody.containsKey("choices")) { + List> choices = (List>) responseBody.get("choices"); + if (!choices.isEmpty()) { + Map firstChoice = choices.get(0); + Map message = (Map) firstChoice.get("message"); + return message.getOrDefault("content", "No response from GPT."); + } + } + return "No response from GPT."; } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java b/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java index cdd0fd9..9003418 100644 --- a/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java +++ b/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java @@ -74,7 +74,7 @@ public void createAiRecipe(AddRecipeDTO addRecipeDTO,String userId) { .ingredients(ingredients) .information(addRecipeDTO.information()) .foodImage(null) - .user(null) + .user(user) .isAiGenerated(true) .steps(steps) .build(); diff --git a/src/main/java/com/example/blendish/global/config/JacksonConfig.java b/src/main/java/com/example/blendish/global/config/JacksonConfig.java deleted file mode 100644 index af6cf05..0000000 --- a/src/main/java/com/example/blendish/global/config/JacksonConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.blendish.global.config; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; - -@Configuration -public class JacksonConfig { - - @Primary - @Bean - public ObjectMapper objectMapper() { - return Jackson2ObjectMapperBuilder.json() - .modules(new JavaTimeModule()) - .build(); - } -} diff --git a/src/main/java/com/example/blendish/global/config/RestTemplateConfig.java b/src/main/java/com/example/blendish/global/config/RestTemplateConfig.java new file mode 100644 index 0000000..98200aa --- /dev/null +++ b/src/main/java/com/example/blendish/global/config/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package com.example.blendish.global.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/example/blendish/global/dto/ApiResponseTemplate.java b/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java index 86dcf0f..129577a 100644 --- a/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java +++ b/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java @@ -2,6 +2,8 @@ import com.example.blendish.global.response.ErrorCode; import com.example.blendish.global.response.SuccessCode; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonRawValue; import lombok.*; import java.time.LocalDateTime; @@ -15,8 +17,10 @@ public class ApiResponseTemplate { private final int status; private final String message; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") private final LocalDateTime timestamp; // 추가된 날짜/시간 필드 + @JsonRawValue private T data; public static ApiResponseTemplate success(SuccessCode successCode, T data) { From 2aedad2fdc7b8bfddc51b59bfd507268b72bf1ce Mon Sep 17 00:00:00 2001 From: Junyeong-An <2117ab@gmail.com> Date: Sun, 16 Feb 2025 00:11:56 +0900 Subject: [PATCH 22/26] =?UTF-8?q?[Feat]=20DTO=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../blendish/domain/gpt/dto/MessageDTO.java | 4 ++++ .../blendish/domain/gpt/entity/Message.java | 17 +++++++++++++++++ .../domain/gpt/service/GPTRecipeService.java | 4 ++-- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/blendish/domain/gpt/dto/MessageDTO.java create mode 100644 src/main/java/com/example/blendish/domain/gpt/entity/Message.java diff --git a/src/main/java/com/example/blendish/domain/gpt/dto/MessageDTO.java b/src/main/java/com/example/blendish/domain/gpt/dto/MessageDTO.java new file mode 100644 index 0000000..5cb85ef --- /dev/null +++ b/src/main/java/com/example/blendish/domain/gpt/dto/MessageDTO.java @@ -0,0 +1,4 @@ +package com.example.blendish.domain.gpt.dto; + +public record MessageDTO(String role, String content) { +} diff --git a/src/main/java/com/example/blendish/domain/gpt/entity/Message.java b/src/main/java/com/example/blendish/domain/gpt/entity/Message.java new file mode 100644 index 0000000..390dcaf --- /dev/null +++ b/src/main/java/com/example/blendish/domain/gpt/entity/Message.java @@ -0,0 +1,17 @@ +package com.example.blendish.domain.gpt.entity; + +import jakarta.persistence.Column; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class Message { + private String role; + + @Column + private String content; + +} diff --git a/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java b/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java index 1ef9564..126d6ca 100644 --- a/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java +++ b/src/main/java/com/example/blendish/domain/gpt/service/GPTRecipeService.java @@ -99,9 +99,9 @@ private String generateRecipePrompt(CustomRecipeReqDTO request, String country) - 조리 시간: [시간]분 - 난이도: [쉬움/보통/어려움] - 특징: [레시피의 특징] - - 재료: [재료 목록] + - 재료: [재료 목록 및 계량] - 재료 팁: [%s에서 재료를 구할 수 있는 팁] - - 조리 순서 (6단계): + - 조리 순서 (단계별로 설명): """, country, request.tastes(), request.difficulty(), request.cookingTime(), request.category(), request.spiceLevel(), country ); From c1ed0075ed632a84787ad99b28d79818ad2ec555 Mon Sep 17 00:00:00 2001 From: dlqja Date: Sun, 16 Feb 2025 03:06:48 +0900 Subject: [PATCH 23/26] [Chore] Remove cors #33 --- .../domain/user/config/SecurityConfig.java | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/src/main/java/com/example/blendish/domain/user/config/SecurityConfig.java b/src/main/java/com/example/blendish/domain/user/config/SecurityConfig.java index b3b0a4b..e172fc9 100644 --- a/src/main/java/com/example/blendish/domain/user/config/SecurityConfig.java +++ b/src/main/java/com/example/blendish/domain/user/config/SecurityConfig.java @@ -54,27 +54,6 @@ public BCryptPasswordEncoder bCryptPasswordEncoder() { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - - http - .cors((corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() { - - @Override - public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - - CorsConfiguration configuration = new CorsConfiguration(); - - configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000")); - configuration.setAllowedMethods(Collections.singletonList("*")); - configuration.setAllowCredentials(true); - configuration.setAllowedHeaders(Collections.singletonList("*")); - configuration.setMaxAge(3600L); - - configuration.setExposedHeaders(Collections.singletonList("Authorization")); - - return configuration; - } - }))); - http .csrf((auth) -> auth.disable()); From 47a650648e2d0a75175e05425331fd7e99b65ca6 Mon Sep 17 00:00:00 2001 From: Junyeong-An <2117ab@gmail.com> Date: Sun, 16 Feb 2025 03:14:40 +0900 Subject: [PATCH 24/26] =?UTF-8?q?[refactor]=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/blendish/domain/gpt/dto/GPTRequestDTO.java | 6 ------ .../com/example/blendish/domain/gpt/dto/MessageDTO.java | 4 ---- .../example/blendish/global/dto/ApiResponseTemplate.java | 4 +--- src/main/resources/application.yml | 2 +- 4 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 src/main/java/com/example/blendish/domain/gpt/dto/GPTRequestDTO.java delete mode 100644 src/main/java/com/example/blendish/domain/gpt/dto/MessageDTO.java diff --git a/src/main/java/com/example/blendish/domain/gpt/dto/GPTRequestDTO.java b/src/main/java/com/example/blendish/domain/gpt/dto/GPTRequestDTO.java deleted file mode 100644 index d39a0a9..0000000 --- a/src/main/java/com/example/blendish/domain/gpt/dto/GPTRequestDTO.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.blendish.domain.gpt.dto; - -import java.util.List; - -public record GPTRequestDTO(String model, List messages) { -} diff --git a/src/main/java/com/example/blendish/domain/gpt/dto/MessageDTO.java b/src/main/java/com/example/blendish/domain/gpt/dto/MessageDTO.java deleted file mode 100644 index 5cb85ef..0000000 --- a/src/main/java/com/example/blendish/domain/gpt/dto/MessageDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.blendish.domain.gpt.dto; - -public record MessageDTO(String role, String content) { -} diff --git a/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java b/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java index 129577a..098b9ac 100644 --- a/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java +++ b/src/main/java/com/example/blendish/global/dto/ApiResponseTemplate.java @@ -3,7 +3,6 @@ import com.example.blendish.global.response.ErrorCode; import com.example.blendish.global.response.SuccessCode; import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonRawValue; import lombok.*; import java.time.LocalDateTime; @@ -18,9 +17,8 @@ public class ApiResponseTemplate { private final String message; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") - private final LocalDateTime timestamp; // 추가된 날짜/시간 필드 + private final LocalDateTime timestamp; - @JsonRawValue private T data; public static ApiResponseTemplate success(SuccessCode successCode, T data) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 12d70cf..cf6699a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,7 +12,7 @@ spring: database: mysql database-platform: org.hibernate.dialect.MySQLDialect hibernate: - ddl-auto: create-drop + ddl-auto: create show-sql: true properties: hibernate: From a8fd89c8e6e1bfd89840ed7fefed1f79839d96a4 Mon Sep 17 00:00:00 2001 From: Junyeong-An <2117ab@gmail.com> Date: Sun, 16 Feb 2025 03:17:30 +0900 Subject: [PATCH 25/26] =?UTF-8?q?[refactor]=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #35 --- .../blendish/controller/CommentController.java | 1 - .../blendish/controller/RecipeController.java | 1 - .../blendish/controller/RecipeSwagger.java | 1 - .../blendish/domain/gpt/entity/Message.java | 17 ----------------- 4 files changed, 20 deletions(-) delete mode 100644 src/main/java/com/example/blendish/domain/gpt/entity/Message.java diff --git a/src/main/java/com/example/blendish/controller/CommentController.java b/src/main/java/com/example/blendish/controller/CommentController.java index abd8e04..08982b4 100644 --- a/src/main/java/com/example/blendish/controller/CommentController.java +++ b/src/main/java/com/example/blendish/controller/CommentController.java @@ -3,7 +3,6 @@ import com.example.blendish.domain.comments.dto.CommentAllDTO; import com.example.blendish.domain.comments.dto.CommentDTO; import com.example.blendish.domain.comments.service.CommentService; -import com.example.blendish.domain.recipe.dto.CommunityHotRecipeDTO; import com.example.blendish.global.dto.ApiResponseTemplate; import com.example.blendish.global.response.SuccessCode; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/src/main/java/com/example/blendish/controller/RecipeController.java b/src/main/java/com/example/blendish/controller/RecipeController.java index 63c640a..aec4ab5 100644 --- a/src/main/java/com/example/blendish/controller/RecipeController.java +++ b/src/main/java/com/example/blendish/controller/RecipeController.java @@ -1,7 +1,6 @@ package com.example.blendish.controller; import com.example.blendish.domain.recipe.dto.AddRecipeDTO; -import com.example.blendish.domain.recipe.entity.Recipe; import com.example.blendish.domain.recipe.service.RecipeService; import com.example.blendish.global.dto.ApiResponseTemplate; import com.example.blendish.global.response.SuccessCode; diff --git a/src/main/java/com/example/blendish/controller/RecipeSwagger.java b/src/main/java/com/example/blendish/controller/RecipeSwagger.java index 87db2e9..562eb33 100644 --- a/src/main/java/com/example/blendish/controller/RecipeSwagger.java +++ b/src/main/java/com/example/blendish/controller/RecipeSwagger.java @@ -2,7 +2,6 @@ import com.example.blendish.domain.recipe.dto.AddRecipeDTO; import com.example.blendish.global.dto.ApiResponseTemplate; -import com.example.blendish.global.response.SuccessCode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/com/example/blendish/domain/gpt/entity/Message.java b/src/main/java/com/example/blendish/domain/gpt/entity/Message.java deleted file mode 100644 index 390dcaf..0000000 --- a/src/main/java/com/example/blendish/domain/gpt/entity/Message.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.blendish.domain.gpt.entity; - -import jakarta.persistence.Column; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -public class Message { - private String role; - - @Column - private String content; - -} From 62b79c46463a06b751a58f3af0babf0467b6233e Mon Sep 17 00:00:00 2001 From: Junyeong-An <2117ab@gmail.com> Date: Sun, 16 Feb 2025 12:55:45 +0900 Subject: [PATCH 26/26] =?UTF-8?q?[Feat]=20Ai=20RecipeStep=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/recipe/dto/AddRecipeDTO.java | 1 - .../domain/recipe/entity/AiRecipeSteps.java | 36 +++++++++++++++++++ .../blendish/domain/recipe/entity/Recipe.java | 3 ++ .../domain/recipe/service/RecipeService.java | 7 ++-- .../blendish/global/config/SwaggerConfig.java | 9 ++++- src/main/resources/application.yml | 2 +- 6 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/example/blendish/domain/recipe/entity/AiRecipeSteps.java diff --git a/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java b/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java index 50026f8..8a53b7e 100644 --- a/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java +++ b/src/main/java/com/example/blendish/domain/recipe/dto/AddRecipeDTO.java @@ -1,7 +1,6 @@ package com.example.blendish.domain.recipe.dto; import com.example.blendish.domain.recipe.entity.Ingredient; -import com.example.blendish.domain.recipe.entity.RecipeSteps; import java.util.List; public record AddRecipeDTO(String name, diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/AiRecipeSteps.java b/src/main/java/com/example/blendish/domain/recipe/entity/AiRecipeSteps.java new file mode 100644 index 0000000..70084dc --- /dev/null +++ b/src/main/java/com/example/blendish/domain/recipe/entity/AiRecipeSteps.java @@ -0,0 +1,36 @@ +package com.example.blendish.domain.recipe.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiRecipeSteps { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long rsId; + + @Column(nullable = false) + private int stepNum; + + @Column(length = 200) + private String details; + + @Column(length = 255) + private String stepImage; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipeId", nullable = false) + private Recipe recipe; + + public void updateRecipe(Recipe recipe) { + this.recipe = recipe; + } +} diff --git a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java index b287f36..97f1673 100644 --- a/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java +++ b/src/main/java/com/example/blendish/domain/recipe/entity/Recipe.java @@ -58,6 +58,9 @@ public class Recipe { @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) private List steps; + @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) + private List aiSteps; + @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true) private List comment; diff --git a/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java b/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java index 9003418..40cff34 100644 --- a/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java +++ b/src/main/java/com/example/blendish/domain/recipe/service/RecipeService.java @@ -1,6 +1,7 @@ package com.example.blendish.domain.recipe.service; import com.example.blendish.domain.recipe.dto.AddRecipeDTO; +import com.example.blendish.domain.recipe.entity.AiRecipeSteps; import com.example.blendish.domain.recipe.entity.Ingredient; import com.example.blendish.domain.recipe.entity.Recipe; import com.example.blendish.domain.recipe.entity.RecipeSteps; @@ -59,8 +60,8 @@ public void createAiRecipe(AddRecipeDTO addRecipeDTO,String userId) { throw new IllegalArgumentException("유저를 찾을 수 없습니다."); } - List steps = addRecipeDTO.steps().stream() - .map(stepDTO -> RecipeSteps.builder() + List steps = addRecipeDTO.steps().stream() + .map(stepDTO -> AiRecipeSteps.builder() .stepNum(stepDTO.stepNumber()) .details(stepDTO.details()) .build()) @@ -76,7 +77,7 @@ public void createAiRecipe(AddRecipeDTO addRecipeDTO,String userId) { .foodImage(null) .user(user) .isAiGenerated(true) - .steps(steps) + .aiSteps(steps) .build(); steps.forEach(step -> step.updateRecipe(recipe)); diff --git a/src/main/java/com/example/blendish/global/config/SwaggerConfig.java b/src/main/java/com/example/blendish/global/config/SwaggerConfig.java index c289a9f..6966847 100644 --- a/src/main/java/com/example/blendish/global/config/SwaggerConfig.java +++ b/src/main/java/com/example/blendish/global/config/SwaggerConfig.java @@ -5,6 +5,8 @@ import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -14,6 +16,10 @@ public class SwaggerConfig { public OpenAPI openAPI() { final String securitySchemeName = "bearerAuth"; + Server server = new Server(); + server.setUrl("https://junyeongan.store"); + server.setDescription("Blendish Server"); + return new OpenAPI() .components(new Components() .addSecuritySchemes(securitySchemeName, @@ -25,7 +31,8 @@ public OpenAPI openAPI() { ) ) .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) - .info(apiInfo()); + .info(apiInfo()) + .servers(List.of(server)); } private Info apiInfo() { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index cf6699a..f61151c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,7 +12,7 @@ spring: database: mysql database-platform: org.hibernate.dialect.MySQLDialect hibernate: - ddl-auto: create + ddl-auto: update show-sql: true properties: hibernate: