Skip to content

[Feat] 뉴스 API#44

Merged
chaeyuuu merged 27 commits intomainfrom
feat/#33/news-api
Aug 1, 2025
Merged

[Feat] 뉴스 API#44
chaeyuuu merged 27 commits intomainfrom
feat/#33/news-api

Conversation

@chaeyuuu
Copy link
Copy Markdown
Contributor

@chaeyuuu chaeyuuu commented Aug 1, 2025

💻 Related Issue

close #33


🚀 Work Description

  • 뉴스 조회 api 완성
  • 다음 뉴스 크롤링 및 스케줄러 도입
    : 실시간으로 조회하는 방식 대신 스케줄러를 통해 일정 시간 간격마다 DB에 크롤링해서 저장하고 프론트에 반환하는 형식으로 구현하였습니다. 현재는 새벽 4시마다 스케줄러가 실행되는데 추후에 시간 간격이나 특정 시간에 조회하도록 변경하겠습니다.
image : category에 아무것도 입력하지 않으면 default로 전체 뉴스를 조회합니다. image : 카테고리에에 '보이스피싱' 을 입력하면 해당 카테고리에 저장되어있는 기사들을 출력합니다. 카테고리에는 총 '보이스피싱', '메신저피싱', '스미싱', '기타' 총 4가지가 존재합니다. 피그마에 작성되어있는 것들을 토대로 반영하였습니다. image : 현재 '기타' 카테고리를 조회하면 몸캠 관련 뉴스들이 출력되도록 하였습니다. image : sort에 `published_at_desc`를 입력하면 최신순으로, `publised_at_asc` 를 입력하면 오래된 순으로 기사가 조회됩니다.

🙇🏻‍♀️ To Reviewer

  • 네이버 open api는 정확도 이슈로, 크롤링은 막혀있어서 다음 뉴스로 대체하였습니다.
  • category enum을 추가하였습니다.
  • NewsArticle 도메인에 isFilteredOut 필드를 추가하여 true로 표시된 것은 반환되지 않도록 하였습니다. 현재 카테고리 키워드가 포함되어 있는 기사들만 추출하는데, 그럼에도 관련없는 기사들이 종종 있어 우선 추가해놓았습니다. 추후 필요하다면 관리자용 api를 통해 관리하는 등으로 확장하면 될 것 같습니다.
  • 최근 60일 이내의 기사만 추출합니다.

Summary by CodeRabbit

Summary by CodeRabbit

  • 신규 기능

    • 뉴스 기사 목록을 조회할 수 있는 REST API가 추가되었습니다. (카테고리, 정렬, 페이지네이션 지원)
    • 뉴스 기사 크롤링 및 저장 기능이 추가되었습니다. (Daum 뉴스 기반, 자동 스케줄러 및 수동 크롤링 API 포함)
    • 뉴스 기사에 카테고리 및 필터링 여부 필드가 추가되었습니다.
  • 버그 수정

    • 없음
  • 문서화

    • Swagger에 뉴스 조회 관련 응답 및 에러 코드가 추가되었습니다.
  • 테스트

    • 뉴스 크롤러의 통합 테스트가 추가되었습니다.
  • 기타

    • 보안 설정에 뉴스 관련 엔드포인트가 인증 없이 접근 가능하도록 허용되었습니다.
    • 내부 패키지 구조 일부가 정비되었습니다.
    • 관련 라이브러리(jsoup) 의존성이 추가되었습니다.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 1, 2025

Walkthrough

뉴스 기사 조회 기능이 도입되었습니다. 뉴스 크롤링, 저장, 조회를 위한 도메인, 서비스, API, DTO, 스케줄러, 리포지토리, 에러/성공 코드, 스웨거 응답, 보안 예외 경로, 테스트 코드 등이 추가 및 수정되었습니다. 일부 패키지 구조도 변경되었습니다.

Changes

Cohort / File(s) Change Summary
뉴스 API 및 서비스 계층 추가
src/main/java/com/blockguard/server/domain/news/api/NewsApi.java, .../application/NewsService.java
뉴스 기사 조회 REST API 및 서비스 로직 추가.
뉴스 엔티티, 카테고리, 리포지토리
.../domain/NewsArticle.java, .../domain/enums/Category.java, .../dao/NewsRepository.java
뉴스 엔티티에 카테고리, 필터링 필드 추가. 카테고리 enum 및 JPA 리포지토리 신설.
뉴스 DTO 및 페이지네이션
.../dto/response/NewsArticleResponse.java, .../dto/response/NewsPageResponse.java, .../dto/response/PageableInfo.java
뉴스 기사 응답 및 페이지네이션 DTO 추가.
뉴스 크롤러 및 스케줄러
.../infra/crawler/DaumNewsCrawler.java, .../scheduler/NewsSaveScheduler.java
Daum 뉴스 크롤러 및 스케줄러 클래스 추가.
테스트 코드
src/test/java/com/blockguard/server/infra/crawler/DaumNewsCrawlerTest.java
DaumNewsCrawler 통합 테스트 추가.
에러/성공/스웨거 코드
.../global/common/codes/ErrorCode.java, .../SuccessCode.java, .../swagger/SwaggerResponseDescription.java
뉴스 크롤링 실패 에러코드, 뉴스 조회 성공코드, 스웨거 응답 설명 추가.
보안 설정
.../global/config/SecurityConfig.java
/api/news, /api/admin/crawl 엔드포인트 인증 예외 처리 추가.
라이브러리 및 패키지 구조
build.gradle, .../infra/naver/ocr/ByteArrayResourceWithFilename.java, .../infra/naver/ocr/NaverOcrClient.java, .../domain/analysis/application/FraudAnalysisService.java
jsoup 라이브러리 추가, OCR 관련 패키지 구조 변경, 관련 import 수정.
관리자 API 크롤링 엔드포인트 추가
src/main/java/com/blockguard/server/domain/admin/api/AdminApi.java
수동 뉴스 크롤링을 위한 POST /crawl 엔드포인트 추가.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant NewsApi
    participant NewsService
    participant NewsRepository

    Client->>NewsApi: GET /api/news?page=&size=&sort=&category=
    NewsApi->>NewsService: getNewsList(page, size, sort, category)
    NewsService->>NewsRepository: findAllByIsFilteredOutFalse(...) / findByCategoryAndIsFilteredOutFalse(...)
    NewsRepository-->>NewsService: Page<NewsArticle>
    NewsService-->>NewsApi: NewsPageResponse
    NewsApi-->>Client: BaseResponse<NewsPageResponse>
Loading
sequenceDiagram
    participant Scheduler
    participant DaumNewsCrawler
    participant NewsRepository

    Scheduler->>DaumNewsCrawler: fetchNewsFromDaum(keyword)
    DaumNewsCrawler->>NewsRepository: existsByUrl(url)
    NewsRepository-->>DaumNewsCrawler: boolean
    DaumNewsCrawler->>NewsRepository: save(newsArticle)
    NewsRepository-->>DaumNewsCrawler: NewsArticle
Loading
sequenceDiagram
    participant AdminClient
    participant AdminApi
    participant NewsSaveScheduler
    participant DaumNewsCrawler
    participant NewsRepository

    AdminClient->>AdminApi: POST /api/admin/crawl
    AdminApi->>NewsSaveScheduler: crawlingForAdmin()
    NewsSaveScheduler->>DaumNewsCrawler: fetchNewsFromDaum(keyword) x4
    DaumNewsCrawler->>NewsRepository: existsByUrl(url)
    NewsRepository-->>DaumNewsCrawler: boolean
    DaumNewsCrawler->>NewsRepository: save(newsArticle)
    NewsRepository-->>DaumNewsCrawler: NewsArticle
    AdminApi-->>AdminClient: BaseResponse<Void>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Assessment against linked issues

Objective Addressed Explanation
뉴스 API 구현 (#33)
뉴스 크롤링 및 저장 (#33)
뉴스 카테고리, 필터링, 페이지네이션, 정렬 (#33)
테스트 코드 추가 (#33)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
OCR 관련 패키지 구조 변경 및 import 수정 (src/main/java/com/blockguard/server/infra/naver/ocr/ByteArrayResourceWithFilename.java, .../NaverOcrClient.java, .../FraudAnalysisService.java) 뉴스 API와 직접적으로 연관된 변경이 아니며, OCR 기능 관련 패키지 구조 변경은 본 이슈의 범위를 벗어남.
jsoup 라이브러리 추가 (build.gradle) 뉴스 크롤러 구현에 필요하나, 명확히 이슈에 명시되어 있지 않음. 그러나 기능 구현상 불가피한 부분으로 판단됨.

Possibly related PRs

  • [Feat] URL 사기분석 API #32: Admin API에 사기 URL 업데이트 기능을 추가한 PR로, 본 PR의 뉴스 크롤링 및 뉴스 API 관련 AdminApi 변경과 동일 클래스 내 별도 엔드포인트 구현으로 연관됨.

Suggested reviewers

  • yeonju73

Poem

📰🐰

새벽마다 토끼가 뉴스 줍고,
크롤러가 달려가 소식 담고,
카테고리 따라 소식 정렬,
API로 한아름 기사 전달!

리뷰어님, 오늘도 한 줄 뉴스처럼
코드 속 세상 밝혀주세요!

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26b64c2 and caba25c.

📒 Files selected for processing (2)
  • src/main/java/com/blockguard/server/domain/admin/api/AdminApi.java (1 hunks)
  • src/main/java/com/blockguard/server/domain/news/scheduler/NewsSaveScheduler.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/blockguard/server/domain/admin/api/AdminApi.java
  • src/main/java/com/blockguard/server/domain/news/scheduler/NewsSaveScheduler.java
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#33/news-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (11)
src/main/java/com/blockguard/server/infra/naver/ocr/NaverOcrClient.java (1)

1-4: 같은 패키지 내 클래스는 import 불필요

현재 파일과 동일한 com.blockguard.server.infra.naver.ocr 패키지의 ByteArrayResourceWithFilename 를 import 하고 있습니다. 같은 패키지의 클래스는 import 문이 필요 없으므로 제거하여 불필요한 라인을 줄일 수 있습니다.

-import com.blockguard.server.infra.naver.ocr.ByteArrayResourceWithFilename;
build.gradle (1)

49-51: Jsoup 버전을 중앙 관리 변수 또는 BOM으로 추출하는 방안을 고려하세요
라이브러리 버전을 직접 하드코딩하면 추후 여러 모듈에서 동일 의존성을 사용할 때 일관성 유지가 어렵습니다. ext(gradle.properties) 혹은 Spring Dependency Management의 importBom을 사용해 버전 통제 지점을 하나로 두면 유지보수가 수월합니다.

src/main/java/com/blockguard/server/domain/news/dto/response/PageableInfo.java (1)

7-15: Java 21에서는 레코드(record)로 간결하게 표현할 수 있습니다
불변 DTO이며 모든 필드가 private final 성격이므로 Lombok + 필드 선언 대신 레코드를 사용하면 보일러플레이트를 제거할 수 있습니다.

-@Getter
-@AllArgsConstructor
-@Builder
-public class PageableInfo {
-    private int page;
-    private int size;
-    private long totalElements;
-    private int totalPages;
-}
+public record PageableInfo(
+        int page,
+        int size,
+        long totalElements,
+        int totalPages
+) {}
src/main/java/com/blockguard/server/global/common/codes/ErrorCode.java (1)

38-38: 상수명에 어색한 전치사 ‘TO’ 사용 – 가독성 향상을 위해 수정 권장
FAIL_TO_CRAWLING_NEWS → 일반적으로 “~에 실패” 패턴은 FAIL_NEWS_CRAWLING 또는 FAIL_TO_CRAWL_NEWS 형태가 자연스럽습니다.

src/main/java/com/blockguard/server/global/config/swagger/SwaggerResponseDescription.java (1)

99-102: 성공 응답 코드도 Swagger 문서에 병행 추가 필요
실패 코드만 추가되어 있어 성공 시 무엇이 반환되는지 Swagger 스펙이 불완전합니다. GET_NEWS_ARTICLES_SUCCESSSuccessCode와 매핑하여 문서화하는 것을 권장합니다.

src/main/java/com/blockguard/server/domain/news/scheduler/NewsSaveScheduler.java (1)

19-22: 에러 처리 및 성능 개선을 고려해보세요.

현재 순차적으로 크롤링을 실행하고 있어 다음과 같은 개선점이 있습니다:

  1. 하나의 크롤링이 실패하면 나머지도 실행되지 않을 수 있습니다
  2. 순차 실행으로 인한 성능 이슈가 있을 수 있습니다

다음과 같이 개선할 수 있습니다:

 @Scheduled(cron = "0 0 4 * * *")
 public void saveNewsArticles(){
     log.info("뉴스 크롤링 스케줄링 시작");
 
-    daumNewsCrawler.fetchNewsFromDaum("보이스피싱");
-    daumNewsCrawler.fetchNewsFromDaum("스미싱");
-    daumNewsCrawler.fetchNewsFromDaum("메신저 피싱");
-    daumNewsCrawler.fetchNewsFromDaum("몸캠");
+    String[] keywords = {"보이스피싱", "스미싱", "메신저 피싱", "몸캠"};
+    for (String keyword : keywords) {
+        try {
+            daumNewsCrawler.fetchNewsFromDaum(keyword);
+        } catch (Exception e) {
+            log.error("키워드 '{}' 크롤링 중 오류 발생: {}", keyword, e.getMessage(), e);
+        }
+    }
 
     log.info("뉴스 크롤링 스케줄링 완료");
 }
src/main/java/com/blockguard/server/domain/news/dto/response/NewsPageResponse.java (1)

13-16: 필드 명명 및 문서화 개선 필요

필드명과 클래스에 대한 문서화를 추가하는 것이 좋겠습니다. 특히 sort 필드의 경우 어떤 형식의 값이 들어가는지 명확하지 않습니다.

+/**
+ * 페이지네이션된 뉴스 기사 목록 응답 DTO
+ */
 @Getter
 @Builder
 @AllArgsConstructor
 public class NewsPageResponse {
+    /** 뉴스 기사 목록 */
     private List<NewsArticleResponse> news;
+    /** 페이지네이션 정보 */
     private PageableInfo pageableInfo;
+    /** 정렬 기준 (예: "published_at_desc", "published_at_asc") */
     private String sort;
src/main/java/com/blockguard/server/infra/crawler/DaumNewsCrawler.java (1)

87-88: Thread.sleep 대신 스케줄링 메커니즘 사용 권장

Thread.sleep()은 스레드를 블로킹하여 리소스를 비효율적으로 사용합니다. 대량의 크롤링 작업 시 성능 문제가 발생할 수 있습니다.

Spring의 @AsyncCompletableFuture를 사용하거나, 전용 스케줄러를 사용하여 비동기적으로 처리하는 것을 고려해보세요. 또한 rate limiting을 위해 RateLimiter (예: Guava) 사용을 권장합니다.

src/main/java/com/blockguard/server/domain/news/dto/response/NewsArticleResponse.java (1)

34-46: 시간 포맷팅 로직 분리 고려

시간 포맷팅 로직이 DTO에 포함되어 있는데, 이는 단일 책임 원칙에 어긋날 수 있습니다. 별도의 유틸리티 클래스로 분리하는 것을 고려해보세요.

TimeFormatUtils 같은 유틸리티 클래스를 만들어 시간 관련 포맷팅 로직을 중앙화하면 재사용성과 테스트 용이성이 향상됩니다. 또한 타임존 처리도 명시적으로 할 수 있습니다.

public class TimeFormatUtils {
    public static String formatRelativeTime(LocalDateTime time, ZoneId zoneId) {
        // 타임존을 고려한 시간 계산
        ZonedDateTime zonedTime = time.atZone(zoneId);
        ZonedDateTime now = ZonedDateTime.now(zoneId);
        // ... 포맷팅 로직
    }
}
src/main/java/com/blockguard/server/domain/news/application/NewsService.java (2)

31-31: 하드코딩된 문자열 상수화를 권장합니다.

"전체" 문자열을 상수로 정의하여 유지보수성을 향상시키는 것이 좋겠습니다.

+    private static final String ALL_CATEGORY = "전체";
+
     public NewsPageResponse getNewsList(int page, int size, String sort, String category) {
         Pageable pageable = PageRequest.of(page -1, size, getSort(sort));
         Page<NewsArticle> newsPage;
 
-        if(category.equals("전체")){
+        if(ALL_CATEGORY.equals(category)){

54-60: 정렬 로직이 잘 구현되었습니다.

최신 Java switch 표현식을 사용하여 깔끔하게 구현되었습니다. 필드명을 상수로 추출하면 더욱 좋겠습니다.

+    private static final String PUBLISHED_AT_FIELD = "publishedAt";
+
     private Sort getSort(String sort) {
         return switch (sort) {
-            case "published_at_asc" -> Sort.by("publishedAt").ascending();
-            case "published_at_desc" -> Sort.by("publishedAt").descending();
-            default -> Sort.by("publishedAt").descending();
+            case "published_at_asc" -> Sort.by(PUBLISHED_AT_FIELD).ascending();
+            case "published_at_desc" -> Sort.by(PUBLISHED_AT_FIELD).descending();
+            default -> Sort.by(PUBLISHED_AT_FIELD).descending();
         };
     }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4c1884 and cf67962.

📒 Files selected for processing (19)
  • build.gradle (1 hunks)
  • src/main/java/com/blockguard/server/domain/analysis/application/FraudAnalysisService.java (1 hunks)
  • src/main/java/com/blockguard/server/domain/news/api/NewsApi.java (1 hunks)
  • src/main/java/com/blockguard/server/domain/news/application/NewsService.java (1 hunks)
  • src/main/java/com/blockguard/server/domain/news/dao/NewsRepository.java (1 hunks)
  • src/main/java/com/blockguard/server/domain/news/domain/NewsArticle.java (2 hunks)
  • src/main/java/com/blockguard/server/domain/news/domain/enums/Category.java (1 hunks)
  • src/main/java/com/blockguard/server/domain/news/dto/response/NewsArticleResponse.java (1 hunks)
  • src/main/java/com/blockguard/server/domain/news/dto/response/NewsPageResponse.java (1 hunks)
  • src/main/java/com/blockguard/server/domain/news/dto/response/PageableInfo.java (1 hunks)
  • src/main/java/com/blockguard/server/domain/news/scheduler/NewsSaveScheduler.java (1 hunks)
  • src/main/java/com/blockguard/server/global/common/codes/ErrorCode.java (1 hunks)
  • src/main/java/com/blockguard/server/global/common/codes/SuccessCode.java (1 hunks)
  • src/main/java/com/blockguard/server/global/config/SecurityConfig.java (1 hunks)
  • src/main/java/com/blockguard/server/global/config/swagger/SwaggerResponseDescription.java (1 hunks)
  • src/main/java/com/blockguard/server/infra/crawler/DaumNewsCrawler.java (1 hunks)
  • src/main/java/com/blockguard/server/infra/naver/ocr/ByteArrayResourceWithFilename.java (1 hunks)
  • src/main/java/com/blockguard/server/infra/naver/ocr/NaverOcrClient.java (1 hunks)
  • src/test/java/com/blockguard/server/infra/crawler/DaumNewsCrawlerTest.java (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (6)
src/main/java/com/blockguard/server/domain/analysis/application/FraudAnalysisService.java (1)
src/main/java/com/blockguard/server/infra/ocr/NaverOcrClient.java (2)
  • NaverOcrClient (19-103)
  • extractTextFromImage (32-90)
src/main/java/com/blockguard/server/domain/news/domain/NewsArticle.java (1)
src/main/java/com/blockguard/server/global/common/entity/BaseEntity.java (1)
  • BaseEntity (11-24)
src/main/java/com/blockguard/server/domain/news/dto/response/PageableInfo.java (2)
src/main/java/com/blockguard/server/domain/news/dto/response/NewsArticleResponse.java (1)
  • Getter (11-47)
src/main/java/com/blockguard/server/domain/news/dto/response/NewsPageResponse.java (1)
  • Getter (9-16)
src/main/java/com/blockguard/server/domain/news/domain/enums/Category.java (1)
src/main/java/com/blockguard/server/domain/analysis/domain/enums/FraudType.java (2)
  • FraudType (5-38)
  • FraudType (34-36)
src/test/java/com/blockguard/server/infra/crawler/DaumNewsCrawlerTest.java (1)
src/test/java/com/blockguard/server/ServerApplicationTests.java (1)
  • ServerApplicationTests (6-13)
src/main/java/com/blockguard/server/domain/news/dao/NewsRepository.java (3)
src/main/java/com/blockguard/server/domain/guardian/dao/GuardianRepository.java (1)
  • GuardianRepository (12-27)
src/main/java/com/blockguard/server/domain/fraud/dao/FraudUrlRepository.java (1)
  • FraudUrlRepository (8-11)
src/main/java/com/blockguard/server/domain/user/dao/UserRepository.java (1)
  • UserRepository (9-13)
🔇 Additional comments (9)
src/main/java/com/blockguard/server/infra/naver/ocr/ByteArrayResourceWithFilename.java (1)

1-1: 패키지 재배치 👍

infra.naver.ocr 하위로 이동하면서 네임스페이스가 더 명확해졌습니다. 별다른 로직 변경이 없으므로 영향 범위는 패키지 스캔에 한정되며, 기존 com.blockguard.server 루트 스캔 설정이면 문제없습니다.

src/main/java/com/blockguard/server/domain/analysis/application/FraudAnalysisService.java (1)

9-9: 레거시 NaverOcrClient 경로 참조 없음 확인 완료
rg 검색 결과, com.blockguard.server.infra.ocr.NaverOcrClient를 참조하는 파일이 더 이상 존재하지 않으므로 추가 조치가 필요 없습니다.

src/main/java/com/blockguard/server/global/common/codes/SuccessCode.java (1)

34-34: LGTM! 성공 코드 추가가 적절합니다.

새로운 뉴스 조회 API를 위한 성공 코드가 기존 패턴과 일관성 있게 추가되었습니다. 코드 번호 순서도 올바르고 한국어 메시지도 적절합니다.

src/main/java/com/blockguard/server/domain/news/scheduler/NewsSaveScheduler.java (1)

15-15: 크론 표현식이 정확합니다.

매일 오전 4시에 실행되는 스케줄링이 올바르게 설정되었습니다.

src/main/java/com/blockguard/server/domain/news/domain/NewsArticle.java (1)

37-42: 새로운 필드 추가가 적절합니다.

카테고리와 필터링 여부를 나타내는 필드가 올바르게 추가되었습니다. EnumType.STRING 사용과 nullable = false 설정이 적절합니다.

참고: isFilteredOut 필드명이 boolean 타입에 대해 일반적인 Java 네이밍 규칙인 filteredOut로 하는 것도 고려해볼 수 있지만, 현재 명명도 의미가 명확하므로 문제없습니다.

src/main/java/com/blockguard/server/domain/news/dao/NewsRepository.java (1)

11-16: Repository 메서드 구현이 적절함

카테고리별 필터링과 필터링된 기사 제외 로직이 잘 구현되어 있습니다. Spring Data JPA 명명 규칙을 잘 따르고 있습니다.

src/main/java/com/blockguard/server/domain/news/api/NewsApi.java (1)

25-25: 페이지 인덱싱 변환 로직 정상 작동 확인
NewsService#getNewsList에서 다음 코드로 page 파라미터를 1-based → 0-based로 올바르게 변환하고 있습니다.
• src/main/java/com/blockguard/server/domain/news/application/NewsService.java

Pageable pageable = PageRequest.of(page - 1, size, getSort(sort));

따라서 API의 @RequestParam(defaultValue = "1") int page 설정과 충돌이 없습니다.

src/main/java/com/blockguard/server/domain/news/application/NewsService.java (2)

1-19: 패키지 선언과 임포트가 잘 구성되어 있습니다.

필요한 의존성만 임포트하고 있으며, 패키지 구조도 도메인 주도 설계 원칙을 따르고 있습니다.


28-28: 페이지 인덱스 변환이 올바르게 구현되었습니다.

1-based 페이지 번호를 0-based로 올바르게 변환하고 있습니다.

Comment on lines +24 to +28
public BaseResponse<NewsPageResponse> getNewsArticles(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "published_at_desc") String sort,
@RequestParam(defaultValue = "전체") String category) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

입력 파라미터 검증 필요

페이지와 사이즈 파라미터에 대한 검증이 필요합니다. 특히 사이즈 파라미터의 경우 너무 큰 값이 입력되면 성능 문제가 발생할 수 있습니다.

 public BaseResponse<NewsPageResponse> getNewsArticles(
-        @RequestParam(defaultValue = "1") int page,
-        @RequestParam(defaultValue = "10") int size,
+        @RequestParam(defaultValue = "1") @Min(1) int page,
+        @RequestParam(defaultValue = "10") @Min(1) @Max(100) int size,
         @RequestParam(defaultValue = "published_at_desc") String sort,
         @RequestParam(defaultValue = "전체") String category) {

추가로 클래스 레벨에 @Validated 애노테이션을 추가해야 합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public BaseResponse<NewsPageResponse> getNewsArticles(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "published_at_desc") String sort,
@RequestParam(defaultValue = "전체") String category) {
public BaseResponse<NewsPageResponse> getNewsArticles(
@RequestParam(defaultValue = "1") @Min(1) int page,
@RequestParam(defaultValue = "10") @Min(1) @Max(100) int size,
@RequestParam(defaultValue = "published_at_desc") String sort,
@RequestParam(defaultValue = "전체") String category) {
🤖 Prompt for AI Agents
In src/main/java/com/blockguard/server/domain/news/api/NewsApi.java around lines
24 to 28, input validation is missing for the page and size parameters. Add
validation annotations such as @Min(1) for page and @Min(1) and @Max(sensible
upper limit) for size to prevent invalid or excessively large values. Also, add
the @Validated annotation at the class level to enable validation processing for
the controller.

Comment on lines +27 to +36
public NewsPageResponse getNewsList(int page, int size, String sort, String category) {
Pageable pageable = PageRequest.of(page -1, size, getSort(sort));
Page<NewsArticle> newsPage;

if(category.equals("전체")){
newsPage = newsRepository.findAllByIsFilteredOutFalse(pageable);
} else{
Category enumCategory = Category.from(category);
newsPage = newsRepository.findByCategoryAndIsFilteredOutFalse(enumCategory, pageable);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

입력 검증 및 에러 처리 추가를 고려해주세요.

메서드에 다음과 같은 개선사항을 적용하는 것을 권장합니다:

  1. 입력 매개변수에 대한 null 체크
  2. Category.from() 메서드의 예외 처리
  3. 페이지 크기에 대한 범위 검증
 public NewsPageResponse getNewsList(int page, int size, String sort, String category) {
+    if (category == null || sort == null) {
+        throw new IllegalArgumentException("Category and sort parameters cannot be null");
+    }
+    if (page < 1 || size < 1) {
+        throw new IllegalArgumentException("Page and size must be positive numbers");
+    }
+    
     Pageable pageable = PageRequest.of(page -1, size, getSort(sort));
     Page<NewsArticle> newsPage;

     if(category.equals("전체")){
         newsPage = newsRepository.findAllByIsFilteredOutFalse(pageable);
     } else{
-        Category enumCategory = Category.from(category);
+        try {
+            Category enumCategory = Category.from(category);
+            newsPage = newsRepository.findByCategoryAndIsFilteredOutFalse(enumCategory, pageable);
+        } catch (IllegalArgumentException e) {
+            log.warn("Invalid category provided: {}", category);
+            throw new InvalidCategoryException("Invalid category: " + category);
+        }
-        newsPage = newsRepository.findByCategoryAndIsFilteredOutFalse(enumCategory, pageable);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public NewsPageResponse getNewsList(int page, int size, String sort, String category) {
Pageable pageable = PageRequest.of(page -1, size, getSort(sort));
Page<NewsArticle> newsPage;
if(category.equals("전체")){
newsPage = newsRepository.findAllByIsFilteredOutFalse(pageable);
} else{
Category enumCategory = Category.from(category);
newsPage = newsRepository.findByCategoryAndIsFilteredOutFalse(enumCategory, pageable);
}
public NewsPageResponse getNewsList(int page, int size, String sort, String category) {
// 1. Null checks for required String parameters
if (category == null || sort == null) {
throw new IllegalArgumentException("Category and sort parameters cannot be null");
}
// 2. Range checks for pagination parameters
if (page < 1 || size < 1) {
throw new IllegalArgumentException("Page and size must be positive numbers");
}
Pageable pageable = PageRequest.of(page - 1, size, getSort(sort));
Page<NewsArticle> newsPage;
if (category.equals("전체")) {
newsPage = newsRepository.findAllByIsFilteredOutFalse(pageable);
} else {
try {
// 3. Validate and convert category, wrapping any IllegalArgumentException
Category enumCategory = Category.from(category);
newsPage = newsRepository.findByCategoryAndIsFilteredOutFalse(enumCategory, pageable);
} catch (IllegalArgumentException e) {
log.warn("Invalid category provided: {}", category);
throw new InvalidCategoryException("Invalid category: " + category, e);
}
}
// ... (rest of method that builds and returns NewsPageResponse)
}
🤖 Prompt for AI Agents
In src/main/java/com/blockguard/server/domain/news/application/NewsService.java
around lines 27 to 36, add input validation and error handling to the
getNewsList method. First, check for null values in the input parameters page,
size, sort, and category, and handle them appropriately. Second, wrap the call
to Category.from(category) in a try-catch block to handle any exceptions from
invalid category strings. Third, validate that the page size is within an
acceptable range (e.g., greater than zero and below a maximum limit) and handle
invalid sizes by throwing an exception or returning a default value.

Comment thread src/main/java/com/blockguard/server/domain/news/dao/NewsRepository.java Outdated
Comment on lines +6 to +13
public static Category from(String input){
return switch (input){
case "보이스피싱" -> VOICE_PHISHING;
case "스미싱" -> SMISHING;
case "메신저 피싱", "메신저피싱" -> MESSAGE_VOICE_PHISHING;
default -> ETC;
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

from 메서드 구현이 적절합니다.

한국어 키워드를 enum 상수로 매핑하는 로직이 잘 구현되었습니다. 스위치 표현식 사용과 "메신저 피싱"/"메신저피싱" 둘 다 처리하는 부분이 좋습니다.

중복 코드 검토 제안: src/main/java/com/blockguard/server/domain/analysis/domain/enums/FraudType.java에 이미 유사한 카테고리들이 정의되어 있습니다 (VOICE_PHISHING("보이스피싱"), MESSENGER_PHISHING("메신저피싱"), SMISHING("스미싱")). 코드 중복을 피하기 위해 두 enum 간의 통합이나 공통 매핑 로직 사용을 고려해보세요.


🏁 Script executed:

#!/bin/bash
# 기존 FraudType enum과의 중복 확인
rg -A 10 -B 5 "VOICE_PHISHING|SMISHING|MESSENGER_PHISHING" --type java

Length of output: 3575


Category와 FraudType enum 매핑 로직 통합 제안

현재 Category enum과 FraudType enum이 동일한 한글 키워드(“보이스피싱”, “메신저피싱”, “스미싱”)를 별도 구현하고 있어 중복이 발생하고 있습니다. 아래 위치를 참조하여 공통화 방안을 검토해주세요.

  • src/main/java/com/blockguard/server/domain/news/domain/enums/Category.java
    • enum 상수: VOICE_PHISHING, SMISHING, MESSAGE_VOICE_PHISHING
    public static Category from(String input) 메서드에서 한글 키워드 매핑

  • src/main/java/com/blockguard/server/domain/analysis/domain/enums/FraudType.java
    • enum 상수: VOICE_PHISHING("보이스피싱"), MESSENGER_PHISHING("메신저피싱"), SMISHING("스미싱")

제안 예시:

  • 하나의 FraudType.from(input) 공통 메서드를 통해 매핑 결과를 받아 Category로 변환
  • 또는 공통 유틸리티 클래스로 한글→Enum 매핑 로직을 추출

이렇게 하면 중복된 문자열 처리 로직을 제거하고 유지보수성을 높일 수 있습니다.

🤖 Prompt for AI Agents
In src/main/java/com/blockguard/server/domain/news/domain/enums/Category.java
around lines 6 to 13, the string-to-enum mapping logic duplicates the same
Korean keywords found in FraudType.java. To fix this, refactor
Category.from(String input) to delegate the string mapping to a common method,
such as FraudType.from(input), then convert the resulting FraudType enum to the
corresponding Category enum. Alternatively, extract the shared string-to-enum
mapping logic into a utility class used by both enums. This removes duplication
and centralizes the keyword mapping logic for easier maintenance.

Comment thread src/main/java/com/blockguard/server/infra/crawler/DaumNewsCrawler.java Outdated
Comment on lines +90 to +94
} catch (IOException e) {
throw new BusinessExceptionHandler(ErrorCode.FAIL_TO_CRAWLING_NEWS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

예외 처리 개선 필요

현재 IOException을 BusinessException으로 감싸고 있는데, 크롤링 실패의 구체적인 원인을 알 수 없습니다. 또한 InterruptedException을 RuntimeException으로 다시 던지는 것은 적절하지 않습니다.

 } catch (IOException e) {
+    log.error("Failed to crawl news from Daum for keyword: {}", keyword, e);
     throw new BusinessExceptionHandler(ErrorCode.FAIL_TO_CRAWLING_NEWS);
 } catch (InterruptedException e) {
-    throw new RuntimeException(e);
+    Thread.currentThread().interrupt();
+    log.warn("News crawling interrupted for keyword: {}", keyword);
+    throw new BusinessExceptionHandler(ErrorCode.CRAWLING_INTERRUPTED);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (IOException e) {
throw new BusinessExceptionHandler(ErrorCode.FAIL_TO_CRAWLING_NEWS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} catch (IOException e) {
log.error("Failed to crawl news from Daum for keyword: {}", keyword, e);
throw new BusinessExceptionHandler(ErrorCode.FAIL_TO_CRAWLING_NEWS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("News crawling interrupted for keyword: {}", keyword);
throw new BusinessExceptionHandler(ErrorCode.CRAWLING_INTERRUPTED);
}
🤖 Prompt for AI Agents
In src/main/java/com/blockguard/server/infra/crawler/DaumNewsCrawler.java around
lines 90 to 94, improve exception handling by including the original IOException
cause when throwing BusinessExceptionHandler to preserve the specific crawling
failure details. For InterruptedException, restore the thread's interrupted
status by calling Thread.currentThread().interrupt() instead of rethrowing as
RuntimeException, and handle or propagate it appropriately without losing the
interrupt signal.

@chaeyuuu chaeyuuu changed the title [Feat] News API [Feat] 뉴스 API Aug 1, 2025
Copy link
Copy Markdown
Contributor

@yeonju73 yeonju73 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다!! 하나 궁금한게 response body에서 imageUrl 그대로 웹에 입력하면 그 기사 이미지가 뜨는거 맞나요!?

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/java/com/blockguard/server/domain/admin/api/AdminApi.java (1)

37-37: SuccessCode 상수명에 오타가 있습니다.

CRWAL_DAUM_NEWS_SUCCESS에서 "CRWAL"은 "CRAWL"의 오타로 보입니다.

SuccessCode 클래스에서 해당 상수명을 수정하는 것이 좋겠습니다:

-CRWAL_DAUM_NEWS_SUCCESS
+CRAWL_DAUM_NEWS_SUCCESS
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23497af and 26b64c2.

📒 Files selected for processing (3)
  • src/main/java/com/blockguard/server/domain/admin/api/AdminApi.java (2 hunks)
  • src/main/java/com/blockguard/server/global/common/codes/SuccessCode.java (1 hunks)
  • src/main/java/com/blockguard/server/global/config/SecurityConfig.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/blockguard/server/global/config/SecurityConfig.java
  • src/main/java/com/blockguard/server/global/common/codes/SuccessCode.java
🔇 Additional comments (1)
src/main/java/com/blockguard/server/domain/admin/api/AdminApi.java (1)

5-5: 새로운 의존성 주입이 적절히 구현되었습니다.

DaumNewsCrawler 의존성이 올바르게 import되고 주입되었습니다.

Also applies to: 20-20

Comment thread src/main/java/com/blockguard/server/domain/admin/api/AdminApi.java
@chaeyuuu chaeyuuu merged commit daf00f1 into main Aug 1, 2025
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Sep 4, 2025
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 뉴스 API

2 participants