diff --git a/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java b/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java index 607f75dad..da0d9ac0b 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java @@ -1,5 +1,6 @@ package in.koreatech.koin.domain.community.article.repository; +import static in.koreatech.koin.domain.community.article.service.ArticleService.LOST_ITEM_BOARD_ID; import static in.koreatech.koin.domain.community.article.service.ArticleService.NOTICE_BOARD_ID; import java.time.LocalDate; @@ -29,6 +30,8 @@ public interface ArticleRepository extends Repository { Page
findAll(Pageable pageable); + Page
findAllByBoardIdNot(Integer boardId, PageRequest pageRequest); + Page
findAllByBoardId(Integer boardId, PageRequest pageRequest); Page
findAllByIdIn(List articleIds, PageRequest pageRequest); @@ -43,8 +46,10 @@ public interface ArticleRepository extends Repository { LEFT JOIN FETCH l.author LEFT JOIN FETCH a.koinNotice WHERE a.id IN :ids + AND a.board.id <> :excludedBoardId """) - List
findAllForHotArticlesByIdIn(List ids); + List
findAllForHotArticlesByIdInExcludingBoardId(@Param("ids") List ids, + @Param("excludedBoardId") Integer excludedBoardId); default Article getById(Integer articleId) { Article found = findById(articleId) @@ -72,11 +77,12 @@ default Article getById(Integer articleId) { Page
findAllByBoardIdAndTitleContaining(@Param("boardId") Integer boardId, @Param("query") String query, Pageable pageable); @Query( - value = "SELECT * FROM new_articles WHERE MATCH(title) AGAINST(CONCAT(:query, '*') IN BOOLEAN MODE) AND is_deleted = false", - countQuery = "SELECT count(*) FROM new_articles WHERE MATCH(title) AGAINST(CONCAT(:query, '*') IN BOOLEAN MODE) AND is_deleted = false", + value = "SELECT * FROM new_articles WHERE MATCH(title) AGAINST(CONCAT(:query, '*') IN BOOLEAN MODE) AND board_id <> :excludedBoardId AND is_deleted = false", + countQuery = "SELECT count(*) FROM new_articles WHERE MATCH(title) AGAINST(CONCAT(:query, '*') IN BOOLEAN MODE) AND board_id <> :excludedBoardId AND is_deleted = false", nativeQuery = true ) - Page
findAllByTitleContaining(@Param("query") String query, Pageable pageable); + Page
findAllByTitleContainingExcludingBoardId(@Param("query") String query, + @Param("excludedBoardId") Integer excludedBoardId, Pageable pageable); @Query( value = "SELECT * FROM new_articles WHERE is_notice = true AND MATCH(title) AGAINST(CONCAT(:query, '*') IN BOOLEAN MODE) AND is_deleted = false", @@ -87,6 +93,8 @@ default Article getById(Integer articleId) { Long countBy(); + Long countByBoardIdNot(Integer boardId); + @Query(value = "SELECT * FROM new_articles a " + "WHERE a.id < :articleId AND a.is_notice = true AND a.is_deleted = false " + "ORDER BY a.id DESC LIMIT 1", nativeQuery = true) @@ -98,9 +106,10 @@ default Article getById(Integer articleId) { Optional
findPreviousArticle(@Param("articleId") Integer articleId, @Param("boardId") Integer boardId); @Query(value = "SELECT * FROM new_articles a " - + "WHERE a.id < :articleId AND a.is_deleted = false " + + "WHERE a.id < :articleId AND a.board_id <> :excludedBoardId AND a.is_deleted = false " + "ORDER BY a.id DESC LIMIT 1", nativeQuery = true) - Optional
findPreviousAllArticle(@Param("articleId") Integer articleId); + Optional
findPreviousAllArticle(@Param("articleId") Integer articleId, + @Param("excludedBoardId") Integer excludedBoardId); @Query(value = "SELECT * FROM new_articles a " + "WHERE a.id > :articleId AND a.is_notice = true AND a.is_deleted = false " @@ -113,9 +122,10 @@ default Article getById(Integer articleId) { Optional
findNextArticle(@Param("articleId") Integer articleId, @Param("boardId") Integer boardId); @Query(value = "SELECT * FROM new_articles a " - + "WHERE a.id > :articleId AND a.is_deleted = false " + + "WHERE a.id > :articleId AND a.board_id <> :excludedBoardId AND a.is_deleted = false " + "ORDER BY a.id ASC LIMIT 1", nativeQuery = true) - Optional
findNextAllArticle(@Param("articleId") Integer articleId); + Optional
findNextAllArticle(@Param("articleId") Integer articleId, + @Param("excludedBoardId") Integer excludedBoardId); default Article getPreviousArticle(Board board, Article article) { if (board.isNotice() && board.getId().equals(NOTICE_BOARD_ID)) { @@ -125,7 +135,7 @@ default Article getPreviousArticle(Board board, Article article) { } default Article getPreviousAllArticle(Article article) { - return findPreviousAllArticle(article.getId()).orElse(null); + return findPreviousAllArticle(article.getId(), LOST_ITEM_BOARD_ID).orElse(null); } default Article getNextArticle(Board board, Article article) { @@ -136,7 +146,7 @@ default Article getNextArticle(Board board, Article article) { } default Article getNextAllArticle(Article article) { - return findNextAllArticle(article.getId()).orElse(null); + return findNextAllArticle(article.getId(), LOST_ITEM_BOARD_ID).orElse(null); } @Query(""" @@ -152,9 +162,11 @@ default Article getNextAllArticle(Article article) { OR (ka IS NULL AND a.createdAt > :registeredAt) ) AND a.isDeleted = false + AND a.board.id <> :excludedBoardId ORDER BY (a.hit + COALESCE(ka.portalHit, 0)) DESC, a.id DESC """) - List
findMostHitArticles(LocalDate registeredAt, Pageable pageable); + List
findMostHitArticlesExcludingBoardId(@Param("registeredAt") LocalDate registeredAt, + Pageable pageable, @Param("excludedBoardId") Integer excludedBoardId); @Query(value = "SELECT a.* FROM new_articles a " + "LEFT JOIN new_koreatech_articles ka ON ka.article_id = a.id " @@ -162,8 +174,10 @@ ORDER BY (a.hit + COALESCE(ka.portalHit, 0)) DESC, a.id DESC + " (ka.article_id IS NOT NULL AND ka.registered_at > :registeredAt) " + " OR (ka.article_id IS NULL AND a.created_at > :registeredAt) " + ") " + + "AND a.board_id <> :excludedBoardId " + "AND a.is_deleted = false ", nativeQuery = true) - List
findAllByRegisteredAtIsAfter(LocalDate registeredAt); + List
findAllByRegisteredAtIsAfterExcludingBoardId(@Param("registeredAt") LocalDate registeredAt, + @Param("excludedBoardId") Integer excludedBoardId); @Query("SELECT a.title FROM Article a WHERE a.id = :id") String getTitleById(@Param("id") Integer id); diff --git a/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java index 71b1116c1..030745114 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java @@ -3,9 +3,11 @@ import java.time.Clock; import java.time.LocalDate; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import org.springframework.data.domain.Page; @@ -14,6 +16,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import in.koreatech.koin.common.model.Criteria; import in.koreatech.koin.domain.community.article.dto.ArticleHotKeywordResponse; import in.koreatech.koin.domain.community.article.dto.ArticleResponse; import in.koreatech.koin.domain.community.article.dto.ArticlesResponse; @@ -21,15 +24,14 @@ import in.koreatech.koin.domain.community.article.exception.ArticleBoardMisMatchException; import in.koreatech.koin.domain.community.article.model.Article; import in.koreatech.koin.domain.community.article.model.Board; +import in.koreatech.koin.domain.community.article.model.KeywordRankingManager; import in.koreatech.koin.domain.community.article.model.redis.ArticleHitUser; import in.koreatech.koin.domain.community.article.model.redis.PopularKeywordTracker; -import in.koreatech.koin.domain.community.article.model.KeywordRankingManager; import in.koreatech.koin.domain.community.article.repository.ArticleRepository; import in.koreatech.koin.domain.community.article.repository.BoardRepository; import in.koreatech.koin.domain.community.article.repository.redis.ArticleHitUserRepository; import in.koreatech.koin.domain.community.article.repository.redis.HotArticleRepository; import in.koreatech.koin.global.exception.custom.KoinIllegalArgumentException; -import in.koreatech.koin.common.model.Criteria; import in.koreatech.koin.infrastructure.s3.client.S3Client; import lombok.RequiredArgsConstructor; @@ -39,6 +41,7 @@ public class ArticleService { public static final int NOTICE_BOARD_ID = 4; + public static final int LOST_ITEM_BOARD_ID = 14; private static final int HOT_ARTICLE_BEFORE_DAYS = 30; private static final int HOT_ARTICLE_LIMIT = 10; private static final int MAXIMUM_SEARCH_LENGTH = 100; @@ -49,7 +52,6 @@ public class ArticleService { Sort.Order.desc("id") ); - private final ArticleRepository articleRepository; private final BoardRepository boardRepository; private final HotArticleRepository hotArticleRepository; @@ -75,11 +77,13 @@ public ArticleResponse getArticle(Integer boardId, Integer articleId, String pub } public ArticlesResponse getArticles(Integer boardId, Integer page, Integer limit, Integer userId) { - Long total = articleRepository.countBy(); + Long total = boardId == null + ? articleRepository.countByBoardIdNot(LOST_ITEM_BOARD_ID) + : articleRepository.countBy(); Criteria criteria = Criteria.of(page, limit, total.intValue()); PageRequest pageRequest = PageRequest.of(criteria.getPage(), criteria.getLimit(), ARTICLES_SORT); if (boardId == null) { - Page
articles = articleRepository.findAll(pageRequest); + Page
articles = articleRepository.findAllByBoardIdNot(LOST_ITEM_BOARD_ID, pageRequest); return ArticlesResponse.of(articles, criteria, userId); } if (boardId == NOTICE_BOARD_ID) { @@ -92,27 +96,58 @@ public ArticlesResponse getArticles(Integer boardId, Integer page, Integer limit public List getHotArticles() { List hotArticlesIds = hotArticleRepository.getHotArticles(HOT_ARTICLE_LIMIT); - List
articles = articleRepository.findAllForHotArticlesByIdIn(hotArticlesIds); + List
cachedArticles = articleRepository.findAllForHotArticlesByIdInExcludingBoardId( + hotArticlesIds, + LOST_ITEM_BOARD_ID + ); - Map articleMap = articles.stream() + Map articleMap = cachedArticles.stream() .collect(Collectors.toMap(Article::getId, article -> article)); - List
cacheList = new ArrayList<>(hotArticlesIds.stream() - .map(articleMap::get) - .filter(Objects::nonNull) - .toList()); + List
result = new ArrayList<>(HOT_ARTICLE_LIMIT); + Set seen = new HashSet<>(HOT_ARTICLE_LIMIT * 2); + + for (Integer id : hotArticlesIds) { + Article article = articleMap.get(id); + + if (article == null) { + continue; + } - if (cacheList.size() < HOT_ARTICLE_LIMIT) { - List
highestHitArticles = articleRepository.findMostHitArticles( + if (seen.add(article.getId())) { + result.add(article); + + if (result.size() == HOT_ARTICLE_LIMIT) { + break; + } + } + } + + if (result.size() < HOT_ARTICLE_LIMIT) { + List
highestHitArticles = articleRepository.findMostHitArticlesExcludingBoardId( LocalDate.now(clock).minusDays(HOT_ARTICLE_BEFORE_DAYS), - PageRequest.of(0, HOT_ARTICLE_LIMIT) + PageRequest.of(0, HOT_ARTICLE_LIMIT), + LOST_ITEM_BOARD_ID ); - cacheList.addAll(highestHitArticles); - return cacheList.stream().limit(HOT_ARTICLE_LIMIT) - .map(HotArticleItemResponse::from) - .toList(); + + for (Article article : highestHitArticles) { + if (article == null) { + continue; + } + + if (seen.add(article.getId())) { + result.add(article); + + if (result.size() == HOT_ARTICLE_LIMIT) { + break; + } + } + } } - return cacheList.stream().map(HotArticleItemResponse::from).toList(); + + return result.stream() + .map(HotArticleItemResponse::from) + .toList(); } @Transactional @@ -123,7 +158,7 @@ public ArticlesResponse searchArticles(String query, Integer boardId, Integer pa PageRequest pageRequest = PageRequest.of(criteria.getPage(), criteria.getLimit(), NATIVE_ARTICLES_SORT); Page
articles; if (boardId == null) { - articles = articleRepository.findAllByTitleContaining(query, pageRequest); + articles = articleRepository.findAllByTitleContainingExcludingBoardId(query, LOST_ITEM_BOARD_ID, pageRequest); } else if (boardId == NOTICE_BOARD_ID) { articles = articleRepository.findAllByIsNoticeIsTrueAndTitleContaining(query, pageRequest); } else { diff --git a/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleSyncService.java b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleSyncService.java index 2c850b8a0..28ebcf226 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleSyncService.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleSyncService.java @@ -53,7 +53,10 @@ public void updateHotArticles() { List articleHits = articleHitRepository.findAll(); articleHitRepository.deleteAll(); List
allArticles = - articleRepository.findAllByRegisteredAtIsAfter(LocalDate.now(clock).minusDays(HOT_ARTICLE_BEFORE_DAYS)); + articleRepository.findAllByRegisteredAtIsAfterExcludingBoardId( + LocalDate.now(clock).minusDays(HOT_ARTICLE_BEFORE_DAYS), + ArticleService.LOST_ITEM_BOARD_ID + ); articleHitRepository.saveAll(allArticles.stream().map(ArticleHit::from).toList()); Map articlesIdWithHit = new HashMap<>();