store update

This commit is contained in:
youbeen
2025-06-18 09:52:51 +09:00
parent 569404a73d
commit 96bbc3d83c
15 changed files with 1527 additions and 1523 deletions
@@ -1,35 +1,35 @@
package com.ktds.hi.analytics.biz.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.time.LocalDateTime;
import java.util.List;
/**
* AI 피드백 도메인 클래스
* AI가 생성한 피드백 정보를 나타냄
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class AiFeedback {
private Long id;
private Long storeId;
private String summary;
private List<String> positivePoints;
private List<String> negativePoints;
private List<String> improvementPoints;
private List<String> recommendations;
private String sentimentAnalysis;
private Double confidenceScore;
private LocalDateTime generatedAt;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
package com.ktds.hi.analytics.biz.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.time.LocalDateTime;
import java.util.List;
/**
* AI 피드백 도메인 클래스
* AI가 생성한 피드백 정보를 나타냄
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class AiFeedback {
private Long id;
private Long storeId;
private String summary;
private List<String> positivePoints;
private List<String> negativePoints;
private List<String> improvementPoints;
private List<String> recommendations;
private String sentimentAnalysis;
private Double confidenceScore;
private LocalDateTime generatedAt;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -1,166 +1,166 @@
package com.ktds.hi.analytics.infra.controller;
import com.ktds.hi.analytics.biz.usecase.in.AnalyticsUseCase;
import com.ktds.hi.analytics.infra.dto.*;
import com.ktds.hi.common.dto.ErrorResponse;
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.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.constraints.*;
import java.time.LocalDate;
import java.util.List;
/**
* 분석 서비스 컨트롤러 클래스
* 매장 분석, AI 피드백, 통계 조회 API를 제공
*/
@Slf4j
@RestController
@RequestMapping("/api/analytics")
@RequiredArgsConstructor
@Tag(name = "Analytics API", description = "매장 분석 및 AI 피드백 API")
public class AnalyticsController {
private final AnalyticsUseCase analyticsUseCase;
/**
* 매장 분석 데이터 조회
*/
@Operation(summary = "매장 분석 데이터 조회", description = "매장의 전반적인 분석 데이터를 조회합니다.")
@GetMapping("/stores/{storeId}")
public ResponseEntity<SuccessResponse<StoreAnalyticsResponse>> getStoreAnalytics(
@Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId) {
log.info("매장 분석 데이터 조회 요청: storeId={}", storeId);
StoreAnalyticsResponse response = analyticsUseCase.getStoreAnalytics(storeId);
return ResponseEntity.ok(SuccessResponse.of(response, "매장 분석 데이터 조회 성공"));
}
/**
* AI 피드백 상세 조회
*/
@Operation(summary = "AI 피드백 상세 조회", description = "매장의 AI 피드백 상세 정보를 조회합니다.")
@GetMapping("/stores/{storeId}/ai-feedback")
public ResponseEntity<SuccessResponse<AiFeedbackDetailResponse>> getAIFeedbackDetail(
@Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId) {
log.info("AI 피드백 상세 조회 요청: storeId={}", storeId);
AiFeedbackDetailResponse response = analyticsUseCase.getAIFeedbackDetail(storeId);
return ResponseEntity.ok(SuccessResponse.of(response, "AI 피드백 상세 조회 성공"));
}
/**
* 매장 통계 조회
*/
@Operation(summary = "매장 통계 조회", description = "기간별 매장 주문 통계를 조회합니다.")
@GetMapping("/stores/{storeId}/statistics")
public ResponseEntity<SuccessResponse<StoreStatisticsResponse>> getStoreStatistics(
@Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId,
@Parameter(description = "시작 날짜 (YYYY-MM-DD)", required = true)
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@Parameter(description = "종료 날짜 (YYYY-MM-DD)", required = true)
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
log.info("매장 통계 조회 요청: storeId={}, period={} ~ {}", storeId, startDate, endDate);
StoreStatisticsResponse response = analyticsUseCase.getStoreStatistics(storeId, startDate, endDate);
return ResponseEntity.ok(SuccessResponse.of(response, "매장 통계 조회 성공"));
}
/**
* AI 피드백 요약 조회
*/
@Operation(summary = "AI 피드백 요약 조회", description = "매장의 AI 피드백 요약 정보를 조회합니다.")
@GetMapping("/stores/{storeId}/ai-feedback/summary")
public ResponseEntity<SuccessResponse<AiFeedbackSummaryResponse>> getAIFeedbackSummary(
@Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId) {
log.info("AI 피드백 요약 조회 요청: storeId={}", storeId);
AiFeedbackSummaryResponse response = analyticsUseCase.getAIFeedbackSummary(storeId);
return ResponseEntity.ok(SuccessResponse.of(response, "AI 피드백 요약 조회 성공"));
}
/**
* 리뷰 분석 조회
*/
@Operation(summary = "리뷰 분석 조회", description = "매장의 리뷰 감정 분석 결과를 조회합니다.")
@GetMapping("/stores/{storeId}/review-analysis")
public ResponseEntity<SuccessResponse<ReviewAnalysisResponse>> getReviewAnalysis(
@Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId) {
log.info("리뷰 분석 조회 요청: storeId={}", storeId);
ReviewAnalysisResponse response = analyticsUseCase.getReviewAnalysis(storeId);
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<Void>> generateActionPlans(
@Parameter(description = "AI 피드백 ID", required = true)
@PathVariable @NotNull Long feedbackId,
@RequestBody ActionPlanCreateRequest request) {
// validation 체크
if (request.getActionPlanSelect() == null || request.getActionPlanSelect().isEmpty()) {
throw new IllegalArgumentException("실행계획을 생성하려면 개선포인트를 선택해주세요.");
}
List<String> actionPlans = analyticsUseCase.generateActionPlansFromFeedback(request,feedbackId);
return ResponseEntity.ok(SuccessResponse.of("실행계획 생성 완료"));
}
}
package com.ktds.hi.analytics.infra.controller;
import com.ktds.hi.analytics.biz.usecase.in.AnalyticsUseCase;
import com.ktds.hi.analytics.infra.dto.*;
import com.ktds.hi.common.dto.ErrorResponse;
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.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.constraints.*;
import java.time.LocalDate;
import java.util.List;
/**
* 분석 서비스 컨트롤러 클래스
* 매장 분석, AI 피드백, 통계 조회 API를 제공
*/
@Slf4j
@RestController
@RequestMapping("/api/analytics")
@RequiredArgsConstructor
@Tag(name = "Analytics API", description = "매장 분석 및 AI 피드백 API")
public class AnalyticsController {
private final AnalyticsUseCase analyticsUseCase;
/**
* 매장 분석 데이터 조회
*/
@Operation(summary = "매장 분석 데이터 조회", description = "매장의 전반적인 분석 데이터를 조회합니다.")
@GetMapping("/stores/{storeId}")
public ResponseEntity<SuccessResponse<StoreAnalyticsResponse>> getStoreAnalytics(
@Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId) {
log.info("매장 분석 데이터 조회 요청: storeId={}", storeId);
StoreAnalyticsResponse response = analyticsUseCase.getStoreAnalytics(storeId);
return ResponseEntity.ok(SuccessResponse.of(response, "매장 분석 데이터 조회 성공"));
}
/**
* AI 피드백 상세 조회
*/
@Operation(summary = "AI 피드백 상세 조회", description = "매장의 AI 피드백 상세 정보를 조회합니다.")
@GetMapping("/stores/{storeId}/ai-feedback")
public ResponseEntity<SuccessResponse<AiFeedbackDetailResponse>> getAIFeedbackDetail(
@Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId) {
log.info("AI 피드백 상세 조회 요청: storeId={}", storeId);
AiFeedbackDetailResponse response = analyticsUseCase.getAIFeedbackDetail(storeId);
return ResponseEntity.ok(SuccessResponse.of(response, "AI 피드백 상세 조회 성공"));
}
/**
* 매장 통계 조회
*/
@Operation(summary = "매장 통계 조회", description = "기간별 매장 주문 통계를 조회합니다.")
@GetMapping("/stores/{storeId}/statistics")
public ResponseEntity<SuccessResponse<StoreStatisticsResponse>> getStoreStatistics(
@Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId,
@Parameter(description = "시작 날짜 (YYYY-MM-DD)", required = true)
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@Parameter(description = "종료 날짜 (YYYY-MM-DD)", required = true)
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
log.info("매장 통계 조회 요청: storeId={}, period={} ~ {}", storeId, startDate, endDate);
StoreStatisticsResponse response = analyticsUseCase.getStoreStatistics(storeId, startDate, endDate);
return ResponseEntity.ok(SuccessResponse.of(response, "매장 통계 조회 성공"));
}
/**
* AI 피드백 요약 조회
*/
@Operation(summary = "AI 피드백 요약 조회", description = "매장의 AI 피드백 요약 정보를 조회합니다.")
@GetMapping("/stores/{storeId}/ai-feedback/summary")
public ResponseEntity<SuccessResponse<AiFeedbackSummaryResponse>> getAIFeedbackSummary(
@Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId) {
log.info("AI 피드백 요약 조회 요청: storeId={}", storeId);
AiFeedbackSummaryResponse response = analyticsUseCase.getAIFeedbackSummary(storeId);
return ResponseEntity.ok(SuccessResponse.of(response, "AI 피드백 요약 조회 성공"));
}
/**
* 리뷰 분석 조회
*/
@Operation(summary = "리뷰 분석 조회", description = "매장의 리뷰 감정 분석 결과를 조회합니다.")
@GetMapping("/stores/{storeId}/review-analysis")
public ResponseEntity<SuccessResponse<ReviewAnalysisResponse>> getReviewAnalysis(
@Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId) {
log.info("리뷰 분석 조회 요청: storeId={}", storeId);
ReviewAnalysisResponse response = analyticsUseCase.getReviewAnalysis(storeId);
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<Void>> generateActionPlans(
@Parameter(description = "AI 피드백 ID", required = true)
@PathVariable @NotNull Long feedbackId,
@RequestBody ActionPlanCreateRequest request) {
// validation 체크
if (request.getActionPlanSelect() == null || request.getActionPlanSelect().isEmpty()) {
throw new IllegalArgumentException("실행계획을 생성하려면 개선포인트를 선택해주세요.");
}
List<String> actionPlans = analyticsUseCase.generateActionPlansFromFeedback(request,feedbackId);
return ResponseEntity.ok(SuccessResponse.of("실행계획 생성 완료"));
}
}
@@ -1,57 +1,57 @@
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> negativePoints;
@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;
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> negativePoints;
@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;
}
@@ -1,47 +1,47 @@
package com.ktds.hi.analytics.infra.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* AI 피드백 상세 응답 DTO
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AiFeedbackDetailResponse {
private Long feedbackId;
private Long storeId;
private String summary;
private List<String> positivePoints;
private List<String> negativePoints;
private List<String> improvementPoints;
private List<String> existActionPlan; // improvemnetPoints 중에서 처리 된것.
private List<String> recommendations;
private String sentimentAnalysis;
private Double confidenceScore;
private LocalDateTime generatedAt;
public void updateImprovementCheck(List<String> actionPlanTitle){
Set<String> trimmedTitles = actionPlanTitle.stream()
.map(String::trim)
.collect(Collectors.toSet());
this.existActionPlan =
improvementPoints.stream()
.map(String::trim)
.filter(point -> trimmedTitles.stream()
.anyMatch(title -> title.contains(point)))
.toList();
}
}
package com.ktds.hi.analytics.infra.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* AI 피드백 상세 응답 DTO
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AiFeedbackDetailResponse {
private Long feedbackId;
private Long storeId;
private String summary;
private List<String> positivePoints;
private List<String> negativePoints;
private List<String> improvementPoints;
private List<String> existActionPlan; // improvemnetPoints 중에서 처리 된것.
private List<String> recommendations;
private String sentimentAnalysis;
private Double confidenceScore;
private LocalDateTime generatedAt;
public void updateImprovementCheck(List<String> actionPlanTitle){
Set<String> trimmedTitles = actionPlanTitle.stream()
.map(String::trim)
.collect(Collectors.toSet());
this.existActionPlan =
improvementPoints.stream()
.map(String::trim)
.filter(point -> trimmedTitles.stream()
.anyMatch(title -> title.contains(point)))
.toList();
}
}
File diff suppressed because it is too large Load Diff
@@ -1,168 +1,168 @@
package com.ktds.hi.analytics.infra.gateway;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
/**
* 분석 리포지토리 어댑터 클래스 (완성버전)
* Analytics Port를 구현하여 데이터 영속성 기능을 제공
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AnalyticsRepositoryAdapter implements AnalyticsPort {
private final AnalyticsJpaRepository analyticsJpaRepository;
private final AiFeedbackJpaRepository aiFeedbackJpaRepository;
private final ObjectMapper objectMapper;
@Override
public Optional<Analytics> findAnalyticsByStoreId(Long storeId) {
return analyticsJpaRepository.findLatestByStoreId(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.findLatestByStoreId(storeId)
.map(this::toAiFeedbackDomain);
}
@Override
public AiFeedback saveAIFeedback(AiFeedback feedback) {
AiFeedbackEntity entity = toAiFeedbackEntity(feedback);
AiFeedbackEntity saved = aiFeedbackJpaRepository.save(entity);
return toAiFeedbackDomain(saved);
}
@Override
public Optional<AiFeedback> findAIFeedbackById(Long feedbackId) {
return aiFeedbackJpaRepository.findById(feedbackId)
.map(this::toAiFeedbackDomain);
}
/**
* Analytics 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())
.positiveReviewRate(entity.getPositiveReviewRate())
.negativeReviewRate(entity.getNegativeReviewRate())
.lastAnalysisDate(entity.getLastAnalysisDate())
.createdAt(entity.getCreatedAt())
.updatedAt(entity.getUpdatedAt())
.build();
}
/**
* Analytics 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())
.positiveReviewRate(domain.getPositiveReviewRate())
.negativeReviewRate(domain.getNegativeReviewRate())
.lastAnalysisDate(domain.getLastAnalysisDate())
.build();
}
/**
* AiFeedback Entity를 Domain으로 변환
*/
private AiFeedback toAiFeedbackDomain(AiFeedbackEntity entity) {
return AiFeedback.builder()
.id(entity.getId())
.storeId(entity.getStoreId())
.summary(entity.getSummary())
.positivePoints(parseJsonToList(entity.getPositivePointsJson()))
.negativePoints(parseJsonToList(entity.getNegativePointsJson()))
.improvementPoints(parseJsonToList(entity.getImprovementPointsJson()))
.recommendations(parseJsonToList(entity.getRecommendationsJson()))
.sentimentAnalysis(entity.getSentimentAnalysis())
.confidenceScore(entity.getConfidenceScore())
.generatedAt(entity.getGeneratedAt())
.createdAt(entity.getCreatedAt())
.updatedAt(entity.getUpdatedAt())
.build();
}
/**
* AiFeedback Domain을 Entity로 변환
*/
private AiFeedbackEntity toAiFeedbackEntity(AiFeedback domain) {
return AiFeedbackEntity.builder()
.id(domain.getId())
.storeId(domain.getStoreId())
.summary(domain.getSummary())
.positivePointsJson(parseListToJson(domain.getPositivePoints()))
.negativePointsJson(parseListToJson(domain.getNegativePoints()))
.improvementPointsJson(parseListToJson(domain.getImprovementPoints()))
.recommendationsJson(parseListToJson(domain.getRecommendations()))
.sentimentAnalysis(domain.getSentimentAnalysis())
.confidenceScore(domain.getConfidenceScore())
.generatedAt(domain.getGeneratedAt())
.build();
}
/**
* JSON 문자열을 List로 변환
*/
private List<String> parseJsonToList(String json) {
if (json == null || json.trim().isEmpty()) {
return List.of();
}
try {
return objectMapper.readValue(json, new TypeReference<List<String>>() {});
} catch (JsonProcessingException e) {
log.warn("JSON 파싱 실패: {}", json, e);
return List.of();
}
}
/**
* List를 JSON 문자열로 변환
*/
private String parseListToJson(List<String> list) {
if (list == null || list.isEmpty()) {
return "[]";
}
try {
return objectMapper.writeValueAsString(list);
} catch (JsonProcessingException e) {
log.warn("JSON 직렬화 실패: {}", list, e);
return "[]";
}
}
}
package com.ktds.hi.analytics.infra.gateway;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
/**
* 분석 리포지토리 어댑터 클래스 (완성버전)
* Analytics Port를 구현하여 데이터 영속성 기능을 제공
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AnalyticsRepositoryAdapter implements AnalyticsPort {
private final AnalyticsJpaRepository analyticsJpaRepository;
private final AiFeedbackJpaRepository aiFeedbackJpaRepository;
private final ObjectMapper objectMapper;
@Override
public Optional<Analytics> findAnalyticsByStoreId(Long storeId) {
return analyticsJpaRepository.findLatestByStoreId(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.findLatestByStoreId(storeId)
.map(this::toAiFeedbackDomain);
}
@Override
public AiFeedback saveAIFeedback(AiFeedback feedback) {
AiFeedbackEntity entity = toAiFeedbackEntity(feedback);
AiFeedbackEntity saved = aiFeedbackJpaRepository.save(entity);
return toAiFeedbackDomain(saved);
}
@Override
public Optional<AiFeedback> findAIFeedbackById(Long feedbackId) {
return aiFeedbackJpaRepository.findById(feedbackId)
.map(this::toAiFeedbackDomain);
}
/**
* Analytics 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())
.positiveReviewRate(entity.getPositiveReviewRate())
.negativeReviewRate(entity.getNegativeReviewRate())
.lastAnalysisDate(entity.getLastAnalysisDate())
.createdAt(entity.getCreatedAt())
.updatedAt(entity.getUpdatedAt())
.build();
}
/**
* Analytics 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())
.positiveReviewRate(domain.getPositiveReviewRate())
.negativeReviewRate(domain.getNegativeReviewRate())
.lastAnalysisDate(domain.getLastAnalysisDate())
.build();
}
/**
* AiFeedback Entity를 Domain으로 변환
*/
private AiFeedback toAiFeedbackDomain(AiFeedbackEntity entity) {
return AiFeedback.builder()
.id(entity.getId())
.storeId(entity.getStoreId())
.summary(entity.getSummary())
.positivePoints(parseJsonToList(entity.getPositivePointsJson()))
.negativePoints(parseJsonToList(entity.getNegativePointsJson()))
.improvementPoints(parseJsonToList(entity.getImprovementPointsJson()))
.recommendations(parseJsonToList(entity.getRecommendationsJson()))
.sentimentAnalysis(entity.getSentimentAnalysis())
.confidenceScore(entity.getConfidenceScore())
.generatedAt(entity.getGeneratedAt())
.createdAt(entity.getCreatedAt())
.updatedAt(entity.getUpdatedAt())
.build();
}
/**
* AiFeedback Domain을 Entity로 변환
*/
private AiFeedbackEntity toAiFeedbackEntity(AiFeedback domain) {
return AiFeedbackEntity.builder()
.id(domain.getId())
.storeId(domain.getStoreId())
.summary(domain.getSummary())
.positivePointsJson(parseListToJson(domain.getPositivePoints()))
.negativePointsJson(parseListToJson(domain.getNegativePoints()))
.improvementPointsJson(parseListToJson(domain.getImprovementPoints()))
.recommendationsJson(parseListToJson(domain.getRecommendations()))
.sentimentAnalysis(domain.getSentimentAnalysis())
.confidenceScore(domain.getConfidenceScore())
.generatedAt(domain.getGeneratedAt())
.build();
}
/**
* JSON 문자열을 List로 변환
*/
private List<String> parseJsonToList(String json) {
if (json == null || json.trim().isEmpty()) {
return List.of();
}
try {
return objectMapper.readValue(json, new TypeReference<List<String>>() {});
} catch (JsonProcessingException e) {
log.warn("JSON 파싱 실패: {}", json, e);
return List.of();
}
}
/**
* List를 JSON 문자열로 변환
*/
private String parseListToJson(List<String> list) {
if (list == null || list.isEmpty()) {
return "[]";
}
try {
return objectMapper.writeValueAsString(list);
} catch (JsonProcessingException e) {
log.warn("JSON 직렬화 실패: {}", list, e);
return "[]";
}
}
}
@@ -1,72 +1,72 @@
package com.ktds.hi.analytics.infra.gateway.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import jakarta.persistence.*;
import java.time.LocalDateTime;
/**
* AI 피드백 엔티티
* AI가 생성한 피드백 정보를 저장
*/
@Entity
@Table(name = "ai_feedback",
indexes = {
@Index(name = "idx_ai_feedback_store_id", columnList = "store_id"),
@Index(name = "idx_ai_feedback_generated_at", columnList = "generated_at"),
@Index(name = "idx_ai_feedback_created_at", columnList = "created_at"),
@Index(name = "idx_ai_feedback_confidence_score", columnList = "confidence_score")
})
@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(name = "summary", length = 1000)
private String summary;
@Column(name = "positive_points", columnDefinition = "TEXT")
private String positivePointsJson;
@Column(name = "negative_points", columnDefinition = "TEXT")
private String negativePointsJson;
@Column(name = "improvement_points", columnDefinition = "TEXT")
private String improvementPointsJson;
@Column(name = "recommendations", columnDefinition = "TEXT")
private String recommendationsJson;
@Column(name = "sentiment_analysis", length = 500)
private String sentimentAnalysis;
@Column(name = "confidence_score")
private Double confidenceScore;
@Column(name = "generated_at")
private LocalDateTime generatedAt;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
package com.ktds.hi.analytics.infra.gateway.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import jakarta.persistence.*;
import java.time.LocalDateTime;
/**
* AI 피드백 엔티티
* AI가 생성한 피드백 정보를 저장
*/
@Entity
@Table(name = "ai_feedback",
indexes = {
@Index(name = "idx_ai_feedback_store_id", columnList = "store_id"),
@Index(name = "idx_ai_feedback_generated_at", columnList = "generated_at"),
@Index(name = "idx_ai_feedback_created_at", columnList = "created_at"),
@Index(name = "idx_ai_feedback_confidence_score", columnList = "confidence_score")
})
@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(name = "summary", length = 1000)
private String summary;
@Column(name = "positive_points", columnDefinition = "TEXT")
private String positivePointsJson;
@Column(name = "negative_points", columnDefinition = "TEXT")
private String negativePointsJson;
@Column(name = "improvement_points", columnDefinition = "TEXT")
private String improvementPointsJson;
@Column(name = "recommendations", columnDefinition = "TEXT")
private String recommendationsJson;
@Column(name = "sentiment_analysis", length = 500)
private String sentimentAnalysis;
@Column(name = "confidence_score")
private Double confidenceScore;
@Column(name = "generated_at")
private LocalDateTime generatedAt;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}