Skip to content

Commit 34c210f

Browse files
authored
feat: community-board / recruit-board 검색 조회 (elastic search) (#225)
* feature(search): elasticsearch 의존성 추가 * feature(search): elasticsearch document, repository 생성 * feature(search): repository에 elasticsearch 관련 메서드 추가 - keyword로 검색 결과 반환 - elasticsearch에 저장 - 전체 데이터 조회 - elasticsearch 데이터 삭제 * feature(search): service 추가 - elasticsearch 저장 - elasticsearch 키워드 검색 - 전체글 조회 * feature(search): controller 키워드 검색 추가 * feature(search): elasticsearch 저장 스케쥴러 추가 * feature(search): elasticsearch config 추가 * feature(search): community 게시글 fixture 추가 * test(search): community 게시글 검색 테스트 추가 * test(search): recruit 모집글 검색 테스트 추가 * test(search): import 경로 수정 * refactor(search): scheduler 수정 * test(search): 코드 리뷰 사항 반영 - application-test.yml 수정 - elastic search uris -> uri test(search): 코드 리뷰 사항 반영 - application-test.yml 수정 * feature(search): 검색 keyword가 null인 경우 전체 조회 * feature(search): 위치 기반 검색 추가 및 키워드 condition에 추가 * test(search): 커뮤니티 검색 테스트 추가 및 주석 * test(search): 봉사활동 모집글 검색 테스트 추가 및 주석 * chore(search): 개행 추가 * refactor(search): 기존 recruitBoard 검색 / elasicsearch 검색 분리 * test(search): 기존 recruitBoard 검색 / elasicsearch 검색 분리 테스트 수정 * test(search): 불필요한 import 제거 * test(search): elastic search uri 변경 * test(search): test 수정 * refactor(search): elastic search 업데이트 스케쥴러 cron 추출 및 네이밍 변경 * test(search): elastic search 업데이트 스케쥴러 테스트 추가 * test(search): public 키워드 삭제
1 parent b26fa48 commit 34c210f

File tree

39 files changed

+1488
-50
lines changed

39 files changed

+1488
-50
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ dependencies {
6767
implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.6.0'
6868
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: '3.3.3'
6969

70+
//elastic-search
71+
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
7072

7173
//test
7274
testImplementation 'org.springframework.boot:spring-boot-starter-test'

src/main/java/com/somemore/SomemoreApplication.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
6+
import org.springframework.scheduling.annotation.EnableScheduling;
67

78
@SpringBootApplication
89
@EnableJpaAuditing
10+
@EnableScheduling
911
public class SomemoreApplication {
1012

1113
public static void main(String[] args) {
1214
SpringApplication.run(SomemoreApplication.class, args);
1315
}
14-
1516
}

src/main/java/com/somemore/community/controller/CommunityBoardQueryApiController.java

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

33
import com.somemore.community.dto.response.CommunityBoardDetailResponseDto;
44
import com.somemore.community.dto.response.CommunityBoardResponseDto;
5+
import com.somemore.community.usecase.board.CommunityBoardDocumentUseCase;
56
import com.somemore.community.usecase.board.CommunityBoardQueryUseCase;
67
import com.somemore.global.common.response.ApiResponse;
78
import io.swagger.v3.oas.annotations.Operation;
@@ -20,6 +21,7 @@
2021
public class CommunityBoardQueryApiController {
2122

2223
private final CommunityBoardQueryUseCase communityBoardQueryUseCase;
24+
private final CommunityBoardDocumentUseCase communityBoardDocumentUseCase;
2325

2426
@GetMapping("/community-boards")
2527
@Operation(summary = "전체 커뮤니티 게시글 조회", description = "전체 커뮤니티 게시글 목록을 조회합니다.")
@@ -46,6 +48,19 @@ public ApiResponse<Page<CommunityBoardResponseDto>> getByWriterId(
4648
);
4749
}
4850

51+
@GetMapping("/community-boards/search")
52+
@Operation(summary = "커뮤니티 게시글 키워드 검색", description = "키워드로 포함한 커뮤니티 게시글 목록을 조회합니다.")
53+
public ApiResponse<Page<CommunityBoardResponseDto>> getCommunityBoardsBySearch(
54+
String keyword,
55+
Pageable pageable
56+
) {
57+
return ApiResponse.ok(
58+
200,
59+
communityBoardDocumentUseCase.getCommunityBoardBySearch(keyword, pageable.getPageNumber()),
60+
"커뮤니티 게시글 검색 리스트 조회 성공"
61+
);
62+
}
63+
4964
@GetMapping("/community-board/{id}")
5065
@Operation(summary = "커뮤니티 게시글 상세 조회", description = "커뮤니티 게시글의 상세 정보를 조회합니다.")
5166
public ApiResponse<CommunityBoardDetailResponseDto> getById(
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.somemore.community.domain;
2+
3+
import jakarta.persistence.Id;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import org.springframework.data.elasticsearch.annotations.Document;
7+
import org.springframework.data.elasticsearch.annotations.Field;
8+
import org.springframework.data.elasticsearch.annotations.FieldType;
9+
10+
@Getter
11+
@Document(indexName = "community_board")
12+
public class CommunityBoardDocument {
13+
14+
@Id
15+
private Long id;
16+
17+
@Field(type = FieldType.Text, analyzer = "nori_analyzer")
18+
private String title;
19+
20+
@Field(type = FieldType.Text, analyzer = "nori_analyzer")
21+
private String content;
22+
23+
@Builder
24+
public CommunityBoardDocument(Long id, String title, String content) {
25+
this.id = id;
26+
this.title = title;
27+
this.content = content;
28+
}
29+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.somemore.community.repository.board;
2+
3+
import com.somemore.community.domain.CommunityBoardDocument;
4+
import org.springframework.data.elasticsearch.annotations.Query;
5+
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
6+
7+
import java.util.List;
8+
9+
public interface CommunityBoardDocumentRepository extends ElasticsearchRepository<CommunityBoardDocument, Long> {
10+
List<CommunityBoardDocument> findAll();
11+
@Query("{\"multi_match\": {\"query\": \"?0\", \"fields\": [\"title\", \"content\"]}}")
12+
List<CommunityBoardDocument> findIdsByTitleOrContentContaining(String keyword);
13+
}

src/main/java/com/somemore/community/repository/board/CommunityBoardRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.springframework.data.domain.Page;
66
import org.springframework.data.domain.Pageable;
77

8+
import java.util.List;
89
import java.util.Optional;
910
import java.util.UUID;
1011

@@ -18,4 +19,9 @@ default boolean doesNotExistById(Long id) {
1819
return !existsById(id);
1920
}
2021
void deleteAllInBatch();
22+
23+
Page<CommunityBoardView> findByCommunityBoardsContaining(String keyword, Pageable pageable);
24+
void saveDocuments(List<CommunityBoard> communityBoards);
25+
List<CommunityBoard> findAll();
26+
void deleteDocument(Long id);
2127
}

src/main/java/com/somemore/community/repository/board/CommunityBoardRepositoryImpl.java

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.querydsl.jpa.impl.JPAQuery;
66
import com.querydsl.jpa.impl.JPAQueryFactory;
77
import com.somemore.community.domain.CommunityBoard;
8+
import com.somemore.community.domain.CommunityBoardDocument;
89
import com.somemore.community.repository.mapper.CommunityBoardView;
910
import com.somemore.community.domain.QCommunityBoard;
1011
import com.somemore.volunteer.domain.QVolunteer;
@@ -14,6 +15,7 @@
1415
import org.springframework.data.support.PageableExecutionUtils;
1516
import org.springframework.stereotype.Repository;
1617

18+
import java.util.ArrayList;
1719
import java.util.List;
1820
import java.util.Optional;
1921
import java.util.UUID;
@@ -24,6 +26,7 @@ public class CommunityBoardRepositoryImpl implements CommunityBoardRepository {
2426

2527
private final JPAQueryFactory queryFactory;
2628
private final CommunityBoardJpaRepository communityBoardJpaRepository;
29+
private final CommunityBoardDocumentRepository documentRepository;
2730

2831
private static final QCommunityBoard communityBoard = QCommunityBoard.communityBoard;
2932
private static final QVolunteer volunteer = QVolunteer.volunteer;
@@ -83,6 +86,52 @@ public boolean existsById(Long id) {
8386
return communityBoardJpaRepository.existsByIdAndDeletedFalse(id);
8487
}
8588

89+
@Override
90+
public Page<CommunityBoardView> findByCommunityBoardsContaining(String keyword, Pageable pageable) {
91+
List<CommunityBoardDocument> boardDocuments = getBoardDocuments(keyword);
92+
93+
List<Long> boardIds = boardDocuments.stream()
94+
.map(CommunityBoardDocument::getId)
95+
.toList();
96+
97+
List<CommunityBoardView> content = getCommunityBoardsQuery()
98+
.where(communityBoard.id.in(boardIds)
99+
.and(isNotDeleted()))
100+
.offset(pageable.getOffset())
101+
.limit(pageable.getPageSize())
102+
.fetch();
103+
104+
JPAQuery<Long> countQuery = queryFactory
105+
.select(communityBoard.count())
106+
.from(communityBoard)
107+
.join(volunteer).on(communityBoard.writerId.eq(volunteer.id))
108+
.where(communityBoard.id.in(boardIds)
109+
.and(isNotDeleted()));
110+
111+
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
112+
}
113+
114+
@Override
115+
public void saveDocuments(List<CommunityBoard> communityBoards) {
116+
List<CommunityBoardDocument> communityBoardDocuments = convertEntityToDocuments(communityBoards);
117+
documentRepository.saveAll(communityBoardDocuments);
118+
}
119+
120+
@Override
121+
public void deleteDocument(Long id) {
122+
documentRepository.deleteById(id);
123+
}
124+
125+
@Override
126+
public List<CommunityBoard> findAll() {
127+
return communityBoardJpaRepository.findAll();
128+
}
129+
130+
@Override
131+
public void deleteAllInBatch() {
132+
communityBoardJpaRepository.deleteAllInBatch();
133+
}
134+
86135
private JPAQuery<CommunityBoardView> getCommunityBoardsQuery() {
87136
return queryFactory
88137
.select(Projections.constructor(CommunityBoardView.class,
@@ -93,15 +142,31 @@ private JPAQuery<CommunityBoardView> getCommunityBoardsQuery() {
93142
.orderBy(communityBoard.createdAt.desc());
94143
}
95144

96-
97-
@Override
98-
public void deleteAllInBatch() {
99-
communityBoardJpaRepository.deleteAllInBatch();
145+
private List<CommunityBoardDocument> convertEntityToDocuments(List<CommunityBoard> communityBoards) {
146+
List<CommunityBoardDocument> communityBoardDocuments = new ArrayList<>();
147+
148+
for (CommunityBoard communityboard : communityBoards) {
149+
CommunityBoardDocument document = CommunityBoardDocument.builder()
150+
.id(communityboard.getId())
151+
.title(communityboard.getTitle())
152+
.content(communityboard.getContent())
153+
.build();
154+
communityBoardDocuments.add(document);
155+
}
156+
return communityBoardDocuments;
100157
}
101158

102159
private BooleanExpression isNotDeleted() {
103160
return communityBoard.deleted.eq(false);
104161
}
105162

106163
private BooleanExpression isWriter(UUID writerId) {return communityBoard.writerId.eq(writerId); }
164+
165+
private List<CommunityBoardDocument> getBoardDocuments(String keyword) {
166+
167+
if (keyword == null || keyword.isEmpty()) {
168+
return documentRepository.findAll();
169+
}
170+
return documentRepository.findIdsByTitleOrContentContaining(keyword);
171+
}
107172
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.somemore.community.scheduler;
2+
3+
import com.somemore.community.domain.CommunityBoard;
4+
import com.somemore.community.usecase.board.CommunityBoardDocumentUseCase;
5+
import com.somemore.community.usecase.board.CommunityBoardQueryUseCase;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.scheduling.annotation.Scheduled;
8+
import org.springframework.stereotype.Component;
9+
10+
import java.util.List;
11+
12+
@Component
13+
@RequiredArgsConstructor
14+
public class CommunityBoardUpdateScheduler {
15+
16+
private final CommunityBoardQueryUseCase communityBoardQueryUseCase;
17+
private final CommunityBoardDocumentUseCase communityBoardDocumentUseCase;
18+
19+
20+
@Scheduled(cron = "${spring.schedules.cron.updateCommunityBoardDocuments}")
21+
public void updateCommunityBoardDocuments() {
22+
List<CommunityBoard> communityBoards = communityBoardQueryUseCase.getAllCommunityBoards();
23+
communityBoardDocumentUseCase.saveCommunityBoardDocuments(communityBoards);
24+
}
25+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.somemore.community.service.board;
2+
3+
import com.somemore.community.domain.CommunityBoard;
4+
import com.somemore.community.dto.response.CommunityBoardResponseDto;
5+
import com.somemore.community.repository.board.CommunityBoardRepository;
6+
import com.somemore.community.repository.mapper.CommunityBoardView;
7+
import com.somemore.community.usecase.board.CommunityBoardDocumentUseCase;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.data.domain.Page;
10+
import org.springframework.data.domain.PageRequest;
11+
import org.springframework.data.domain.Pageable;
12+
import org.springframework.stereotype.Service;
13+
import org.springframework.transaction.annotation.Transactional;
14+
15+
import java.util.List;
16+
17+
@RequiredArgsConstructor
18+
@Service
19+
public class CommunityBoardDocumentService implements CommunityBoardDocumentUseCase {
20+
21+
private final CommunityBoardRepository communityBoardRepository;
22+
private static final int PAGE_SIZE = 10;
23+
24+
@Transactional(readOnly = true)
25+
@Override
26+
public Page<CommunityBoardResponseDto> getCommunityBoardBySearch(String keyword, int page) {
27+
Pageable pageable = PageRequest.of(page, PAGE_SIZE);
28+
Page<CommunityBoardView> boards = communityBoardRepository.findByCommunityBoardsContaining(keyword, pageable);
29+
return boards.map(CommunityBoardResponseDto::from);
30+
}
31+
32+
@Transactional
33+
@Override
34+
public void saveCommunityBoardDocuments(List<CommunityBoard> communityBoards) {
35+
communityBoardRepository.saveDocuments(communityBoards);
36+
}
37+
}

src/main/java/com/somemore/community/service/board/CommunityBoardQueryService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.springframework.stereotype.Service;
1515
import org.springframework.transaction.annotation.Transactional;
1616

17+
import java.util.List;
1718
import java.util.UUID;
1819

1920
import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_COMMUNITY_BOARD;
@@ -46,4 +47,9 @@ public CommunityBoardDetailResponseDto getCommunityBoardDetail(Long id) {
4647
.orElseThrow(() -> new BadRequestException(NOT_EXISTS_COMMUNITY_BOARD.getMessage()));
4748
return CommunityBoardDetailResponseDto.from(board);
4849
}
50+
51+
@Override
52+
public List<CommunityBoard> getAllCommunityBoards() {
53+
return communityBoardRepository.findAll();
54+
}
4955
}

0 commit comments

Comments
 (0)