feat : 분석기능 추가.
This commit is contained in:
parent
ea25b5a502
commit
bc51e15662
@ -399,4 +399,138 @@ public class AnalyticsService implements AnalyticsUseCase {
|
||||
// 실제로는 AI 서비스를 통한 감정 분석 필요
|
||||
return (int) (reviews.size() * 0.2); // 20% 가정
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public AiAnalysisResponse generateAIAnalysis(Long storeId, AiAnalysisRequest request) {
|
||||
log.info("AI 분석 시작: storeId={}, days={}", storeId, request.getDays());
|
||||
|
||||
try {
|
||||
// 1. 기존 generateAIFeedback 메서드를 실제 AI 호출로 수정하여 사용
|
||||
AiFeedback aiFeedback = generateRealAIFeedback(storeId, request.getDays());
|
||||
|
||||
// 2. 실행계획 생성 (요청 시)
|
||||
List<String> actionPlans = null;
|
||||
if (Boolean.TRUE.equals(request.getGenerateActionPlan())) {
|
||||
actionPlans = aiServicePort.generateActionPlan(aiFeedback);
|
||||
}
|
||||
|
||||
// 3. 응답 생성
|
||||
AiAnalysisResponse response = AiAnalysisResponse.builder()
|
||||
.storeId(storeId)
|
||||
.feedbackId(aiFeedback.getId())
|
||||
.summary(aiFeedback.getSummary())
|
||||
.positivePoints(aiFeedback.getPositivePoints())
|
||||
.improvementPoints(aiFeedback.getImprovementPoints())
|
||||
.recommendations(aiFeedback.getRecommendations())
|
||||
.sentimentAnalysis(aiFeedback.getSentimentAnalysis())
|
||||
.confidenceScore(aiFeedback.getConfidenceScore())
|
||||
.totalReviewsAnalyzed(getTotalReviewsCount(storeId, request.getDays()))
|
||||
.actionPlans(actionPlans)
|
||||
.analyzedAt(aiFeedback.getGeneratedAt())
|
||||
.build();
|
||||
|
||||
log.info("AI 분석 완료: storeId={}, feedbackId={}", storeId, aiFeedback.getId());
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI 분석 중 오류 발생: storeId={}", storeId, e);
|
||||
return createErrorAnalysisResponse(storeId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> generateActionPlansFromFeedback(Long feedbackId) {
|
||||
log.info("실행계획 생성: feedbackId={}", feedbackId);
|
||||
|
||||
try {
|
||||
// 1. AI 피드백 조회
|
||||
var aiFeedback = analyticsPort.findAIFeedbackByStoreId(feedbackId); // 실제로는 feedbackId로 조회하는 메서드 필요
|
||||
|
||||
if (aiFeedback.isEmpty()) {
|
||||
throw new RuntimeException("AI 피드백을 찾을 수 없습니다: " + feedbackId);
|
||||
}
|
||||
|
||||
// 2. 기존 AIServicePort.generateActionPlan 메서드 활용
|
||||
List<String> actionPlans = aiServicePort.generateActionPlan(aiFeedback.get());
|
||||
|
||||
log.info("실행계획 생성 완료: feedbackId={}, planCount={}", feedbackId, actionPlans.size());
|
||||
return actionPlans;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("실행계획 생성 중 오류 발생: feedbackId={}", feedbackId, e);
|
||||
return List.of("서비스 개선을 위한 기본 실행계획을 수립하세요.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제 AI를 호출하는 개선된 피드백 생성 메서드
|
||||
* 기존 generateAIFeedback()의 하드코딩 부분을 실제 AI 호출로 수정
|
||||
*/
|
||||
@Transactional
|
||||
public AiFeedback generateRealAIFeedback(Long storeId, Integer days) {
|
||||
log.info("실제 AI 피드백 생성: storeId={}, days={}", storeId, days);
|
||||
|
||||
try {
|
||||
// 1. 리뷰 데이터 수집
|
||||
List<String> reviewData = externalReviewPort.getRecentReviews(storeId, days);
|
||||
|
||||
if (reviewData.isEmpty()) {
|
||||
log.warn("AI 피드백 생성을 위한 리뷰 데이터가 없습니다: storeId={}", storeId);
|
||||
return createDefaultAIFeedback(storeId);
|
||||
}
|
||||
|
||||
// 2. 실제 AI 서비스 호출 (기존 하드코딩 부분을 수정)
|
||||
AiFeedback aiFeedback = aiServicePort.generateFeedback(reviewData);
|
||||
|
||||
// 3. 도메인 객체 속성 설정
|
||||
AiFeedback completeAiFeedback = AiFeedback.builder()
|
||||
.storeId(storeId)
|
||||
.summary(aiFeedback.getSummary())
|
||||
.positivePoints(aiFeedback.getPositivePoints())
|
||||
.improvementPoints(aiFeedback.getImprovementPoints())
|
||||
.recommendations(aiFeedback.getRecommendations())
|
||||
.sentimentAnalysis(aiFeedback.getSentimentAnalysis())
|
||||
.confidenceScore(aiFeedback.getConfidenceScore())
|
||||
.generatedAt(LocalDateTime.now())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 4. 데이터베이스에 저장
|
||||
AiFeedback saved = analyticsPort.saveAIFeedback(completeAiFeedback);
|
||||
|
||||
log.info("실제 AI 피드백 생성 완료: storeId={}, reviewCount={}", storeId, reviewData.size());
|
||||
return saved;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("실제 AI 피드백 생성 중 오류 발생: storeId={}", storeId, e);
|
||||
return createDefaultAIFeedback(storeId);
|
||||
}
|
||||
}
|
||||
|
||||
private Integer getTotalReviewsCount(Long storeId, Integer days) {
|
||||
try {
|
||||
return externalReviewPort.getRecentReviews(storeId, days).size();
|
||||
} catch (Exception e) {
|
||||
log.warn("리뷰 수 조회 실패: storeId={}", storeId, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private AiAnalysisResponse createErrorAnalysisResponse(Long storeId) {
|
||||
return AiAnalysisResponse.builder()
|
||||
.storeId(storeId)
|
||||
.summary("분석 중 오류가 발생했습니다.")
|
||||
.positivePoints(List.of("현재 분석이 불가능합니다"))
|
||||
.improvementPoints(List.of("시스템 안정화 후 재시도 필요"))
|
||||
.recommendations(List.of("잠시 후 다시 분석을 요청해주세요"))
|
||||
.sentimentAnalysis("분석 실패")
|
||||
.confidenceScore(0.0)
|
||||
.totalReviewsAnalyzed(0)
|
||||
.actionPlans(List.of("기본 실행계획을 수립하세요"))
|
||||
.analyzedAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package com.ktds.hi.analytics.biz.usecase.in;
|
||||
import com.ktds.hi.analytics.infra.dto.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 분석 서비스 UseCase 인터페이스
|
||||
@ -34,4 +35,15 @@ public interface AnalyticsUseCase {
|
||||
* 리뷰 분석 조회
|
||||
*/
|
||||
ReviewAnalysisResponse getReviewAnalysis(Long storeId);
|
||||
|
||||
/**
|
||||
* AI 리뷰 분석 및 실행계획 생성
|
||||
*/
|
||||
AiAnalysisResponse generateAIAnalysis(Long storeId, AiAnalysisRequest request);
|
||||
|
||||
/**
|
||||
* AI 피드백 기반 실행계획 생성
|
||||
*/
|
||||
List<String> generateActionPlansFromFeedback(Long feedbackId);
|
||||
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import com.ktds.hi.common.dto.SuccessResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 분석 서비스 컨트롤러 클래스
|
||||
@ -113,4 +115,44 @@ public class AnalyticsController {
|
||||
|
||||
return ResponseEntity.ok(SuccessResponse.of(response, "리뷰 분석 조회 성공"));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* AI 리뷰 분석 및 실행계획 생성
|
||||
*/
|
||||
@Operation(summary = "AI 리뷰 분석", description = "매장 리뷰를 AI로 분석하고 실행계획을 생성합니다.")
|
||||
@PostMapping("/stores/{storeId}/ai-analysis")
|
||||
public ResponseEntity<SuccessResponse<AiAnalysisResponse>> generateAIAnalysis(
|
||||
@Parameter(description = "매장 ID", required = true)
|
||||
@PathVariable @NotNull Long storeId,
|
||||
|
||||
@Parameter(description = "분석 요청 정보")
|
||||
@RequestBody(required = false) @Valid AiAnalysisRequest request) {
|
||||
|
||||
log.info("AI 리뷰 분석 요청: storeId={}", storeId);
|
||||
|
||||
if (request == null) {
|
||||
request = AiAnalysisRequest.builder().build();
|
||||
}
|
||||
|
||||
AiAnalysisResponse response = analyticsUseCase.generateAIAnalysis(storeId, request);
|
||||
|
||||
return ResponseEntity.ok(SuccessResponse.of(response, "AI 분석 완료"));
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 피드백 기반 실행계획 생성
|
||||
*/
|
||||
@Operation(summary = "실행계획 생성", description = "AI 피드백을 기반으로 실행계획을 생성합니다.")
|
||||
@PostMapping("/ai-feedback/{feedbackId}/action-plans")
|
||||
public ResponseEntity<SuccessResponse<List<String>>> generateActionPlans(
|
||||
@Parameter(description = "AI 피드백 ID", required = true)
|
||||
@PathVariable @NotNull Long feedbackId) {
|
||||
|
||||
log.info("실행계획 생성 요청: feedbackId={}", feedbackId);
|
||||
|
||||
List<String> actionPlans = analyticsUseCase.generateActionPlansFromFeedback(feedbackId);
|
||||
|
||||
return ResponseEntity.ok(SuccessResponse.of(actionPlans, "실행계획 생성 완료"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package com.ktds.hi.analytics.infra.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
|
||||
/**
|
||||
* AI 분석 요청 DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "AI 리뷰 분석 요청")
|
||||
public class AiAnalysisRequest {
|
||||
|
||||
@Schema(description = "분석할 최근 일수", example = "30")
|
||||
@Min(value = 1, message = "분석 기간은 최소 1일 이상이어야 합니다")
|
||||
@Max(value = 365, message = "분석 기간은 최대 365일까지 가능합니다")
|
||||
@Builder.Default
|
||||
private Integer days = 30;
|
||||
|
||||
@Schema(description = "실행계획 자동 생성 여부", example = "true")
|
||||
@Builder.Default
|
||||
private Boolean generateActionPlan = true;
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package com.ktds.hi.analytics.infra.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI 분석 응답 DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "AI 리뷰 분석 결과")
|
||||
public class AiAnalysisResponse {
|
||||
|
||||
@Schema(description = "매장 ID")
|
||||
private Long storeId;
|
||||
|
||||
@Schema(description = "AI 피드백 ID")
|
||||
private Long feedbackId;
|
||||
|
||||
@Schema(description = "분석 요약")
|
||||
private String summary;
|
||||
|
||||
@Schema(description = "긍정적 요소")
|
||||
private List<String> positivePoints;
|
||||
|
||||
@Schema(description = "개선점")
|
||||
private List<String> improvementPoints;
|
||||
|
||||
@Schema(description = "추천사항")
|
||||
private List<String> recommendations;
|
||||
|
||||
@Schema(description = "감정 분석 결과")
|
||||
private String sentimentAnalysis;
|
||||
|
||||
@Schema(description = "신뢰도 점수")
|
||||
private Double confidenceScore;
|
||||
|
||||
@Schema(description = "분석된 리뷰 수")
|
||||
private Integer totalReviewsAnalyzed;
|
||||
|
||||
@Schema(description = "생성된 실행계획")
|
||||
private List<String> actionPlans;
|
||||
|
||||
@Schema(description = "분석 완료 시간")
|
||||
private LocalDateTime analyzedAt;
|
||||
}
|
||||
@ -18,6 +18,7 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@ -96,6 +97,9 @@ public class AIServiceAdapter implements AIServicePort {
|
||||
throw new RuntimeException("AI 피드백 생성에 실패했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public SentimentType analyzeSentiment(String content) {
|
||||
@ -175,40 +179,70 @@ public class AIServiceAdapter implements AIServicePort {
|
||||
* 긍정적 요소 생성
|
||||
*/
|
||||
private List<String> generatePositivePoints(List<String> reviewData, List<SentimentType> sentiments) {
|
||||
// 실제로는 자연어 처리를 통해 긍정적 키워드 추출
|
||||
return Arrays.asList(
|
||||
"음식 맛에 대한 긍정적 평가가 많습니다",
|
||||
"직원 서비스에 대한 만족도가 높습니다",
|
||||
"가격 대비 만족도가 좋습니다"
|
||||
);
|
||||
List<String> positivePoints = new ArrayList<>();
|
||||
|
||||
long positiveCount = sentiments.stream().mapToLong(s -> s == SentimentType.POSITIVE ? 1 : 0).sum();
|
||||
double positiveRate = (double) positiveCount / reviewData.size() * 100;
|
||||
|
||||
if (positiveRate > 70) {
|
||||
positivePoints.add("고객 만족도가 매우 높습니다");
|
||||
positivePoints.add("전반적으로 긍정적인 평가를 받고 있습니다");
|
||||
positivePoints.add("재방문 의향이 높은 고객들이 많습니다");
|
||||
} else if (positiveRate > 50) {
|
||||
positivePoints.add("평균 이상의 고객 만족도를 보입니다");
|
||||
positivePoints.add("많은 고객들이 만족하고 있습니다");
|
||||
} else {
|
||||
positivePoints.add("일부 고객들이 긍정적으로 평가하고 있습니다");
|
||||
positivePoints.add("개선의 여지가 있습니다");
|
||||
}
|
||||
|
||||
return positivePoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 개선점 생성
|
||||
*/
|
||||
private List<String> generateImprovementPoints(List<String> reviewData, List<SentimentType> sentiments) {
|
||||
// 실제로는 자연어 처리를 통해 부정적 키워드 추출
|
||||
return Arrays.asList(
|
||||
"배달 시간 단축이 필요합니다",
|
||||
"음식 포장 상태 개선이 필요합니다",
|
||||
"메뉴 다양성 확대를 고려해보세요"
|
||||
);
|
||||
List<String> improvementPoints = new ArrayList<>();
|
||||
|
||||
long negativeCount = sentiments.stream().mapToLong(s -> s == SentimentType.NEGATIVE ? 1 : 0).sum();
|
||||
double negativeRate = (double) negativeCount / reviewData.size() * 100;
|
||||
|
||||
if (negativeRate > 30) {
|
||||
improvementPoints.add("고객 서비스 품질 개선이 시급합니다");
|
||||
improvementPoints.add("부정적 피드백에 대한 체계적 대응이 필요합니다");
|
||||
improvementPoints.add("근본적인 서비스 개선 방안을 마련해야 합니다");
|
||||
} else if (negativeRate > 15) {
|
||||
improvementPoints.add("일부 서비스 영역에서 개선이 필요합니다");
|
||||
improvementPoints.add("고객 만족도 향상을 위한 노력이 필요합니다");
|
||||
} else {
|
||||
improvementPoints.add("현재 서비스 수준을 유지하며 세부 개선점을 찾아보세요");
|
||||
improvementPoints.add("더 높은 고객 만족을 위한 차별화 요소를 개발하세요");
|
||||
}
|
||||
|
||||
return improvementPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 추천사항 생성
|
||||
*/
|
||||
private List<String> generateRecommendations(double positiveRate, double negativeRate) {
|
||||
List<String> recommendations = Arrays.asList(
|
||||
"고객 피드백을 정기적으로 모니터링하고 대응하세요",
|
||||
"리뷰에 적극적으로 댓글을 달아 고객과 소통하세요",
|
||||
"메뉴 품질 관리 체계를 강화하세요"
|
||||
);
|
||||
|
||||
if (negativeRate > 30) {
|
||||
recommendations.add("긴급히 서비스 품질 개선 계획을 수립하세요");
|
||||
List<String> recommendations = new ArrayList<>();
|
||||
|
||||
if (positiveRate > 70) {
|
||||
recommendations.add("현재의 우수한 서비스를 유지하면서 브랜드 가치를 높이세요");
|
||||
recommendations.add("긍정적 리뷰를 마케팅 자료로 활용하세요");
|
||||
recommendations.add("고객 충성도 프로그램을 도입하세요");
|
||||
} else if (negativeRate > 30) {
|
||||
recommendations.add("고객 불만사항에 대한 즉각적인 대응 체계를 구축하세요");
|
||||
recommendations.add("직원 교육을 통한 서비스 품질 향상에 집중하세요");
|
||||
recommendations.add("고객 피드백 수집 및 분석 프로세스를 강화하세요");
|
||||
} else {
|
||||
recommendations.add("지속적인 품질 관리와 고객 만족도 모니터링을 실시하세요");
|
||||
recommendations.add("차별화된 서비스 제공을 통해 경쟁력을 강화하세요");
|
||||
recommendations.add("고객과의 소통을 늘려 관계를 강화하세요");
|
||||
}
|
||||
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
@ -216,25 +250,34 @@ public class AIServiceAdapter implements AIServicePort {
|
||||
* 신뢰도 점수 계산
|
||||
*/
|
||||
private double calculateConfidenceScore(int reviewCount) {
|
||||
if (reviewCount >= 100) return 0.95;
|
||||
if (reviewCount >= 50) return 0.85;
|
||||
if (reviewCount >= 20) return 0.75;
|
||||
if (reviewCount >= 10) return 0.65;
|
||||
return 0.5;
|
||||
if (reviewCount >= 50) {
|
||||
return 0.9;
|
||||
} else if (reviewCount >= 20) {
|
||||
return 0.75;
|
||||
} else if (reviewCount >= 10) {
|
||||
return 0.6;
|
||||
} else if (reviewCount >= 5) {
|
||||
return 0.4;
|
||||
} else {
|
||||
return 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 개선점을 실행 계획으로 변환
|
||||
*/
|
||||
private String convertToActionPlan(String improvementPoint) {
|
||||
// 개선점을 구체적인 실행 계획으로 변환하는 로직
|
||||
if (improvementPoint.contains("배달 시간")) {
|
||||
return "배달 시간 단축을 위한 배달 경로 최적화 및 인력 증원 검토";
|
||||
} else if (improvementPoint.contains("포장")) {
|
||||
return "음식 포장 재료 교체 및 포장 방법 개선";
|
||||
// 개선점을 구체적인 실행계획으로 변환
|
||||
if (improvementPoint.contains("서비스 품질")) {
|
||||
return "직원 서비스 교육 프로그램 실시 (월 1회, 2시간)";
|
||||
} else if (improvementPoint.contains("대기시간")) {
|
||||
return "주문 처리 시스템 개선 및 대기열 관리 체계 도입";
|
||||
} else if (improvementPoint.contains("가격")) {
|
||||
return "경쟁사 가격 분석 및 합리적 가격 정책 수립";
|
||||
} else if (improvementPoint.contains("메뉴")) {
|
||||
return "고객 선호도 조사를 통한 신메뉴 개발 계획 수립";
|
||||
return "고객 선호도 조사를 통한 메뉴 다양화 방안 검토";
|
||||
} else {
|
||||
return "고객 피드백 기반 서비스 개선 계획 수립";
|
||||
}
|
||||
return improvementPoint + "을 위한 구체적인 실행 방안 수립";
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,9 +52,9 @@ ai-api:
|
||||
# 외부 서비스 설정
|
||||
external:
|
||||
services:
|
||||
review: ${EXTERNAL_SERVICES_REVIEW:http://localhost:8082}
|
||||
store: ${EXTERNAL_SERVICES_STORE:http://localhost:8081}
|
||||
member: ${EXTERNAL_SERVICES_MEMBER:http://localhost:8080}
|
||||
review: ${EXTERNAL_SERVICES_REVIEW:http://localhost:8083}
|
||||
store: ${EXTERNAL_SERVICES_STORE:http://localhost:8082}
|
||||
member: ${EXTERNAL_SERVICES_MEMBER:http://localhost:8081}
|
||||
|
||||
#springdoc:
|
||||
# api-docs:
|
||||
@ -83,6 +83,9 @@ ai:
|
||||
key: ${AI_AZURE_COGNITIVE_KEY:your-cognitive-service-key}
|
||||
openai:
|
||||
api-key: ${AI_OPENAI_API_KEY:your-openai-api-key}
|
||||
claude:
|
||||
api-key: ${AI_CLAUDE_KEY:your-claude-key}
|
||||
endpoint: ${AI_CLAUDE_ENDPOINT:https://api.anthropic.com}
|
||||
|
||||
# Azure Event Hub 설정
|
||||
azure:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user