Skip to content

Commit daf00f1

Browse files
authored
Merge pull request #44 from Block-Guard/feat/#33/news-api
[Feat] 뉴스 API
2 parents f4c1884 + caba25c commit daf00f1

File tree

20 files changed

+444
-7
lines changed

20 files changed

+444
-7
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ dependencies {
4646
implementation(platform("software.amazon.awssdk:bom:2.31.58"))
4747
implementation 'software.amazon.awssdk:s3'
4848
implementation 'software.amazon.awssdk:auth'
49+
50+
implementation 'org.jsoup:jsoup:1.17.2'
4951
}
5052

5153
tasks.named('test') {
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,42 @@
11
package com.blockguard.server.domain.admin.api;
22

3+
import com.blockguard.server.domain.news.application.NewsService;
4+
import com.blockguard.server.domain.news.scheduler.NewsSaveScheduler;
35
import com.blockguard.server.global.common.codes.SuccessCode;
46
import com.blockguard.server.global.common.response.BaseResponse;
7+
import com.blockguard.server.infra.crawler.DaumNewsCrawler;
58
import com.blockguard.server.infra.importer.FraudUrlImporter;
69
import io.swagger.v3.oas.annotations.Operation;
710
import lombok.AllArgsConstructor;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.http.ResponseEntity;
813
import org.springframework.web.bind.annotation.PostMapping;
914
import org.springframework.web.bind.annotation.RequestMapping;
1015
import org.springframework.web.bind.annotation.RestController;
1116

1217
@RestController
1318
@AllArgsConstructor
1419
@RequestMapping("/api/admin")
20+
@Slf4j
1521
public class AdminApi {
1622

1723
private final FraudUrlImporter fraudUrlImporter;
24+
private NewsSaveScheduler newsSaveScheduler;
1825

1926
@PostMapping("/update/fraud-url")
2027
@Operation(summary = "공공 api 데이터 호출 - 관리자용")
21-
public BaseResponse<Void> syncFraudUrls(){
28+
public BaseResponse<Void> syncFraudUrls() {
2229
fraudUrlImporter.syncFraudUrlsFromOpenApi();
2330
return BaseResponse.of(SuccessCode.IMPORT_OPEN_API_SUCCESS);
2431
}
32+
33+
34+
@PostMapping("/crawl")
35+
@Operation(summary = "뉴스 크롤링 - 관리자용")
36+
public BaseResponse<Void> crawlNewsManually() {
37+
newsSaveScheduler.crawlingForAdmin();
38+
return BaseResponse.of(SuccessCode.CRWAL_DAUM_NEWS_SUCCESS);
39+
}
2540
}
41+
42+

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import com.blockguard.server.domain.analysis.dto.response.FraudAnalysisResponse;
77
import com.blockguard.server.domain.analysis.dto.response.GptResponse;
88
import com.blockguard.server.infra.gpt.GptApiClient;
9-
import com.blockguard.server.infra.ocr.NaverOcrClient;
9+
import com.blockguard.server.infra.naver.ocr.NaverOcrClient;
1010
import lombok.RequiredArgsConstructor;
1111
import lombok.extern.slf4j.Slf4j;
1212
import org.springframework.stereotype.Service;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.blockguard.server.domain.news.api;
2+
3+
import com.blockguard.server.domain.news.application.NewsService;
4+
import com.blockguard.server.domain.news.dto.response.NewsPageResponse;
5+
import com.blockguard.server.global.common.codes.SuccessCode;
6+
import com.blockguard.server.global.common.response.BaseResponse;
7+
import com.blockguard.server.global.config.swagger.CustomExceptionDescription;
8+
import com.blockguard.server.global.config.swagger.SwaggerResponseDescription;
9+
import io.swagger.v3.oas.annotations.Operation;
10+
import lombok.AllArgsConstructor;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.web.bind.annotation.*;
13+
14+
@RestController
15+
@RequestMapping("/api/news")
16+
@AllArgsConstructor
17+
@Slf4j
18+
public class NewsApi {
19+
private final NewsService newsService;
20+
21+
@GetMapping
22+
@CustomExceptionDescription(SwaggerResponseDescription.GET_NEWS_ARTICLES_FAIL)
23+
@Operation(summary = "뉴스 조회")
24+
public BaseResponse<NewsPageResponse> getNewsArticles(
25+
@RequestParam(defaultValue = "1") int page,
26+
@RequestParam(defaultValue = "10") int size,
27+
@RequestParam(defaultValue = "published_at_desc") String sort,
28+
@RequestParam(defaultValue = "전체") String category) {
29+
NewsPageResponse newsPageResponse = newsService.getNewsList(page, size, sort, category);
30+
return BaseResponse.of(SuccessCode.GET_NEWS_ARTICLES_SUCCESS, newsPageResponse);
31+
}
32+
33+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.blockguard.server.domain.news.application;
2+
3+
import com.blockguard.server.domain.news.dao.NewsRepository;
4+
import com.blockguard.server.domain.news.domain.NewsArticle;
5+
import com.blockguard.server.domain.news.domain.enums.Category;
6+
import com.blockguard.server.domain.news.dto.response.NewsArticleResponse;
7+
import com.blockguard.server.domain.news.dto.response.NewsPageResponse;
8+
import com.blockguard.server.domain.news.dto.response.PageableInfo;
9+
import com.blockguard.server.global.common.codes.ErrorCode;
10+
import com.blockguard.server.global.exception.BusinessExceptionHandler;
11+
import lombok.AllArgsConstructor;
12+
import lombok.extern.slf4j.Slf4j;
13+
import org.springframework.data.domain.Page;
14+
import org.springframework.data.domain.PageRequest;
15+
import org.springframework.data.domain.Pageable;
16+
import org.springframework.data.domain.Sort;
17+
import org.springframework.stereotype.Service;
18+
19+
import java.util.List;
20+
21+
@Service
22+
@Slf4j
23+
@AllArgsConstructor
24+
public class NewsService {
25+
private final NewsRepository newsRepository;
26+
27+
public NewsPageResponse getNewsList(int page, int size, String sort, String category) {
28+
Pageable pageable = PageRequest.of(page - 1, size, getSort(sort));
29+
Page<NewsArticle> newsPage;
30+
31+
if (page < 1 || size < 1) {
32+
throw new BusinessExceptionHandler(ErrorCode.MUST_BE_POSITIVE_NUMBER);
33+
}
34+
35+
if (category.equals("전체")) {
36+
newsPage = newsRepository.findAllByIsFilteredOutFalse(pageable);
37+
} else {
38+
Category enumCategory = Category.from(category);
39+
newsPage = newsRepository.findByCategoryAndIsFilteredOutFalse(enumCategory, pageable);
40+
}
41+
42+
List<NewsArticleResponse> articleResponses = newsPage.stream()
43+
.map(NewsArticleResponse::from)
44+
.toList();
45+
46+
return NewsPageResponse.builder()
47+
.news(articleResponses)
48+
.sort(sort)
49+
.pageableInfo(PageableInfo.builder()
50+
.page(newsPage.getNumber() + 1)
51+
.size(newsPage.getSize())
52+
.totalElements(newsPage.getTotalElements())
53+
.totalPages(newsPage.getTotalPages())
54+
.build())
55+
.build();
56+
}
57+
58+
private Sort getSort(String sort) {
59+
return switch (sort) {
60+
case "published_at_asc" -> Sort.by("publishedAt").ascending();
61+
case "published_at_desc" -> Sort.by("publishedAt").descending();
62+
default -> Sort.by("publishedAt").descending();
63+
};
64+
}
65+
66+
67+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.blockguard.server.domain.news.dao;
2+
3+
import com.blockguard.server.domain.news.domain.NewsArticle;
4+
import com.blockguard.server.domain.news.domain.enums.Category;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
7+
import org.springframework.data.jpa.repository.JpaRepository;
8+
9+
public interface NewsRepository extends JpaRepository<NewsArticle, Long> {
10+
Page<NewsArticle> findByCategoryAndIsFilteredOutFalse(Category category, Pageable pageable);
11+
boolean existsByUrl(String url);
12+
Page<NewsArticle> findAllByIsFilteredOutFalse(Pageable pageable);
13+
}

src/main/java/com/blockguard/server/domain/news/domain/NewsArticle.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.blockguard.server.domain.news.domain;
22

3+
import com.blockguard.server.domain.news.domain.enums.Category;
34
import com.blockguard.server.global.common.entity.BaseEntity;
45
import jakarta.persistence.*;
56
import lombok.*;
@@ -32,4 +33,11 @@ public class NewsArticle extends BaseEntity {
3233
private String newspaper;
3334

3435
private String imageUrl;
36+
37+
@Enumerated(EnumType.STRING)
38+
@Column(nullable = false)
39+
private Category category;
40+
41+
@Column(nullable = false)
42+
private boolean isFilteredOut;
3543
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.blockguard.server.domain.news.domain.enums;
2+
3+
public enum Category {
4+
VOICE_PHISHING, SMISHING, MESSAGE_VOICE_PHISHING, ETC;
5+
6+
public static Category from(String input){
7+
return switch (input){
8+
case "보이스피싱" -> VOICE_PHISHING;
9+
case "스미싱" -> SMISHING;
10+
case "메신저 피싱", "메신저피싱" -> MESSAGE_VOICE_PHISHING;
11+
default -> ETC;
12+
};
13+
}
14+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.blockguard.server.domain.news.dto.response;
2+
3+
import com.blockguard.server.domain.news.domain.NewsArticle;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
import java.time.Duration;
9+
import java.time.LocalDateTime;
10+
11+
@Getter
12+
@Builder(toBuilder = true)
13+
@AllArgsConstructor
14+
public class NewsArticleResponse {
15+
private Long id;
16+
private String title;
17+
private String publishedAt;
18+
private String url;
19+
private String newspaper;
20+
private String imageUrl;
21+
22+
public static NewsArticleResponse from(NewsArticle newsArticle) {
23+
return NewsArticleResponse.builder()
24+
.id(newsArticle.getId())
25+
.title(newsArticle.getTitle())
26+
.publishedAt(formatTime(newsArticle.getPublishedAt()))
27+
.imageUrl(newsArticle.getImageUrl())
28+
.newspaper(newsArticle.getNewspaper())
29+
.url(newsArticle.getUrl())
30+
.build();
31+
}
32+
33+
34+
private static String formatTime(LocalDateTime time) {
35+
Duration duration = Duration.between(time, LocalDateTime.now());
36+
37+
if (duration.toMinutes() < 60) {
38+
return duration.toMinutes() + "분 전";
39+
} else if (duration.toHours() < 24) {
40+
return duration.toHours() + "시간 전";
41+
} else if (duration.toDays() == 1) {
42+
return "어제";
43+
} else {
44+
return time.toLocalDate().toString();
45+
}
46+
}
47+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.blockguard.server.domain.news.dto.response;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
import java.util.List;
8+
9+
@Getter
10+
@Builder
11+
@AllArgsConstructor
12+
public class NewsPageResponse {
13+
private List<NewsArticleResponse> news;
14+
private PageableInfo pageableInfo;
15+
private String sort;
16+
}

0 commit comments

Comments
 (0)