From 25232937e8061c530a43f7070453221d7c03960d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Mon, 19 Jan 2026 11:50:19 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EB=B6=84=EC=8B=A4=EB=AC=BC=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=EC=9D=80=20=EC=A0=9C=EC=99=B8?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/repository/ArticleRepository.java | 38 +++++++++++++------ .../article/service/ArticleService.java | 19 +++++++--- 2 files changed, 39 insertions(+), 18 deletions(-) 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 607f75dad8..7b7d75d73d 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 71b1116c14..1e7f49d08a 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 @@ -39,6 +39,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; @@ -75,11 +76,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,7 +95,10 @@ 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
articles = articleRepository.findAllForHotArticlesByIdInExcludingBoardId( + hotArticlesIds, + LOST_ITEM_BOARD_ID + ); Map articleMap = articles.stream() .collect(Collectors.toMap(Article::getId, article -> article)); @@ -103,9 +109,10 @@ public List getHotArticles() { .toList()); if (cacheList.size() < HOT_ARTICLE_LIMIT) { - List
highestHitArticles = articleRepository.findMostHitArticles( + 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) @@ -123,7 +130,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 { From 1de3894e164b246c2545cd63e876520b85be7622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Mon, 19 Jan 2026 11:50:35 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=EC=9D=B8=EA=B8=B0=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EC=A7=91=EA=B3=84=20=EC=8B=9C=20=EB=B6=84?= =?UTF-8?q?=EC=8B=A4=EB=AC=BC=20=EA=B2=8C=EC=8B=9C=EA=B8=80=EC=9D=80=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/community/article/service/ArticleSyncService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 2c850b8a08..28ebcf226d 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<>(); From bef667961e571696330b2e3d280771854983458b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Mon, 19 Jan 2026 12:21:19 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EC=BA=90=EC=8B=B1=EB=90=9C=20?= =?UTF-8?q?=EC=9D=B8=EA=B8=B0=20=EA=B2=8C=EC=8B=9C=EA=B8=80=EC=9D=98=20?= =?UTF-8?q?=EC=88=98=EA=B0=80=20=EB=B6=80=EC=A1=B1=ED=95=98=EC=97=AC=20DB?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B3=B4=EC=B6=A9=20=EC=8B=9C=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EC=A4=91=EB=B3=B5=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/service/ArticleService.java | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) 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 1e7f49d08a..0307451140 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; @@ -50,7 +52,6 @@ public class ArticleService { Sort.Order.desc("id") ); - private final ArticleRepository articleRepository; private final BoardRepository boardRepository; private final HotArticleRepository hotArticleRepository; @@ -95,31 +96,58 @@ public ArticlesResponse getArticles(Integer boardId, Integer page, Integer limit public List getHotArticles() { List hotArticlesIds = hotArticleRepository.getHotArticles(HOT_ARTICLE_LIMIT); - List
articles = articleRepository.findAllForHotArticlesByIdInExcludingBoardId( + 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) { + 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), 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 From 1923708c12cde9b8b6bfd4f5c7ee65b8e3b6bc49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Mon, 19 Jan 2026 12:31:50 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20not-equals=20=EC=97=B0=EC=82=B0?= =?UTF-8?q?=EC=9E=90=20<>=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../community/article/repository/ArticleRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7b7d75d73d..da0d9ac0ba 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 @@ -77,8 +77,8 @@ 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 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", + 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
findAllByTitleContainingExcludingBoardId(@Param("query") String query,