Skip to content

Commit 90e0c17

Browse files
authored
Merge pull request #65 from BackEndSchoolPlus3th/SMS-95-feature-게시판-검색기능-추가
SMS-95-feature-게시판-검색기능-추가
2 parents eb1d8bd + 1e434a6 commit 90e0c17

17 files changed

Lines changed: 333 additions & 33 deletions

File tree

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ tasks {
115115
}
116116
ignoreFailures = true // 테스트 실패해도 빌드 진행
117117
}
118-
118+
119119
processResources {
120120
// 리소스 파일 복사 확인
121121
doFirst {

src/main/java/org/com/stocknote/config/WebConfig.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ public class WebConfig implements WebMvcConfigurer {
99
@Override
1010
public void addCorsMappings(CorsRegistry registry) {
1111

12-
registry.addMapping("/**")
13-
.allowedOrigins(AppConfig.getSiteFrontUrl())
14-
.allowedOrigins(AppConfig.getSiteBackUrl())
12+
registry.addMapping("/**").allowedOrigins(AppConfig.getSiteFrontUrl())
1513
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
1614
.allowedHeaders("*").allowCredentials(true);
1715
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package org.com.stocknote.domain.notification.service;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.com.stocknote.domain.hashtag.service.HashtagService;
5+
import org.com.stocknote.domain.keyword.entity.Keyword;
6+
import org.com.stocknote.domain.keyword.repository.KeywordRepository;
7+
import org.com.stocknote.domain.notification.dto.KeywordNotificationResponse;
8+
import org.com.stocknote.domain.notification.entity.KeywordNotification;
9+
import org.com.stocknote.domain.notification.repository.KeywordNotificationRepository;
10+
import org.com.stocknote.domain.post.entity.Post;
11+
import org.com.stocknote.domain.post.entity.PostCategory;
12+
import org.com.stocknote.domain.searchDoc.document.PostDoc;
13+
import org.com.stocknote.domain.searchDoc.repository.PostDocRepository;
14+
import org.com.stocknote.global.error.ErrorCode;
15+
import org.com.stocknote.global.exception.CustomException;
16+
import org.springframework.data.domain.Page;
17+
import org.springframework.data.domain.PageRequest;
18+
import org.springframework.stereotype.Service;
19+
import org.springframework.transaction.annotation.Transactional;
20+
21+
import java.time.LocalDateTime;
22+
import java.util.List;
23+
import java.util.stream.Collectors;
24+
25+
@Service
26+
@RequiredArgsConstructor
27+
public class KeywordNotificationElasticService {
28+
private final KeywordRepository keywordRepository;
29+
private final KeywordNotificationRepository keywordNotificationRepository;
30+
private final SseEmitterService sseEmitterService;
31+
private final PostDocRepository postDocRepository;
32+
33+
@Transactional
34+
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+
}
42+
43+
// 각 키워드별로 매칭 확인
44+
keywords.forEach(keyword -> {
45+
if (postDocRepository.existsByTitleOrHashtagsContaining(keyword.getKeyword())) {
46+
KeywordNotification keywordNotification = KeywordNotification.builder()
47+
.memberId(keyword.getMemberId())
48+
.relatedPostId(Long.valueOf(postDoc.getId()))
49+
.keyword(keyword.getKeyword())
50+
.postCategory(keyword.getPostCategory())
51+
.isRead(false)
52+
.content(createNotificationContent(postDoc, keyword.getKeyword()))
53+
.build();
54+
55+
keywordNotificationRepository.save(keywordNotification);
56+
57+
// SSE로 실시간 알림 전송
58+
sseEmitterService.sendKeywordNotification(
59+
keyword.getMemberId().toString(),
60+
KeywordNotificationResponse.from(keywordNotification)
61+
);
62+
}
63+
});
64+
}
65+
66+
private String createNotificationContent(PostDoc postDoc, String keyword) {
67+
return String.format("'%s' 키워드와 관련된 게시글이 등록되었습니다: %s",
68+
keyword,
69+
postDoc.getTitle()
70+
);
71+
}
72+
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+
}
89+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import jakarta.validation.Valid;
66
import lombok.RequiredArgsConstructor;
77
import org.com.stocknote.domain.member.entity.Member;
8+
import org.com.stocknote.domain.notification.service.KeywordNotificationElasticService;
89
import org.com.stocknote.domain.notification.service.KeywordNotificationService;
910
import org.com.stocknote.domain.post.dto.PostCreateDto;
1011
import org.com.stocknote.domain.post.dto.PostModifyDto;
@@ -32,6 +33,7 @@ public class PostController {
3233

3334
private final PostService postService;
3435
private final KeywordNotificationService keywordNotificationService;
36+
private final KeywordNotificationElasticService keywordNotificationElasticService;
3537

3638
@PostMapping
3739
@Operation(summary = "게시글 작성")
@@ -117,4 +119,4 @@ public GlobalResponse<Page<PostResponseDto>> searchPosts(
117119
return GlobalResponse.success(postService.searchPosts(condition, pageable));
118120
}
119121

120-
}
122+
}

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.com.stocknote.domain.comment.dto.CommentDetailResponse;
44
import org.com.stocknote.domain.post.entity.Post;
55
import org.com.stocknote.domain.post.entity.PostCategory;
6+
import org.com.stocknote.domain.searchDoc.document.PostDoc;
67

78
import java.time.LocalDateTime;
89
import java.util.List;
@@ -17,7 +18,7 @@ public record PostResponseDto(
1718
List<CommentDetailResponse> comments,
1819
LocalDateTime createdAt,
1920
PostCategory category,
20-
List<String>hashtags,
21+
List<String> hashtags,
2122
int likeCount,
2223
int commentCount
2324
) {
@@ -40,4 +41,21 @@ public static PostResponseDto fromPost(Post post, List<String> hashtags) {
4041
post.getComments().size()
4142
);
4243
}
43-
}
44+
45+
public static PostResponseDto fromPost(PostDoc postDoc) {
46+
return new PostResponseDto(
47+
Long.valueOf(postDoc.getId()),
48+
postDoc.getTitle(),
49+
postDoc.getBody(),
50+
Long.valueOf(postDoc.getMemberDoc().getId()),
51+
postDoc.getMemberDoc().getName(),
52+
postDoc.getMemberDoc().getProfile(),
53+
null,
54+
postDoc.getCreatedAt(),
55+
postDoc.getCategory(),
56+
null,
57+
postDoc.getLikeCount(),
58+
postDoc.getCommentCount()
59+
);
60+
}
61+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public class PostSearchConditionDto {
1111
private SearchType searchType;
1212
private PostCategory category;
1313

14+
1415
public enum SearchType {
1516
TITLE, CONTENT, HASHTAG, USERNAME, ALL
1617
}

src/main/java/org/com/stocknote/domain/searchDoc/controller/SearchDocController.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@
33
import io.swagger.v3.oas.annotations.Operation;
44
import io.swagger.v3.oas.annotations.tags.Tag;
55
import lombok.RequiredArgsConstructor;
6-
import org.com.stocknote.domain.portfolio.portfolio.dto.response.PortfolioResponse;
6+
import org.com.stocknote.domain.post.dto.PostResponseDto;
7+
import org.com.stocknote.domain.post.dto.PostSearchConditionDto;
78
import org.com.stocknote.domain.searchDoc.document.PortfolioDoc;
89
import org.com.stocknote.domain.searchDoc.document.PortfolioStockDoc;
10+
import org.com.stocknote.domain.searchDoc.document.PostDoc;
911
import org.com.stocknote.domain.searchDoc.document.StockDoc;
1012
import org.com.stocknote.domain.searchDoc.dto.request.SearchKeyword;
1113
import org.com.stocknote.domain.searchDoc.dto.response.SearchPortfolioResponse;
1214
import org.com.stocknote.domain.searchDoc.dto.response.SearchedStockResponse;
13-
import org.com.stocknote.domain.searchDoc.service.StockDocService;
15+
import org.com.stocknote.domain.searchDoc.service.SearchDocService;
1416
import org.com.stocknote.global.globalDto.GlobalResponse;
1517
import org.com.stocknote.oauth.entity.PrincipalDetails;
18+
import org.springframework.data.domain.Page;
19+
import org.springframework.data.domain.Pageable;
20+
import org.springframework.data.domain.Sort;
21+
import org.springframework.data.web.PageableDefault;
1622
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1723
import org.springframework.web.bind.annotation.*;
1824

@@ -24,14 +30,14 @@
2430
@RequestMapping("/api/v1/searchDocs")
2531
@Tag(name = "검색 API", description = "검색(Search)")
2632
public class SearchDocController {
27-
private final StockDocService stockDocService;
33+
private final SearchDocService searchDocService;
2834

2935
@PostMapping("/stock")
3036
@Operation(summary = "종목 조회")
3137
public GlobalResponse<List<SearchedStockResponse>> searchStocks(
3238
@RequestBody SearchKeyword searchKeyword
3339
) {
34-
List<StockDoc> stockList = stockDocService.searchStocks(searchKeyword.getKeyword());
40+
List<StockDoc> stockList = searchDocService.searchStocks(searchKeyword.getKeyword());
3541
List<SearchedStockResponse> response =
3642
stockList.stream().map(SearchedStockResponse::of).collect(Collectors.toList());
3743
return GlobalResponse.success(response);
@@ -43,11 +49,23 @@ public GlobalResponse<SearchPortfolioResponse> getMyPortfolioList(
4349
@AuthenticationPrincipal PrincipalDetails principalDetails
4450
) {
4551
String email = principalDetails.getUsername();
46-
PortfolioDoc portfolioDoc = stockDocService.getMyPortfolioList(email);
47-
List<PortfolioStockDoc> portfolioStockDocList = stockDocService.getMyPortfolioStockList(email);
52+
PortfolioDoc portfolioDoc = searchDocService.getMyPortfolioList(email);
53+
List<PortfolioStockDoc> portfolioStockDocList = searchDocService.getMyPortfolioStockList(email);
4854
SearchPortfolioResponse
4955
response = SearchPortfolioResponse.from(portfolioDoc, portfolioStockDocList);
5056
return GlobalResponse.success(response);
5157

5258
}
59+
60+
@GetMapping("/post/search")
61+
@Operation(summary = "게시글 검색")
62+
public GlobalResponse<Page<PostResponseDto>> searchPosts(
63+
@ModelAttribute PostSearchConditionDto condition,
64+
@PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable
65+
) {
66+
Page<PostDoc> postDocs = searchDocService.searchPosts(condition, pageable);
67+
Page<PostResponseDto> response = postDocs.map(PostResponseDto::fromPost);
68+
return GlobalResponse.success(response);
69+
}
70+
5371
}

src/main/java/org/com/stocknote/domain/searchDoc/document/MemberDoc.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ public class MemberDoc {
2121
@Field(type = FieldType.Text)
2222
private String name;
2323

24+
@Field(type = FieldType.Keyword)
25+
private String profile;
26+
2427
@Field(type = FieldType.Keyword)
2528
private String provider;
2629

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.com.stocknote.domain.searchDoc.document;
2+
3+
import jakarta.persistence.EnumType;
4+
import jakarta.persistence.Enumerated;
5+
import jakarta.persistence.Id;
6+
import lombok.*;
7+
import org.com.stocknote.domain.post.entity.PostCategory;
8+
import org.springframework.data.elasticsearch.annotations.Document;
9+
import org.springframework.data.elasticsearch.annotations.Field;
10+
import org.springframework.data.elasticsearch.annotations.FieldType;
11+
12+
import java.time.LocalDateTime;
13+
import java.util.List;
14+
15+
@Document(indexName = "stocknote_post", createIndex = true)
16+
@Getter
17+
@Setter
18+
@Builder
19+
@NoArgsConstructor // 추가
20+
@AllArgsConstructor // 추가
21+
public class PostDoc {
22+
@Id
23+
private String id;
24+
25+
@Field(type = FieldType.Date, name = "created_at")
26+
private LocalDateTime createdAt;
27+
28+
@Field(type = FieldType.Date, name = "modified_at")
29+
private LocalDateTime modifiedAt;
30+
31+
@Field(type = FieldType.Text)
32+
private String title;
33+
34+
@Field(type = FieldType.Text)
35+
private String body;
36+
37+
@Enumerated(EnumType.STRING)
38+
private PostCategory category;
39+
40+
@Field(type = FieldType.Integer, name = "comment_count")
41+
private int commentCount;
42+
43+
@Field(type = FieldType.Integer, name = "like_count")
44+
private int likeCount;
45+
46+
@Field(type = FieldType.Text)
47+
private List<String> hashtags;
48+
49+
@Field(type = FieldType.Nested, name = "member_doc")
50+
private MemberDoc memberDoc;
51+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package org.com.stocknote.domain.searchDoc.dto.request;
2+
3+
public class PostSearchRequest {
4+
}

0 commit comments

Comments
 (0)