Skip to content

Commit babdd2e

Browse files
limdododddhi7
andauthored
feat(ai): 데이터분석 연동 (#103)
* feat(ai): 데이터분석 연동 * fix(ai): body 수정 * fix: 환경변수 수정 * feat: sentiment분석 추가 * feat: 주석 해제+import 추가 * refactor : 응답 구조 변경 * fix : 테마 예측 응답 구조 변경 * feat : Jackson 설정 * feat : 텍스트 전처리 * chore : 불필요한 코드 정리 * feat:docker-compose.yml 수정 * feat(summary): 모델 연동 * feat(ai): 모델 연결 URL 수정 * fix(ai): 환경변수 수정 * feat(news): enum 수정 * feat(ai): 크롤링 시 모델 API 요청 * fix: 크롤링 시간 수정 * feat(ai): 감성점수 연동 * feat(ai): 감성 분석 크롤링 연결 * fix: 감성 점수 둘째자리 반올림 * fix: 감성 점수 둘째자리 반올림 --------- Co-authored-by: ddhi7 <tjsanf211323@gmail.com>
1 parent 418a137 commit babdd2e

17 files changed

Lines changed: 487 additions & 66 deletions

File tree

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,17 @@ dependencies {
4848

4949
implementation 'org.springframework.boot:spring-boot-starter-actuator'
5050

51+
implementation 'org.springframework.boot:spring-boot-starter-webflux'
5152

5253
implementation 'org.springframework.boot:spring-boot-starter-validation'
5354
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
5455

5556
//firebase
5657
implementation 'com.google.firebase:firebase-admin:9.2.0'
5758

59+
implementation 'io.projectreactor.netty:reactor-netty-http:1.1.4' // 예시 버전, 최신 버전 확인 필요
60+
61+
5862
}
5963

6064
tasks.named('test') {

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ services:
2121
- AWS_ACCESS_KEY=${AWS_ACCESS_KEY}
2222
- AWS_SECRET_KEY=${AWS_SECRET_KEY}
2323
- AWS_REGION=${AWS_REGION}
24+
- AI_BASE_URL_THEMA=${AI_BASE_URL_THEMA}
25+
- AI_BASE_URL=${AI_BASE_URL}
2426

2527
redis:
2628
image: redis:6.2
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.tave.alarmissue.ai.controller;
2+
3+
import com.tave.alarmissue.ai.dto.request.SentimentRequest;
4+
import com.tave.alarmissue.ai.dto.request.SummaryRequest;
5+
import com.tave.alarmissue.ai.dto.request.ThemaRequest;
6+
import com.tave.alarmissue.ai.dto.response.SentimentResponse;
7+
import com.tave.alarmissue.ai.dto.response.SummaryResponse;
8+
import com.tave.alarmissue.ai.dto.response.ThemaResponse;
9+
import com.tave.alarmissue.ai.service.AiService;
10+
import lombok.RequiredArgsConstructor;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.http.MediaType;
13+
import org.springframework.web.bind.annotation.PostMapping;
14+
import org.springframework.web.bind.annotation.RequestBody;
15+
import org.springframework.web.bind.annotation.RequestMapping;
16+
import org.springframework.web.bind.annotation.RestController;
17+
import reactor.core.publisher.Mono;
18+
19+
import java.util.List;
20+
21+
22+
@RestController
23+
@RequestMapping("/ai")
24+
@RequiredArgsConstructor
25+
@Slf4j
26+
public class AiController {
27+
28+
private final AiService aiService;
29+
30+
@PostMapping(value = "/thema", consumes = MediaType.APPLICATION_JSON_VALUE)
31+
public Mono<ThemaResponse> analyzeThema(@RequestBody ThemaRequest request) {
32+
33+
String cleanText = request.getText().replaceAll("\\r?\\n", " ");
34+
35+
return extractTextOrError(cleanText)
36+
.flatMap(aiService::analyzeThema);
37+
}
38+
39+
@PostMapping(value = "/summary", consumes = MediaType.APPLICATION_JSON_VALUE)
40+
public Mono<SummaryResponse> analyzeSummary(@RequestBody SummaryRequest request) {
41+
42+
String cleanText = request.getText().replaceAll("\\r?\\n", " ");
43+
44+
return extractTextOrError(cleanText)
45+
.flatMap(aiService::analyzeSummary);
46+
}
47+
48+
@PostMapping("/sentiment")
49+
public Mono<SentimentResponse> analyzeSentiment(@RequestBody List<String> titles) {
50+
return aiService.analyzeSentiment(titles);
51+
}
52+
53+
/*
54+
private method 분리
55+
*/
56+
private <T> Mono<String> extractTextOrError(String text) {
57+
if (text == null || text.isEmpty()) {
58+
log.error("Cannot extract text from JSON");
59+
return Mono.error(new IllegalArgumentException("텍스트 추출 오류"));
60+
}
61+
62+
log.info("Successfully extracted text length: {}", text.length());
63+
return Mono.just(text);
64+
}
65+
66+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.tave.alarmissue.ai.dto.request;
2+
3+
import lombok.Data;
4+
5+
import java.util.List;
6+
7+
@Data
8+
public class SentimentRequest {
9+
private List<String> titles;
10+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.tave.alarmissue.ai.dto.request;
2+
3+
import lombok.Data;
4+
5+
@Data
6+
public class SummaryRequest {
7+
private String text;
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.tave.alarmissue.ai.dto.request;
2+
3+
import lombok.Data;
4+
5+
@Data
6+
public class ThemaRequest {
7+
private String text;
8+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.tave.alarmissue.ai.dto.response;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
7+
@Getter
8+
@NoArgsConstructor
9+
@AllArgsConstructor
10+
public class SentimentResponse {
11+
private Float score;
12+
13+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.tave.alarmissue.ai.dto.response;
2+
3+
import lombok.AllArgsConstructor;
4+
5+
public record SummaryResponse(String summary) {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.tave.alarmissue.ai.dto.response;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
7+
public record ThemaResponse(String theme) {}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.tave.alarmissue.ai.service;
2+
3+
import com.tave.alarmissue.ai.dto.response.SentimentResponse;
4+
import com.tave.alarmissue.ai.dto.response.SummaryResponse;
5+
import com.tave.alarmissue.ai.dto.response.ThemaResponse;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.core.ParameterizedTypeReference;
9+
import org.springframework.http.HttpHeaders;
10+
import org.springframework.http.MediaType;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.web.reactive.function.client.WebClient;
13+
import reactor.core.publisher.Mono;
14+
15+
import java.math.BigDecimal;
16+
import java.math.RoundingMode;
17+
import java.util.List;
18+
import java.util.Map;
19+
20+
@Service
21+
@RequiredArgsConstructor
22+
@Slf4j
23+
public class AiService {
24+
25+
private final WebClient webClientForThema;
26+
private final WebClient webClientForSummary;
27+
private final WebClient webClientForSentiment;
28+
29+
public Mono<ThemaResponse> analyzeThema(String text) {
30+
31+
return webClientForThema.post()
32+
.uri("/predict")
33+
.contentType(MediaType.APPLICATION_JSON) // 요청 바디 타입
34+
.bodyValue(Map.of("text", text))
35+
.retrieve()
36+
.bodyToMono(ThemaResponse.class);
37+
}
38+
39+
public Mono<SummaryResponse> analyzeSummary(String text) {
40+
return webClientForSummary.post()
41+
.uri("/summarize")
42+
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
43+
.bodyValue(Map.of("text", text))
44+
.retrieve()
45+
.bodyToMono(SummaryResponse.class);
46+
}
47+
48+
public Mono<SentimentResponse> analyzeSentiment(List<String> titles) {
49+
log.info("Received /sentiment request with titles: {}", titles);
50+
return webClientForSentiment.post()
51+
.uri("/sentiment")
52+
.contentType(MediaType.APPLICATION_JSON)
53+
.bodyValue(titles)
54+
.exchangeToMono(response -> {
55+
log.info("FastAPI response status: {}", response.statusCode());
56+
if (response.statusCode().is2xxSuccessful()) {
57+
return response.bodyToMono(new ParameterizedTypeReference<List<Map<String, Double>>>() {
58+
});
59+
} else {
60+
return response.createException().flatMap(Mono::error);
61+
}
62+
})
63+
.map(results -> {
64+
Double score = results.isEmpty() ? 0.0 : results.get(0).get("score");
65+
66+
float roundedScore = new BigDecimal(score)
67+
.setScale(2, RoundingMode.HALF_UP)
68+
.floatValue();
69+
70+
return new SentimentResponse(roundedScore);
71+
});
72+
}
73+
}

0 commit comments

Comments
 (0)