Skip to content

Commit f0516c3

Browse files
authored
Merge pull request #79 from Block-Guard/feat/#76/fraud-type-news-api
[Feat] 사기 유형별 관련 뉴스 사례 조회 API
2 parents b29e3b6 + 4e88f0d commit f0516c3

File tree

8 files changed

+112
-45
lines changed

8 files changed

+112
-45
lines changed

src/main/java/com/blockguard/server/domain/admin/api/AdminApi.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class AdminApi {
3232
private String adminSecret;
3333

3434
@PostMapping("/login")
35+
@Operation(summary = "관리자 로그인")
3536
public BaseResponse<JwtToken> loginAdmin(@RequestParam String key) {
3637
if (!adminSecret.equals(key)) {
3738
throw new BusinessExceptionHandler(ErrorCode.INVALID_TOKEN);
@@ -42,15 +43,15 @@ public BaseResponse<JwtToken> loginAdmin(@RequestParam String key) {
4243
}
4344

4445
@PostMapping("/update/fraud-url")
45-
@Operation(summary = "공공 api 데이터 호출 - 관리자용")
46+
@Operation(summary = "공공 api 데이터 호출")
4647
public BaseResponse<Void> syncFraudUrls() {
4748
fraudUrlImporter.syncFraudUrlsFromOpenApi();
4849
return BaseResponse.of(SuccessCode.IMPORT_OPEN_API_SUCCESS);
4950
}
5051

5152

5253
@PostMapping("/crawl")
53-
@Operation(summary = "뉴스 크롤링 - 관리자용")
54+
@Operation(summary = "뉴스 크롤링")
5455
public BaseResponse<Void> crawlNewsManually() {
5556
newsSaveScheduler.crawlingForAdmin();
5657
return BaseResponse.of(SuccessCode.CRWAL_DAUM_NEWS_SUCCESS);

src/main/java/com/blockguard/server/domain/fraud/api/FraudApi.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class FraudApi {
2323
private final FraudService fraudService;
2424

2525
@PostMapping("/url")
26+
@CustomExceptionDescription(SwaggerResponseDescription.CHECK_FRAUD_URL_FAIL)
2627
@Operation(summary = "입력된 url 사기 분석")
2728
public BaseResponse<FraudRiskLevelResponse> fraudUrl(@Valid @RequestBody FraudUrlRequest fraudUrlRequest){
2829
FraudRiskLevelResponse fraudRiskLevelResponse = fraudService.checkFraudUrl(fraudUrlRequest);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public class NewsArticle extends BaseEntity {
3535
private String imageUrl;
3636

3737
@Enumerated(EnumType.STRING)
38-
@Column(nullable = false)
38+
@Column(nullable = false, length = 255)
3939
private Category category;
4040

4141
@Column(nullable = false)
Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,73 @@
11
package com.blockguard.server.domain.news.domain.enums;
22

3+
import java.util.Map;
4+
import java.util.Optional;
5+
36
public enum Category {
4-
VOICE_PHISHING, SMISHING, MESSAGE_VOICE_PHISHING, ETC;
7+
VOICE_PHISHING, SMISHING, MESSAGE_VOICE_PHISHING, ETC,
8+
INSTITUTION_IMPERSONATION, // 기관 사칭
9+
LOAN_FRAUD, // 대출 사기
10+
CARD_IMPERSONATION, // 카드사 사칭
11+
FAMILY_IMPERSONATION, // 가족/지인 사칭
12+
EVENT_IMPERSONATION, // 경조사 사칭
13+
PUBLIC_IMPERSONATION, // 공공기관 사칭 (범칙금/과태로 부과, 건강보험공단 사칭형, 경찰출석요구형 ,국세청 사칭형)
14+
PART_TIME_SCAM, // 알바/부업 사기
15+
GOVERNMENT_GRANT_SCAM, // 정부지원금 위장
16+
DELIVERY_SCAM, // 택배 사기
17+
INVESTMENT_SCAM, // 투자 사기
18+
FALSE_PAYMENT_SCAM; // 허위결제 사기
19+
20+
private static final Map<String, Category> MAPPING = Map.ofEntries(
21+
Map.entry("보이스피싱", VOICE_PHISHING),
22+
Map.entry("보이스 피싱", VOICE_PHISHING),
23+
Map.entry("스미싱", SMISHING),
24+
Map.entry("메신저피싱", MESSAGE_VOICE_PHISHING),
25+
Map.entry("메신저 피싱", MESSAGE_VOICE_PHISHING),
26+
Map.entry("기관 사칭형", INSTITUTION_IMPERSONATION),
27+
Map.entry("대출 사기형", LOAN_FRAUD),
28+
Map.entry("카드사 사칭형", CARD_IMPERSONATION),
29+
Map.entry("가족/지인 사칭형", FAMILY_IMPERSONATION),
30+
Map.entry("경조사 사칭형", EVENT_IMPERSONATION),
31+
Map.entry("공공기관 사칭형", PUBLIC_IMPERSONATION),
32+
Map.entry("알바/부업 사기형", PART_TIME_SCAM),
33+
Map.entry("정부지원금 위장형", GOVERNMENT_GRANT_SCAM),
34+
Map.entry("택배 사기형", DELIVERY_SCAM),
35+
Map.entry("투자 사기형", INVESTMENT_SCAM),
36+
Map.entry("허위결제 사기형", FALSE_PAYMENT_SCAM),
37+
38+
Map.entry("돌잔치 초대장형", EVENT_IMPERSONATION),
39+
Map.entry("모바일 청첩장형", EVENT_IMPERSONATION),
40+
Map.entry("부고문자형", EVENT_IMPERSONATION),
41+
Map.entry("범칙금/과태료 부과형", PUBLIC_IMPERSONATION),
42+
Map.entry("건강보험공단 사칭형", PUBLIC_IMPERSONATION),
43+
Map.entry("경찰출석요구형", PUBLIC_IMPERSONATION),
44+
Map.entry("국세청 사칭형", PUBLIC_IMPERSONATION),
45+
Map.entry("가상화폐 사기형", INVESTMENT_SCAM),
46+
Map.entry("주식투자 사기형", INVESTMENT_SCAM),
47+
Map.entry("청약 공모주 사기형", INVESTMENT_SCAM)
48+
);
549

650
public static Category from(String input) {
7-
return switch (input) {
51+
return Optional.ofNullable(MAPPING.get(input.trim()))
52+
.orElse(ETC);
53+
}
54+
/*return switch (input.trim()) {
855
case "보이스피싱", "보이스 피싱" -> VOICE_PHISHING;
956
case "스미싱" -> SMISHING;
1057
case "메신저 피싱", "메신저피싱" -> MESSAGE_VOICE_PHISHING;
58+
59+
case "기관 사칭형" -> INSTITUTION_IMPERSONATION;
60+
case "대출 사기형" -> LOAN_FRAUD;
61+
case "카드사 사칭형" -> CARD_IMPERSONATION;
62+
case "가족/지인 사칭형" -> FAMILY_IMPERSONATION;
63+
case "경조사 사칭형" -> EVENT_IMPERSONATION;
64+
case "공공기관 사칭형" -> PUBLIC_IMPERSONATION;
65+
case "알바/부업 사기형" -> PART_TIME_SCAM;
66+
case "정부지원금 위장형" -> GOVERNMENT_GRANT_SCAM;
67+
case "택배 사기형" -> DELIVERY_SCAM;
68+
case "투자 사기형" -> INVESTMENT_SCAM;
69+
case "허위결제 사기형" -> FALSE_PAYMENT_SCAM;
1170
default -> ETC;
12-
};
13-
}
71+
};*/
72+
1473
}
Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,60 @@
11
package com.blockguard.server.domain.news.scheduler;
22

3+
import com.blockguard.server.domain.news.domain.enums.Category;
34
import com.blockguard.server.infra.crawler.DaumNewsCrawler;
45
import lombok.RequiredArgsConstructor;
56
import lombok.extern.slf4j.Slf4j;
67
import org.springframework.scheduling.annotation.Scheduled;
78
import org.springframework.stereotype.Component;
89

10+
import java.util.List;
11+
import java.util.Map;
12+
913
@Component
1014
@Slf4j
1115
@RequiredArgsConstructor
1216
public class NewsSaveScheduler {
1317
private final DaumNewsCrawler daumNewsCrawler;
18+
private static final Map<Category, List<String>> KEYWORDS_PER_CATEGORY = Map.ofEntries(
19+
Map.entry(Category.VOICE_PHISHING, List.of("보이스피싱", "보이스 피싱")),
20+
Map.entry(Category.SMISHING, List.of("스미싱")),
21+
Map.entry(Category.MESSAGE_VOICE_PHISHING, List.of("메신저 피싱", "메신저피싱")),
22+
23+
Map.entry(Category.INSTITUTION_IMPERSONATION, List.of("기관 사칭", "검찰 사칭", "경찰 사칭", "수사기관 사칭")),
24+
Map.entry(Category.LOAN_FRAUD, List.of("대출 사기", "저금리 대출 사기", "대출 빙자 사기")),
25+
Map.entry(Category.CARD_IMPERSONATION, List.of("카드사 사칭", "카드사 피싱", "신용카드 사칭")),
26+
Map.entry(Category.FAMILY_IMPERSONATION, List.of("가족 사칭", "지인 사칭")),
27+
Map.entry(Category.EVENT_IMPERSONATION, List.of("경조사 사칭", "경조사 문자 사기")),
28+
Map.entry(Category.PUBLIC_IMPERSONATION, List.of("공공기관 사칭", "국세청 사칭", "경찰 출석 요구 사칭", "검찰 사칭", "과태료 사칭")),
29+
Map.entry(Category.PART_TIME_SCAM, List.of("알바 사기", "부업 사기", "구인 사기")),
30+
Map.entry(Category.GOVERNMENT_GRANT_SCAM, List.of("정부지원금 사기", "보조금 사기", "지원금 사칭")),
31+
Map.entry(Category.DELIVERY_SCAM, List.of("택배 사기", "택배 문자 사기", "배송 사기")),
32+
Map.entry(Category.INVESTMENT_SCAM, List.of("투자 사기", "코인 투자 사기", "주식 리딩방 사기")),
33+
Map.entry(Category.FALSE_PAYMENT_SCAM, List.of("허위 결제 사기", "결제 승인 사기", "결제 피싱")),
34+
Map.entry(Category.ETC, List.of("몸캠"))
35+
);
36+
1437

1538
public void crawlingForAdmin() {
16-
String[] keywords = {"보이스 피싱", "보이스피싱", "스미싱", "메신저 피싱", "몸캠"};
17-
for (String keyword : keywords) {
18-
try {
19-
daumNewsCrawler.fetchNewsFromDaum(keyword);
20-
} catch (Exception e) {
21-
log.warn("Failed to crawl news for keyword: {}", keyword, e);
22-
}
23-
}
39+
crawlAll();
2440
}
2541

26-
@Scheduled(cron = "0 0 4 * * *")
42+
// @Scheduled(cron = "0 0 4 * * *")
2743
public void saveNewsArticles() {
28-
log.info("뉴스 크롤링 스케줄링 시작");
29-
30-
daumNewsCrawler.fetchNewsFromDaum("보이스 피싱");
31-
daumNewsCrawler.fetchNewsFromDaum("스미싱");
32-
daumNewsCrawler.fetchNewsFromDaum("메신저 피싱");
33-
daumNewsCrawler.fetchNewsFromDaum("몸캠");
44+
crawlAll();
45+
}
3446

47+
private void crawlAll() {
48+
log.info("뉴스 크롤링 스케줄링 시작");
49+
KEYWORDS_PER_CATEGORY.forEach((category, keywords) -> {
50+
for (String keyword : keywords) {
51+
try {
52+
daumNewsCrawler.fetchNewsFromDaum(keyword, category);
53+
} catch (Exception e) {
54+
log.warn("크롤 실패: category={}, keyword={}", category, keyword, e);
55+
}
56+
}
57+
});
3558
log.info("뉴스 크롤링 스케줄링 완료");
3659
}
3760
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ public enum SwaggerResponseDescription {
111111
ErrorCode.FRAUD_NUMBER_SERVER_ERROR
112112
))),
113113

114+
CHECK_FRAUD_URL_FAIL(new LinkedHashSet<>(Set.of(
115+
))),
116+
114117
ANALYZE_FRAUD_FAIL(new LinkedHashSet<>(Set.of(
115118
ErrorCode.INVALID_TOKEN,
116119
ErrorCode.AI_SERVER_ERROR,

src/main/java/com/blockguard/server/infra/crawler/DaumNewsCrawler.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@
2626
public class DaumNewsCrawler {
2727
private final NewsRepository newsRepository;
2828
private static final int MAX_PAGES = 20;
29-
private static final int ARTICLE_RETENTION_DAYS = 1095;
29+
private static final int ARTICLE_RETENTION_DAYS = 365;
3030
private static final long CRAWL_DELAY_MS = 500L;
3131

32-
public void fetchNewsFromDaum(String keyword) {
33-
Category category = Category.from(keyword);
32+
public void fetchNewsFromDaum(String keyword, Category forceCategory) {
33+
Category category = (forceCategory != null) ? forceCategory : Category.from(keyword);
3434
int savedCount = 0;
3535

3636
try {
@@ -44,7 +44,6 @@ public void fetchNewsFromDaum(String keyword) {
4444
.get();
4545

4646
Elements newsItems = doc.select("li[data-docid]");
47-
4847
if (newsItems.isEmpty()) break;
4948

5049
for (Element item : newsItems) {

src/test/java/com/blockguard/server/infra/crawler/DaumNewsCrawlerTest.java

Lines changed: 0 additions & 19 deletions
This file was deleted.

0 commit comments

Comments
 (0)