From facfc17e033668891f9e02e161f8ca990058ffa3 Mon Sep 17 00:00:00 2001 From: mdy3722 Date: Sat, 11 Oct 2025 15:48:12 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[refactor(BE)]=20:=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EC=9E=84=EB=B2=A0=EB=94=A9=20=EC=9A=94=EC=B2=AD=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bubblog/domain/post/entity/BlogPost.java | 2 +- .../post/service/BlogPostServiceImpl.java | 63 +++++++++++-------- .../global/exception/CustomException.java | 5 ++ .../bubblog/global/exception/ErrorCode.java | 3 +- .../global/service/EmbeddingProducer.java | 40 ++++++++++++ src/main/resources/application.yml | 7 ++- 6 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 src/main/java/Bubble/bubblog/global/service/EmbeddingProducer.java diff --git a/src/main/java/Bubble/bubblog/domain/post/entity/BlogPost.java b/src/main/java/Bubble/bubblog/domain/post/entity/BlogPost.java index bb5bc08..5690bcc 100644 --- a/src/main/java/Bubble/bubblog/domain/post/entity/BlogPost.java +++ b/src/main/java/Bubble/bubblog/domain/post/entity/BlogPost.java @@ -64,7 +64,7 @@ public class BlogPost { @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) private List likes = new ArrayList<>(); - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) private List postTags = new ArrayList<>(); @OneToMany(mappedBy = "post", fetch = FetchType.LAZY) // Soft delete 이므로 CASCADE 설정 X diff --git a/src/main/java/Bubble/bubblog/domain/post/service/BlogPostServiceImpl.java b/src/main/java/Bubble/bubblog/domain/post/service/BlogPostServiceImpl.java index 1cf7dbf..0d1f293 100644 --- a/src/main/java/Bubble/bubblog/domain/post/service/BlogPostServiceImpl.java +++ b/src/main/java/Bubble/bubblog/domain/post/service/BlogPostServiceImpl.java @@ -21,18 +21,18 @@ import Bubble.bubblog.global.exception.CustomException; import Bubble.bubblog.global.exception.ErrorCode; import Bubble.bubblog.global.service.AiService; +import Bubble.bubblog.global.service.EmbeddingProducer; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; +@Slf4j @Service @RequiredArgsConstructor public class BlogPostServiceImpl implements BlogPostService { @@ -46,6 +46,7 @@ public class BlogPostServiceImpl implements BlogPostService { private final PostTagRepository postTagRepository; private final AiService aiService; private final CommentRepository commentRepository; + private final EmbeddingProducer embeddingProducer; @Transactional @Override @@ -73,16 +74,17 @@ public BlogPostDetailDTO createPost(BlogPostRequestDTO request, UUID userId) { List tags = request.getTags(); // tag 리스트를 프론트에서 받음 - for (String tagName : tags) { - Tag tag = tagRepository.findByName(tagName) - .orElseGet(() -> tagRepository.save(new Tag(tagName))); + if (tags != null) { + for (String tagName : tags) { + Tag tag = tagRepository.findByName(tagName) + .orElseGet(() -> tagRepository.save(new Tag(tagName))); - postTagRepository.save(new PostTag(post, tag)); + postTagRepository.save(new PostTag(post, tag)); + } } - // AI 서버에 임베딩 요청 - aiService.handlePostTitle(post.getId(), post.getTitle()); - aiService.handlePostContent(post.getId(), post.getContent()); + // Redis 큐로 임베딩 요청 전송 + embeddingProducer.sendEmbeddingRequest(post.getId(), true, true); return new BlogPostDetailDTO(post, categoryList, tags); } @@ -206,14 +208,6 @@ public BlogPostDetailDTO updatePost(Long postId, BlogPostRequestDTO request, UUI boolean titleChanged = !Objects.equals(oldTitle, request.getTitle()); boolean contentChanged = !Objects.equals(oldContent, request.getContent()); - // 분기 처리 - if (titleChanged) { - aiService.handlePostTitle(post.getId(), request.getTitle()); - } - if (contentChanged) { - aiService.handlePostContent(post.getId(), request.getContent()); - } - post.update( request.getTitle(), request.getContent(), @@ -223,16 +217,33 @@ public BlogPostDetailDTO updatePost(Long postId, BlogPostRequestDTO request, UUI category ); - // 기존 태그 관계 삭제 - postTagRepository.deleteByPost(post); - + // 기존 태그 관계를 모두 끊음 + post.getPostTags().clear(); + + // flush()로 예약 되어 있던 clear()명령어를 바로 처리 + // 처리하지 않으면 clear()를 예약한 상태에서 Insert가 먼저 진행되고, + // 만약 tag 내용의 변화가 없을 때 수정된 게시글 Insert 시 (postId, tagId)키값의 중복 제약이 걸림 + blogPostRepository.flush(); + + // 요청받은 태그 리스트를 가져옴 List tags = request.getTags(); // 새 태그 저장 - for (String tagName : tags) { - Tag tag = tagRepository.findByName(tagName) - .orElseGet(() -> tagRepository.save(new Tag(tagName))); - post.addTag(tag); + if (tags != null) { + for (String tagName : tags) { + Tag tag = tagRepository.findByName(tagName) + .orElseGet(() -> tagRepository.save(new Tag(tagName))); + post.addTag(tag); + } + } + + // 레디스 큐로 LPUSH + if(titleChanged || contentChanged){ + embeddingProducer.sendEmbeddingRequest( + post.getId(), + titleChanged, // title 수정 여부 + contentChanged // content 수정 여부 + ); } return new BlogPostDetailDTO(post, categoryList, tags); diff --git a/src/main/java/Bubble/bubblog/global/exception/CustomException.java b/src/main/java/Bubble/bubblog/global/exception/CustomException.java index 9d3e585..1e47274 100644 --- a/src/main/java/Bubble/bubblog/global/exception/CustomException.java +++ b/src/main/java/Bubble/bubblog/global/exception/CustomException.java @@ -8,6 +8,11 @@ public CustomException(ErrorCode errorCode) { this.errorCode = errorCode; } + public CustomException(ErrorCode errorCode, Throwable cause) { + super(errorCode.getMessage(), cause); // 원인 예외(cause)를 함께 넘겨줍니다. + this.errorCode = errorCode; + } + public int getCode() { return errorCode.getCode(); } diff --git a/src/main/java/Bubble/bubblog/global/exception/ErrorCode.java b/src/main/java/Bubble/bubblog/global/exception/ErrorCode.java index b9bc04d..3daaf6e 100644 --- a/src/main/java/Bubble/bubblog/global/exception/ErrorCode.java +++ b/src/main/java/Bubble/bubblog/global/exception/ErrorCode.java @@ -49,8 +49,9 @@ public enum ErrorCode { NO_PERMISSION_TO_EDIT_COMMENT(403, "해당 댓글을 수정할 권한이 없습니다."), NO_PERMISSION_TO_DELETE_COMMENT(403, "해당 댓글을 삭제할 권한이 없습니다."), NOT_ROOT_COMMENT(400, "자식 댓글의 스레드를 조회할 수 없습니다. 루트 댓글의 스레드를 조회해주세요."), - CANNOT_LIKE_DELETED_COMMENT(400, "삭제된 댓글에 좋아요를 누를 수 없습니다."); + CANNOT_LIKE_DELETED_COMMENT(400, "삭제된 댓글에 좋아요를 누를 수 없습니다."), + SERIALIZATION_ERROR(500, "데이터 직렬화에 실패했습니다."); // S3 private final int code; diff --git a/src/main/java/Bubble/bubblog/global/service/EmbeddingProducer.java b/src/main/java/Bubble/bubblog/global/service/EmbeddingProducer.java new file mode 100644 index 0000000..c608b1d --- /dev/null +++ b/src/main/java/Bubble/bubblog/global/service/EmbeddingProducer.java @@ -0,0 +1,40 @@ +package Bubble.bubblog.global.service; + +import Bubble.bubblog.global.exception.CustomException; +import Bubble.bubblog.global.exception.ErrorCode; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EmbeddingProducer { + private final StringRedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + private static final String QUEUE_KEY = "embedding:queue"; + + public void sendEmbeddingRequest(Long postId, Boolean title, Boolean content) { + try { + Map message = Map.of( + "postId", postId, + "title", title, + "content", content + ); + + String json = objectMapper.writeValueAsString(message); + redisTemplate.opsForList().leftPush(QUEUE_KEY, json); + + log.info("✅ [Redis] Sent embedding request for postId: {}", postId); + } catch (JsonProcessingException e) { + log.error("❌ Failed to serialize embedding request for post {}", postId, e); + throw new CustomException(ErrorCode.SERIALIZATION_ERROR, e); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3535d46..ef3b3f4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,7 +16,8 @@ springdoc: logging: level: - root: DEBUG - org.springframework: DEBUG - org.hibernate.SQL: DEBUG + root: INFO # 애플리케이션 전체는 INFO로 + org.springframework: WARN # 스프링 내부 디버그 로그는 최소화 + org.hibernate.SQL: DEBUG # SQL 쿼리만 보고 싶으면 DEBUG 유지 + org.hibernate.type.descriptor.sql.BasicBinder: TRACE # 바인딩 값 보고 싶으면 From ffaabbcbd14fa0a95d18cf5ebdd4b685621c0110 Mon Sep 17 00:00:00 2001 From: mdy3722 Date: Fri, 24 Oct 2025 10:20:04 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix=20:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EC=9D=91=EB=8B=B5=20DTO?= =?UTF-8?q?=EC=97=90=EC=84=9C=20null=EC=9D=B8=20tag=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=B2=98=EB=A6=AC=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=95=84=20=EC=84=9C=EB=B2=84=20=EC=98=A4=EB=A5=98=EA=B0=80=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20=ED=98=84=EC=83=81?= =?UTF-8?q?=EC=9D=84=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Bubble/bubblog/domain/post/dto/res/BlogPostDetailDTO.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/Bubble/bubblog/domain/post/dto/res/BlogPostDetailDTO.java b/src/main/java/Bubble/bubblog/domain/post/dto/res/BlogPostDetailDTO.java index 5516bbe..521db06 100644 --- a/src/main/java/Bubble/bubblog/domain/post/dto/res/BlogPostDetailDTO.java +++ b/src/main/java/Bubble/bubblog/domain/post/dto/res/BlogPostDetailDTO.java @@ -40,6 +40,6 @@ public BlogPostDetailDTO(BlogPost post, List categoryList, List this.userId = post.getUser().getId(); this.nickname = post.getUser().getNickname(); this.categoryList = categoryList; - this.tags = tags; + this.tags = tags != null ? tags : List.of(); } } From e6c2b6d9503f9dc240870d4803164583d688c54e Mon Sep 17 00:00:00 2001 From: mdy3722 Date: Sat, 25 Oct 2025 19:08:06 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore=20:=20AI=EC=84=9C=EB=B2=84(Node.js)?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=A0=88=EB=94=94=EC=8A=A4=EC=97=90=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20docker-compose.yml=20=ED=8C=8C=EC=9D=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index ef3f6a4..b7f66e9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,9 +5,13 @@ services: image: redis:latest container_name: redis restart: always + ports: + - "6379:6379" # 외부 접속 가능하도록 포트 바인딩 추가 + command: ["redis-server", "--bind", "0.0.0.0", "--protected-mode", "no"] # 외부 허용 설정 volumes: - redis_data:/data + spring: image: mdy3722/bubblog-springboot:latest container_name: bubblog