init
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
dependencies {
|
||||
implementation project(':common')
|
||||
|
||||
// AI APIs
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.ktds.hi.analytics.infra.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 실행 계획 완료 요청 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ActionPlanCompleteRequest {
|
||||
|
||||
private String note;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.ktds.hi.analytics.infra.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 실행 계획 완료 응답 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ActionPlanCompleteResponse {
|
||||
|
||||
private Boolean success;
|
||||
private String message;
|
||||
private LocalDateTime completedAt;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ktds.hi.analytics.infra.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 실행 계획 삭제 응답 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ActionPlanDeleteResponse {
|
||||
|
||||
private Boolean success;
|
||||
private String message;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ktds.hi.analytics.infra.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 실행 계획 저장 응답 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ActionPlanSaveResponse {
|
||||
|
||||
private Boolean success;
|
||||
private String message;
|
||||
private Long planId;
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package com.ktds.hi.analytics.infra.gateway;
|
||||
|
||||
import com.ktds.hi.analytics.biz.domain.ActionPlan;
|
||||
import com.ktds.hi.analytics.biz.domain.PlanStatus;
|
||||
import com.ktds.hi.analytics.biz.usecase.out.ActionPlanPort;
|
||||
import com.ktds.hi.analytics.infra.gateway.entity.ActionPlanEntity;
|
||||
import com.ktds.hi.analytics.infra.gateway.repository.ActionPlanJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 실행 계획 리포지토리 어댑터 클래스
|
||||
* ActionPlan Port를 구현하여 데이터 영속성 기능을 제공
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ActionPlanRepositoryAdapter implements ActionPlanPort {
|
||||
|
||||
private final ActionPlanJpaRepository actionPlanJpaRepository;
|
||||
|
||||
@Override
|
||||
public List<ActionPlan> findActionPlansByStoreId(Long storeId) {
|
||||
return actionPlanJpaRepository.findByStoreIdOrderByCreatedAtDesc(storeId)
|
||||
.stream()
|
||||
.map(this::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ActionPlan> findActionPlanById(Long planId) {
|
||||
return actionPlanJpaRepository.findById(planId)
|
||||
.map(this::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionPlan saveActionPlan(ActionPlan actionPlan) {
|
||||
ActionPlanEntity entity = toEntity(actionPlan);
|
||||
ActionPlanEntity saved = actionPlanJpaRepository.save(entity);
|
||||
return toDomain(saved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteActionPlan(Long planId) {
|
||||
actionPlanJpaRepository.deleteById(planId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity를 Domain으로 변환
|
||||
*/
|
||||
private ActionPlan toDomain(ActionPlanEntity entity) {
|
||||
return ActionPlan.builder()
|
||||
.id(entity.getId())
|
||||
.storeId(entity.getStoreId())
|
||||
.userId(entity.getUserId())
|
||||
.title(entity.getTitle())
|
||||
.description(entity.getDescription())
|
||||
.period(entity.getPeriod())
|
||||
.status(entity.getStatus())
|
||||
.tasks(entity.getTasksJson() != null ? parseTasksJson(entity.getTasksJson()) : List.of())
|
||||
.note(entity.getNote())
|
||||
.createdAt(entity.getCreatedAt())
|
||||
.completedAt(entity.getCompletedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain을 Entity로 변환
|
||||
*/
|
||||
private ActionPlanEntity toEntity(ActionPlan domain) {
|
||||
return ActionPlanEntity.builder()
|
||||
.id(domain.getId())
|
||||
.storeId(domain.getStoreId())
|
||||
.userId(domain.getUserId())
|
||||
.title(domain.getTitle())
|
||||
.description(domain.getDescription())
|
||||
.period(domain.getPeriod())
|
||||
.status(domain.getStatus())
|
||||
.tasksJson(domain.getTasks() != null ? toTasksJsonString(domain.getTasks()) : "[]")
|
||||
.note(domain.getNote())
|
||||
.createdAt(domain.getCreatedAt())
|
||||
.completedAt(domain.getCompletedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 문자열을 Tasks List로 파싱
|
||||
*/
|
||||
private List<String> parseTasksJson(String json) {
|
||||
if (json == null || json.trim().isEmpty() || "[]".equals(json.trim())) {
|
||||
return List.of();
|
||||
}
|
||||
return Arrays.asList(json.replace("[", "").replace("]", "").replace("\"", "").split(","));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tasks List를 JSON 문자열로 변환
|
||||
*/
|
||||
private String toTasksJsonString(List<String> tasks) {
|
||||
if (tasks == null || tasks.isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
return "[\"" + String.join("\",\"", tasks) + "\"]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package com.ktds.hi.analytics.infra.gateway;
|
||||
|
||||
import com.ktds.hi.analytics.biz.domain.AiFeedback;
|
||||
import com.ktds.hi.analytics.biz.domain.SentimentType;
|
||||
import com.ktds.hi.analytics.biz.usecase.out.AIServicePort;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 서비스 어댑터 클래스
|
||||
* AI Service Port를 구현하여 외부 AI API 연동 기능을 제공
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AiServiceAdapter implements AIServicePort {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@Value("${external-api.openai.api-key:}")
|
||||
private String openaiApiKey;
|
||||
|
||||
@Value("${external-api.claude.api-key:}")
|
||||
private String claudeApiKey;
|
||||
|
||||
@Override
|
||||
public AiFeedback generateFeedback(List<String> reviewData) {
|
||||
log.info("AI 피드백 생성 시작: reviewCount={}", reviewData.size());
|
||||
|
||||
try {
|
||||
// OpenAI API를 사용한 감정 분석
|
||||
String combinedReviews = String.join(" ", reviewData);
|
||||
SentimentType sentiment = analyzeSentiment(combinedReviews);
|
||||
|
||||
// Mock AI 피드백 생성 (실제로는 OpenAI/Claude API 호출)
|
||||
AiFeedback feedback = AiFeedback.builder()
|
||||
.summary(generateMockSummary(reviewData, sentiment))
|
||||
.sentiment(sentiment)
|
||||
.positivePoints(generateMockPositivePoints())
|
||||
.negativePoints(generateMockNegativePoints())
|
||||
.recommendations(generateMockRecommendations())
|
||||
.confidence(calculateConfidence(reviewData))
|
||||
.analysisDate(LocalDate.now())
|
||||
.build();
|
||||
|
||||
log.info("AI 피드백 생성 완료: sentiment={}, confidence={}", sentiment, feedback.getConfidence());
|
||||
return feedback;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI 피드백 생성 실패: error={}", e.getMessage(), e);
|
||||
return createFallbackFeedback();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SentimentType analyzeSentiment(String content) {
|
||||
log.debug("감정 분석 시작: contentLength={}", content.length());
|
||||
|
||||
try {
|
||||
// 실제로는 OpenAI API 호출
|
||||
if (openaiApiKey != null && !openaiApiKey.isEmpty()) {
|
||||
return callOpenAISentimentAPI(content);
|
||||
}
|
||||
|
||||
// Fallback: 간단한 키워드 기반 감정 분석
|
||||
return performKeywordBasedSentiment(content);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("감정 분석 실패: error={}", e.getMessage(), e);
|
||||
return SentimentType.NEUTRAL;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> generateActionPlan(AiFeedback feedback) {
|
||||
log.info("실행 계획 생성 시작: sentiment={}", feedback.getSentiment());
|
||||
|
||||
try {
|
||||
// AI 기반 실행 계획 생성 (Mock)
|
||||
List<String> actionPlan = List.of(
|
||||
"고객 서비스 개선을 위한 직원 교육 실시",
|
||||
"주방 청결도 점검 및 개선",
|
||||
"대기시간 단축을 위한 주문 시스템 개선",
|
||||
"메뉴 다양성 확대 검토",
|
||||
"고객 피드백 수집 시스템 구축"
|
||||
);
|
||||
|
||||
log.info("실행 계획 생성 완료: planCount={}", actionPlan.size());
|
||||
return actionPlan;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("실행 계획 생성 실패: error={}", e.getMessage(), e);
|
||||
return List.of("AI 분석을 통한 개선사항 검토");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI API를 통한 감정 분석
|
||||
*/
|
||||
private SentimentType callOpenAISentimentAPI(String content) {
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer " + openaiApiKey);
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
Map<String, Object> requestBody = Map.of(
|
||||
"model", "gpt-3.5-turbo",
|
||||
"messages", List.of(
|
||||
Map.of("role", "user", "content",
|
||||
"다음 리뷰의 감정을 분석해주세요. POSITIVE, NEGATIVE, NEUTRAL 중 하나로 답해주세요: " + content)
|
||||
),
|
||||
"max_tokens", 10
|
||||
);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
ResponseEntity<Map> response = restTemplate.exchange(
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
HttpMethod.POST,
|
||||
entity,
|
||||
Map.class
|
||||
);
|
||||
|
||||
// API 응답 파싱
|
||||
Map<String, Object> responseBody = response.getBody();
|
||||
if (responseBody != null && responseBody.containsKey("choices")) {
|
||||
List<Map<String, Object>> choices = (List<Map<String, Object>>) responseBody.get("choices");
|
||||
if (!choices.isEmpty()) {
|
||||
Map<String, Object> message = (Map<String, Object>) choices.get(0).get("message");
|
||||
String result = (String) message.get("content");
|
||||
return SentimentType.valueOf(result.trim().toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
return SentimentType.NEUTRAL;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("OpenAI API 호출 실패, 키워드 분석으로 대체: error={}", e.getMessage());
|
||||
return performKeywordBasedSentiment(content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 키워드 기반 감정 분석
|
||||
*/
|
||||
private SentimentType performKeywordBasedSentiment(String content) {
|
||||
String lowerContent = content.toLowerCase();
|
||||
|
||||
List<String> positiveKeywords = List.of("맛있", "좋", "최고", "추천", "만족", "친절", "깔끔");
|
||||
List<String> negativeKeywords = List.of("맛없", "나쁘", "별로", "실망", "불친절", "더러", "느리");
|
||||
|
||||
long positiveCount = positiveKeywords.stream()
|
||||
.mapToLong(keyword -> countOccurrences(lowerContent, keyword))
|
||||
.sum();
|
||||
|
||||
long negativeCount = negativeKeywords.stream()
|
||||
.mapToLong(keyword -> countOccurrences(lowerContent, keyword))
|
||||
.sum();
|
||||
|
||||
if (positiveCount > negativeCount) {
|
||||
return SentimentType.POSITIVE;
|
||||
} else if (negativeCount > positiveCount) {
|
||||
return SentimentType.NEGATIVE;
|
||||
} else {
|
||||
return SentimentType.NEUTRAL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열에서 특정 키워드 출현 횟수 계산
|
||||
*/
|
||||
private long countOccurrences(String text, String keyword) {
|
||||
return (text.length() - text.replace(keyword, "").length()) / keyword.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 요약 생성
|
||||
*/
|
||||
private String generateMockSummary(List<String> reviewData, SentimentType sentiment) {
|
||||
switch (sentiment) {
|
||||
case POSITIVE:
|
||||
return "고객들이 음식의 맛과 서비스에 대해 전반적으로 만족하고 있습니다. 특히 음식의 품질과 직원의 친절함이 높이 평가받고 있습니다.";
|
||||
case NEGATIVE:
|
||||
return "일부 고객들이 음식의 맛이나 서비스에 대해 불만을 표현하고 있습니다. 주로 대기시간과 음식의 온도에 대한 개선이 필요해 보입니다.";
|
||||
default:
|
||||
return "고객 리뷰가 긍정적인 면과 개선이 필요한 면이 혼재되어 있습니다. 지속적인 품질 관리가 필요합니다.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 긍정 포인트 생성
|
||||
*/
|
||||
private List<String> generateMockPositivePoints() {
|
||||
return List.of(
|
||||
"음식의 맛이 좋다는 평가",
|
||||
"직원들이 친절하다는 의견",
|
||||
"매장이 깔끔하고 청결함",
|
||||
"가격 대비 만족스러운 품질"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 부정 포인트 생성
|
||||
*/
|
||||
private List<String> generateMockNegativePoints() {
|
||||
return List.of(
|
||||
"주문 후 대기시간이 다소 길음",
|
||||
"일부 메뉴의 간이 짜다는 의견",
|
||||
"주차 공간이 부족함"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 추천사항 생성
|
||||
*/
|
||||
private List<String> generateMockRecommendations() {
|
||||
return List.of(
|
||||
"주문 처리 시간 단축을 위한 시스템 개선",
|
||||
"메뉴별 간 조절에 대한 재검토",
|
||||
"고객 대기 공간 개선",
|
||||
"직원 서비스 교육 지속 실시",
|
||||
"주차 환경 개선 방안 검토"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 신뢰도 계산
|
||||
*/
|
||||
private Double calculateConfidence(List<String> reviewData) {
|
||||
// 리뷰 수와 내용 길이를 기반으로 신뢰도 계산
|
||||
int reviewCount = reviewData.size();
|
||||
double avgLength = reviewData.stream()
|
||||
.mapToInt(String::length)
|
||||
.average()
|
||||
.orElse(0.0);
|
||||
|
||||
// 기본 신뢰도 계산 로직
|
||||
double confidence = Math.min(0.95, 0.5 + (reviewCount * 0.05) + (avgLength * 0.001));
|
||||
return Math.round(confidence * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback 피드백 생성
|
||||
*/
|
||||
private AiFeedback createFallbackFeedback() {
|
||||
return AiFeedback.builder()
|
||||
.summary("AI 분석을 수행할 수 없어 기본 분석 결과를 제공합니다.")
|
||||
.sentiment(SentimentType.NEUTRAL)
|
||||
.positivePoints(List.of("분석 데이터 부족"))
|
||||
.negativePoints(List.of("분석 데이터 부족"))
|
||||
.recommendations(List.of("더 많은 리뷰 데이터 수집 필요"))
|
||||
.confidence(0.3)
|
||||
.analysisDate(LocalDate.now())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.ktds.hi.analytics.infra.gateway;
|
||||
|
||||
import com.ktds.hi.analytics.biz.domain.AiFeedback;
|
||||
import com.ktds.hi.analytics.biz.domain.SentimentType;
|
||||
import com.ktds.hi.analytics.biz.usecase.out.AIServicePort;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap; // 추가
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package com.ktds.hi.analytics.infra.gateway;
|
||||
|
||||
import com.ktds.hi.analytics.biz.domain.Analytics;
|
||||
import com.ktds.hi.analytics.biz.domain.AiFeedback;
|
||||
import com.ktds.hi.analytics.biz.usecase.out.AnalyticsPort;
|
||||
import com.ktds.hi.analytics.infra.gateway.entity.AnalyticsEntity;
|
||||
import com.ktds.hi.analytics.infra.gateway.entity.AiFeedbackEntity;
|
||||
import com.ktds.hi.analytics.infra.gateway.repository.AnalyticsJpaRepository;
|
||||
import com.ktds.hi.analytics.infra.gateway.repository.AiFeedbackJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 분석 리포지토리 어댑터 클래스
|
||||
* Analytics Port를 구현하여 데이터 영속성 기능을 제공
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AnalyticsRepositoryAdapter implements AnalyticsPort {
|
||||
|
||||
private final AnalyticsJpaRepository analyticsJpaRepository;
|
||||
private final AiFeedbackJpaRepository aiFeedbackJpaRepository;
|
||||
|
||||
@Override
|
||||
public Optional<Analytics> findAnalyticsByStoreId(Long storeId) {
|
||||
return analyticsJpaRepository.findByStoreId(storeId)
|
||||
.map(this::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Analytics saveAnalytics(Analytics analytics) {
|
||||
AnalyticsEntity entity = toEntity(analytics);
|
||||
AnalyticsEntity saved = analyticsJpaRepository.save(entity);
|
||||
return toDomain(saved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AiFeedback> findAIFeedbackByStoreId(Long storeId) {
|
||||
return aiFeedbackJpaRepository.findByStoreId(storeId)
|
||||
.map(this::toAiFeedbackDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiFeedback saveAIFeedback(AiFeedback feedback) {
|
||||
AiFeedbackEntity entity = toAiFeedbackEntity(feedback);
|
||||
AiFeedbackEntity saved = aiFeedbackJpaRepository.save(entity);
|
||||
return toAiFeedbackDomain(saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity를 Domain으로 변환
|
||||
*/
|
||||
private Analytics toDomain(AnalyticsEntity entity) {
|
||||
return Analytics.builder()
|
||||
.id(entity.getId())
|
||||
.storeId(entity.getStoreId())
|
||||
.totalReviews(entity.getTotalReviews())
|
||||
.averageRating(entity.getAverageRating())
|
||||
.sentimentScore(entity.getSentimentScore())
|
||||
.createdAt(entity.getCreatedAt())
|
||||
.updatedAt(entity.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain을 Entity로 변환
|
||||
*/
|
||||
private AnalyticsEntity toEntity(Analytics domain) {
|
||||
return AnalyticsEntity.builder()
|
||||
.id(domain.getId())
|
||||
.storeId(domain.getStoreId())
|
||||
.totalReviews(domain.getTotalReviews())
|
||||
.averageRating(domain.getAverageRating())
|
||||
.sentimentScore(domain.getSentimentScore())
|
||||
.createdAt(domain.getCreatedAt())
|
||||
.updatedAt(domain.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* AiFeedback Entity를 Domain으로 변환
|
||||
*/
|
||||
private AiFeedback toAiFeedbackDomain(AiFeedbackEntity entity) {
|
||||
return AiFeedback.builder()
|
||||
.id(entity.getId())
|
||||
.storeId(entity.getStoreId())
|
||||
.summary(entity.getSummary())
|
||||
.sentiment(entity.getSentiment())
|
||||
.positivePoints(entity.getPositivePointsJson() != null ?
|
||||
parseJsonList(entity.getPositivePointsJson()) : List.of())
|
||||
.negativePoints(entity.getNegativePointsJson() != null ?
|
||||
parseJsonList(entity.getNegativePointsJson()) : List.of())
|
||||
.recommendations(entity.getRecommendationsJson() != null ?
|
||||
parseJsonList(entity.getRecommendationsJson()) : List.of())
|
||||
.confidence(entity.getConfidence())
|
||||
.analysisDate(entity.getAnalysisDate())
|
||||
.createdAt(entity.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* AiFeedback Domain을 Entity로 변환
|
||||
*/
|
||||
private AiFeedbackEntity toAiFeedbackEntity(AiFeedback domain) {
|
||||
return AiFeedbackEntity.builder()
|
||||
.id(domain.getId())
|
||||
.storeId(domain.getStoreId())
|
||||
.summary(domain.getSummary())
|
||||
.sentiment(domain.getSentiment())
|
||||
.positivePointsJson(toJsonString(domain.getPositivePoints()))
|
||||
.negativePointsJson(toJsonString(domain.getNegativePoints()))
|
||||
.recommendationsJson(toJsonString(domain.getRecommendations()))
|
||||
.confidence(domain.getConfidence())
|
||||
.analysisDate(domain.getAnalysisDate())
|
||||
.createdAt(domain.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 문자열을 List로 파싱
|
||||
*/
|
||||
private List<String> parseJsonList(String json) {
|
||||
// 실제로는 Jackson 등을 사용하여 파싱
|
||||
if (json == null || json.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return Arrays.asList(json.replace("[", "").replace("]", "").replace("\"", "").split(","));
|
||||
}
|
||||
|
||||
/**
|
||||
* List를 JSON 문자열로 변환
|
||||
*/
|
||||
private String toJsonString(List<String> list) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
return "[\"" + String.join("\",\"", list) + "\"]";
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.ktds.hi.analytics.infra.gateway;
|
||||
|
||||
import com.ktds.hi.analytics.biz.domain.Analytics;
|
||||
import com.ktds.hi.analytics.biz.domain.AiFeedback;
|
||||
import com.ktds.hi.analytics.biz.usecase.out.AnalyticsPort;
|
||||
import com.ktds.hi.analytics.infra.gateway.entity.AnalyticsEntity;
|
||||
import com.ktds.hi.analytics.infra.gateway.entity.AiFeedbackEntity;
|
||||
import com.ktds.hi.analytics.infra.gateway.repository.AnalyticsJpaRepository;
|
||||
import com.ktds.hi.analytics.infra.gateway.repository.AiFeedbackJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays; // 추가
|
||||
import java.util.List; // 추가
|
||||
import java.util.Optional;
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.ktds.hi.analytics.infra.gateway;
|
||||
|
||||
import com.ktds.hi.analytics.biz.domain.ActionPlan;
|
||||
import com.ktds.hi.analytics.biz.usecase.out.EventPort;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 이벤트 어댑터 클래스
|
||||
* Event Port를 구현하여 이벤트 발행 기능을 제공
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class EventAdapter implements EventPort {
|
||||
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@Override
|
||||
public void publishActionPlanCreatedEvent(ActionPlan actionPlan) {
|
||||
log.info("실행 계획 생성 이벤트 발행: planId={}, storeId={}", actionPlan.getId(), actionPlan.getStoreId());
|
||||
|
||||
try {
|
||||
// 실행 계획 생성 이벤트 객체 생성 및 발행
|
||||
ActionPlanCreatedEvent event = new ActionPlanCreatedEvent(actionPlan);
|
||||
eventPublisher.publishEvent(event);
|
||||
|
||||
log.info("실행 계획 생성 이벤트 발행 완료: planId={}", actionPlan.getId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("실행 계획 생성 이벤트 발행 실패: planId={}, error={}", actionPlan.getId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 실행 계획 생성 이벤트 클래스
|
||||
*/
|
||||
public static class ActionPlanCreatedEvent {
|
||||
private final ActionPlan actionPlan;
|
||||
|
||||
public ActionPlanCreatedEvent(ActionPlan actionPlan) {
|
||||
this.actionPlan = actionPlan;
|
||||
}
|
||||
|
||||
public ActionPlan getActionPlan() {
|
||||
return actionPlan;
|
||||
}
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package com.ktds.hi.analytics.infra.gateway;
|
||||
|
||||
import com.ktds.hi.analytics.biz.usecase.out.ExternalReviewPort;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 외부 리뷰 어댑터 클래스
|
||||
* External Review Port를 구현하여 외부 리뷰 데이터 연동 기능을 제공
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ExternalReviewAdapter implements ExternalReviewPort {
|
||||
|
||||
@Override
|
||||
public List<String> getReviewData(Long storeId) {
|
||||
log.info("외부 리뷰 데이터 조회 시작: storeId={}", storeId);
|
||||
|
||||
try {
|
||||
// 실제로는 Review Service와 연동하여 리뷰 데이터를 가져옴
|
||||
// Mock 데이터 반환
|
||||
List<String> reviews = new ArrayList<>();
|
||||
reviews.add("음식이 정말 맛있고 서비스도 친절해요. 다음에 또 올게요!");
|
||||
reviews.add("가격 대비 양이 많고 맛도 괜찮습니다. 추천해요.");
|
||||
reviews.add("분위기가 좋고 음식도 맛있어요. 특히 김치찌개가 일품이네요.");
|
||||
reviews.add("조금 대기시간이 길었지만 음식은 만족스러웠습니다.");
|
||||
reviews.add("깔끔하고 맛있어요. 직원분들도 친절하시고 좋았습니다.");
|
||||
reviews.add("기대보다는 평범했지만 나쁘지 않았어요.");
|
||||
reviews.add("음식이 너무 짜서 별로였습니다. 개선이 필요할 것 같아요.");
|
||||
reviews.add("가성비 좋고 맛도 괜찮아요. 재방문 의사 있습니다.");
|
||||
reviews.add("위생 상태가 좋고 음식도 깔끔해요. 만족합니다.");
|
||||
reviews.add("주차하기 어려워서 불편했지만 음식은 맛있었어요.");
|
||||
|
||||
log.info("외부 리뷰 데이터 조회 완료: storeId={}, count={}", storeId, reviews.size());
|
||||
return reviews;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("외부 리뷰 데이터 조회 실패: storeId={}, error={}", storeId, e.getMessage(), e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRecentReviews(Long storeId, Integer days) {
|
||||
log.info("최근 리뷰 데이터 조회 시작: storeId={}, days={}", storeId, days);
|
||||
|
||||
try {
|
||||
// 실제로는 최근 N일간의 리뷰만 필터링
|
||||
List<String> allReviews = getReviewData(storeId);
|
||||
|
||||
// Mock: 최근 리뷰는 전체 리뷰의 70% 정도로 가정
|
||||
int recentCount = (int) (allReviews.size() * 0.7);
|
||||
List<String> recentReviews = allReviews.subList(0, Math.min(recentCount, allReviews.size()));
|
||||
|
||||
log.info("최근 리뷰 데이터 조회 완료: storeId={}, days={}, count={}", storeId, days, recentReviews.size());
|
||||
return recentReviews;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("최근 리뷰 데이터 조회 실패: storeId={}, days={}, error={}", storeId, days, e.getMessage(), e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getReviewCount(Long storeId) {
|
||||
log.info("리뷰 수 조회 시작: storeId={}", storeId);
|
||||
|
||||
try {
|
||||
List<String> reviews = getReviewData(storeId);
|
||||
int count = reviews.size();
|
||||
|
||||
log.info("리뷰 수 조회 완료: storeId={}, count={}", storeId, count);
|
||||
return count;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("리뷰 수 조회 실패: storeId={}, error={}", storeId, e.getMessage(), e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.ktds.hi.analytics.infra.gateway.entity;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ktds.hi.analytics.biz.domain.PlanStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 실행 계획 엔티티 클래스
|
||||
* 데이터베이스 action_plans 테이블과 매핑되는 JPA 엔티티
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "action_plans")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class ActionPlanEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "store_id", nullable = false)
|
||||
private Long storeId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(nullable = false, length = 200)
|
||||
private String title;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(length = 50)
|
||||
private String period;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private PlanStatus status = PlanStatus.PLANNED;
|
||||
|
||||
@Column(name = "feedback_ids_json", columnDefinition = "TEXT")
|
||||
private String feedbackIdsJson;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String note;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "completed_at")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
/**
|
||||
* JSON 문자열을 List로 변환
|
||||
*/
|
||||
public List<Long> getFeedbackIdsList() {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.readValue(feedbackIdsJson, new TypeReference<List<Long>>() {});
|
||||
} catch (Exception e) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package com.ktds.hi.analytics.infra.gateway.entity;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 피드백 엔티티 클래스
|
||||
* 데이터베이스 ai_feedback 테이블과 매핑되는 JPA 엔티티
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "ai_feedback")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class AiFeedbackEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "store_id", nullable = false)
|
||||
private Long storeId;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String summary;
|
||||
|
||||
@Column(length = 20)
|
||||
private String sentiment;
|
||||
|
||||
@Column(name = "positive_points_json", columnDefinition = "TEXT")
|
||||
private String positivePointsJson;
|
||||
|
||||
@Column(name = "negative_points_json", columnDefinition = "TEXT")
|
||||
private String negativePointsJson;
|
||||
|
||||
@Column(name = "recommendations_json", columnDefinition = "TEXT")
|
||||
private String recommendationsJson;
|
||||
|
||||
@Column(precision = 3, scale = 2)
|
||||
private BigDecimal confidence;
|
||||
|
||||
@Column(name = "analysis_date")
|
||||
private LocalDate analysisDate;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* JSON 문자열을 객체로 변환하는 메서드들
|
||||
*/
|
||||
public Map<String, Object> getPositivePointsMap() {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.readValue(positivePointsJson, new TypeReference<Map<String, Object>>() {});
|
||||
} catch (Exception e) {
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> getNegativePointsMap() {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.readValue(negativePointsJson, new TypeReference<Map<String, Object>>() {});
|
||||
} catch (Exception e) {
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getRecommendationsList() {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.readValue(recommendationsJson, new TypeReference<List<String>>() {});
|
||||
} catch (Exception e) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package com.ktds.hi.analytics.infra.gateway.entity;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 통계 엔티티 클래스
|
||||
* 데이터베이스 order_statistics 테이블과 매핑되는 JPA 엔티티
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "order_statistics")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class StatisticsEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "store_id", nullable = false)
|
||||
private Long storeId;
|
||||
|
||||
@Column(name = "analysis_date")
|
||||
private LocalDate analysisDate;
|
||||
|
||||
@Column(name = "total_orders")
|
||||
private Integer totalOrders;
|
||||
|
||||
@Column(name = "total_revenue", precision = 15, scale = 2)
|
||||
private BigDecimal totalRevenue;
|
||||
|
||||
@Column(name = "avg_order_amount", precision = 10, scale = 2)
|
||||
private BigDecimal avgOrderAmount;
|
||||
|
||||
@Column(name = "peak_hour")
|
||||
private Integer peakHour;
|
||||
|
||||
@Column(name = "age_statistics_json", columnDefinition = "TEXT")
|
||||
private String ageStatisticsJson;
|
||||
|
||||
@Column(name = "gender_statistics_json", columnDefinition = "TEXT")
|
||||
private String genderStatisticsJson;
|
||||
|
||||
@Column(name = "time_statistics_json", columnDefinition = "TEXT")
|
||||
private String timeStatisticsJson;
|
||||
|
||||
@Column(name = "menu_popularity_json", columnDefinition = "TEXT")
|
||||
private String menuPopularityJson;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* JSON 문자열을 객체로 변환하는 메서드들
|
||||
*/
|
||||
public Map<String, Object> getAgeStatisticsMap() {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.readValue(ageStatisticsJson, new TypeReference<Map<String, Object>>() {});
|
||||
} catch (Exception e) {
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> getGenderStatisticsMap() {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.readValue(genderStatisticsJson, new TypeReference<Map<String, Object>>() {});
|
||||
} catch (Exception e) {
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> getTimeStatisticsMap() {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.readValue(timeStatisticsJson, new TypeReference<Map<String, Object>>() {});
|
||||
} catch (Exception e) {
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> getMenuPopularityMap() {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.readValue(menuPopularityJson, new TypeReference<Map<String, Object>>() {});
|
||||
} catch (Exception e) {
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.ktds.hi.analytics.infra.gateway.repository;
|
||||
|
||||
import com.ktds.hi.analytics.biz.domain.PlanStatus;
|
||||
import com.ktds.hi.analytics.infra.gateway.entity.ActionPlanEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 실행 계획 JPA 리포지토리 인터페이스
|
||||
* 실행 계획 데이터의 CRUD 작업을 담당
|
||||
*/
|
||||
@Repository
|
||||
public interface ActionPlanJpaRepository extends JpaRepository<ActionPlanEntity, Long> {
|
||||
|
||||
/**
|
||||
* 매장 ID로 실행 계획 목록 조회 (최신순)
|
||||
*/
|
||||
List<ActionPlanEntity> findByStoreIdOrderByCreatedAtDesc(Long storeId);
|
||||
|
||||
/**
|
||||
* 매장 ID와 상태로 실행 계획 목록 조회 (최신순)
|
||||
*/
|
||||
List<ActionPlanEntity> findByStoreIdAndStatusOrderByCreatedAtDesc(Long storeId, PlanStatus status);
|
||||
|
||||
/**
|
||||
* 사용자 ID로 실행 계획 목록 조회
|
||||
*/
|
||||
List<ActionPlanEntity> findByUserIdOrderByCreatedAtDesc(Long userId);
|
||||
|
||||
/**
|
||||
* 매장 ID와 사용자 ID로 실행 계획 목록 조회
|
||||
*/
|
||||
List<ActionPlanEntity> findByStoreIdAndUserIdOrderByCreatedAtDesc(Long storeId, Long userId);
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.ktds.hi.analytics.infra.gateway.repository;
|
||||
|
||||
import com.ktds.hi.analytics.infra.gateway.entity.AiFeedbackEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI 피드백 JPA 리포지토리 인터페이스
|
||||
* AI 피드백 데이터의 CRUD 작업을 담당
|
||||
*/
|
||||
@Repository
|
||||
public interface AiFeedbackJpaRepository extends JpaRepository<AiFeedbackEntity, Long> {
|
||||
|
||||
/**
|
||||
* 매장 ID와 분석 기간으로 AI 피드백 목록 조회
|
||||
*/
|
||||
List<AiFeedbackEntity> findByStoreIdAndAnalysisDateBetweenOrderByAnalysisDateDesc(
|
||||
Long storeId, LocalDate startDate, LocalDate endDate);
|
||||
|
||||
/**
|
||||
* 매장 ID로 최신 AI 피드백 조회
|
||||
*/
|
||||
AiFeedbackEntity findTopByStoreIdOrderByCreatedAtDesc(Long storeId);
|
||||
|
||||
/**
|
||||
* 특정 날짜의 AI 피드백 조회
|
||||
*/
|
||||
List<AiFeedbackEntity> findByStoreIdAndAnalysisDate(Long storeId, LocalDate analysisDate);
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.ktds.hi.analytics.infra.gateway.repository;
|
||||
|
||||
import com.ktds.hi.analytics.infra.gateway.entity.StatisticsEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 통계 JPA 리포지토리 인터페이스
|
||||
* 통계 데이터의 CRUD 작업을 담당
|
||||
*/
|
||||
@Repository
|
||||
public interface StatisticsJpaRepository extends JpaRepository<StatisticsEntity, Long> {
|
||||
|
||||
/**
|
||||
* 매장 ID와 분석 기간으로 통계 조회
|
||||
*/
|
||||
List<StatisticsEntity> findByStoreIdAndAnalysisDateBetween(
|
||||
Long storeId, LocalDate startDate, LocalDate endDate);
|
||||
|
||||
/**
|
||||
* 매장 ID와 특정 날짜로 통계 조회
|
||||
*/
|
||||
Optional<StatisticsEntity> findByStoreIdAndAnalysisDate(Long storeId, LocalDate analysisDate);
|
||||
|
||||
/**
|
||||
* 매장 ID로 최신 통계 조회
|
||||
*/
|
||||
Optional<StatisticsEntity> findTopByStoreIdOrderByAnalysisDateDesc(Long storeId);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
server:
|
||||
port: ${ANALYTICS_SERVICE_PORT:8084}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: analytics-service
|
||||
|
||||
datasource:
|
||||
url: ${ANALYTICS_DB_URL:jdbc:postgresql://localhost:5432/hiorder_analytics}
|
||||
username: ${ANALYTICS_DB_USERNAME:hiorder_user}
|
||||
password: ${ANALYTICS_DB_PASSWORD:hiorder_pass}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:validate}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
|
||||
ai-api:
|
||||
openai:
|
||||
api-key: ${OPENAI_API_KEY:}
|
||||
base-url: https://api.openai.com/v1
|
||||
model: gpt-4o-mini
|
||||
claude:
|
||||
api-key: ${CLAUDE_API_KEY:}
|
||||
base-url: https://api.anthropic.com
|
||||
model: claude-3-sonnet-20240229
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
@@ -0,0 +1,46 @@
|
||||
server:
|
||||
port: ${ANALYTICS_SERVICE_PORT:8084}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: analytics-service
|
||||
|
||||
datasource:
|
||||
url: ${ANALYTICS_DB_URL:jdbc:postgresql://localhost:5432/hiorder_analytics}
|
||||
username: ${ANALYTICS_DB_USERNAME:hiorder_user}
|
||||
password: ${ANALYTICS_DB_PASSWORD:hiorder_pass}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:validate}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
|
||||
external-api:
|
||||
openai:
|
||||
api-key: ${OPENAI_API_KEY:}
|
||||
base-url: https://api.openai.com
|
||||
claude:
|
||||
api-key: ${CLAUDE_API_KEY:}
|
||||
base-url: https://api.anthropic.com
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
Reference in New Issue
Block a user