This commit is contained in:
UNGGU0704 2025-06-18 17:59:52 +09:00
commit f12d1d5a8c
11 changed files with 142 additions and 11 deletions

View File

@ -29,6 +29,9 @@ public class AiFeedback {
private List<String> recommendations; private List<String> recommendations;
private String sentimentAnalysis; private String sentimentAnalysis;
private Double confidenceScore; private Double confidenceScore;
private String positiveSummary;
private LocalDateTime generatedAt; private LocalDateTime generatedAt;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; private LocalDateTime updatedAt;

View File

@ -537,6 +537,7 @@ public class AnalyticsService implements AnalyticsUseCase {
.recommendations(aiFeedback.getRecommendations()) .recommendations(aiFeedback.getRecommendations())
.sentimentAnalysis(aiFeedback.getSentimentAnalysis()) .sentimentAnalysis(aiFeedback.getSentimentAnalysis())
.confidenceScore(aiFeedback.getConfidenceScore()) .confidenceScore(aiFeedback.getConfidenceScore())
.positiveSummary(aiFeedback.getPositiveSummary())
.totalReviewsAnalyzed(getTotalReviewsCount(storeId, request.getDays())) .totalReviewsAnalyzed(getTotalReviewsCount(storeId, request.getDays()))
.actionPlans(actionPlans) //TODO : 사용하는 값은 아니지만 의존성을 위해 그대로 , 추후에 변경 필요. .actionPlans(actionPlans) //TODO : 사용하는 값은 아니지만 의존성을 위해 그대로 , 추후에 변경 필요.
.analyzedAt(aiFeedback.getGeneratedAt()) .analyzedAt(aiFeedback.getGeneratedAt())
@ -581,6 +582,11 @@ public class AnalyticsService implements AnalyticsUseCase {
} }
} }
@Override
public CustomerPositiveReviewResponse getCustomerPositiveReview(Long storeId) {
return null;
}
/** /**
* 실제 AI를 호출하는 개선된 피드백 생성 메서드 * 실제 AI를 호출하는 개선된 피드백 생성 메서드
* 기존 generateAIFeedback() 하드코딩 부분을 실제 AI 호출로 수정 * 기존 generateAIFeedback() 하드코딩 부분을 실제 AI 호출로 수정
@ -613,6 +619,7 @@ public class AnalyticsService implements AnalyticsUseCase {
.recommendations(aiFeedback.getRecommendations()) .recommendations(aiFeedback.getRecommendations())
.sentimentAnalysis(aiFeedback.getSentimentAnalysis()) .sentimentAnalysis(aiFeedback.getSentimentAnalysis())
.confidenceScore(aiFeedback.getConfidenceScore()) .confidenceScore(aiFeedback.getConfidenceScore())
.positiveSummary(aiFeedback.getPositiveSummary())
.generatedAt(LocalDateTime.now()) .generatedAt(LocalDateTime.now())
.createdAt(LocalDateTime.now()) .createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now()) .updatedAt(LocalDateTime.now())

View File

@ -46,4 +46,8 @@ public interface AnalyticsUseCase {
*/ */
List<String> generateActionPlansFromFeedback(ActionPlanCreateRequest request,Long feedbackId); List<String> generateActionPlansFromFeedback(ActionPlanCreateRequest request,Long feedbackId);
// 🔥 고객용 긍정 리뷰 조회 API 추가
CustomerPositiveReviewResponse getCustomerPositiveReview(Long storeId);
} }

View File

@ -35,4 +35,12 @@ public interface AIServicePort {
* 실행 계획 생성 * 실행 계획 생성
*/ */
List<String> generateActionPlan(List<String> actionPlanSelect, AiFeedback feedback); List<String> generateActionPlan(List<String> actionPlanSelect, AiFeedback feedback);
// 🔥 고객용 긍정 리뷰 요약 생성 메서드 추가
/**
* 긍정적인 리뷰만을 분석하여 고객용 요약 생성
* @param positiveReviews 긍정적인 리뷰 목록
* @return 고객에게 보여줄 긍정적인 요약
*/
String generateCustomerPositiveSummary(List<String> positiveReviews);
} }

View File

@ -18,6 +18,14 @@ public interface ExternalReviewPort {
*/ */
List<String> getRecentReviews(Long storeId, Integer days); List<String> getRecentReviews(Long storeId, Integer days);
// 🔥 긍정적인 리뷰만 조회하는 메서드 추가
/**
* 긍정적인 리뷰만 조회 (평점 4점 이상)
* @param storeId 매장 ID
* @param days 조회 기간 ()
* @return 긍정적인 리뷰 목록
*/
List<String> getPositiveReviews(Long storeId, Integer days);
/** /**
* 리뷰 개수 조회 * 리뷰 개수 조회

View File

@ -43,6 +43,9 @@ public class AiAnalysisResponse {
@Schema(description = "감정 분석 결과") @Schema(description = "감정 분석 결과")
private String sentimentAnalysis; private String sentimentAnalysis;
@Schema(description = "긍정 분석 요약")
private String positiveSummary;
@Schema(description = "신뢰도 점수") @Schema(description = "신뢰도 점수")
private Double confidenceScore; private Double confidenceScore;

View File

@ -0,0 +1,35 @@
package com.ktds.hi.analytics.infra.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* 고객용 긍정 리뷰 응답 DTO
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "고객용 긍정 리뷰 응답")
public class CustomerPositiveReviewResponse {
@Schema(description = "매장 ID")
private Long storeId;
@Schema(description = "긍정적인 리뷰 요약")
private String positiveSummary;
@Schema(description = "분석된 총 리뷰 수")
private Integer totalReviewsAnalyzed;
@Schema(description = "분석 일시")
private LocalDateTime analyzedAt;
}

View File

@ -275,6 +275,11 @@ public class AIServiceAdapter implements AIServicePort {
} }
} }
@Override
public String generateCustomerPositiveSummary(List<String> positiveReviews) {
return "";
}
/** /**
* OpenAI API를 호출하여 전체 리뷰 분석 수행 * OpenAI API를 호출하여 전체 리뷰 분석 수행
*/ */
@ -286,13 +291,14 @@ public class AIServiceAdapter implements AIServicePort {
다음은 매장의 고객 리뷰들입니다. 이를 분석하여 다음 JSON 형식으로 답변해주세요: 다음은 매장의 고객 리뷰들입니다. 이를 분석하여 다음 JSON 형식으로 답변해주세요:
{ {
"summary": "전체적인 분석 요약(2-3문장)", "summary": "전체적인 분석 요약(1-2문장)",
"positivePoints": ["긍정적 요소1", "긍정적 요소2", "긍정적 요소3"], "positivePoints": ["긍정적 요소1", "긍정적 요소2", "긍정적 요소3"],
"negativePoints": ["부정적 요소1", "부정적 요소2", "부정적 요소3"], "negativePoints": ["부정적 요소1", "부정적 요소2", "부정적 요소3"],
"improvementPoints": ["개선점1", "개선점2", "개선점3"], "improvementPoints": ["개선점1", "개선점2", "개선점3"],
"recommendations": ["추천사항1", "추천사항2", "추천사항3"], "recommendations": ["추천사항1", "추천사항2", "추천사항3"],
"sentimentAnalysis": "전체적인 감정 분석 결과", "sentimentAnalysis": "전체적인 감정 분석 결과",
"confidenceScore": 0.85 "confidenceScore": 0.85
"positiveSummary": "리뷰중에 긍정적인 내용만 분석 요약(1~2문장)"
} }
리뷰 목록: 리뷰 목록:
@ -306,6 +312,7 @@ public class AIServiceAdapter implements AIServicePort {
4. 신뢰도 점수는 0.0-1.0 사이의 값으로 리뷰정보를 보고 적절히 판단. 4. 신뢰도 점수는 0.0-1.0 사이의 값으로 리뷰정보를 보고 적절히 판단.
5. summary에는 전체적인 리뷰 분석에 대한 요약이 담기게 작성하고 **같은 강조하는 문자 없이 텍스트로만 나타내주세요 5. summary에는 전체적인 리뷰 분석에 대한 요약이 담기게 작성하고 **같은 강조하는 문자 없이 텍스트로만 나타내주세요
6. 분석한 내용에 `(백틱) 들어가지 않도록 해주세요. 6. 분석한 내용에 `(백틱) 들어가지 않도록 해주세요.
7. positiveSummary에는 긍정적인 내용만 있어야 합니다, summary에 있는 내용에서 긍정적인 부분만 작성해주세요.
""", """,
reviewsText reviewsText
); );
@ -402,6 +409,7 @@ public class AIServiceAdapter implements AIServicePort {
.improvementPoints((List<String>) result.get("improvementPoints")) .improvementPoints((List<String>) result.get("improvementPoints"))
.recommendations((List<String>) result.get("recommendations")) .recommendations((List<String>) result.get("recommendations"))
.sentimentAnalysis((String) result.get("sentimentAnalysis")) .sentimentAnalysis((String) result.get("sentimentAnalysis"))
.positiveSummary((String) result.get("positiveSummary"))
.confidenceScore(((Number) result.get("confidenceScore")).doubleValue()) .confidenceScore(((Number) result.get("confidenceScore")).doubleValue())
.generatedAt(LocalDateTime.now()) .generatedAt(LocalDateTime.now())
.build(); .build();

View File

@ -109,6 +109,7 @@ public class AnalyticsRepositoryAdapter implements AnalyticsPort {
.improvementPoints(parseJsonToList(entity.getImprovementPointsJson())) .improvementPoints(parseJsonToList(entity.getImprovementPointsJson()))
.recommendations(parseJsonToList(entity.getRecommendationsJson())) .recommendations(parseJsonToList(entity.getRecommendationsJson()))
.sentimentAnalysis(entity.getSentimentAnalysis()) .sentimentAnalysis(entity.getSentimentAnalysis())
.positiveSummary(entity.getPositiveSummary())
.confidenceScore(entity.getConfidenceScore()) .confidenceScore(entity.getConfidenceScore())
.generatedAt(entity.getGeneratedAt()) .generatedAt(entity.getGeneratedAt())
.createdAt(entity.getCreatedAt()) .createdAt(entity.getCreatedAt())
@ -128,6 +129,7 @@ public class AnalyticsRepositoryAdapter implements AnalyticsPort {
.negativePointsJson(parseListToJson(domain.getNegativePoints())) .negativePointsJson(parseListToJson(domain.getNegativePoints()))
.improvementPointsJson(parseListToJson(domain.getImprovementPoints())) .improvementPointsJson(parseListToJson(domain.getImprovementPoints()))
.recommendationsJson(parseListToJson(domain.getRecommendations())) .recommendationsJson(parseListToJson(domain.getRecommendations()))
.positiveSummary(domain.getPositiveSummary())
.sentimentAnalysis(domain.getSentimentAnalysis()) .sentimentAnalysis(domain.getSentimentAnalysis())
.confidenceScore(domain.getConfidenceScore()) .confidenceScore(domain.getConfidenceScore())
.generatedAt(domain.getGeneratedAt()) .generatedAt(domain.getGeneratedAt())

View File

@ -19,8 +19,13 @@ import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -71,23 +76,62 @@ public class ExternalReviewAdapter implements ExternalReviewPort {
log.info("최근 리뷰 데이터 조회: storeId={}, days={}", storeId, days); log.info("최근 리뷰 데이터 조회: storeId={}, days={}", storeId, days);
try { try {
// String url = reviewServiceUrl + "/api/reviews/stores/" + storeId + "/recent?days=" + days;
// String url = reviewServiceUrl + "/api/reviews/stores/" + storeId + "?size=100";
//최근 데이터를 가져오도록 변경 //최근 데이터를 가져오도록 변경
String url = reviewServiceUrl + "/api/reviews/stores/recent/" + storeId + "?size=100&days=" + days; // String url = reviewServiceUrl + "/api/reviews/stores/recent/" + storeId + "?size=100&days=" + days;
//
// // ReviewListResponse 배열로 직접 받기
// ReviewListResponse[] reviewArray = restTemplate.getForObject(url, ReviewListResponse[].class);
// ReviewListResponse 배열로 직접 받기 int totalSize = 200;
ReviewListResponse[] reviewArray = restTemplate.getForObject(url, ReviewListResponse[].class); int threadCount = 4;
int pageSize = totalSize / threadCount; // 50개씩
if (reviewArray == null || reviewArray.length == 0) { // ExecutorService 생성
log.info("매장에 최근 리뷰가 없습니다: storeId={}", storeId); ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
return List.of(); List<CompletableFuture<ReviewListResponse[]>> futures = new ArrayList<>();
// 4개의 비동기 요청 생성 (limit 50, offset 0/50/100/150)
for (int i = 0; i < threadCount; i++) {
final int offset = i * pageSize;
final int limit = pageSize;
CompletableFuture<ReviewListResponse[]> future = CompletableFuture.supplyAsync(() -> {
String url = reviewServiceUrl + "/api/reviews/stores/recent/" + storeId
+ "?size=" + limit + "&offset=" + offset + "&days=" + days;
log.debug("스레드 {}에서 URL 호출: {}", Thread.currentThread().getName(), url);
// 기존과 동일한 방식으로 API 호출
ReviewListResponse[] reviewArray = restTemplate.getForObject(url, ReviewListResponse[].class);
if (reviewArray == null) {
log.debug("스레드 {}에서 빈 응답 수신", Thread.currentThread().getName());
return new ReviewListResponse[0];
}
log.debug("스레드 {}에서 {} 개 리뷰 수신", Thread.currentThread().getName(), reviewArray.length);
return reviewArray;
}, executorService);
futures.add(future);
} }
// 모든 요청 완료 대기 결과 합치기
List<ReviewListResponse> allReviewResponses = new ArrayList<>();
for (CompletableFuture<ReviewListResponse[]> future : futures) {
ReviewListResponse[] reviewArray = future.get(30, TimeUnit.SECONDS); // 30초 타임아웃
allReviewResponses.addAll(Arrays.asList(reviewArray));
}
executorService.shutdown();
// 최근 N일 이내의 리뷰만 필터링 // 최근 N일 이내의 리뷰만 필터링
LocalDateTime cutoffDate = LocalDateTime.now().minusDays(days); LocalDateTime cutoffDate = LocalDateTime.now().minusDays(days);
List<String> recentReviews = Arrays.stream(reviewArray) List<String> recentReviews = allReviewResponses.stream()
.filter(review -> review.getCreatedAt() != null && review.getCreatedAt().isAfter(cutoffDate)) .filter(review -> review.getCreatedAt() != null && review.getCreatedAt().isAfter(cutoffDate))
.map(ReviewListResponse::getContent) .map(ReviewListResponse::getContent)
.filter(content -> content != null && !content.trim().isEmpty()) .filter(content -> content != null && !content.trim().isEmpty())
@ -108,7 +152,12 @@ public class ExternalReviewAdapter implements ExternalReviewPort {
return getDummyRecentReviews(storeId); return getDummyRecentReviews(storeId);
} }
} }
@Override
public List<String> getPositiveReviews(Long storeId, Integer days) {
return List.of();
}
@Override @Override
public Integer getReviewCount(Long storeId) { public Integer getReviewCount(Long storeId) {
log.info("리뷰 개수 조회: storeId={}", storeId); log.info("리뷰 개수 조회: storeId={}", storeId);

View File

@ -57,6 +57,10 @@ public class AiFeedbackEntity {
@Column(name = "confidence_score") @Column(name = "confidence_score")
private Double confidenceScore; private Double confidenceScore;
// 🔥 고객용 긍정 리뷰 요약 컬럼 추가
@Column(name = "customer_positive_summary", columnDefinition = "TEXT")
private String positiveSummary;
@Column(name = "generated_at") @Column(name = "generated_at")
private LocalDateTime generatedAt; private LocalDateTime generatedAt;