diff --git a/build.gradle b/build.gradle index 43202b9f..aad1c88b 100644 --- a/build.gradle +++ b/build.gradle @@ -111,6 +111,9 @@ dependencies { implementation 'io.micrometer:micrometer-registry-prometheus' implementation 'org.springframework.boot:spring-boot-starter-actuator' + // ElasticSearch + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + implementation 'jakarta.json:jakarta.json-api:2.1.1' } test { diff --git a/docker-compose.yml b/docker-compose.yml index f13a85b2..6b22cf9b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -104,6 +104,16 @@ services: volumes: - redis_data:/data + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.12.1 + container_name: es + environment: + - discovery.type=single-node + - xpack.security.enabled=false + - network.host=0.0.0.0 + ports: + - "9200:9200" + networks: playhive-net: external: true diff --git a/src/main/java/org/myteam/server/news/news/domain/News.java b/src/main/java/org/myteam/server/news/news/domain/News.java index b1aff9ce..2d1d79be 100644 --- a/src/main/java/org/myteam/server/news/news/domain/News.java +++ b/src/main/java/org/myteam/server/news/news/domain/News.java @@ -2,15 +2,9 @@ import java.time.LocalDateTime; +import jakarta.persistence.*; import org.myteam.server.global.domain.Base; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Lob; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/org/myteam/server/news/news/domain/NewsDocument.java b/src/main/java/org/myteam/server/news/news/domain/NewsDocument.java new file mode 100644 index 00000000..e9aeeb2b --- /dev/null +++ b/src/main/java/org/myteam/server/news/news/domain/NewsDocument.java @@ -0,0 +1,24 @@ +package org.myteam.server.news.news.domain; + +import jakarta.persistence.Id; +import lombok.*; + +import org.springframework.data.elasticsearch.annotations.Document; +import java.time.LocalDateTime; + +@Document(indexName = "news") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class NewsDocument { + + @Id + private String id; + + private String title; + private String content; + private String category; + private Long postDate; +} diff --git a/src/main/java/org/myteam/server/news/news/dto/repository/NewsDto.java b/src/main/java/org/myteam/server/news/news/dto/repository/NewsDto.java index 8ae223d6..e9c921a4 100644 --- a/src/main/java/org/myteam/server/news/news/dto/repository/NewsDto.java +++ b/src/main/java/org/myteam/server/news/news/dto/repository/NewsDto.java @@ -50,6 +50,18 @@ public NewsDto(Long id, Category category, String title, String thumbImg, String this.isHot = isHot; } + public NewsDto(Long id, String category, String title, String thumbImg, String content, + int commentCount, LocalDateTime postDate) { + this.id = id; + this.category = Category.valueOf(category); // ✅ 직접 enum으로 변환 + this.title = title; + this.thumbImg = extractCleanThumbImg(thumbImg, this.category); + this.content = content; + this.commentCount = commentCount; + this.postDate = postDate; + this.isHot = false; // or your logic + } + public void updateNewsCommentSearchDto(NewsCommentSearchDto newsCommentSearchDto) { this.newsCommentSearchDto = newsCommentSearchDto; } diff --git a/src/main/java/org/myteam/server/news/news/repository/NewsQueryRepository.java b/src/main/java/org/myteam/server/news/news/repository/NewsQueryRepository.java index ac02c279..7a20bd34 100644 --- a/src/main/java/org/myteam/server/news/news/repository/NewsQueryRepository.java +++ b/src/main/java/org/myteam/server/news/news/repository/NewsQueryRepository.java @@ -5,6 +5,9 @@ import static org.myteam.server.news.news.domain.QNews.*; import static org.myteam.server.news.newsCount.domain.QNewsCount.*; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.List; import java.util.Optional; @@ -16,6 +19,7 @@ import org.myteam.server.comment.domain.QNewsComment; import org.myteam.server.global.domain.Category; import org.myteam.server.global.util.domain.TimePeriod; +import org.myteam.server.news.news.domain.NewsDocument; import org.myteam.server.news.news.dto.repository.NewsCommentSearchDto; import org.myteam.server.news.news.dto.repository.NewsDto; import org.myteam.server.news.news.dto.service.request.NewsServiceRequest; @@ -32,14 +36,23 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; +import org.springframework.util.StringUtils; @Repository @RequiredArgsConstructor public class NewsQueryRepository { private final JPAQueryFactory queryFactory; + private final NewsSearchRepository newsSearchRepository; public Page getNewsList(NewsServiceRequest newsServiceRequest) { + if (useElasticsearch(newsServiceRequest)) { + return searchFromElastic(newsServiceRequest); + } + return searchFromQueryDSL(newsServiceRequest); + } + + public Page searchFromQueryDSL(NewsServiceRequest newsServiceRequest) { Category category = newsServiceRequest.getCategory(); OrderType orderType = newsServiceRequest.getOrderType(); BoardSearchType searchType = newsServiceRequest.getSearchType(); @@ -49,27 +62,27 @@ public Page getNewsList(NewsServiceRequest newsServiceRequest) { int startIndex = newsServiceRequest.getStartIndex(); List contents = queryFactory - .select(Projections.constructor(NewsDto.class, - news.id, - news.category, - news.title, - news.thumbImg, - news.content, - newsCount.commentCount, - news.postDate, - news.id.in(getHotNewsIdList()) - )) - .from(news) - .join(newsCount).on(newsCount.news.id.eq(news.id)) - .where( - isCategoryEqualTo(category), - isSearchTypeLikeTo(searchType, search), - isPostDateAfter(timePeriod) - ) - .orderBy(isOrderByEqualToOrderCategory(orderType)) - .offset(pageable.getOffset() + startIndex) - .limit(pageable.getPageSize()) - .fetch(); + .select(Projections.constructor(NewsDto.class, + news.id, + news.category, + news.title, + news.thumbImg, + news.content, + newsCount.commentCount, + news.postDate, + news.id.in(getHotNewsIdList()) + )) + .from(news) + .join(newsCount).on(newsCount.news.id.eq(news.id)) + .where( + isCategoryEqualTo(category), + isSearchTypeLikeTo(searchType, search), + isPostDateAfter(timePeriod) + ) + .orderBy(isOrderByEqualToOrderCategory(orderType)) + .offset(pageable.getOffset() + startIndex) + .limit(pageable.getPageSize()) + .fetch(); // searchType이 COMMENT일 경우, 댓글 데이터 추가 if (searchType == BoardSearchType.COMMENT) { @@ -84,6 +97,25 @@ public Page getNewsList(NewsServiceRequest newsServiceRequest) { return new PageImpl<>(contents, pageable, total); } + public Page searchFromElastic(NewsServiceRequest request) { + Pageable pageable = request.toPageable(); + + Page result = newsSearchRepository + .findByTitleContainingOrContentContaining( + request.getSearch(), request.getSearch(), pageable); + + return result.map(doc -> new NewsDto( + Long.parseLong(doc.getId()), + Category.valueOf(doc.getCategory()), + doc.getTitle(), + null, // 썸네일은 MySQL에만 있을 수 있으므로 제외 + doc.getContent(), + 0, // 댓글 수는 ES에 없으므로 제외 + LocalDateTime.ofInstant(Instant.ofEpochMilli(doc.getPostDate()), ZoneId.systemDefault()), + false // isHot 여부도 ES에는 없을 수 있음 + )); + } + public Page getTotalList(TimePeriod timePeriod, BoardOrderType orderType, BoardSearchType searchType, String search, Pageable pageable) { List contents = queryFactory @@ -286,4 +318,9 @@ private CommentSearchDto getSearchNewsComment(Long newsId, String search) { newsComment.comment.asc() ).fetchFirst(); } + + private boolean useElasticsearch(NewsServiceRequest request) { + return request.getSearchType() == BoardSearchType.TITLE_CONTENT + && StringUtils.hasText(request.getSearch()); + } } diff --git a/src/main/java/org/myteam/server/news/news/repository/NewsSearchRepository.java b/src/main/java/org/myteam/server/news/news/repository/NewsSearchRepository.java new file mode 100644 index 00000000..afcd5a4a --- /dev/null +++ b/src/main/java/org/myteam/server/news/news/repository/NewsSearchRepository.java @@ -0,0 +1,14 @@ +package org.myteam.server.news.news.repository; + +import org.myteam.server.news.news.domain.NewsDocument; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +public interface NewsSearchRepository extends ElasticsearchRepository { + + // 기본 쿼리: 제목 또는 내용에 키워드 포함 + Page findByTitleContainingOrContentContaining( + String title, String content, Pageable pageable + ); +} diff --git a/src/main/resources/application-es.yml b/src/main/resources/application-es.yml new file mode 100644 index 00000000..da5313fa --- /dev/null +++ b/src/main/resources/application-es.yml @@ -0,0 +1,3 @@ +spring: + elasticsearch: + uris: http://es:9200 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bf8c8c61..a0a0f3d8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: profiles: active: dev - include: auth, mail, swagger + include: auth, mail, swagger, es application: name: myteam-server \ No newline at end of file