store update
This commit is contained in:
parent
569404a73d
commit
96bbc3d83c
@ -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;
|
||||
|
||||
}
|
||||
|
||||
@ -1,33 +1,33 @@
|
||||
package com.ktds.hi.review.infra.config;
|
||||
|
||||
import com.azure.messaging.eventhubs.EventHubClientBuilder;
|
||||
import com.azure.messaging.eventhubs.EventHubConsumerClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Azure Event Hub 설정 클래스 (단일 EntityPath 포함된 connection string 사용)
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class EventHubConfig {
|
||||
|
||||
@Value("${azure.eventhub.connection-string}")
|
||||
private String connectionString;
|
||||
|
||||
@Value("${azure.eventhub.consumer-group:$Default}")
|
||||
private String consumerGroup;
|
||||
|
||||
/**
|
||||
* 외부 리뷰 이벤트 수신용 Consumer
|
||||
*/
|
||||
@Bean("externalReviewEventConsumer")
|
||||
public EventHubConsumerClient externalReviewEventConsumer() {
|
||||
return new EventHubClientBuilder()
|
||||
.connectionString(connectionString)
|
||||
.consumerGroup(consumerGroup)
|
||||
.buildConsumerClient();
|
||||
}
|
||||
}
|
||||
package com.ktds.hi.review.infra.config;
|
||||
|
||||
import com.azure.messaging.eventhubs.EventHubClientBuilder;
|
||||
import com.azure.messaging.eventhubs.EventHubConsumerClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Azure Event Hub 설정 클래스 (단일 EntityPath 포함된 connection string 사용)
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class EventHubConfig {
|
||||
|
||||
@Value("${azure.eventhub.connection-string}")
|
||||
private String connectionString;
|
||||
|
||||
@Value("${azure.eventhub.consumer-group:$Default}")
|
||||
private String consumerGroup;
|
||||
|
||||
/**
|
||||
* 외부 리뷰 이벤트 수신용 Consumer
|
||||
*/
|
||||
@Bean("externalReviewEventConsumer")
|
||||
public EventHubConsumerClient externalReviewEventConsumer() {
|
||||
return new EventHubClientBuilder()
|
||||
.connectionString(connectionString)
|
||||
.consumerGroup(consumerGroup)
|
||||
.buildConsumerClient();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,240 +1,240 @@
|
||||
package com.ktds.hi.review.infra.gateway;
|
||||
|
||||
import com.azure.messaging.eventhubs.EventData;
|
||||
import com.azure.messaging.eventhubs.EventHubConsumerClient;
|
||||
import com.azure.messaging.eventhubs.models.EventPosition;
|
||||
import com.azure.messaging.eventhubs.models.PartitionEvent;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ktds.hi.review.biz.domain.Review;
|
||||
import com.ktds.hi.review.biz.domain.ReviewStatus;
|
||||
import com.ktds.hi.review.biz.usecase.out.ReviewRepository;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Azure Event Hub 어댑터 클래스 (단순화)
|
||||
* 외부 리뷰 이벤트 수신 및 Review 테이블 저장
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ExternalReviewEventHubAdapter {
|
||||
|
||||
@Qualifier("externalReviewEventConsumer")
|
||||
private final EventHubConsumerClient externalReviewEventConsumer;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ReviewRepository reviewRepository;
|
||||
|
||||
private final ExecutorService executorService = Executors.newFixedThreadPool(3);
|
||||
private volatile boolean isRunning = false;
|
||||
|
||||
@PostConstruct
|
||||
public void startEventListening() {
|
||||
log.info("외부 리뷰 Event Hub 리스너 시작");
|
||||
isRunning = true;
|
||||
|
||||
// 외부 리뷰 이벤트 수신 시작
|
||||
executorService.submit(this::listenToExternalReviewEvents);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stopEventListening() {
|
||||
log.info("외부 리뷰 Event Hub 리스너 종료");
|
||||
isRunning = false;
|
||||
executorService.shutdown();
|
||||
externalReviewEventConsumer.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 리뷰 이벤트 수신 처리
|
||||
*/
|
||||
private void listenToExternalReviewEvents() {
|
||||
log.info("외부 리뷰 이벤트 수신 시작");
|
||||
|
||||
try {
|
||||
while (isRunning) {
|
||||
Iterable<PartitionEvent> events = externalReviewEventConsumer.receiveFromPartition(
|
||||
"4", // 파티션 ID (0으로 수정)
|
||||
100, // 최대 이벤트 수
|
||||
EventPosition.earliest(), // 시작 위치
|
||||
Duration.ofSeconds(30) // 타임아웃
|
||||
);
|
||||
|
||||
for (PartitionEvent partitionEvent : events) {
|
||||
handleExternalReviewEvent(partitionEvent);
|
||||
}
|
||||
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
log.info("외부 리뷰 이벤트 수신 중단됨");
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Exception e) {
|
||||
log.error("외부 리뷰 이벤트 수신 중 오류 발생", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 리뷰 이벤트 처리
|
||||
*/
|
||||
private void handleExternalReviewEvent(PartitionEvent partitionEvent) {
|
||||
try {
|
||||
EventData eventData = partitionEvent.getData();
|
||||
String eventBody = eventData.getBodyAsString();
|
||||
|
||||
Map<String, Object> event = objectMapper.readValue(eventBody, Map.class);
|
||||
String eventType = (String) event.get("eventType");
|
||||
Long storeId = Long.valueOf(event.get("storeId").toString());
|
||||
|
||||
log.info("외부 리뷰 이벤트 수신: type={}, storeId={}", eventType, storeId);
|
||||
|
||||
if ("EXTERNAL_REVIEW_SYNC".equals(eventType)) {
|
||||
handleExternalReviewSyncEvent(storeId, event);
|
||||
} else {
|
||||
log.warn("알 수 없는 외부 리뷰 이벤트 타입: {}", eventType);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("외부 리뷰 이벤트 처리 중 오류 발생", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 리뷰 동기화 이벤트 처리 - 여러 리뷰를 배치로 처리
|
||||
*/
|
||||
private void handleExternalReviewSyncEvent(Long storeId, Map<String, Object> event) {
|
||||
try {
|
||||
String platform = (String) event.get("platform");
|
||||
Integer syncedCount = (Integer) event.get("syncedCount");
|
||||
|
||||
// Store에서 발행하는 reviews 배열 처리
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> reviews = (List<Map<String, Object>>) event.get("reviews");
|
||||
|
||||
if (reviews == null || reviews.isEmpty()) {
|
||||
log.warn("리뷰 데이터가 없습니다: platform={}, storeId={}", platform, storeId);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("외부 리뷰 동기화 처리 시작: platform={}, storeId={}, count={}",
|
||||
platform, storeId, reviews.size());
|
||||
|
||||
int savedCount = 0;
|
||||
for (Map<String, Object> reviewData : reviews) {
|
||||
try {
|
||||
Review savedReview = saveExternalReview(storeId, platform, reviewData);
|
||||
if (savedReview != null) {
|
||||
savedCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("개별 리뷰 저장 실패: platform={}, storeId={}, error={}",
|
||||
platform, storeId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("외부 리뷰 동기화 완료: platform={}, storeId={}, expected={}, saved={}",
|
||||
platform, storeId, reviews.size(), savedCount);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("외부 리뷰 동기화 이벤트 처리 실패: storeId={}, error={}", storeId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 외부 리뷰 저장 (단순화)
|
||||
*/
|
||||
private Review saveExternalReview(Long storeId, String platform, Map<String, Object> reviewData) {
|
||||
try {
|
||||
// ✅ 단순화된 매핑
|
||||
Review review = Review.builder()
|
||||
.storeId(storeId)
|
||||
.memberId(null) // 외부 리뷰는 회원 ID 없음
|
||||
.memberNickname(createMemberNickname(platform, reviewData))
|
||||
.rating(extractRating(reviewData))
|
||||
.content(extractContent(reviewData))
|
||||
.imageUrls(new ArrayList<>()) // 외부 리뷰는 이미지 없음
|
||||
.status(ReviewStatus.ACTIVE)
|
||||
.likeCount(0) // ✅ 고정값 0
|
||||
.dislikeCount(0)
|
||||
.build();
|
||||
|
||||
// Review 테이블에 저장
|
||||
Review savedReview = reviewRepository.saveReview(review);
|
||||
|
||||
log.debug("외부 리뷰 저장 완료: reviewId={}, platform={}, storeId={}, author={}",
|
||||
savedReview.getId(), platform, storeId, savedReview.getMemberNickname());
|
||||
|
||||
return savedReview;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("외부 리뷰 저장 실패: platform={}, storeId={}, error={}",
|
||||
platform, storeId, e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플랫폼별 회원 닉네임 생성 (카카오 API 필드명 수정)
|
||||
*/
|
||||
private String createMemberNickname(String platform, Map<String, Object> reviewData) {
|
||||
String authorName = null;
|
||||
|
||||
// ✅ 카카오 API 구조에 맞춰 수정
|
||||
if ("KAKAO".equalsIgnoreCase(platform)) {
|
||||
authorName = (String) reviewData.get("reviewer_name");
|
||||
} else {
|
||||
// 다른 플랫폼 대비
|
||||
authorName = (String) reviewData.get("author_name");
|
||||
if (authorName == null) {
|
||||
authorName = (String) reviewData.get("authorName");
|
||||
}
|
||||
}
|
||||
|
||||
if (authorName == null || authorName.trim().isEmpty()) {
|
||||
return platform.toUpperCase() + " 사용자";
|
||||
}
|
||||
|
||||
return authorName + "(" + platform.toUpperCase() + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* 평점 추출 (기본값: 5)
|
||||
*/
|
||||
private Integer extractRating(Map<String, Object> reviewData) {
|
||||
Object rating = reviewData.get("rating");
|
||||
if (rating instanceof Number) {
|
||||
int ratingValue = ((Number) rating).intValue();
|
||||
return (ratingValue >= 1 && ratingValue <= 5) ? ratingValue : 5;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* 리뷰 내용 추출
|
||||
*/
|
||||
private String extractContent(Map<String, Object> reviewData) {
|
||||
String content = (String) reviewData.get("content");
|
||||
if (content == null || content.trim().isEmpty()) {
|
||||
return "외부 플랫폼 리뷰";
|
||||
}
|
||||
|
||||
// 내용이 너무 길면 자르기 (reviews 테이블 length 제한 대비)
|
||||
if (content.length() > 1900) {
|
||||
content = content.substring(0, 1900) + "...";
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
package com.ktds.hi.review.infra.gateway;
|
||||
|
||||
import com.azure.messaging.eventhubs.EventData;
|
||||
import com.azure.messaging.eventhubs.EventHubConsumerClient;
|
||||
import com.azure.messaging.eventhubs.models.EventPosition;
|
||||
import com.azure.messaging.eventhubs.models.PartitionEvent;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ktds.hi.review.biz.domain.Review;
|
||||
import com.ktds.hi.review.biz.domain.ReviewStatus;
|
||||
import com.ktds.hi.review.biz.usecase.out.ReviewRepository;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Azure Event Hub 어댑터 클래스 (단순화)
|
||||
* 외부 리뷰 이벤트 수신 및 Review 테이블 저장
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ExternalReviewEventHubAdapter {
|
||||
|
||||
@Qualifier("externalReviewEventConsumer")
|
||||
private final EventHubConsumerClient externalReviewEventConsumer;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ReviewRepository reviewRepository;
|
||||
|
||||
private final ExecutorService executorService = Executors.newFixedThreadPool(3);
|
||||
private volatile boolean isRunning = false;
|
||||
|
||||
@PostConstruct
|
||||
public void startEventListening() {
|
||||
log.info("외부 리뷰 Event Hub 리스너 시작");
|
||||
isRunning = true;
|
||||
|
||||
// 외부 리뷰 이벤트 수신 시작
|
||||
executorService.submit(this::listenToExternalReviewEvents);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stopEventListening() {
|
||||
log.info("외부 리뷰 Event Hub 리스너 종료");
|
||||
isRunning = false;
|
||||
executorService.shutdown();
|
||||
externalReviewEventConsumer.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 리뷰 이벤트 수신 처리
|
||||
*/
|
||||
private void listenToExternalReviewEvents() {
|
||||
log.info("외부 리뷰 이벤트 수신 시작");
|
||||
|
||||
try {
|
||||
while (isRunning) {
|
||||
Iterable<PartitionEvent> events = externalReviewEventConsumer.receiveFromPartition(
|
||||
"4", // 파티션 ID (0으로 수정)
|
||||
100, // 최대 이벤트 수
|
||||
EventPosition.earliest(), // 시작 위치
|
||||
Duration.ofSeconds(30) // 타임아웃
|
||||
);
|
||||
|
||||
for (PartitionEvent partitionEvent : events) {
|
||||
handleExternalReviewEvent(partitionEvent);
|
||||
}
|
||||
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
log.info("외부 리뷰 이벤트 수신 중단됨");
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Exception e) {
|
||||
log.error("외부 리뷰 이벤트 수신 중 오류 발생", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 리뷰 이벤트 처리
|
||||
*/
|
||||
private void handleExternalReviewEvent(PartitionEvent partitionEvent) {
|
||||
try {
|
||||
EventData eventData = partitionEvent.getData();
|
||||
String eventBody = eventData.getBodyAsString();
|
||||
|
||||
Map<String, Object> event = objectMapper.readValue(eventBody, Map.class);
|
||||
String eventType = (String) event.get("eventType");
|
||||
Long storeId = Long.valueOf(event.get("storeId").toString());
|
||||
|
||||
log.info("외부 리뷰 이벤트 수신: type={}, storeId={}", eventType, storeId);
|
||||
|
||||
if ("EXTERNAL_REVIEW_SYNC".equals(eventType)) {
|
||||
handleExternalReviewSyncEvent(storeId, event);
|
||||
} else {
|
||||
log.warn("알 수 없는 외부 리뷰 이벤트 타입: {}", eventType);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("외부 리뷰 이벤트 처리 중 오류 발생", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 리뷰 동기화 이벤트 처리 - 여러 리뷰를 배치로 처리
|
||||
*/
|
||||
private void handleExternalReviewSyncEvent(Long storeId, Map<String, Object> event) {
|
||||
try {
|
||||
String platform = (String) event.get("platform");
|
||||
Integer syncedCount = (Integer) event.get("syncedCount");
|
||||
|
||||
// Store에서 발행하는 reviews 배열 처리
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> reviews = (List<Map<String, Object>>) event.get("reviews");
|
||||
|
||||
if (reviews == null || reviews.isEmpty()) {
|
||||
log.warn("리뷰 데이터가 없습니다: platform={}, storeId={}", platform, storeId);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("외부 리뷰 동기화 처리 시작: platform={}, storeId={}, count={}",
|
||||
platform, storeId, reviews.size());
|
||||
|
||||
int savedCount = 0;
|
||||
for (Map<String, Object> reviewData : reviews) {
|
||||
try {
|
||||
Review savedReview = saveExternalReview(storeId, platform, reviewData);
|
||||
if (savedReview != null) {
|
||||
savedCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("개별 리뷰 저장 실패: platform={}, storeId={}, error={}",
|
||||
platform, storeId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("외부 리뷰 동기화 완료: platform={}, storeId={}, expected={}, saved={}",
|
||||
platform, storeId, reviews.size(), savedCount);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("외부 리뷰 동기화 이벤트 처리 실패: storeId={}, error={}", storeId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 외부 리뷰 저장 (단순화)
|
||||
*/
|
||||
private Review saveExternalReview(Long storeId, String platform, Map<String, Object> reviewData) {
|
||||
try {
|
||||
// ✅ 단순화된 매핑
|
||||
Review review = Review.builder()
|
||||
.storeId(storeId)
|
||||
.memberId(null) // 외부 리뷰는 회원 ID 없음
|
||||
.memberNickname(createMemberNickname(platform, reviewData))
|
||||
.rating(extractRating(reviewData))
|
||||
.content(extractContent(reviewData))
|
||||
.imageUrls(new ArrayList<>()) // 외부 리뷰는 이미지 없음
|
||||
.status(ReviewStatus.ACTIVE)
|
||||
.likeCount(0) // ✅ 고정값 0
|
||||
.dislikeCount(0)
|
||||
.build();
|
||||
|
||||
// Review 테이블에 저장
|
||||
Review savedReview = reviewRepository.saveReview(review);
|
||||
|
||||
log.debug("외부 리뷰 저장 완료: reviewId={}, platform={}, storeId={}, author={}",
|
||||
savedReview.getId(), platform, storeId, savedReview.getMemberNickname());
|
||||
|
||||
return savedReview;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("외부 리뷰 저장 실패: platform={}, storeId={}, error={}",
|
||||
platform, storeId, e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플랫폼별 회원 닉네임 생성 (카카오 API 필드명 수정)
|
||||
*/
|
||||
private String createMemberNickname(String platform, Map<String, Object> reviewData) {
|
||||
String authorName = null;
|
||||
|
||||
// ✅ 카카오 API 구조에 맞춰 수정
|
||||
if ("KAKAO".equalsIgnoreCase(platform)) {
|
||||
authorName = (String) reviewData.get("reviewer_name");
|
||||
} else {
|
||||
// 다른 플랫폼 대비
|
||||
authorName = (String) reviewData.get("author_name");
|
||||
if (authorName == null) {
|
||||
authorName = (String) reviewData.get("authorName");
|
||||
}
|
||||
}
|
||||
|
||||
if (authorName == null || authorName.trim().isEmpty()) {
|
||||
return platform.toUpperCase() + " 사용자";
|
||||
}
|
||||
|
||||
return authorName + "(" + platform.toUpperCase() + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* 평점 추출 (기본값: 5)
|
||||
*/
|
||||
private Integer extractRating(Map<String, Object> reviewData) {
|
||||
Object rating = reviewData.get("rating");
|
||||
if (rating instanceof Number) {
|
||||
int ratingValue = ((Number) rating).intValue();
|
||||
return (ratingValue >= 1 && ratingValue <= 5) ? ratingValue : 5;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* 리뷰 내용 추출
|
||||
*/
|
||||
private String extractContent(Map<String, Object> reviewData) {
|
||||
String content = (String) reviewData.get("content");
|
||||
if (content == null || content.trim().isEmpty()) {
|
||||
return "외부 플랫폼 리뷰";
|
||||
}
|
||||
|
||||
// 내용이 너무 길면 자르기 (reviews 테이블 length 제한 대비)
|
||||
if (content.length() > 1900) {
|
||||
content = content.substring(0, 1900) + "...";
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
@ -1,74 +1,74 @@
|
||||
package com.ktds.hi.review.infra.gateway.entity;
|
||||
|
||||
import com.ktds.hi.review.biz.domain.ReviewStatus;
|
||||
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.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 리뷰 엔티티 클래스
|
||||
* 데이터베이스 reviews 테이블과 매핑되는 JPA 엔티티
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "reviews")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class ReviewEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "store_id", nullable = false)
|
||||
private Long storeId;
|
||||
|
||||
@Column(name = "member_id", nullable = true)
|
||||
private Long memberId;
|
||||
|
||||
@Column(name = "member_nickname", nullable = false, length = 50)
|
||||
private String memberNickname;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer rating;
|
||||
|
||||
@Column(nullable = false, length = 1000)
|
||||
private String content;
|
||||
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "review_images",
|
||||
joinColumns = @JoinColumn(name = "review_id"))
|
||||
@Column(name = "image_url")
|
||||
private List<String> imageUrls;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
private ReviewStatus status = ReviewStatus.ACTIVE;
|
||||
|
||||
@Column(name = "like_count")
|
||||
@Builder.Default
|
||||
private Integer likeCount = 0;
|
||||
|
||||
@Column(name = "dislike_count")
|
||||
@Builder.Default
|
||||
private Integer dislikeCount = 0;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
package com.ktds.hi.review.infra.gateway.entity;
|
||||
|
||||
import com.ktds.hi.review.biz.domain.ReviewStatus;
|
||||
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.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 리뷰 엔티티 클래스
|
||||
* 데이터베이스 reviews 테이블과 매핑되는 JPA 엔티티
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "reviews")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class ReviewEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "store_id", nullable = false)
|
||||
private Long storeId;
|
||||
|
||||
@Column(name = "member_id", nullable = true)
|
||||
private Long memberId;
|
||||
|
||||
@Column(name = "member_nickname", nullable = false, length = 50)
|
||||
private String memberNickname;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer rating;
|
||||
|
||||
@Column(nullable = false, length = 1000)
|
||||
private String content;
|
||||
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "review_images",
|
||||
joinColumns = @JoinColumn(name = "review_id"))
|
||||
@Column(name = "image_url")
|
||||
private List<String> imageUrls;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
private ReviewStatus status = ReviewStatus.ACTIVE;
|
||||
|
||||
@Column(name = "like_count")
|
||||
@Builder.Default
|
||||
private Integer likeCount = 0;
|
||||
|
||||
@Column(name = "dislike_count")
|
||||
@Builder.Default
|
||||
private Integer dislikeCount = 0;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
@ -1,48 +1,48 @@
|
||||
server:
|
||||
port: ${REVIEW_SERVICE_PORT:8083}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: review-service
|
||||
|
||||
datasource:
|
||||
url: ${REVIEW_DB_URL:jdbc:postgresql://20.214.91.15:5432/hiorder_review}
|
||||
username: ${REVIEW_DB_USERNAME:hiorder_user}
|
||||
password: ${REVIEW_DB_PASSWORD:hiorder_pass}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: ${MAX_FILE_SIZE:10MB}
|
||||
max-request-size: ${MAX_REQUEST_SIZE:50MB}
|
||||
|
||||
azure:
|
||||
eventhub:
|
||||
connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING}
|
||||
consumer-group: $Default
|
||||
|
||||
file-storage:
|
||||
base-path: ${FILE_STORAGE_PATH:/var/hiorder/uploads}
|
||||
allowed-extensions: jpg,jpeg,png,gif,webp
|
||||
max-file-size: 10485760 # 10MB
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /docs/review/api-docs
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
path: /docs/review/swagger-ui.html
|
||||
server:
|
||||
port: ${REVIEW_SERVICE_PORT:8083}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: review-service
|
||||
|
||||
datasource:
|
||||
url: ${REVIEW_DB_URL:jdbc:postgresql://20.214.91.15:5432/hiorder_review}
|
||||
username: ${REVIEW_DB_USERNAME:hiorder_user}
|
||||
password: ${REVIEW_DB_PASSWORD:hiorder_pass}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: ${MAX_FILE_SIZE:10MB}
|
||||
max-request-size: ${MAX_REQUEST_SIZE:50MB}
|
||||
|
||||
azure:
|
||||
eventhub:
|
||||
connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING}
|
||||
consumer-group: $Default
|
||||
|
||||
file-storage:
|
||||
base-path: ${FILE_STORAGE_PATH:/var/hiorder/uploads}
|
||||
allowed-extensions: jpg,jpeg,png,gif,webp
|
||||
max-file-size: 10485760 # 10MB
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /docs/review/api-docs
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
path: /docs/review/swagger-ui.html
|
||||
|
||||
@ -130,7 +130,7 @@ public class StoreService implements StoreUseCase {
|
||||
.orElseThrow(() -> new BusinessException("STORE_ACCESS_DENIED", "매장에 대한 권한이 없습니다."));
|
||||
|
||||
store.updateInfo(request.getStoreName(), request.getAddress(), request.getDescription(),
|
||||
request.getPhone(), request.getOperatingHours());
|
||||
request.getPhone(), request.getOperatingHours(), request.getImageUrl());
|
||||
|
||||
storeJpaRepository.save(store);
|
||||
|
||||
|
||||
@ -38,4 +38,7 @@ public class StoreUpdateRequest {
|
||||
|
||||
@Schema(description = "매장 태그 목록", example = "[\"맛집\", \"혼밥\", \"가성비\"]")
|
||||
private List<String> tags;
|
||||
|
||||
@Schema(description = "매장 이미지")
|
||||
private String imageUrl;
|
||||
}
|
||||
|
||||
@ -132,13 +132,14 @@ public class StoreEntity {
|
||||
* 매장 기본 정보 업데이트
|
||||
*/
|
||||
public void updateInfo(String storeName, String address, String description,
|
||||
String phone, String operatingHours) {
|
||||
String phone, String operatingHours, String imageUrl) {
|
||||
this.storeName = storeName;
|
||||
this.address = address;
|
||||
this.description = description;
|
||||
this.phone = phone;
|
||||
this.operatingHours = operatingHours;
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,60 +1,60 @@
|
||||
server:
|
||||
port: ${STORE_SERVICE_PORT:8082}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: store-service
|
||||
|
||||
datasource:
|
||||
url: ${STORE_DB_URL:jdbc:postgresql://20.249.154.116:5432/hiorder_store}
|
||||
username: ${STORE_DB_USERNAME:hiorder_user}
|
||||
password: ${STORE_DB_PASSWORD:hiorder_pass}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
# Azure Event Hub 설정 (추가)
|
||||
azure:
|
||||
eventhub:
|
||||
connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING}
|
||||
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
external-api:
|
||||
naver:
|
||||
client-id: ${NAVER_CLIENT_ID:}
|
||||
client-secret: ${NAVER_CLIENT_SECRET:}
|
||||
base-url: https://openapi.naver.com
|
||||
kakao:
|
||||
api-key: ${KAKAO_API_KEY:}
|
||||
base-url: http://kakao-review-api-service.ai-review-ns.svc.cluster.local
|
||||
google:
|
||||
api-key: ${GOOGLE_API_KEY:}
|
||||
base-url: https://maps.googleapis.com
|
||||
hiorder:
|
||||
api-key: ${HIORDER_API_KEY:}
|
||||
base-url: ${HIORDER_BASE_URL:https://api.hiorder.com}
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /docs/store/api-docs
|
||||
swagger-ui:
|
||||
path: /docs/store/swagger-ui.html
|
||||
server:
|
||||
port: ${STORE_SERVICE_PORT:8082}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: store-service
|
||||
|
||||
datasource:
|
||||
url: ${STORE_DB_URL:jdbc:postgresql://20.249.154.116:5432/hiorder_store}
|
||||
username: ${STORE_DB_USERNAME:hiorder_user}
|
||||
password: ${STORE_DB_PASSWORD:hiorder_pass}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
# Azure Event Hub 설정 (추가)
|
||||
azure:
|
||||
eventhub:
|
||||
connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING}
|
||||
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
external-api:
|
||||
naver:
|
||||
client-id: ${NAVER_CLIENT_ID:}
|
||||
client-secret: ${NAVER_CLIENT_SECRET:}
|
||||
base-url: https://openapi.naver.com
|
||||
kakao:
|
||||
api-key: ${KAKAO_API_KEY:}
|
||||
base-url: http://kakao-review-api-service.ai-review-ns.svc.cluster.local
|
||||
google:
|
||||
api-key: ${GOOGLE_API_KEY:}
|
||||
base-url: https://maps.googleapis.com
|
||||
hiorder:
|
||||
api-key: ${HIORDER_API_KEY:}
|
||||
base-url: ${HIORDER_BASE_URL:https://api.hiorder.com}
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /docs/store/api-docs
|
||||
swagger-ui:
|
||||
path: /docs/store/swagger-ui.html
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user