diff --git a/build.gradle b/build.gradle index c82330d96..eade7e24a 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,8 @@ dependencies { implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.6.0' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: '3.3.3' + //elastic-search + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' //test testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/com/somemore/SomemoreApplication.java b/src/main/java/com/somemore/SomemoreApplication.java index dec2413b5..076b1cca3 100644 --- a/src/main/java/com/somemore/SomemoreApplication.java +++ b/src/main/java/com/somemore/SomemoreApplication.java @@ -3,13 +3,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableJpaAuditing +@EnableScheduling public class SomemoreApplication { public static void main(String[] args) { SpringApplication.run(SomemoreApplication.class, args); } - } diff --git a/src/main/java/com/somemore/community/controller/CommunityBoardQueryApiController.java b/src/main/java/com/somemore/community/controller/CommunityBoardQueryApiController.java index 18f112412..4af56163f 100644 --- a/src/main/java/com/somemore/community/controller/CommunityBoardQueryApiController.java +++ b/src/main/java/com/somemore/community/controller/CommunityBoardQueryApiController.java @@ -2,6 +2,7 @@ import com.somemore.community.dto.response.CommunityBoardDetailResponseDto; import com.somemore.community.dto.response.CommunityBoardResponseDto; +import com.somemore.community.usecase.board.CommunityBoardDocumentUseCase; import com.somemore.community.usecase.board.CommunityBoardQueryUseCase; import com.somemore.global.common.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; @@ -20,6 +21,7 @@ public class CommunityBoardQueryApiController { private final CommunityBoardQueryUseCase communityBoardQueryUseCase; + private final CommunityBoardDocumentUseCase communityBoardDocumentUseCase; @GetMapping("/community-boards") @Operation(summary = "전체 커뮤니티 게시글 조회", description = "전체 커뮤니티 게시글 목록을 조회합니다.") @@ -46,6 +48,19 @@ public ApiResponse> getByWriterId( ); } + @GetMapping("/community-boards/search") + @Operation(summary = "커뮤니티 게시글 키워드 검색", description = "키워드로 포함한 커뮤니티 게시글 목록을 조회합니다.") + public ApiResponse> getCommunityBoardsBySearch( + String keyword, + Pageable pageable + ) { + return ApiResponse.ok( + 200, + communityBoardDocumentUseCase.getCommunityBoardBySearch(keyword, pageable.getPageNumber()), + "커뮤니티 게시글 검색 리스트 조회 성공" + ); + } + @GetMapping("/community-board/{id}") @Operation(summary = "커뮤니티 게시글 상세 조회", description = "커뮤니티 게시글의 상세 정보를 조회합니다.") public ApiResponse getById( diff --git a/src/main/java/com/somemore/community/domain/CommunityBoardDocument.java b/src/main/java/com/somemore/community/domain/CommunityBoardDocument.java new file mode 100644 index 000000000..c031e0e58 --- /dev/null +++ b/src/main/java/com/somemore/community/domain/CommunityBoardDocument.java @@ -0,0 +1,29 @@ +package com.somemore.community.domain; + +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +@Getter +@Document(indexName = "community_board") +public class CommunityBoardDocument { + + @Id + private Long id; + + @Field(type = FieldType.Text, analyzer = "nori_analyzer") + private String title; + + @Field(type = FieldType.Text, analyzer = "nori_analyzer") + private String content; + + @Builder + public CommunityBoardDocument(Long id, String title, String content) { + this.id = id; + this.title = title; + this.content = content; + } +} diff --git a/src/main/java/com/somemore/community/repository/board/CommunityBoardDocumentRepository.java b/src/main/java/com/somemore/community/repository/board/CommunityBoardDocumentRepository.java new file mode 100644 index 000000000..41241b1c1 --- /dev/null +++ b/src/main/java/com/somemore/community/repository/board/CommunityBoardDocumentRepository.java @@ -0,0 +1,12 @@ +package com.somemore.community.repository.board; + +import com.somemore.community.domain.CommunityBoardDocument; +import org.springframework.data.elasticsearch.annotations.Query; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +import java.util.List; + +public interface CommunityBoardDocumentRepository extends ElasticsearchRepository { + @Query("{\"multi_match\": {\"query\": \"?0\", \"fields\": [\"title\", \"content\"]}}") + List findIdsByTitleOrContentContaining(String keyword); +} diff --git a/src/main/java/com/somemore/community/repository/board/CommunityBoardRepository.java b/src/main/java/com/somemore/community/repository/board/CommunityBoardRepository.java index e767e837c..3e37727b7 100644 --- a/src/main/java/com/somemore/community/repository/board/CommunityBoardRepository.java +++ b/src/main/java/com/somemore/community/repository/board/CommunityBoardRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -18,4 +19,9 @@ default boolean doesNotExistById(Long id) { return !existsById(id); } void deleteAllInBatch(); + + Page findByCommunityBoardsContaining(String keyword, Pageable pageable); + void saveDocuments(List communityBoards); + List findAll(); + void deleteDocument(Long id); } diff --git a/src/main/java/com/somemore/community/repository/board/CommunityBoardRepositoryImpl.java b/src/main/java/com/somemore/community/repository/board/CommunityBoardRepositoryImpl.java index 0450c7630..48ccccb1d 100644 --- a/src/main/java/com/somemore/community/repository/board/CommunityBoardRepositoryImpl.java +++ b/src/main/java/com/somemore/community/repository/board/CommunityBoardRepositoryImpl.java @@ -5,6 +5,7 @@ import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import com.somemore.community.domain.CommunityBoard; +import com.somemore.community.domain.CommunityBoardDocument; import com.somemore.community.repository.mapper.CommunityBoardView; import com.somemore.community.domain.QCommunityBoard; import com.somemore.volunteer.domain.QVolunteer; @@ -14,6 +15,7 @@ import org.springframework.data.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -24,6 +26,7 @@ public class CommunityBoardRepositoryImpl implements CommunityBoardRepository { private final JPAQueryFactory queryFactory; private final CommunityBoardJpaRepository communityBoardJpaRepository; + private final CommunityBoardDocumentRepository documentRepository; private static final QCommunityBoard communityBoard = QCommunityBoard.communityBoard; private static final QVolunteer volunteer = QVolunteer.volunteer; @@ -83,6 +86,51 @@ public boolean existsById(Long id) { return communityBoardJpaRepository.existsByIdAndDeletedFalse(id); } + @Override + public Page findByCommunityBoardsContaining(String keyword, Pageable pageable) { + List boardDocuments = documentRepository.findIdsByTitleOrContentContaining(keyword); + + List boardIds = boardDocuments.stream() + .map(CommunityBoardDocument::getId) + .toList(); + + List content = getCommunityBoardsQuery() + .where(communityBoard.id.in(boardIds) + .and(isNotDeleted())) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = queryFactory + .select(communityBoard.count()) + .from(communityBoard) + .join(volunteer).on(communityBoard.writerId.eq(volunteer.id)) + .where(communityBoard.id.in(boardIds)); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + + @Override + public void saveDocuments(List communityBoards) { + List communityBoardDocuments = convertEntityToDocuments(communityBoards); + documentRepository.saveAll(communityBoardDocuments); + } + + @Override + public void deleteDocument(Long id) { + documentRepository.deleteById(id); + } + + @Override + public List findAll() { + return communityBoardJpaRepository.findAll(); + } + + @Override + public void deleteAllInBatch() { + communityBoardJpaRepository.deleteAllInBatch(); + } + private JPAQuery getCommunityBoardsQuery() { return queryFactory .select(Projections.constructor(CommunityBoardView.class, @@ -93,10 +141,18 @@ private JPAQuery getCommunityBoardsQuery() { .orderBy(communityBoard.createdAt.desc()); } - - @Override - public void deleteAllInBatch() { - communityBoardJpaRepository.deleteAllInBatch(); + private List convertEntityToDocuments(List communityBoards) { + List communityBoardDocuments = new ArrayList<>(); + + for (CommunityBoard communityboard : communityBoards) { + CommunityBoardDocument document = CommunityBoardDocument.builder() + .id(communityboard.getId()) + .title(communityboard.getTitle()) + .content(communityboard.getContent()) + .build(); + communityBoardDocuments.add(document); + } + return communityBoardDocuments; } private BooleanExpression isNotDeleted() { diff --git a/src/main/java/com/somemore/community/scheduler/CommunityScheduler.java b/src/main/java/com/somemore/community/scheduler/CommunityScheduler.java new file mode 100644 index 000000000..6eb624405 --- /dev/null +++ b/src/main/java/com/somemore/community/scheduler/CommunityScheduler.java @@ -0,0 +1,25 @@ +package com.somemore.community.scheduler; + +import com.somemore.community.domain.CommunityBoard; +import com.somemore.community.usecase.board.CommunityBoardDocumentUseCase; +import com.somemore.community.usecase.board.CommunityBoardQueryUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class CommunityScheduler { + + private final CommunityBoardQueryUseCase communityBoardQueryUseCase; + private final CommunityBoardDocumentUseCase communityBoardDocumentUseCase; + + + @Scheduled(cron = "10 * * * * *") + public void updateCommunityBoardDocuments() { + List communityBoards = communityBoardQueryUseCase.getAllCommunityBoards(); + communityBoardDocumentUseCase.saveCommunityBoardDocuments(communityBoards); + } +} diff --git a/src/main/java/com/somemore/community/service/board/CommunityBoardDocumentService.java b/src/main/java/com/somemore/community/service/board/CommunityBoardDocumentService.java new file mode 100644 index 000000000..a53935563 --- /dev/null +++ b/src/main/java/com/somemore/community/service/board/CommunityBoardDocumentService.java @@ -0,0 +1,37 @@ +package com.somemore.community.service.board; + +import com.somemore.community.domain.CommunityBoard; +import com.somemore.community.dto.response.CommunityBoardResponseDto; +import com.somemore.community.repository.board.CommunityBoardRepository; +import com.somemore.community.repository.mapper.CommunityBoardView; +import com.somemore.community.usecase.board.CommunityBoardDocumentUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@RequiredArgsConstructor +@Service +public class CommunityBoardDocumentService implements CommunityBoardDocumentUseCase { + + private final CommunityBoardRepository communityBoardRepository; + private static final int PAGE_SIZE = 10; + + @Transactional(readOnly = true) + @Override + public Page getCommunityBoardBySearch(String keyword, int page) { + Pageable pageable = PageRequest.of(page, PAGE_SIZE); + Page boards = communityBoardRepository.findByCommunityBoardsContaining(keyword, pageable); + return boards.map(CommunityBoardResponseDto::from); + } + + @Transactional + @Override + public void saveCommunityBoardDocuments(List communityBoards) { + communityBoardRepository.saveDocuments(communityBoards); + } +} diff --git a/src/main/java/com/somemore/community/service/board/CommunityBoardQueryService.java b/src/main/java/com/somemore/community/service/board/CommunityBoardQueryService.java index 0e516630a..b91d50f22 100644 --- a/src/main/java/com/somemore/community/service/board/CommunityBoardQueryService.java +++ b/src/main/java/com/somemore/community/service/board/CommunityBoardQueryService.java @@ -14,6 +14,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.UUID; import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_COMMUNITY_BOARD; @@ -46,4 +47,9 @@ public CommunityBoardDetailResponseDto getCommunityBoardDetail(Long id) { .orElseThrow(() -> new BadRequestException(NOT_EXISTS_COMMUNITY_BOARD.getMessage())); return CommunityBoardDetailResponseDto.from(board); } + + @Override + public List getAllCommunityBoards() { + return communityBoardRepository.findAll(); + } } diff --git a/src/main/java/com/somemore/community/usecase/board/CommunityBoardDocumentUseCase.java b/src/main/java/com/somemore/community/usecase/board/CommunityBoardDocumentUseCase.java new file mode 100644 index 000000000..86f7ca902 --- /dev/null +++ b/src/main/java/com/somemore/community/usecase/board/CommunityBoardDocumentUseCase.java @@ -0,0 +1,12 @@ +package com.somemore.community.usecase.board; + +import com.somemore.community.domain.CommunityBoard; +import com.somemore.community.dto.response.CommunityBoardResponseDto; +import org.springframework.data.domain.Page; + +import java.util.List; + +public interface CommunityBoardDocumentUseCase { + Page getCommunityBoardBySearch(String keyword, int page); + void saveCommunityBoardDocuments(List communityBoards); +} diff --git a/src/main/java/com/somemore/community/usecase/board/CommunityBoardQueryUseCase.java b/src/main/java/com/somemore/community/usecase/board/CommunityBoardQueryUseCase.java index 6e1d4c5c9..5a4011f85 100644 --- a/src/main/java/com/somemore/community/usecase/board/CommunityBoardQueryUseCase.java +++ b/src/main/java/com/somemore/community/usecase/board/CommunityBoardQueryUseCase.java @@ -1,13 +1,16 @@ package com.somemore.community.usecase.board; +import com.somemore.community.domain.CommunityBoard; import com.somemore.community.dto.response.CommunityBoardDetailResponseDto; import com.somemore.community.dto.response.CommunityBoardResponseDto; import org.springframework.data.domain.Page; +import java.util.List; import java.util.UUID; public interface CommunityBoardQueryUseCase { Page getCommunityBoards(int page); Page getCommunityBoardsByWriterId(UUID writerId, int page); CommunityBoardDetailResponseDto getCommunityBoardDetail(Long id); + List getAllCommunityBoards(); } diff --git a/src/main/java/com/somemore/global/configure/ElasticsearchConfig.java b/src/main/java/com/somemore/global/configure/ElasticsearchConfig.java new file mode 100644 index 000000000..e415fbe8c --- /dev/null +++ b/src/main/java/com/somemore/global/configure/ElasticsearchConfig.java @@ -0,0 +1,24 @@ +package com.somemore.global.configure; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration; + +@Configuration +public class ElasticsearchConfig extends ElasticsearchConfiguration { + @Value("${elastic.search.uris}") + private String uri; + @Value("${elastic.search.username}") + private String username; + @Value("${elastic.search.password}") + private String password; + + @Override + public ClientConfiguration clientConfiguration() { + return ClientConfiguration.builder() + .connectedTo(uri) + .withBasicAuth(username, password) + .build(); + } +} diff --git a/src/main/java/com/somemore/recruitboard/controller/RecruitBoardQueryApiController.java b/src/main/java/com/somemore/recruitboard/controller/RecruitBoardQueryApiController.java index 5465f066b..dfe7c26cc 100644 --- a/src/main/java/com/somemore/recruitboard/controller/RecruitBoardQueryApiController.java +++ b/src/main/java/com/somemore/recruitboard/controller/RecruitBoardQueryApiController.java @@ -11,6 +11,7 @@ import com.somemore.recruitboard.dto.response.RecruitBoardResponseDto; import com.somemore.recruitboard.dto.response.RecruitBoardWithCenterResponseDto; import com.somemore.recruitboard.dto.response.RecruitBoardWithLocationResponseDto; +import com.somemore.recruitboard.usecase.query.RecruitBoardDocumentUseCase; import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -32,6 +33,7 @@ public class RecruitBoardQueryApiController { private final RecruitBoardQueryUseCase recruitBoardQueryUseCase; + private final RecruitBoardDocumentUseCase recruitBoardDocumentUseCase; @GetMapping("/recruit-board/{id}") @Operation(summary = "봉사 모집글 상세 조회", description = "특정 모집글의 상세 정보를 조회합니다.") @@ -83,7 +85,7 @@ public ApiResponse> getAllBySearch( return ApiResponse.ok( 200, - recruitBoardQueryUseCase.getAllWithCenter(condition), + recruitBoardDocumentUseCase.getRecruitBoardBySearch(keyword, condition), "봉사 활동 모집글 검색 조회 성공" ); } diff --git a/src/main/java/com/somemore/recruitboard/domain/RecruitBoardDocument.java b/src/main/java/com/somemore/recruitboard/domain/RecruitBoardDocument.java new file mode 100644 index 000000000..e95aa422f --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/domain/RecruitBoardDocument.java @@ -0,0 +1,30 @@ +package com.somemore.recruitboard.domain; + +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +@Getter +@Document(indexName = "recruit_board") +public class RecruitBoardDocument { + + @Id + private Long id; + + @Field(type = FieldType.Text, analyzer = "nori_analyzer") + private String title; + + @Field(type = FieldType.Text, analyzer = "nori_analyzer") + private String content; + + + @Builder + public RecruitBoardDocument(Long id, String title, String content) { + this.id = id; + this.title = title; + this.content = content; + } +} \ No newline at end of file diff --git a/src/main/java/com/somemore/recruitboard/repository/RecruitBoardDocumentRepository.java b/src/main/java/com/somemore/recruitboard/repository/RecruitBoardDocumentRepository.java new file mode 100644 index 000000000..aaa0fe9b6 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/repository/RecruitBoardDocumentRepository.java @@ -0,0 +1,12 @@ +package com.somemore.recruitboard.repository; + +import com.somemore.recruitboard.domain.RecruitBoardDocument; +import org.springframework.data.elasticsearch.annotations.Query; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +import java.util.List; + +public interface RecruitBoardDocumentRepository extends ElasticsearchRepository { + @Query("{\"multi_match\": {\"query\": \"?0\", \"fields\": [\"title\", \"content\"]}}") + List findIdsByTitleOrContentContaining(String keyword); +} diff --git a/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepository.java b/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepository.java index 1f25c824d..b4b34c9f0 100644 --- a/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepository.java +++ b/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepository.java @@ -30,4 +30,9 @@ public interface RecruitBoardRepository { List findNotCompletedIdsByCenterId(UUID centerId); List findAllByIds(List ids); + + Page findByRecruitBoardsContaining(String keyword, RecruitBoardSearchCondition condition); + void saveDocuments(List recruitBoards); + List findAll(); + void deleteDocument(Long id); } diff --git a/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImpl.java b/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImpl.java index a2d68ede6..496c23124 100644 --- a/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImpl.java +++ b/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImpl.java @@ -12,15 +12,14 @@ import com.somemore.center.domain.QCenter; import com.somemore.location.domain.QLocation; import com.somemore.location.utils.GeoUtils; -import com.somemore.recruitboard.domain.QRecruitBoard; -import com.somemore.recruitboard.domain.RecruitBoard; -import com.somemore.recruitboard.domain.RecruitStatus; -import com.somemore.recruitboard.domain.VolunteerCategory; +import com.somemore.recruitboard.domain.*; import com.somemore.recruitboard.dto.condition.RecruitBoardNearByCondition; import com.somemore.recruitboard.dto.condition.RecruitBoardSearchCondition; import com.somemore.recruitboard.repository.mapper.RecruitBoardDetail; import com.somemore.recruitboard.repository.mapper.RecruitBoardWithCenter; import com.somemore.recruitboard.repository.mapper.RecruitBoardWithLocation; + +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -37,6 +36,7 @@ public class RecruitBoardRepositoryImpl implements RecruitBoardRepository { private final RecruitBoardJpaRepository recruitBoardJpaRepository; + private final RecruitBoardDocumentRepository documentRepository; private final JPAQueryFactory queryFactory; @Override @@ -196,6 +196,60 @@ public Page findAllByCenterId(UUID centerId, return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } + @Override + public Page findByRecruitBoardsContaining(String keyword, RecruitBoardSearchCondition condition) { + QRecruitBoard recruitBoard = QRecruitBoard.recruitBoard; + QCenter center = QCenter.center; + + List boardDocuments = documentRepository.findIdsByTitleOrContentContaining(keyword); + + List boardIds = boardDocuments.stream() + .map(RecruitBoardDocument::getId) + .toList(); + + Pageable pageable = condition.pageable(); + BooleanExpression predicate = isNotDeleted() + .and(volunteerCategoryEq(condition.category())) + .and(regionEq(condition.region())) + .and(admittedEq(condition.admitted())) + .and(statusEq(condition.status())); + + List content = queryFactory + .select(getRecruitBoardWithCenterConstructorExpression(recruitBoard, center)) + .from(recruitBoard) + .where(recruitBoard.id.in(boardIds) + .and(predicate)) + .join(center).on(recruitBoard.centerId.eq(center.id)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(toOrderSpecifiers(pageable.getSort())) + .fetch(); + + JPAQuery countQuery = queryFactory + .select(recruitBoard.count()) + .from(recruitBoard) + .join(center).on(recruitBoard.centerId.eq(center.id)) + .where(predicate); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + + @Override + public void saveDocuments(List recruitBoards) { + List recruitBoardDocuments = convertEntityToDocuments(recruitBoards); + documentRepository.saveAll(recruitBoardDocuments); + } + + @Override + public List findAll() { + return recruitBoardJpaRepository.findAll(); + } + + @Override + public void deleteDocument(Long id) { + documentRepository.deleteById(id); + } + private static BooleanExpression idEq(Long id) { return recruitBoard.id.eq(id); } @@ -294,4 +348,17 @@ private static ConstructorExpression getRecruitBoardDetailCo recruitBoard, location.address, location.latitude, location.longitude, center.name); } + private List convertEntityToDocuments(List recruitBoards) { + List communityBoardDocuments = new ArrayList<>(); + + for (RecruitBoard recruitBoard : recruitBoards) { + RecruitBoardDocument document = RecruitBoardDocument.builder() + .id(recruitBoard.getId()) + .title(recruitBoard.getTitle()) + .content(recruitBoard.getContent()) + .build(); + communityBoardDocuments.add(document); + } + return communityBoardDocuments; + } } diff --git a/src/main/java/com/somemore/recruitboard/scheduler/RecruitBoardScheduler.java b/src/main/java/com/somemore/recruitboard/scheduler/RecruitBoardScheduler.java new file mode 100644 index 000000000..41957a64c --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/scheduler/RecruitBoardScheduler.java @@ -0,0 +1,25 @@ +package com.somemore.recruitboard.scheduler; + +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.repository.RecruitBoardRepository; +import com.somemore.recruitboard.usecase.query.RecruitBoardDocumentUseCase; +import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class RecruitBoardScheduler { + + private final RecruitBoardQueryUseCase recruitBoardQueryUseCase; + private final RecruitBoardDocumentUseCase recruitBoardDocumentUseCase; + + @Scheduled(cron = "10 * * * * *") + public void updateRecruitBoardDocuments() { + List recruitBoards = recruitBoardQueryUseCase.getAllRecruitBoards(); + recruitBoardDocumentUseCase.saveRecruitBoardDocuments(recruitBoards); + } +} diff --git a/src/main/java/com/somemore/recruitboard/service/query/RecruitBoardDocumentService.java b/src/main/java/com/somemore/recruitboard/service/query/RecruitBoardDocumentService.java new file mode 100644 index 000000000..f202b3423 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/service/query/RecruitBoardDocumentService.java @@ -0,0 +1,34 @@ +package com.somemore.recruitboard.service.query; + +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.dto.condition.RecruitBoardSearchCondition; +import com.somemore.recruitboard.dto.response.RecruitBoardWithCenterResponseDto; +import com.somemore.recruitboard.repository.RecruitBoardRepository; +import com.somemore.recruitboard.repository.mapper.RecruitBoardWithCenter; +import com.somemore.recruitboard.usecase.query.RecruitBoardDocumentUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@RequiredArgsConstructor +@Service +public class RecruitBoardDocumentService implements RecruitBoardDocumentUseCase { + + private final RecruitBoardRepository recruitBoardRepository; + + @Transactional(readOnly = true) + @Override + public Page getRecruitBoardBySearch(String keyword, RecruitBoardSearchCondition condition) { + Page boards = recruitBoardRepository.findByRecruitBoardsContaining(keyword, condition); + return boards.map(RecruitBoardWithCenterResponseDto::from); + } + + @Transactional + @Override + public void saveRecruitBoardDocuments(List recruitBoards) { + recruitBoardRepository.saveDocuments(recruitBoards); + } +} diff --git a/src/main/java/com/somemore/recruitboard/service/query/RecruitBoardQueryService.java b/src/main/java/com/somemore/recruitboard/service/query/RecruitBoardQueryService.java index c93420b09..f9f768395 100644 --- a/src/main/java/com/somemore/recruitboard/service/query/RecruitBoardQueryService.java +++ b/src/main/java/com/somemore/recruitboard/service/query/RecruitBoardQueryService.java @@ -86,4 +86,8 @@ public List getAllByIds(List ids) { return recruitBoardRepository.findAllByIds(ids); } + @Override + public List getAllRecruitBoards() { + return recruitBoardRepository.findAll(); + } } diff --git a/src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardDocumentUseCase.java b/src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardDocumentUseCase.java new file mode 100644 index 000000000..eb88b44c7 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardDocumentUseCase.java @@ -0,0 +1,13 @@ +package com.somemore.recruitboard.usecase.query; + +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.dto.condition.RecruitBoardSearchCondition; +import com.somemore.recruitboard.dto.response.RecruitBoardWithCenterResponseDto; +import org.springframework.data.domain.Page; + +import java.util.List; + +public interface RecruitBoardDocumentUseCase { + Page getRecruitBoardBySearch(String keyword, RecruitBoardSearchCondition condition); + void saveRecruitBoardDocuments(List recruitBoards); +} diff --git a/src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardQueryUseCase.java b/src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardQueryUseCase.java index f940244b0..e355920db 100644 --- a/src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardQueryUseCase.java +++ b/src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardQueryUseCase.java @@ -30,4 +30,7 @@ Page getRecruitBoardsByCenterId(UUID centerId, List getNotCompletedIdsByCenterIds(UUID centerId); List getAllByIds(List ids); + + List getAllRecruitBoards(); + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b350d50cd..9c220f2e5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -41,6 +41,10 @@ spring: port: ${REDIS_PORT} password: ${REDIS_PASSWORD} + elasticsearch: + repositories: + enabled: true + security: oauth2: client: @@ -105,3 +109,14 @@ server: default: image: url: ${DEFAULT_IMG_URL} + +elastic: + search: + uris: ${ELASTIC_URI} + username: ${ELASTIC_USERNAME} + password: ${ELASTIC_PASSWORD} + +management: + health: + elasticsearch: + enabled: false diff --git a/src/test/java/com/somemore/common/fixture/CommunityBoardFixture.java b/src/test/java/com/somemore/common/fixture/CommunityBoardFixture.java index a55264dc7..4856e49a9 100644 --- a/src/test/java/com/somemore/common/fixture/CommunityBoardFixture.java +++ b/src/test/java/com/somemore/common/fixture/CommunityBoardFixture.java @@ -39,4 +39,12 @@ public static CommunityBoard createCommunityBoard(String title, UUID writerId) { .writerId(writerId) .build(); } + public static CommunityBoard createCommunityBoard(String title, String content, UUID writerId) { + return CommunityBoard.builder() + .title(title) + .content(content) + .imgUrl(IMG_URL) + .writerId(writerId) + .build(); + } } diff --git a/src/test/java/com/somemore/community/controller/CommunityBoardQueryApiControllerTest.java b/src/test/java/com/somemore/community/controller/CommunityBoardQueryApiControllerTest.java index 74e05b775..cf45a35a7 100644 --- a/src/test/java/com/somemore/community/controller/CommunityBoardQueryApiControllerTest.java +++ b/src/test/java/com/somemore/community/controller/CommunityBoardQueryApiControllerTest.java @@ -11,6 +11,7 @@ import com.somemore.ControllerTestSupport; import com.somemore.community.dto.response.CommunityBoardDetailResponseDto; import com.somemore.community.dto.response.CommunityBoardResponseDto; +import com.somemore.community.usecase.board.CommunityBoardDocumentUseCase; import com.somemore.community.usecase.board.CommunityBoardQueryUseCase; import java.util.Collections; import java.util.UUID; @@ -32,6 +33,9 @@ public class CommunityBoardQueryApiControllerTest extends ControllerTestSupport @MockBean private CommunityBoardQueryUseCase communityBoardQueryUseCase; + @MockBean + private CommunityBoardDocumentUseCase communityBoardDocumentUseCase; + @Test @DisplayName("커뮤니티 게시글 전체 조회 성공") void getAll() throws Exception { @@ -101,4 +105,27 @@ void getById() throws Exception { verify(communityBoardQueryUseCase, times(1)) .getCommunityBoardDetail(any()); } + + @Test + @DisplayName("커뮤니티 게시글 검색 조회 성공") + void getBySearch() throws Exception { + // given + Page page = new PageImpl<>(Collections.emptyList()); + + given(communityBoardDocumentUseCase.getCommunityBoardBySearch(any(), anyInt())) + .willReturn(page); + + // when + // then + mockMvc.perform(get("/api/community-boards/search") + .param("keyword", "봉사") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").exists()) + .andExpect(jsonPath("$.message").value("커뮤니티 게시글 검색 리스트 조회 성공")); + + verify(communityBoardDocumentUseCase, times(1)).getCommunityBoardBySearch( + any(), anyInt()); + } } diff --git a/src/test/java/com/somemore/community/repository/CommunityBoardDocumentRepositoryTest.java b/src/test/java/com/somemore/community/repository/CommunityBoardDocumentRepositoryTest.java new file mode 100644 index 000000000..8c17af2db --- /dev/null +++ b/src/test/java/com/somemore/community/repository/CommunityBoardDocumentRepositoryTest.java @@ -0,0 +1,63 @@ +package com.somemore.community.repository; + +import com.somemore.IntegrationTestSupport; +import com.somemore.auth.oauth.OAuthProvider; +import com.somemore.community.domain.CommunityBoard; +import com.somemore.community.repository.board.CommunityBoardRepository; +import com.somemore.community.repository.mapper.CommunityBoardView; +import com.somemore.volunteer.domain.Volunteer; +import com.somemore.volunteer.repository.VolunteerRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.annotation.Transactional; + + +import static com.somemore.common.fixture.CommunityBoardFixture.createCommunityBoard; +import static org.assertj.core.api.Assertions.assertThat; + +@Transactional +public class CommunityBoardDocumentRepositoryTest extends IntegrationTestSupport { + + @Autowired + private CommunityBoardRepository communityBoardRepository; + @Autowired + private VolunteerRepository volunteerRepository; + + @BeforeEach + void setUp() { + String oAuthId = "example-oauth-id2"; + Volunteer volunteer = Volunteer.createDefault(OAuthProvider.NAVER, oAuthId); + volunteerRepository.save(volunteer); + + for (int i = 1; i <= 20; i++) { + String title = "제목" + i; + CommunityBoard communityBoard = createCommunityBoard(title, volunteer.getId()); + communityBoardRepository.save(communityBoard); + } + } + + @DisplayName("검색 키워드가 포함된 게시글을 조회할 수 있다. (repository)") + @Test + void findByCommunityBoardsContaining() throws InterruptedException { + //given + Pageable pageable = getPageable(); + + //when + Page findBoards = communityBoardRepository.findByCommunityBoardsContaining("봉사", pageable); + + //then + assertThat(findBoards).isNotNull(); + assertThat(findBoards.getTotalElements()).isEqualTo(9); + assertThat(findBoards.getSize()).isEqualTo(10); + assertThat(findBoards.getTotalPages()).isEqualTo(1); + } + + private Pageable getPageable() { + return PageRequest.of(0, 10); + } +} diff --git a/src/test/java/com/somemore/community/repository/CommunityBoardRepositoryTest.java b/src/test/java/com/somemore/community/repository/CommunityBoardRepositoryTest.java index 1d7202844..a4949fcd8 100644 --- a/src/test/java/com/somemore/community/repository/CommunityBoardRepositoryTest.java +++ b/src/test/java/com/somemore/community/repository/CommunityBoardRepositoryTest.java @@ -18,6 +18,8 @@ import static com.somemore.common.fixture.CommunityBoardFixture.createCommunityBoard; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -138,6 +140,34 @@ void existsById() { assertThat(isExist).isTrue(); } + @DisplayName("게시글을 elastic search index에 저장할 수 있다. (repository)") + @Test + void saveDocuments() { + //given + Pageable pageable = getPageable(); + + List communityBoards = new ArrayList<>(); + + CommunityBoard communityBoard1 = createCommunityBoard("저장 잘 되나요?", writerId); + CommunityBoard savedBoard1 = communityBoardRepository.save(communityBoard1); + CommunityBoard communityBoard2 = createCommunityBoard("잘 되나요?", "저장이요", writerId); + CommunityBoard savedBoard2 = communityBoardRepository.save(communityBoard2); + communityBoards.add(savedBoard1); + communityBoards.add(savedBoard2); + + //when + communityBoardRepository.saveDocuments(communityBoards); + + //then + Page findBoard = communityBoardRepository.findByCommunityBoardsContaining("저장", pageable); + + assertThat(findBoard).isNotNull(); + assertThat(findBoard.getTotalElements()).isEqualTo(2); + + communityBoardRepository.deleteDocument(savedBoard1.getId()); + communityBoardRepository.deleteDocument(savedBoard2.getId()); + } + private Pageable getPageable() { return PageRequest.of(0, 10); } diff --git a/src/test/java/com/somemore/community/service/board/CommunityBoardDocumentServiceTest.java b/src/test/java/com/somemore/community/service/board/CommunityBoardDocumentServiceTest.java new file mode 100644 index 000000000..49b7678e3 --- /dev/null +++ b/src/test/java/com/somemore/community/service/board/CommunityBoardDocumentServiceTest.java @@ -0,0 +1,68 @@ +package com.somemore.community.service.board; + +import com.somemore.IntegrationTestSupport; +import com.somemore.auth.oauth.OAuthProvider; +import com.somemore.community.domain.CommunityBoard; +import com.somemore.community.dto.response.CommunityBoardResponseDto; +import com.somemore.community.repository.board.CommunityBoardRepository; +import com.somemore.volunteer.domain.Volunteer; +import com.somemore.volunteer.repository.VolunteerRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; + +import java.util.UUID; + +import static com.somemore.common.fixture.CommunityBoardFixture.createCommunityBoard; +import static org.assertj.core.api.Assertions.assertThat; + +public class CommunityBoardDocumentServiceTest extends IntegrationTestSupport { + + @Autowired + private CommunityBoardDocumentService communityBoardDocumentService; + + @Autowired + private CommunityBoardRepository communityBoardRepository; + + @Autowired + private VolunteerRepository volunteerRepository; + + private UUID writerId; + + @BeforeEach + void setUp() { + String oAuthId1 = "example-oauth-id1"; + Volunteer volunteer1 = Volunteer.createDefault(OAuthProvider.NAVER, oAuthId1); + volunteerRepository.save(volunteer1); + writerId = volunteer1.getId(); + + for (int i = 1; i <= 18; i++) { + String title = "제목" + i; + CommunityBoard communityBoard = createCommunityBoard(title, writerId); + communityBoardRepository.save(communityBoard); + } + } + + @AfterEach + void tearDown() { + communityBoardRepository.deleteAllInBatch(); + } + + @DisplayName("검색 키워드가 포함된 게시글을 조회한다. (service)") + @Test + void getCommunityBoardBySearch() { + //given + //when + Page dtos = communityBoardDocumentService.getCommunityBoardBySearch("봉사", 0); + + //then + assertThat(dtos).isNotNull(); + assertThat(dtos.getContent()).isNotNull(); + assertThat(dtos.getTotalElements()).isEqualTo(9); + assertThat(dtos.getSize()).isEqualTo(10); + assertThat(dtos.getTotalPages()).isEqualTo(1); + } +} diff --git a/src/test/java/com/somemore/community/service/board/CommunityBoardQueryServiceTest.java b/src/test/java/com/somemore/community/service/board/CommunityBoardQueryServiceTest.java index 44c05fe98..ac170f2a5 100644 --- a/src/test/java/com/somemore/community/service/board/CommunityBoardQueryServiceTest.java +++ b/src/test/java/com/somemore/community/service/board/CommunityBoardQueryServiceTest.java @@ -21,6 +21,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -43,6 +45,8 @@ class CommunityBoardQueryServiceTest extends IntegrationTestSupport { DeleteCommunityBoardUseCase deleteCommunityBoardUseCase; @Autowired CommunityBoardQueryService communityBoardQueryService; + @Autowired + private CommunityBoardDocumentService communityBoardDocumentService; private UUID writerId1; private Long communityId1; @@ -146,5 +150,30 @@ void getCommunityBoardDetailWithDeletedId() { .isThrownBy(callable) .withMessage(ExceptionMessage.NOT_EXISTS_COMMUNITY_BOARD.getMessage()); } + + @DisplayName("게시글을 elastic search index에 저장한다. (service)") + @Test + void saveCommunityBoardDocuments() { + //given + List communityBoards = new ArrayList<>(); + + CommunityBoard communityBoard = createCommunityBoard("저장 잘 되나요?", "안녕하세요", UUID.randomUUID()); + CommunityBoard savedBoard = communityBoardRepository.save(communityBoard); + communityBoards.add(savedBoard); + + //when + communityBoardDocumentService.saveCommunityBoardDocuments(communityBoards); + + //then + Page dtos = communityBoardDocumentService.getCommunityBoardBySearch("저장", 0); + + assertThat(dtos).isNotNull(); + assertThat(dtos.getContent()).isNotNull(); + assertThat(dtos.getTotalElements()).isEqualTo(1); + assertThat(dtos.getSize()).isEqualTo(10); + assertThat(dtos.getTotalPages()).isEqualTo(1); + + communityBoardRepository.deleteDocument(savedBoard.getId()); + } } diff --git a/src/test/java/com/somemore/recruitboard/controller/RecruitBoardQueryApiControllerTest.java b/src/test/java/com/somemore/recruitboard/controller/RecruitBoardQueryApiControllerTest.java index 5a48b9775..be12ad1a1 100644 --- a/src/test/java/com/somemore/recruitboard/controller/RecruitBoardQueryApiControllerTest.java +++ b/src/test/java/com/somemore/recruitboard/controller/RecruitBoardQueryApiControllerTest.java @@ -17,6 +17,7 @@ import com.somemore.recruitboard.dto.response.RecruitBoardResponseDto; import com.somemore.recruitboard.dto.response.RecruitBoardWithCenterResponseDto; import com.somemore.recruitboard.dto.response.RecruitBoardWithLocationResponseDto; +import com.somemore.recruitboard.usecase.query.RecruitBoardDocumentUseCase; import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase; import java.util.Collections; import java.util.UUID; @@ -37,6 +38,9 @@ class RecruitBoardQueryApiControllerTest extends ControllerTestSupport { @MockBean private RecruitBoardQueryUseCase recruitBoardQueryUseCase; + @MockBean + private RecruitBoardDocumentUseCase documentUseCase; + @Test @DisplayName("모집글 ID로 상세 조회할 수 있다.") void getById() throws Exception { @@ -85,7 +89,7 @@ void getAllBySearch() throws Exception { // given Page page = new PageImpl<>(Collections.emptyList()); - given(recruitBoardQueryUseCase.getAllWithCenter(any(RecruitBoardSearchCondition.class))) + given(documentUseCase.getRecruitBoardBySearch(any(), any(RecruitBoardSearchCondition.class))) .willReturn(page); // when @@ -99,8 +103,8 @@ void getAllBySearch() throws Exception { .andExpect(jsonPath("$.data").exists()) .andExpect(jsonPath("$.message").value("봉사 활동 모집글 검색 조회 성공")); - verify(recruitBoardQueryUseCase, times(1)).getAllWithCenter( - any(RecruitBoardSearchCondition.class)); + verify(documentUseCase, times(1)).getRecruitBoardBySearch( + any(), any(RecruitBoardSearchCondition.class)); } @Test diff --git a/src/test/java/com/somemore/recruitboard/repository/RecruitBoardDocumentRepositoryTest.java b/src/test/java/com/somemore/recruitboard/repository/RecruitBoardDocumentRepositoryTest.java new file mode 100644 index 000000000..a9d508503 --- /dev/null +++ b/src/test/java/com/somemore/recruitboard/repository/RecruitBoardDocumentRepositoryTest.java @@ -0,0 +1,82 @@ +package com.somemore.recruitboard.repository; + +import com.somemore.IntegrationTestSupport; +import com.somemore.center.domain.Center; +import com.somemore.center.repository.center.CenterRepository; +import com.somemore.location.domain.Location; +import com.somemore.location.repository.LocationRepository; +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.dto.condition.RecruitBoardSearchCondition; +import com.somemore.recruitboard.repository.mapper.RecruitBoardWithCenter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +import static com.somemore.common.fixture.CenterFixture.createCenter; +import static com.somemore.common.fixture.LocationFixture.createLocation; +import static com.somemore.common.fixture.RecruitBoardFixture.createRecruitBoard; +import static org.assertj.core.api.Assertions.assertThat; + +@Transactional +public class RecruitBoardDocumentRepositoryTest extends IntegrationTestSupport { + + @Autowired + private RecruitBoardRepository recruitBoardRepository; + + @Autowired + private LocationRepository locationRepository; + + @Autowired + private CenterRepository centerRepository; + + private final List boards = new ArrayList<>(); + + @BeforeEach + void setUp() { + Location location = createLocation(); + locationRepository.save(location); + + Center center = createCenter(); + centerRepository.save(center); + + for (int i = 1; i <= 30; i++) { + String title = "제목" + i; + RecruitBoard board = createRecruitBoard(title, center.getId(), location.getId()); + boards.add(board); + } + recruitBoardRepository.saveAll(boards); + } + + @DisplayName("검색 키워드가 포함된 모집글을 조회할 수 있다.") + @Test + void findByRecruitBoardsContaining() { + //given + Pageable pageable = getPageable(); + RecruitBoardSearchCondition condition = RecruitBoardSearchCondition.builder() + .pageable(pageable) + .build(); + + //when + Page findBoards = recruitBoardRepository.findByRecruitBoardsContaining("없음", condition); + + //then + assertThat(findBoards).isNotNull(); + assertThat(findBoards.getTotalElements()).isEqualTo(4); + assertThat(findBoards.getSize()).isEqualTo(5); + assertThat(findBoards.getTotalPages()).isEqualTo(1); + } + + private Pageable getPageable() { + Sort sort = Sort.by(Sort.Order.desc("created_at")); + return PageRequest.of(0, 5, sort); + } +} diff --git a/src/test/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImplTest.java b/src/test/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImplTest.java index 1b0268c59..b753c9fe8 100644 --- a/src/test/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImplTest.java +++ b/src/test/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImplTest.java @@ -51,6 +51,8 @@ class RecruitBoardRepositoryImplTest extends IntegrationTestSupport { private final List boards = new ArrayList<>(); + private UUID centerId; + @BeforeEach void setUp() { Location location = createLocation(); @@ -58,6 +60,7 @@ void setUp() { Center center = createCenter(); centerRepository.save(center); + centerId = center.getId(); for (int i = 1; i <= 100; i++) { String title = "제목" + i; @@ -426,6 +429,37 @@ void findAllByIds() { assertThat(all).hasSize(3); } + @DisplayName("모집글을 elastic search index에 저장할 수 있다. (repository)") + @Test + void saveDocuments() { + //given + Pageable pageable = getPageable(); + RecruitBoardSearchCondition condition = RecruitBoardSearchCondition.builder() + .pageable(pageable) + .build(); + + List recruitBoards = new ArrayList<>(); + + RecruitBoard board1 = createRecruitBoard("저장 잘 되나요?", centerId); + RecruitBoard savedBoard1 = recruitBoardRepository.save(board1); + RecruitBoard board2 = createRecruitBoard("저장해줘", centerId); + RecruitBoard savedBoard2 = recruitBoardRepository.save(board2); + recruitBoards.add(savedBoard1); + recruitBoards.add(savedBoard2); + + //when + recruitBoardRepository.saveDocuments(recruitBoards); + + //then + Page findBoard = recruitBoardRepository.findByRecruitBoardsContaining("저장", condition); + + assertThat(findBoard).isNotNull(); + assertThat(findBoard.getTotalElements()).isEqualTo(2); + + recruitBoardRepository.deleteDocument(savedBoard1.getId()); + recruitBoardRepository.deleteDocument(savedBoard2.getId()); + } + private Pageable getPageable() { Sort sort = Sort.by(Sort.Order.desc("created_at")); return PageRequest.of(0, 5, sort); diff --git a/src/test/java/com/somemore/recruitboard/service/query/RecruitBoardDocumentServiceTest.java b/src/test/java/com/somemore/recruitboard/service/query/RecruitBoardDocumentServiceTest.java new file mode 100644 index 000000000..2112381b5 --- /dev/null +++ b/src/test/java/com/somemore/recruitboard/service/query/RecruitBoardDocumentServiceTest.java @@ -0,0 +1,87 @@ +package com.somemore.recruitboard.service.query; + +import com.somemore.IntegrationTestSupport; +import com.somemore.center.domain.Center; +import com.somemore.center.repository.center.CenterRepository; +import com.somemore.location.domain.Location; +import com.somemore.location.repository.LocationRepository; +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.dto.condition.RecruitBoardSearchCondition; +import com.somemore.recruitboard.dto.response.RecruitBoardWithCenterResponseDto; +import com.somemore.recruitboard.repository.RecruitBoardRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +import static com.somemore.common.fixture.CenterFixture.createCenter; +import static com.somemore.common.fixture.LocationFixture.createLocation; +import static com.somemore.common.fixture.RecruitBoardFixture.createRecruitBoard; +import static org.assertj.core.api.Assertions.assertThat; + +@Transactional +public class RecruitBoardDocumentServiceTest extends IntegrationTestSupport { + + @Autowired + private RecruitBoardDocumentService recruitBoardDocumentService; + + @Autowired + private RecruitBoardRepository recruitBoardRepository; + + @Autowired + private LocationRepository locationRepository; + + @Autowired + private CenterRepository centerRepository; + + private final List boards = new ArrayList<>(); + + @BeforeEach + public void setUp() { + Location location = createLocation(); + locationRepository.save(location); + + Center center = createCenter(); + centerRepository.save(center); + + for (int i = 1; i <= 30; i++) { + String title = "제목" + i; + RecruitBoard board = createRecruitBoard(title, center.getId(), location.getId()); + boards.add(board); + } + recruitBoardRepository.saveAll(boards); + } + + @DisplayName("검색 키워드가 포함된 모집글을 조회한다. (service)") + @Test + void getRecruitBoardBySearch() { + //given + Pageable pageable = getPageable(); + RecruitBoardSearchCondition condition = RecruitBoardSearchCondition.builder() + .pageable(pageable) + .build(); + + //when + Page dtos = recruitBoardDocumentService.getRecruitBoardBySearch("노인", condition); + + //then + assertThat(dtos).isNotNull(); + assertThat(dtos.getContent()).isNotNull(); + assertThat(dtos.getTotalElements()).isEqualTo(4); + assertThat(dtos.getSize()).isEqualTo(5); + assertThat(dtos.getTotalPages()).isEqualTo(1); + } + + private Pageable getPageable() { + Sort sort = Sort.by(Sort.Order.desc("created_at")); + return PageRequest.of(0, 5, sort); + } +} diff --git a/src/test/java/com/somemore/recruitboard/service/query/RecruitBoardQueryServiceTest.java b/src/test/java/com/somemore/recruitboard/service/query/RecruitBoardQueryServiceTest.java index 7cdcc6b2c..533ef93e3 100644 --- a/src/test/java/com/somemore/recruitboard/service/query/RecruitBoardQueryServiceTest.java +++ b/src/test/java/com/somemore/recruitboard/service/query/RecruitBoardQueryServiceTest.java @@ -25,6 +25,8 @@ import com.somemore.recruitboard.dto.response.RecruitBoardWithCenterResponseDto; import com.somemore.recruitboard.dto.response.RecruitBoardWithLocationResponseDto; import com.somemore.recruitboard.repository.RecruitBoardRepository; + +import java.util.ArrayList; import java.util.List; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; @@ -43,6 +45,9 @@ class RecruitBoardQueryServiceTest extends IntegrationTestSupport { @Autowired private RecruitBoardQueryService recruitBoardQueryService; + @Autowired + private RecruitBoardDocumentService recruitBoardDocumentService; + @Autowired private RecruitBoardRepository recruitBoardRepository; @@ -280,6 +285,40 @@ void getAllByIds() { assertThat(all).hasSize(3); } + @DisplayName("모집글을 elastic search index에 저장한다. (service)") + @Test + void saveRecruitBoardDocuments() { + //given + Center center = createCenter("특별한 기관"); + centerRepository.save(center); + + Pageable pageable = getPageable(); + RecruitBoardSearchCondition condition = RecruitBoardSearchCondition.builder() + .pageable(pageable) + .build(); + + List recruitBoards = new ArrayList<>(); + + RecruitBoard board1 = createRecruitBoard("저장 잘 되나요?", center.getId()); + RecruitBoard savedBoard1 = recruitBoardRepository.save(board1); + RecruitBoard board2 = createRecruitBoard("저장해줘", center.getId()); + RecruitBoard savedBoard2 = recruitBoardRepository.save(board2); + recruitBoards.add(savedBoard1); + recruitBoards.add(savedBoard2); + + //when + recruitBoardDocumentService.saveRecruitBoardDocuments(recruitBoards); + + //then + Page findBoard = recruitBoardDocumentService.getRecruitBoardBySearch("저장", condition); + + assertThat(findBoard).isNotNull(); + assertThat(findBoard.getTotalElements()).isEqualTo(2); + + recruitBoardRepository.deleteDocument(savedBoard1.getId()); + recruitBoardRepository.deleteDocument(savedBoard2.getId()); + } + private Pageable getPageable() { Sort sort = Sort.by(Sort.Order.desc("created_at")); return PageRequest.of(0, 5, sort); diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 537ef65eb..00940b63f 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -33,6 +33,10 @@ spring: port: 6379 password: # 테스트에서는 비밀번호 없이 연결 + elasticsearch: + repositories: + enabled: true + web: locale: ko_KR locale-resolver: fixed @@ -56,10 +60,21 @@ cloud: static: ap-northeast-2 s3: bucket: somemore - base-url: https://somemore-image.s3.ap-northeast-2.amazonaws.com + base-url: https://somemore-images.s3.ap-northeast-2.amazonaws.com/ stack: auto: false default: image: url: "" + +elastic: + search: + uris: localhost:9200 + username: elastic + password: somemore + +management: + health: + elasticsearch: + enabled: false