Skip to content

Commit 6e4e1e7

Browse files
authored
Merge pull request #46 from Block-Guard/feat/#41/phone-number-fraud-api
[Feat] 전화번호 사기분석 API
2 parents 33ef6fc + cd37bb2 commit 6e4e1e7

File tree

9 files changed

+196
-16
lines changed

9 files changed

+196
-16
lines changed

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package com.blockguard.server.domain.fraud.api;
22

33
import com.blockguard.server.domain.fraud.application.FraudService;
4+
import com.blockguard.server.domain.fraud.dto.request.FraudPhoneNumberRequest;
45
import com.blockguard.server.domain.fraud.dto.request.FraudUrlRequest;
5-
import com.blockguard.server.domain.fraud.dto.response.FraudUrlResponse;
6+
import com.blockguard.server.domain.fraud.dto.response.FraudRiskLevelResponse;
67
import com.blockguard.server.global.common.codes.SuccessCode;
78
import com.blockguard.server.global.common.response.BaseResponse;
9+
import com.blockguard.server.global.config.swagger.CustomExceptionDescription;
10+
import com.blockguard.server.global.config.swagger.SwaggerResponseDescription;
811
import io.swagger.v3.oas.annotations.Operation;
912
import jakarta.validation.Valid;
1013
import lombok.AllArgsConstructor;
@@ -21,9 +24,17 @@ public class FraudApi {
2124

2225
@PostMapping("/url")
2326
@Operation(summary = "입력된 url 사기 분석")
24-
public BaseResponse<FraudUrlResponse> fraudUrl(@Valid @RequestBody FraudUrlRequest fraudUrlRequest){
25-
FraudUrlResponse fraudUrlResponse = fraudService.checkFraudUrl(fraudUrlRequest);
26-
return BaseResponse.of(SuccessCode.CHECK_URL_FRAUD_SUCCESS, fraudUrlResponse);
27+
public BaseResponse<FraudRiskLevelResponse> fraudUrl(@Valid @RequestBody FraudUrlRequest fraudUrlRequest){
28+
FraudRiskLevelResponse fraudRiskLevelResponse = fraudService.checkFraudUrl(fraudUrlRequest);
29+
return BaseResponse.of(SuccessCode.CHECK_URL_FRAUD_SUCCESS, fraudRiskLevelResponse);
30+
}
31+
32+
@PostMapping("/number")
33+
@CustomExceptionDescription(SwaggerResponseDescription.CHECK_FRAUD_PHONE_NUMBER_FAIL)
34+
@Operation(summary = "입력된 전화번호 사기 분석", description = "전화번호의 입력 형식 상관없음")
35+
public BaseResponse<FraudRiskLevelResponse> fraudPhoneNumber(@Valid @RequestBody FraudPhoneNumberRequest fraudPhoneNumberRequest){
36+
FraudRiskLevelResponse fraudRiskLevelResponse = fraudService.checkFraudPhoneNumber(fraudPhoneNumberRequest);
37+
return BaseResponse.of(SuccessCode.CHECK_PHONE_NUMBER_FRAUD_SUCCESS, fraudRiskLevelResponse);
2738
}
2839

2940
}

src/main/java/com/blockguard/server/domain/fraud/application/FraudService.java

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,26 @@
22

33
import com.blockguard.server.domain.analysis.domain.enums.RiskLevel;
44
import com.blockguard.server.domain.fraud.dao.FraudUrlRepository;
5+
import com.blockguard.server.domain.fraud.dto.request.FraudPhoneNumberRequest;
56
import com.blockguard.server.domain.fraud.dto.request.FraudUrlRequest;
6-
import com.blockguard.server.domain.fraud.dto.response.FraudUrlResponse;
7+
import com.blockguard.server.domain.fraud.dto.response.FraudRiskLevelResponse;
78
import com.blockguard.server.global.common.codes.ErrorCode;
89
import com.blockguard.server.global.exception.BusinessExceptionHandler;
910
import com.blockguard.server.infra.google.GoogleSafeBrowsingClient;
11+
import com.blockguard.server.infra.number.FraudNumberClient;
1012
import lombok.AllArgsConstructor;
13+
import lombok.extern.slf4j.Slf4j;
1114
import org.springframework.stereotype.Service;
1215

16+
@Slf4j
1317
@Service
1418
@AllArgsConstructor
1519
public class FraudService {
1620
private final FraudUrlRepository fraudUrlRepository;
1721
private final GoogleSafeBrowsingClient googleSafeBrowsingService;
22+
private final FraudNumberClient fraudNumberClient;
1823

19-
public FraudUrlResponse checkFraudUrl(FraudUrlRequest fraudUrlRequest) {
24+
public FraudRiskLevelResponse checkFraudUrl(FraudUrlRequest fraudUrlRequest) {
2025
String url = fraudUrlRequest.getUrl();
2126

2227
if (url == null || url.trim().isEmpty()){
@@ -25,21 +30,52 @@ public FraudUrlResponse checkFraudUrl(FraudUrlRequest fraudUrlRequest) {
2530

2631
// 1차: DB 검사
2732
if(fraudUrlRepository.existsByUrl(url)){
28-
return FraudUrlResponse.builder()
33+
return FraudRiskLevelResponse.builder()
2934
.riskLevel(RiskLevel.Dangers)
3035
.build();
3136
}
3237

3338
// 2차: Google Safe Browsing API 호출
3439
boolean isSafe = googleSafeBrowsingService.isUrlSafe(url);
3540
if (!isSafe){
36-
return FraudUrlResponse.builder()
41+
return FraudRiskLevelResponse.builder()
3742
.riskLevel(RiskLevel.Dangers)
3843
.build();
3944
}
4045

41-
return FraudUrlResponse.builder()
46+
return FraudRiskLevelResponse.builder()
4247
.riskLevel(RiskLevel.Safety)
4348
.build();
4449
}
50+
51+
public FraudRiskLevelResponse checkFraudPhoneNumber(FraudPhoneNumberRequest fraudPhoneNumberRequest) {
52+
String phoneNumber = fraudPhoneNumberRequest.getPhoneNumber().replaceAll("\\s+", "");
53+
54+
// 사기 전화번호 조회 api 호출
55+
String spamCount = fraudNumberClient.checkSpamNumber(phoneNumber).getSpamCount();
56+
57+
if (isFraudNumber(spamCount)){
58+
return FraudRiskLevelResponse.builder()
59+
.riskLevel(RiskLevel.Dangers)
60+
.build();
61+
}
62+
63+
return FraudRiskLevelResponse.builder()
64+
.riskLevel(RiskLevel.Safety)
65+
.build();
66+
}
67+
68+
private Boolean isFraudNumber(String spamCount) {
69+
try {
70+
// spam count 최대 "1000+" 가 나오는 경우 고려
71+
String numericPart = spamCount.replaceAll("\\D+", "");
72+
if (numericPart.isEmpty()){
73+
throw new BusinessExceptionHandler(ErrorCode.FRAUD_NUMBER_SERVER_ERROR);
74+
}
75+
return Integer.parseInt(numericPart) > 0;
76+
} catch (NumberFormatException e) {
77+
log.error("Invalid spam count format: {}", spamCount, e);
78+
throw new BusinessExceptionHandler(ErrorCode.FRAUD_NUMBER_SERVER_ERROR);
79+
}
80+
}
4581
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.blockguard.server.domain.fraud.dto.request;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.Size;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Getter;
7+
8+
@Getter
9+
@AllArgsConstructor
10+
public class FraudPhoneNumberRequest {
11+
@NotBlank
12+
@Size(max = 20)
13+
private String phoneNumber;
14+
}

src/main/java/com/blockguard/server/domain/fraud/dto/response/FraudUrlResponse.java renamed to src/main/java/com/blockguard/server/domain/fraud/dto/response/FraudRiskLevelResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66

77
@Getter
88
@Builder
9-
public class FraudUrlResponse {
9+
public class FraudRiskLevelResponse {
1010
private RiskLevel riskLevel;
1111
}

src/main/java/com/blockguard/server/global/common/codes/ErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public enum ErrorCode {
4040

4141
// 5000~ : server error
4242
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 5000, "서버 오류가 발생했습니다."),
43-
AI_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 5001, "AI 서버와의 통신 오류가 발생했습니다.");
43+
AI_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 5001, "AI 서버와의 통신 오류가 발생했습니다."),
44+
FRAUD_NUMBER_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 5002, "사기 전화번호 제공 서버와의 통신 오류가 발생했습니다.");
4445

4546
private final HttpStatus status;
4647
private final int code;

src/main/java/com/blockguard/server/global/config/SecurityConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti
5555
registry
5656
.requestMatchers("/api/auth/**").permitAll() // 로그인 및 회원가입
5757
.requestMatchers("/api/admin/login").permitAll() // 관리자 로그인
58-
.requestMatchers("/api/fraud/url", "/api/fraud-analysis").permitAll() // 사기 분석
58+
.requestMatchers("/api/fraud/url", "/api/fraud/number","/api/fraud-analysis").permitAll() // 사기 분석
5959
.requestMatchers("/api/news").permitAll() // 뉴스 조회
6060
.requestMatchers("/actuator/health").permitAll() // 헬스체크 허용
6161
.requestMatchers("/", "/index.html", "/favicon.ico").permitAll()

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,6 @@ public enum SwaggerResponseDescription {
8686
ErrorCode.REPORT_ALREADY_IN_PROGRESS
8787
))),
8888

89-
INVALID_TOKEN(new LinkedHashSet<>(Set.of(
90-
ErrorCode.INVALID_TOKEN
91-
))),
92-
9389
GET_STEP_INFO_FAIL(new LinkedHashSet<>(Set.of(
9490
ErrorCode.INVALID_TOKEN,
9591
ErrorCode.REPORT_NOT_FOUND,
@@ -107,6 +103,14 @@ public enum SwaggerResponseDescription {
107103
ErrorCode.INVALID_CHECKBOX_COUNT,
108104
ErrorCode.INVALID_STEP_COMPLETION,
109105
ErrorCode.REPORT_STEP_ALREADY_COMPLETED
106+
))),
107+
108+
CHECK_FRAUD_PHONE_NUMBER_FAIL(new LinkedHashSet<>(Set.of(
109+
ErrorCode.FRAUD_NUMBER_SERVER_ERROR
110+
))),
111+
112+
INVALID_TOKEN(new LinkedHashSet<>(Set.of(
113+
ErrorCode.INVALID_TOKEN
110114
)));
111115

112116
private final Set<ErrorCode> errorCodeList;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.blockguard.server.infra.number;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@Builder
9+
public class CheckFraudNumberResponse {
10+
private DataBlock data;
11+
private ApiBlock api;
12+
13+
@Getter
14+
@Builder
15+
public static class DataBlock {
16+
private String number;
17+
private String spam;
18+
19+
@JsonProperty("spam_count")
20+
private String spamCount;
21+
22+
@JsonProperty("registed_date")
23+
private String registedDate;
24+
25+
@JsonProperty("cyber_crime")
26+
private String cyberCrime;
27+
28+
/** 1=성공, 0=실패, 3: 실패(timeout) */
29+
private int success;
30+
}
31+
32+
@Getter
33+
@Builder
34+
public static class ApiBlock {
35+
private boolean success;
36+
private int cost;
37+
private int ms;
38+
39+
@JsonProperty("pl_id")
40+
private int plId;
41+
}
42+
43+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.blockguard.server.infra.number;
2+
3+
import com.blockguard.server.global.common.codes.ErrorCode;
4+
import com.blockguard.server.global.exception.BusinessExceptionHandler;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.http.HttpEntity;
9+
import org.springframework.http.HttpHeaders;
10+
import org.springframework.http.MediaType;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.stereotype.Component;
13+
import org.springframework.util.LinkedMultiValueMap;
14+
import org.springframework.util.MultiValueMap;
15+
import org.springframework.web.client.RestClientException;
16+
import org.springframework.web.client.RestTemplate;
17+
18+
@Slf4j
19+
@Component
20+
@RequiredArgsConstructor
21+
public class FraudNumberClient {
22+
private final RestTemplate restTemplate;
23+
24+
@Value("${open-api.fraud-number.base-url}")
25+
private String apiUrl;
26+
27+
@Value("${open-api.fraud-number.secret-key}")
28+
private String apiKey;
29+
30+
/**
31+
* 주어진 번호에 대해 스팸 여부를 조회합니다.
32+
* @param number 전화번호(국제전화, 번호 중간 "-" 허용, "-" 없이 숫자만 이어 붙인 번호 허용)
33+
* @return CheckSpamNumberResponse.DataBlock
34+
*/
35+
public CheckFraudNumberResponse.DataBlock checkSpamNumber(String number){
36+
HttpHeaders headers = new HttpHeaders();
37+
headers.add("CL_AUTH_KEY", apiKey);
38+
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
39+
40+
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
41+
body.add("number", number);
42+
43+
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(body, headers);
44+
45+
try{
46+
ResponseEntity<CheckFraudNumberResponse> response =
47+
restTemplate.postForEntity(
48+
apiUrl,
49+
request,
50+
CheckFraudNumberResponse.class
51+
);
52+
if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
53+
log.error("FraudNumber API error: status={}, body={}", response.getStatusCode(), response.getBody());
54+
throw new BusinessExceptionHandler(ErrorCode.FRAUD_NUMBER_SERVER_ERROR);
55+
}
56+
57+
CheckFraudNumberResponse.DataBlock data = response.getBody().getData();
58+
if (data.getSuccess() != 1) {
59+
log.error("SpamNumber API returned success!=1: {}", data);
60+
throw new BusinessExceptionHandler(ErrorCode.FRAUD_NUMBER_SERVER_ERROR);
61+
}
62+
log.info("FraudNumber API Success, getSpamCount: {}", response.getBody().getData().getSpamCount());
63+
return data;
64+
65+
} catch (RestClientException ex){
66+
log.error("Failed to call FraudNumber API", ex);
67+
throw new BusinessExceptionHandler(ErrorCode.FRAUD_NUMBER_SERVER_ERROR);
68+
}
69+
}
70+
71+
}

0 commit comments

Comments
 (0)