From cc3a0b84c532988291faf28685729bcc7f55a26c Mon Sep 17 00:00:00 2001 From: lsh9672 Date: Wed, 18 Jun 2025 15:44:05 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20=EB=B6=84=EC=84=9D=20api=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../biz/service/AnalyticsService.java | 38 ++---- .../biz/usecase/out/AIServicePort.java | 10 ++ .../infra/gateway/AIServiceAdapter.java | 112 ++++++++++++++++++ 3 files changed, 129 insertions(+), 31 deletions(-) diff --git a/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java b/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java index 906c3ae..bc0aa7d 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java @@ -317,10 +317,8 @@ public class AnalyticsService implements AnalyticsUseCase { } /** - * LLM 기반 리뷰 감정 분석 - 한 번의 분석으로 긍정/부정/중립 수 모두 반환 - * - * @param reviews 분석할 리뷰 목록 - * @return ReviewSentimentCount 감정별 리뷰 수 + * 기존 analyzeReviewSentiments 메서드를 대량 분석 방식으로 개선 + * 개별 AI 호출 대신 한 번의 호출로 모든 리뷰 분석 */ private ReviewSentimentCount analyzeReviewSentiments(List reviews) { log.info("LLM 기반 리뷰 감정 분석 시작: 총 리뷰 수={}", reviews.size()); @@ -339,34 +337,12 @@ public class AnalyticsService implements AnalyticsUseCase { return new ReviewSentimentCount(0, 0, 0); } - int positiveCount = 0; - int negativeCount = 0; - int neutralCount = 0; + // 기존 개별 분석 대신 대량 분석 사용 + Map sentimentCounts = aiServicePort.analyzeBulkSentiments(validReviews); - // 각 리뷰를 AI로 감정 분석 - for (String review : validReviews) { - try { - SentimentType sentiment = aiServicePort.analyzeSentiment(review); - - switch (sentiment) { - case POSITIVE: - positiveCount++; - break; - case NEGATIVE: - negativeCount++; - break; - case NEUTRAL: - default: - neutralCount++; - break; - } - - } catch (Exception e) { - log.warn("개별 리뷰 감정 분석 실패, 중립으로 처리: {}", - review.substring(0, Math.min(30, review.length())), e); - neutralCount++; // 분석 실패 시 중립으로 처리 - } - } + int positiveCount = sentimentCounts.get(SentimentType.POSITIVE); + int negativeCount = sentimentCounts.get(SentimentType.NEGATIVE); + int neutralCount = sentimentCounts.get(SentimentType.NEUTRAL); ReviewSentimentCount result = new ReviewSentimentCount(positiveCount, negativeCount, neutralCount); diff --git a/analytics/src/main/java/com/ktds/hi/analytics/biz/usecase/out/AIServicePort.java b/analytics/src/main/java/com/ktds/hi/analytics/biz/usecase/out/AIServicePort.java index 30d4264..e003514 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/biz/usecase/out/AIServicePort.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/biz/usecase/out/AIServicePort.java @@ -4,6 +4,7 @@ import com.ktds.hi.analytics.biz.domain.AiFeedback; import com.ktds.hi.analytics.biz.domain.SentimentType; import java.util.List; +import java.util.Map; /** * AI 서비스 포트 인터페이스 @@ -20,6 +21,15 @@ public interface AIServicePort { * 감정 분석 */ SentimentType analyzeSentiment(String content); + + /** + * 대량 리뷰 감정 분석 (새로 추가) + * 여러 리뷰를 한 번에 분석하여 긍정/부정/중립 개수 반환 + * + * @param reviews 분석할 리뷰 목록 + * @return 감정 타입별 개수 맵 + */ + Map analyzeBulkSentiments(List reviews); /** * 실행 계획 생성 diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/AIServiceAdapter.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/AIServiceAdapter.java index 5c167b0..0de96fa 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/AIServiceAdapter.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/AIServiceAdapter.java @@ -30,8 +30,10 @@ import org.springframework.web.client.RestTemplate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * AI 서비스 어댑터 클래스 @@ -99,6 +101,116 @@ public class AIServiceAdapter implements AIServicePort { } } + @Override + public Map analyzeBulkSentiments(List reviews) { + log.info("대량 리뷰 감정 분석 시작: 리뷰 수={}", reviews.size()); + + try { + if (reviews.isEmpty()) { + return createEmptyResultMap(); + } + + // 유효한 리뷰만 필터링 + List validReviews = reviews.stream() + .filter(review -> review != null && !review.trim().isEmpty()) + .collect(Collectors.toList()); + + if (validReviews.isEmpty()) { + return createEmptyResultMap(); + } + + // 리뷰를 번호와 함께 포맷팅 + StringBuilder reviewsText = new StringBuilder(); + for (int i = 0; i < validReviews.size(); i++) { + reviewsText.append(String.format("%d. %s\n", i + 1, validReviews.get(i))); + } + + String prompt = String.format( + """ + 다음 리뷰들을 분석하여 긍정, 부정, 중립의 개수를 세어주세요. + + 리뷰 목록: + %s + + 결과를 다음 JSON 형식으로만 답변해주세요: + { + "positive": 긍정_개수, + "negative": 부정_개수, + "neutral": 중립_개수 + } + + 다른 설명은 하지 말고 JSON만 답변해주세요. + 긍정,부정,중립 개수를 모두 더했을때, 총 리뷰수와 동일해야 합니다. + 정확하게 세어주세요. + """, + reviewsText.toString() + ); + + // 기존 callOpenAI 메서드 활용 + String result = callOpenAI(prompt); + + // 결과 파싱 + Map sentimentMap = parseBulkSentimentResult(result, validReviews.size()); + + log.info("대량 리뷰 감정 분석 완료: 긍정={}, 부정={}, 중립={}", + sentimentMap.get(SentimentType.POSITIVE), + sentimentMap.get(SentimentType.NEGATIVE), + sentimentMap.get(SentimentType.NEUTRAL)); + + return sentimentMap; + + } catch (Exception e) { + log.error("대량 리뷰 감정 분석 중 오류 발생, fallback 사용", e); + return createFallbackResultMap(reviews.size()); + } + } + + private Map parseBulkSentimentResult(String result, int totalReviews) { + try { + // 기존 objectMapper 필드 사용 + Map jsonResult = objectMapper.readValue(result.trim(), Map.class); + + int positive = ((Number) jsonResult.getOrDefault("positive", 0)).intValue(); + int negative = ((Number) jsonResult.getOrDefault("negative", 0)).intValue(); + int neutral = ((Number) jsonResult.getOrDefault("neutral", 0)).intValue(); + + // 결과 검증 및 보정 + int totalAnalyzed = positive + negative + neutral; + if (totalAnalyzed != totalReviews) { + log.warn("분석 결과 불일치 보정: 분석된 수={}, 실제 리뷰 수={}", totalAnalyzed, totalReviews); + int difference = totalReviews - totalAnalyzed; + neutral += difference; + } + + Map resultMap = new HashMap<>(); + resultMap.put(SentimentType.POSITIVE, Math.max(0, positive)); + resultMap.put(SentimentType.NEGATIVE, Math.max(0, negative)); + resultMap.put(SentimentType.NEUTRAL, Math.max(0, neutral)); + + return resultMap; + + } catch (Exception e) { + log.error("대량 감정 분석 결과 파싱 실패: {}", result, e); + return createFallbackResultMap(totalReviews); + } + } + + private Map createEmptyResultMap() { + Map result = new HashMap<>(); + result.put(SentimentType.POSITIVE, 0); + result.put(SentimentType.NEGATIVE, 0); + result.put(SentimentType.NEUTRAL, 0); + return result; + } + + private Map createFallbackResultMap(int totalReviews) { + Map result = new HashMap<>(); + result.put(SentimentType.POSITIVE, (int) (totalReviews * 0.6)); + result.put(SentimentType.NEGATIVE, (int) (totalReviews * 0.2)); + result.put(SentimentType.NEUTRAL, totalReviews - (int) (totalReviews * 0.6) - (int) (totalReviews * 0.2)); + return result; + } + @Override public SentimentType analyzeSentiment(String content) {