feat : 분석기능 추가.

This commit is contained in:
lsh9672 2025-06-16 10:58:46 +09:00
parent ea25b5a502
commit bc51e15662
7 changed files with 355 additions and 36 deletions

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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, "실행계획 생성 완료"));
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 + "을 위한 구체적인 실행 방안 수립";
}
}

View File

@ -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: