Skip to content

Commit 797d4df

Browse files
authored
Merge pull request #69 from BackEndSchoolPlus3th/SMS-83-feature-keywordnotES
add: keyword alarm with elastic search
2 parents 6641efd + 99ab3a0 commit 797d4df

16 files changed

Lines changed: 231 additions & 150 deletions

File tree

src/main/java/org/com/stocknote/domain/keyword/controller/KeywordController.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.com.stocknote.domain.keyword.dto.KeywordResponse;
88
import org.com.stocknote.domain.keyword.service.KeywordService;
99
import org.com.stocknote.domain.member.entity.Member;
10+
import org.com.stocknote.domain.searchDoc.service.KeywordDocService;
1011
import org.com.stocknote.oauth.entity.PrincipalDetails;
1112
import org.springframework.http.ResponseEntity;
1213
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -19,6 +20,7 @@
1920
public class KeywordController {
2021

2122
private final KeywordService keywordService;
23+
private final KeywordDocService keywordDocService;
2224

2325
// 키워드 전체 조회
2426
@GetMapping("/keywords")
@@ -40,6 +42,7 @@ public ResponseEntity<KeywordResponse> updateKeywords(
4042
) {
4143
Member member = principalDetails.user();
4244
KeywordResponse response = keywordService.updateKeywords(member, request);
45+
keywordDocService.save(member, request);
4346
return ResponseEntity.ok(response);
4447
}
4548
}

src/main/java/org/com/stocknote/domain/notification/controller/KeywordNotificationController.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import lombok.RequiredArgsConstructor;
44
import org.com.stocknote.domain.notification.dto.KeywordNotificationResponse;
5+
import org.com.stocknote.domain.notification.service.KeywordNotificationElasticService;
56
import org.com.stocknote.domain.notification.service.KeywordNotificationService;
67
import org.com.stocknote.oauth.entity.PrincipalDetails;
78
import org.springframework.http.ResponseEntity;
Lines changed: 30 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
package org.com.stocknote.domain.notification.service;
22

33
import lombok.RequiredArgsConstructor;
4-
import org.com.stocknote.domain.hashtag.service.HashtagService;
54
import org.com.stocknote.domain.keyword.entity.Keyword;
65
import org.com.stocknote.domain.keyword.repository.KeywordRepository;
76
import org.com.stocknote.domain.notification.dto.KeywordNotificationResponse;
87
import org.com.stocknote.domain.notification.entity.KeywordNotification;
98
import org.com.stocknote.domain.notification.repository.KeywordNotificationRepository;
10-
import org.com.stocknote.domain.post.entity.Post;
119
import org.com.stocknote.domain.post.entity.PostCategory;
10+
import org.com.stocknote.domain.searchDoc.document.KeywordDoc;
1211
import org.com.stocknote.domain.searchDoc.document.PostDoc;
12+
import org.com.stocknote.domain.searchDoc.repository.KeywordDocRepository;
1313
import org.com.stocknote.domain.searchDoc.repository.PostDocRepository;
1414
import org.com.stocknote.global.error.ErrorCode;
1515
import org.com.stocknote.global.exception.CustomException;
16-
import org.springframework.data.domain.Page;
17-
import org.springframework.data.domain.PageRequest;
1816
import org.springframework.stereotype.Service;
1917
import org.springframework.transaction.annotation.Transactional;
2018

@@ -26,64 +24,52 @@
2624
@RequiredArgsConstructor
2725
public class KeywordNotificationElasticService {
2826
private final KeywordRepository keywordRepository;
27+
private final KeywordDocRepository keywordDocRepository;
2928
private final KeywordNotificationRepository keywordNotificationRepository;
3029
private final SseEmitterService sseEmitterService;
3130
private final PostDocRepository postDocRepository;
3231

3332
@Transactional
3433
public void createKeywordNotification(PostDoc postDoc) {
35-
// 카테고리에 맞는 키워드 구독자 조회
36-
List<Keyword> keywords;
37-
if (postDoc.getCategory() == PostCategory.ALL) {
38-
keywords = keywordRepository.findAll();
39-
} else {
40-
keywords = keywordRepository.findAllByPostCategory(postDoc.getCategory());
41-
}
34+
// Elasticsearch로 매칭되는 키워드 한 번에 조회
35+
System.out.println("postDoc = " + postDoc);
36+
System.out.println("postCategory = " + postDoc.getCategory());
37+
System.out.println("postDoc = " + postDoc.getHashtags());
4238

43-
// 각 키워드별로 매칭 확인
44-
keywords.forEach(keyword -> {
45-
if (postDocRepository.existsByTitleOrHashtagsContaining(keyword.getKeyword())) {
46-
KeywordNotification keywordNotification = KeywordNotification.builder()
47-
.memberId(keyword.getMemberId())
39+
List<KeywordDoc> matchingKeywords = keywordDocRepository.findMatchingKeywords(
40+
postDoc.getCategory(),
41+
postDoc.getTitle(),
42+
String.join(" ", postDoc.getHashtags())
43+
);
44+
45+
System.out.println("matchingKeywords = " + matchingKeywords);
46+
47+
// 매칭된 키워드에 대해 알림 생성
48+
matchingKeywords.forEach(keywordDoc -> {
49+
KeywordNotification notification = KeywordNotification.builder()
50+
.memberId(keywordDoc.getMemberId())
4851
.relatedPostId(Long.valueOf(postDoc.getId()))
49-
.keyword(keyword.getKeyword())
50-
.postCategory(keyword.getPostCategory())
52+
.keyword(keywordDoc.getKeyword())
53+
.postCategory(keywordDoc.getPostCategory())
5154
.isRead(false)
52-
.content(createNotificationContent(postDoc, keyword.getKeyword()))
55+
.content(createNotificationContent(postDoc, keywordDoc.getKeyword()))
5356
.build();
5457

55-
keywordNotificationRepository.save(keywordNotification);
58+
keywordNotificationRepository.save(notification);
5659

57-
// SSE로 실시간 알림 전송
58-
sseEmitterService.sendKeywordNotification(
59-
keyword.getMemberId().toString(),
60-
KeywordNotificationResponse.from(keywordNotification)
61-
);
62-
}
60+
// SSE로 실시간 알림 전송
61+
sseEmitterService.sendKeywordNotification(
62+
keywordDoc.getMemberId().toString(),
63+
KeywordNotificationResponse.from(notification)
64+
);
6365
});
6466
}
6567

6668
private String createNotificationContent(PostDoc postDoc, String keyword) {
6769
return String.format("'%s' 키워드와 관련된 게시글이 등록되었습니다: %s",
68-
keyword,
69-
postDoc.getTitle()
70+
keyword,
71+
postDoc.getTitle()
7072
);
7173
}
7274

73-
public List<KeywordNotificationResponse> getNotificationsByMember(Long memberId) {
74-
LocalDateTime startDate = LocalDateTime.now().minusDays(30);
75-
return keywordNotificationRepository.findByMemberIdAndIsReadFalseAndCreatedAtAfterOrderByCreatedAtDesc(
76-
memberId,
77-
startDate
78-
)
79-
.stream()
80-
.map(KeywordNotificationResponse::from)
81-
.collect(Collectors.toList());
82-
}
83-
84-
public void markAsRead(Long notificationId) {
85-
KeywordNotification keywordNotification = keywordNotificationRepository.findById(notificationId)
86-
.orElseThrow(() -> new CustomException(ErrorCode.ENTITY_NOT_FOUND));
87-
keywordNotification.markAsRead();
88-
}
8975
}

src/main/java/org/com/stocknote/domain/post/controller/PostController.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22

33
import io.swagger.v3.oas.annotations.Operation;
44
import io.swagger.v3.oas.annotations.tags.Tag;
5+
import jakarta.transaction.Transactional;
56
import jakarta.validation.Valid;
67
import lombok.RequiredArgsConstructor;
78
import org.com.stocknote.domain.member.entity.Member;
89
import org.com.stocknote.domain.notification.service.KeywordNotificationElasticService;
910
import org.com.stocknote.domain.notification.service.KeywordNotificationService;
10-
import org.com.stocknote.domain.post.dto.PostCreateDto;
1111
import org.com.stocknote.domain.post.dto.PostModifyDto;
1212
import org.com.stocknote.domain.post.dto.PostResponseDto;
1313
import org.com.stocknote.domain.post.entity.Post;
1414
import org.com.stocknote.domain.post.dto.PostSearchConditionDto;
1515
import org.com.stocknote.domain.post.entity.PostCategory;
1616
import org.com.stocknote.domain.post.service.PostService;
17+
import org.com.stocknote.domain.searchDoc.document.PostDoc;
18+
import org.com.stocknote.domain.searchDoc.service.SearchDocService;
1719
import org.com.stocknote.global.dto.GlobalResponse;
1820
import org.com.stocknote.oauth.entity.PrincipalDetails;
1921
import org.springframework.data.domain.Page;
@@ -23,7 +25,6 @@
2325
import org.springframework.security.core.annotation.AuthenticationPrincipal;
2426
import org.springframework.web.bind.annotation.*;
2527

26-
import static org.com.stocknote.domain.post.entity.QPost.post;
2728

2829
@RestController
2930
@RequestMapping("/api/v1/posts")
@@ -32,18 +33,21 @@
3233
public class PostController {
3334

3435
private final PostService postService;
36+
private final SearchDocService searchDocService;
3537
private final KeywordNotificationService keywordNotificationService;
3638
private final KeywordNotificationElasticService keywordNotificationElasticService;
3739

40+
@Transactional
3841
@PostMapping
3942
@Operation(summary = "게시글 작성")
4043
public GlobalResponse<Long> createPost(
41-
@Valid @RequestBody PostCreateDto postCreateDto,
44+
@Valid @RequestBody PostResponseDto postResponseDto,
4245
@AuthenticationPrincipal PrincipalDetails principalDetails
4346
) {
4447
Member member = principalDetails.user();
45-
Post post = postService.createPost(postCreateDto, member);
46-
keywordNotificationService.createKeywordNotification(post);
48+
Post post = postService.createPost(postResponseDto, member);
49+
PostDoc postDoc= searchDocService.savePostDoc(post);
50+
keywordNotificationElasticService.createKeywordNotification(postDoc);
4751
return GlobalResponse.success(post.getId());
4852
}
4953

src/main/java/org/com/stocknote/domain/post/dto/PostCreateDto.java

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/main/java/org/com/stocknote/domain/post/dto/PostModifyDto.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import jakarta.validation.constraints.NotBlank;
44
import lombok.Getter;
55
import lombok.Setter;
6+
import org.com.stocknote.domain.comment.entity.Comment;
67
import org.com.stocknote.domain.member.entity.Member;
78
import org.com.stocknote.domain.post.entity.Post;
89
import org.com.stocknote.domain.post.entity.PostCategory;

src/main/java/org/com/stocknote/domain/post/dto/PostResponseDto.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package org.com.stocknote.domain.post.dto;
22

33
import org.com.stocknote.domain.comment.dto.CommentDetailResponse;
4+
import org.com.stocknote.domain.member.entity.Member;
45
import org.com.stocknote.domain.post.entity.Post;
56
import org.com.stocknote.domain.post.entity.PostCategory;
67
import org.com.stocknote.domain.searchDoc.document.PostDoc;
78

89
import java.time.LocalDateTime;
10+
import java.util.ArrayList;
911
import java.util.List;
1012

1113
public record PostResponseDto(
@@ -59,4 +61,16 @@ public static PostResponseDto fromPost(PostDoc postDoc) {
5961
postDoc.getCommentCount()
6062
);
6163
}
64+
65+
public Post toEntity(Member member) {
66+
return Post.builder()
67+
.title(this.title)
68+
.body(this.body)
69+
.category(this.category)
70+
.member(member) // 연관관계 설정
71+
.comments(new ArrayList<>()) // 빈 리스트로 초기화
72+
.likes(new ArrayList<>()) // 빈 리스트로 초기화
73+
.build();
74+
}
75+
6276
}

src/main/java/org/com/stocknote/domain/post/service/PostService.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import org.com.stocknote.domain.member.entity.Member;
88
import org.com.stocknote.domain.notification.repository.CommentNotificationRepository;
99
import org.com.stocknote.domain.notification.repository.KeywordNotificationRepository;
10-
import org.com.stocknote.domain.post.dto.PostCreateDto;
1110
import org.com.stocknote.domain.post.dto.PostModifyDto;
1211
import org.com.stocknote.domain.post.dto.PostResponseDto;
1312
import org.com.stocknote.domain.post.dto.PostSearchConditionDto;
@@ -38,12 +37,12 @@ public class PostService {
3837

3938

4039
@Transactional
41-
public Post createPost(PostCreateDto postCreateDto, Member member) {
40+
public Post createPost(PostResponseDto postResponseDto, Member member) {
4241

43-
Post post = postCreateDto.toEntity(member);
42+
Post post = postResponseDto.toEntity(member);
4443
Post savedPost = postRepository.save(post);
4544

46-
hashtagService.createHashtags(savedPost.getId(), postCreateDto.getHashtags());
45+
hashtagService.createHashtags(savedPost.getId(), postResponseDto.hashtags());
4746
return post;
4847
}
4948

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package org.com.stocknote.domain.searchDoc.document;
2+
3+
import lombok.*;
4+
import org.springframework.data.annotation.Id;
5+
import org.com.stocknote.domain.keyword.entity.Keyword;
6+
import org.com.stocknote.domain.post.entity.PostCategory;
7+
import org.springframework.data.elasticsearch.annotations.*;
8+
9+
@Document(indexName = "stocknote_keyword")
10+
@Getter
11+
@Setter
12+
@Builder
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
public class KeywordDoc {
16+
@Id
17+
private String id;
18+
19+
@Field(type = FieldType.Text)
20+
private String keyword;
21+
22+
@Field(type = FieldType.Long, name = "member_id")
23+
private Long memberId;
24+
25+
@Field(type = FieldType.Keyword, name="post_category")
26+
private PostCategory postCategory;
27+
28+
public static KeywordDoc from(Keyword keyword) {
29+
return KeywordDoc.builder()
30+
.id(String.valueOf(keyword.getId()))
31+
.keyword(keyword.getKeyword())
32+
.memberId(keyword.getMemberId())
33+
.postCategory(keyword.getPostCategory())
34+
.build();
35+
}
36+
}
37+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.com.stocknote.domain.searchDoc.repository;
2+
3+
import org.com.stocknote.domain.post.entity.PostCategory;
4+
import org.com.stocknote.domain.searchDoc.document.KeywordDoc;
5+
import org.springframework.data.elasticsearch.annotations.Query;
6+
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
7+
import org.springframework.data.repository.query.Param;
8+
9+
import java.util.List;
10+
11+
public interface KeywordDocRepository extends ElasticsearchRepository<KeywordDoc, String> {
12+
@Query("""
13+
{
14+
"bool": {
15+
"must": [
16+
{
17+
"bool": {
18+
"should": [
19+
{"match": {"post_category": "#{#postCategory}"}},
20+
{"match": {"post_category": "ALL"}}
21+
]
22+
}
23+
},
24+
{
25+
"bool": {
26+
"should": [
27+
{"match": {"keyword": "#{#title}"}},
28+
{"match": {"keyword": "#{#hashtags}"}}
29+
]
30+
}
31+
}
32+
]
33+
}
34+
}
35+
""")
36+
List<KeywordDoc> findMatchingKeywords(
37+
@Param("postCategory") PostCategory postCategory,
38+
@Param("title") String title,
39+
@Param("hashtags") String hashtags
40+
);
41+
}

0 commit comments

Comments
 (0)