Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/somemore/SomemoreApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,6 +21,7 @@
public class CommunityBoardQueryApiController {

private final CommunityBoardQueryUseCase communityBoardQueryUseCase;
private final CommunityBoardDocumentUseCase communityBoardDocumentUseCase;

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

@GetMapping("/community-boards/search")
@Operation(summary = "커뮤니티 게시글 키워드 검색", description = "키워드로 포함한 커뮤니티 게시글 목록을 조회합니다.")
public ApiResponse<Page<CommunityBoardResponseDto>> getCommunityBoardsBySearch(
String keyword,
Pageable pageable
) {
return ApiResponse.ok(
200,
communityBoardDocumentUseCase.getCommunityBoardBySearch(keyword, pageable.getPageNumber()),
"커뮤니티 게시글 검색 리스트 조회 성공"
);
}

@GetMapping("/community-board/{id}")
@Operation(summary = "커뮤니티 게시글 상세 조회", description = "커뮤니티 게시글의 상세 정보를 조회합니다.")
public ApiResponse<CommunityBoardDetailResponseDto> getById(
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<CommunityBoardDocument, Long> {
@Query("{\"multi_match\": {\"query\": \"?0\", \"fields\": [\"title\", \"content\"]}}")
List<CommunityBoardDocument> findIdsByTitleOrContentContaining(String keyword);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -18,4 +19,9 @@ default boolean doesNotExistById(Long id) {
return !existsById(id);
}
void deleteAllInBatch();

Page<CommunityBoardView> findByCommunityBoardsContaining(String keyword, Pageable pageable);
void saveDocuments(List<CommunityBoard> communityBoards);
List<CommunityBoard> findAll();
void deleteDocument(Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -83,6 +86,51 @@ public boolean existsById(Long id) {
return communityBoardJpaRepository.existsByIdAndDeletedFalse(id);
}

@Override
public Page<CommunityBoardView> findByCommunityBoardsContaining(String keyword, Pageable pageable) {
List<CommunityBoardDocument> boardDocuments = documentRepository.findIdsByTitleOrContentContaining(keyword);

List<Long> boardIds = boardDocuments.stream()
.map(CommunityBoardDocument::getId)
.toList();

List<CommunityBoardView> content = getCommunityBoardsQuery()
.where(communityBoard.id.in(boardIds)
.and(isNotDeleted()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

JPAQuery<Long> 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<CommunityBoard> communityBoards) {
List<CommunityBoardDocument> communityBoardDocuments = convertEntityToDocuments(communityBoards);
documentRepository.saveAll(communityBoardDocuments);
}

@Override
public void deleteDocument(Long id) {
documentRepository.deleteById(id);
}

@Override
public List<CommunityBoard> findAll() {
return communityBoardJpaRepository.findAll();
}

@Override
public void deleteAllInBatch() {
communityBoardJpaRepository.deleteAllInBatch();
}

private JPAQuery<CommunityBoardView> getCommunityBoardsQuery() {
return queryFactory
.select(Projections.constructor(CommunityBoardView.class,
Expand All @@ -93,10 +141,18 @@ private JPAQuery<CommunityBoardView> getCommunityBoardsQuery() {
.orderBy(communityBoard.createdAt.desc());
}


@Override
public void deleteAllInBatch() {
communityBoardJpaRepository.deleteAllInBatch();
private List<CommunityBoardDocument> convertEntityToDocuments(List<CommunityBoard> communityBoards) {
List<CommunityBoardDocument> 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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

간격이 타이트 한 것 같습니다 제 생각엔 데이터 최신화를 위한 스케줄러 같은데 맞나요?
맞다면 getAll Community Boards라 부하가 클 거 같다는 생각이 듭니다
새벽에 한번 최신화를 시키고 사이에 벌어지는 업데이트들은
logstash로 저희 RDB를 체크하는게 어떨까요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! 자정으로 수정하겠습니다

List<CommunityBoard> communityBoards = communityBoardQueryUseCase.getAllCommunityBoards();
communityBoardDocumentUseCase.saveCommunityBoardDocuments(communityBoards);
}
}
Original file line number Diff line number Diff line change
@@ -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<CommunityBoardResponseDto> getCommunityBoardBySearch(String keyword, int page) {
Pageable pageable = PageRequest.of(page, PAGE_SIZE);
Page<CommunityBoardView> boards = communityBoardRepository.findByCommunityBoardsContaining(keyword, pageable);
return boards.map(CommunityBoardResponseDto::from);
}

@Transactional
@Override
public void saveCommunityBoardDocuments(List<CommunityBoard> communityBoards) {
communityBoardRepository.saveDocuments(communityBoards);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -46,4 +47,9 @@ public CommunityBoardDetailResponseDto getCommunityBoardDetail(Long id) {
.orElseThrow(() -> new BadRequestException(NOT_EXISTS_COMMUNITY_BOARD.getMessage()));
return CommunityBoardDetailResponseDto.from(board);
}

@Override
public List<CommunityBoard> getAllCommunityBoards() {
return communityBoardRepository.findAll();
}
}
Original file line number Diff line number Diff line change
@@ -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<CommunityBoardResponseDto> getCommunityBoardBySearch(String keyword, int page);
void saveCommunityBoardDocuments(List<CommunityBoard> communityBoards);
}
Original file line number Diff line number Diff line change
@@ -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<CommunityBoardResponseDto> getCommunityBoards(int page);
Page<CommunityBoardResponseDto> getCommunityBoardsByWriterId(UUID writerId, int page);
CommunityBoardDetailResponseDto getCommunityBoardDetail(Long id);
List<CommunityBoard> getAllCommunityBoards();
}
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uris 리스트인가요? 아니라면 url가 더 나은것 같습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다!

@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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,6 +33,7 @@
public class RecruitBoardQueryApiController {

private final RecruitBoardQueryUseCase recruitBoardQueryUseCase;
private final RecruitBoardDocumentUseCase recruitBoardDocumentUseCase;

@GetMapping("/recruit-board/{id}")
@Operation(summary = "봉사 모집글 상세 조회", description = "특정 모집글의 상세 정보를 조회합니다.")
Expand Down Expand Up @@ -83,7 +85,7 @@ public ApiResponse<Page<RecruitBoardWithCenterResponseDto>> getAllBySearch(

return ApiResponse.ok(
200,
recruitBoardQueryUseCase.getAllWithCenter(condition),
recruitBoardDocumentUseCase.getRecruitBoardBySearch(keyword, condition),
"봉사 활동 모집글 검색 조회 성공"
);
}
Expand Down
Loading
Loading