# Conflicts:
#	review/src/main/java/com/ktds/hi/review/infra/gateway/ExternalReviewEventHubAdapter.java
This commit is contained in:
UNGGU0704 2025-06-18 10:06:52 +09:00
commit 3a59c0f279
15 changed files with 1291 additions and 1284 deletions

View File

@ -1,35 +1,35 @@
package com.ktds.hi.analytics.biz.domain; package com.ktds.hi.analytics.biz.domain;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.ToString; import lombok.ToString;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/** /**
* AI 피드백 도메인 클래스 * AI 피드백 도메인 클래스
* AI가 생성한 피드백 정보를 나타냄 * AI가 생성한 피드백 정보를 나타냄
*/ */
@Getter @Getter
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@ToString @ToString
public class AiFeedback { public class AiFeedback {
private Long id; private Long id;
private Long storeId; private Long storeId;
private String summary; private String summary;
private List<String> positivePoints; private List<String> positivePoints;
private List<String> negativePoints; private List<String> negativePoints;
private List<String> improvementPoints; private List<String> improvementPoints;
private List<String> recommendations; private List<String> recommendations;
private String sentimentAnalysis; private String sentimentAnalysis;
private Double confidenceScore; private Double confidenceScore;
private LocalDateTime generatedAt; private LocalDateTime generatedAt;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
} }

View File

@ -1,166 +1,166 @@
package com.ktds.hi.analytics.infra.controller; package com.ktds.hi.analytics.infra.controller;
import com.ktds.hi.analytics.biz.usecase.in.AnalyticsUseCase; import com.ktds.hi.analytics.biz.usecase.in.AnalyticsUseCase;
import com.ktds.hi.analytics.infra.dto.*; import com.ktds.hi.analytics.infra.dto.*;
import com.ktds.hi.common.dto.ErrorResponse; import com.ktds.hi.common.dto.ErrorResponse;
import com.ktds.hi.common.dto.SuccessResponse; import com.ktds.hi.common.dto.SuccessResponse;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import jakarta.validation.constraints.*; import jakarta.validation.constraints.*;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
/** /**
* 분석 서비스 컨트롤러 클래스 * 분석 서비스 컨트롤러 클래스
* 매장 분석, AI 피드백, 통계 조회 API를 제공 * 매장 분석, AI 피드백, 통계 조회 API를 제공
*/ */
@Slf4j @Slf4j
@RestController @RestController
@RequestMapping("/api/analytics") @RequestMapping("/api/analytics")
@RequiredArgsConstructor @RequiredArgsConstructor
@Tag(name = "Analytics API", description = "매장 분석 및 AI 피드백 API") @Tag(name = "Analytics API", description = "매장 분석 및 AI 피드백 API")
public class AnalyticsController { public class AnalyticsController {
private final AnalyticsUseCase analyticsUseCase; private final AnalyticsUseCase analyticsUseCase;
/** /**
* 매장 분석 데이터 조회 * 매장 분석 데이터 조회
*/ */
@Operation(summary = "매장 분석 데이터 조회", description = "매장의 전반적인 분석 데이터를 조회합니다.") @Operation(summary = "매장 분석 데이터 조회", description = "매장의 전반적인 분석 데이터를 조회합니다.")
@GetMapping("/stores/{storeId}") @GetMapping("/stores/{storeId}")
public ResponseEntity<SuccessResponse<StoreAnalyticsResponse>> getStoreAnalytics( public ResponseEntity<SuccessResponse<StoreAnalyticsResponse>> getStoreAnalytics(
@Parameter(description = "매장 ID", required = true) @Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId) { @PathVariable @NotNull Long storeId) {
log.info("매장 분석 데이터 조회 요청: storeId={}", storeId); log.info("매장 분석 데이터 조회 요청: storeId={}", storeId);
StoreAnalyticsResponse response = analyticsUseCase.getStoreAnalytics(storeId); StoreAnalyticsResponse response = analyticsUseCase.getStoreAnalytics(storeId);
return ResponseEntity.ok(SuccessResponse.of(response, "매장 분석 데이터 조회 성공")); return ResponseEntity.ok(SuccessResponse.of(response, "매장 분석 데이터 조회 성공"));
} }
/** /**
* AI 피드백 상세 조회 * AI 피드백 상세 조회
*/ */
@Operation(summary = "AI 피드백 상세 조회", description = "매장의 AI 피드백 상세 정보를 조회합니다.") @Operation(summary = "AI 피드백 상세 조회", description = "매장의 AI 피드백 상세 정보를 조회합니다.")
@GetMapping("/stores/{storeId}/ai-feedback") @GetMapping("/stores/{storeId}/ai-feedback")
public ResponseEntity<SuccessResponse<AiFeedbackDetailResponse>> getAIFeedbackDetail( public ResponseEntity<SuccessResponse<AiFeedbackDetailResponse>> getAIFeedbackDetail(
@Parameter(description = "매장 ID", required = true) @Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId) { @PathVariable @NotNull Long storeId) {
log.info("AI 피드백 상세 조회 요청: storeId={}", storeId); log.info("AI 피드백 상세 조회 요청: storeId={}", storeId);
AiFeedbackDetailResponse response = analyticsUseCase.getAIFeedbackDetail(storeId); AiFeedbackDetailResponse response = analyticsUseCase.getAIFeedbackDetail(storeId);
return ResponseEntity.ok(SuccessResponse.of(response, "AI 피드백 상세 조회 성공")); return ResponseEntity.ok(SuccessResponse.of(response, "AI 피드백 상세 조회 성공"));
} }
/** /**
* 매장 통계 조회 * 매장 통계 조회
*/ */
@Operation(summary = "매장 통계 조회", description = "기간별 매장 주문 통계를 조회합니다.") @Operation(summary = "매장 통계 조회", description = "기간별 매장 주문 통계를 조회합니다.")
@GetMapping("/stores/{storeId}/statistics") @GetMapping("/stores/{storeId}/statistics")
public ResponseEntity<SuccessResponse<StoreStatisticsResponse>> getStoreStatistics( public ResponseEntity<SuccessResponse<StoreStatisticsResponse>> getStoreStatistics(
@Parameter(description = "매장 ID", required = true) @Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId, @PathVariable @NotNull Long storeId,
@Parameter(description = "시작 날짜 (YYYY-MM-DD)", required = true) @Parameter(description = "시작 날짜 (YYYY-MM-DD)", required = true)
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@Parameter(description = "종료 날짜 (YYYY-MM-DD)", required = true) @Parameter(description = "종료 날짜 (YYYY-MM-DD)", required = true)
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
log.info("매장 통계 조회 요청: storeId={}, period={} ~ {}", storeId, startDate, endDate); log.info("매장 통계 조회 요청: storeId={}, period={} ~ {}", storeId, startDate, endDate);
StoreStatisticsResponse response = analyticsUseCase.getStoreStatistics(storeId, startDate, endDate); StoreStatisticsResponse response = analyticsUseCase.getStoreStatistics(storeId, startDate, endDate);
return ResponseEntity.ok(SuccessResponse.of(response, "매장 통계 조회 성공")); return ResponseEntity.ok(SuccessResponse.of(response, "매장 통계 조회 성공"));
} }
/** /**
* AI 피드백 요약 조회 * AI 피드백 요약 조회
*/ */
@Operation(summary = "AI 피드백 요약 조회", description = "매장의 AI 피드백 요약 정보를 조회합니다.") @Operation(summary = "AI 피드백 요약 조회", description = "매장의 AI 피드백 요약 정보를 조회합니다.")
@GetMapping("/stores/{storeId}/ai-feedback/summary") @GetMapping("/stores/{storeId}/ai-feedback/summary")
public ResponseEntity<SuccessResponse<AiFeedbackSummaryResponse>> getAIFeedbackSummary( public ResponseEntity<SuccessResponse<AiFeedbackSummaryResponse>> getAIFeedbackSummary(
@Parameter(description = "매장 ID", required = true) @Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId) { @PathVariable @NotNull Long storeId) {
log.info("AI 피드백 요약 조회 요청: storeId={}", storeId); log.info("AI 피드백 요약 조회 요청: storeId={}", storeId);
AiFeedbackSummaryResponse response = analyticsUseCase.getAIFeedbackSummary(storeId); AiFeedbackSummaryResponse response = analyticsUseCase.getAIFeedbackSummary(storeId);
return ResponseEntity.ok(SuccessResponse.of(response, "AI 피드백 요약 조회 성공")); return ResponseEntity.ok(SuccessResponse.of(response, "AI 피드백 요약 조회 성공"));
} }
/** /**
* 리뷰 분석 조회 * 리뷰 분석 조회
*/ */
@Operation(summary = "리뷰 분석 조회", description = "매장의 리뷰 감정 분석 결과를 조회합니다.") @Operation(summary = "리뷰 분석 조회", description = "매장의 리뷰 감정 분석 결과를 조회합니다.")
@GetMapping("/stores/{storeId}/review-analysis") @GetMapping("/stores/{storeId}/review-analysis")
public ResponseEntity<SuccessResponse<ReviewAnalysisResponse>> getReviewAnalysis( public ResponseEntity<SuccessResponse<ReviewAnalysisResponse>> getReviewAnalysis(
@Parameter(description = "매장 ID", required = true) @Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId) { @PathVariable @NotNull Long storeId) {
log.info("리뷰 분석 조회 요청: storeId={}", storeId); log.info("리뷰 분석 조회 요청: storeId={}", storeId);
ReviewAnalysisResponse response = analyticsUseCase.getReviewAnalysis(storeId); ReviewAnalysisResponse response = analyticsUseCase.getReviewAnalysis(storeId);
return ResponseEntity.ok(SuccessResponse.of(response, "리뷰 분석 조회 성공")); return ResponseEntity.ok(SuccessResponse.of(response, "리뷰 분석 조회 성공"));
} }
/** /**
* AI 리뷰 분석 실행계획 생성 * AI 리뷰 분석 실행계획 생성
*/ */
@Operation(summary = "AI 리뷰 분석", description = "매장 리뷰를 AI로 분석하고 실행계획을 생성합니다.") @Operation(summary = "AI 리뷰 분석", description = "매장 리뷰를 AI로 분석하고 실행계획을 생성합니다.")
@PostMapping("/stores/{storeId}/ai-analysis") @PostMapping("/stores/{storeId}/ai-analysis")
public ResponseEntity<SuccessResponse<AiAnalysisResponse>> generateAIAnalysis( public ResponseEntity<SuccessResponse<AiAnalysisResponse>> generateAIAnalysis(
@Parameter(description = "매장 ID", required = true) @Parameter(description = "매장 ID", required = true)
@PathVariable @NotNull Long storeId, @PathVariable @NotNull Long storeId,
@Parameter(description = "분석 요청 정보") @Parameter(description = "분석 요청 정보")
@RequestBody(required = false) @Valid AiAnalysisRequest request) { @RequestBody(required = false) @Valid AiAnalysisRequest request) {
log.info("AI 리뷰 분석 요청: storeId={}", storeId); log.info("AI 리뷰 분석 요청: storeId={}", storeId);
if (request == null) { if (request == null) {
request = AiAnalysisRequest.builder().build(); request = AiAnalysisRequest.builder().build();
} }
AiAnalysisResponse response = analyticsUseCase.generateAIAnalysis(storeId, request); AiAnalysisResponse response = analyticsUseCase.generateAIAnalysis(storeId, request);
return ResponseEntity.ok(SuccessResponse.of(response, "AI 분석 완료")); return ResponseEntity.ok(SuccessResponse.of(response, "AI 분석 완료"));
} }
/** /**
* AI 피드백 기반 실행계획 생성 * AI 피드백 기반 실행계획 생성
*/ */
@Operation(summary = "실행계획 생성", description = "AI 피드백을 기반으로 실행계획을 생성합니다.") @Operation(summary = "실행계획 생성", description = "AI 피드백을 기반으로 실행계획을 생성합니다.")
@PostMapping("/ai-feedback/{feedbackId}/action-plans") @PostMapping("/ai-feedback/{feedbackId}/action-plans")
public ResponseEntity<SuccessResponse<Void>> generateActionPlans( public ResponseEntity<SuccessResponse<Void>> generateActionPlans(
@Parameter(description = "AI 피드백 ID", required = true) @Parameter(description = "AI 피드백 ID", required = true)
@PathVariable @NotNull Long feedbackId, @PathVariable @NotNull Long feedbackId,
@RequestBody ActionPlanCreateRequest request) { @RequestBody ActionPlanCreateRequest request) {
// validation 체크 // validation 체크
if (request.getActionPlanSelect() == null || request.getActionPlanSelect().isEmpty()) { if (request.getActionPlanSelect() == null || request.getActionPlanSelect().isEmpty()) {
throw new IllegalArgumentException("실행계획을 생성하려면 개선포인트를 선택해주세요."); throw new IllegalArgumentException("실행계획을 생성하려면 개선포인트를 선택해주세요.");
} }
List<String> actionPlans = analyticsUseCase.generateActionPlansFromFeedback(request,feedbackId); List<String> actionPlans = analyticsUseCase.generateActionPlansFromFeedback(request,feedbackId);
return ResponseEntity.ok(SuccessResponse.of("실행계획 생성 완료")); return ResponseEntity.ok(SuccessResponse.of("실행계획 생성 완료"));
} }
} }

View File

@ -1,57 +1,57 @@
package com.ktds.hi.analytics.infra.dto; package com.ktds.hi.analytics.infra.dto;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/** /**
* AI 분석 응답 DTO * AI 분석 응답 DTO
*/ */
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Schema(description = "AI 리뷰 분석 결과") @Schema(description = "AI 리뷰 분석 결과")
public class AiAnalysisResponse { public class AiAnalysisResponse {
@Schema(description = "매장 ID") @Schema(description = "매장 ID")
private Long storeId; private Long storeId;
@Schema(description = "AI 피드백 ID") @Schema(description = "AI 피드백 ID")
private Long feedbackId; private Long feedbackId;
@Schema(description = "분석 요약") @Schema(description = "분석 요약")
private String summary; private String summary;
@Schema(description = "긍정적 요소") @Schema(description = "긍정적 요소")
private List<String> positivePoints; private List<String> positivePoints;
@Schema(description = "부정적 요소") @Schema(description = "부정적 요소")
private List<String> negativePoints; private List<String> negativePoints;
@Schema(description = "개선점") @Schema(description = "개선점")
private List<String> improvementPoints; private List<String> improvementPoints;
@Schema(description = "추천사항") @Schema(description = "추천사항")
private List<String> recommendations; private List<String> recommendations;
@Schema(description = "감정 분석 결과") @Schema(description = "감정 분석 결과")
private String sentimentAnalysis; private String sentimentAnalysis;
@Schema(description = "신뢰도 점수") @Schema(description = "신뢰도 점수")
private Double confidenceScore; private Double confidenceScore;
@Schema(description = "분석된 리뷰 수") @Schema(description = "분석된 리뷰 수")
private Integer totalReviewsAnalyzed; private Integer totalReviewsAnalyzed;
@Schema(description = "생성된 실행계획") @Schema(description = "생성된 실행계획")
private List<String> actionPlans; private List<String> actionPlans;
@Schema(description = "분석 완료 시간") @Schema(description = "분석 완료 시간")
private LocalDateTime analyzedAt; private LocalDateTime analyzedAt;
} }

View File

@ -1,47 +1,47 @@
package com.ktds.hi.analytics.infra.dto; package com.ktds.hi.analytics.infra.dto;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* AI 피드백 상세 응답 DTO * AI 피드백 상세 응답 DTO
*/ */
@Getter @Getter
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class AiFeedbackDetailResponse { public class AiFeedbackDetailResponse {
private Long feedbackId; private Long feedbackId;
private Long storeId; private Long storeId;
private String summary; private String summary;
private List<String> positivePoints; private List<String> positivePoints;
private List<String> negativePoints; private List<String> negativePoints;
private List<String> improvementPoints; private List<String> improvementPoints;
private List<String> existActionPlan; // improvemnetPoints 중에서 처리 된것. private List<String> existActionPlan; // improvemnetPoints 중에서 처리 된것.
private List<String> recommendations; private List<String> recommendations;
private String sentimentAnalysis; private String sentimentAnalysis;
private Double confidenceScore; private Double confidenceScore;
private LocalDateTime generatedAt; private LocalDateTime generatedAt;
public void updateImprovementCheck(List<String> actionPlanTitle){ public void updateImprovementCheck(List<String> actionPlanTitle){
Set<String> trimmedTitles = actionPlanTitle.stream() Set<String> trimmedTitles = actionPlanTitle.stream()
.map(String::trim) .map(String::trim)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
this.existActionPlan = this.existActionPlan =
improvementPoints.stream() improvementPoints.stream()
.map(String::trim) .map(String::trim)
.filter(point -> trimmedTitles.stream() .filter(point -> trimmedTitles.stream()
.anyMatch(title -> title.contains(point))) .anyMatch(title -> title.contains(point)))
.toList(); .toList();
} }
} }

View File

@ -1,168 +1,168 @@
package com.ktds.hi.analytics.infra.gateway; package com.ktds.hi.analytics.infra.gateway;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.ktds.hi.analytics.biz.domain.Analytics; import com.ktds.hi.analytics.biz.domain.Analytics;
import com.ktds.hi.analytics.biz.domain.AiFeedback; import com.ktds.hi.analytics.biz.domain.AiFeedback;
import com.ktds.hi.analytics.biz.usecase.out.AnalyticsPort; 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.AnalyticsEntity;
import com.ktds.hi.analytics.infra.gateway.entity.AiFeedbackEntity; 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.AnalyticsJpaRepository;
import com.ktds.hi.analytics.infra.gateway.repository.AiFeedbackJpaRepository; import com.ktds.hi.analytics.infra.gateway.repository.AiFeedbackJpaRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
/** /**
* 분석 리포지토리 어댑터 클래스 (완성버전) * 분석 리포지토리 어댑터 클래스 (완성버전)
* Analytics Port를 구현하여 데이터 영속성 기능을 제공 * Analytics Port를 구현하여 데이터 영속성 기능을 제공
*/ */
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class AnalyticsRepositoryAdapter implements AnalyticsPort { public class AnalyticsRepositoryAdapter implements AnalyticsPort {
private final AnalyticsJpaRepository analyticsJpaRepository; private final AnalyticsJpaRepository analyticsJpaRepository;
private final AiFeedbackJpaRepository aiFeedbackJpaRepository; private final AiFeedbackJpaRepository aiFeedbackJpaRepository;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@Override @Override
public Optional<Analytics> findAnalyticsByStoreId(Long storeId) { public Optional<Analytics> findAnalyticsByStoreId(Long storeId) {
return analyticsJpaRepository.findLatestByStoreId(storeId) return analyticsJpaRepository.findLatestByStoreId(storeId)
.map(this::toDomain); .map(this::toDomain);
} }
@Override @Override
public Analytics saveAnalytics(Analytics analytics) { public Analytics saveAnalytics(Analytics analytics) {
AnalyticsEntity entity = toEntity(analytics); AnalyticsEntity entity = toEntity(analytics);
AnalyticsEntity saved = analyticsJpaRepository.save(entity); AnalyticsEntity saved = analyticsJpaRepository.save(entity);
return toDomain(saved); return toDomain(saved);
} }
@Override @Override
public Optional<AiFeedback> findAIFeedbackByStoreId(Long storeId) { public Optional<AiFeedback> findAIFeedbackByStoreId(Long storeId) {
return aiFeedbackJpaRepository.findLatestByStoreId(storeId) return aiFeedbackJpaRepository.findLatestByStoreId(storeId)
.map(this::toAiFeedbackDomain); .map(this::toAiFeedbackDomain);
} }
@Override @Override
public AiFeedback saveAIFeedback(AiFeedback feedback) { public AiFeedback saveAIFeedback(AiFeedback feedback) {
AiFeedbackEntity entity = toAiFeedbackEntity(feedback); AiFeedbackEntity entity = toAiFeedbackEntity(feedback);
AiFeedbackEntity saved = aiFeedbackJpaRepository.save(entity); AiFeedbackEntity saved = aiFeedbackJpaRepository.save(entity);
return toAiFeedbackDomain(saved); return toAiFeedbackDomain(saved);
} }
@Override @Override
public Optional<AiFeedback> findAIFeedbackById(Long feedbackId) { public Optional<AiFeedback> findAIFeedbackById(Long feedbackId) {
return aiFeedbackJpaRepository.findById(feedbackId) return aiFeedbackJpaRepository.findById(feedbackId)
.map(this::toAiFeedbackDomain); .map(this::toAiFeedbackDomain);
} }
/** /**
* Analytics Entity를 Domain으로 변환 * Analytics Entity를 Domain으로 변환
*/ */
private Analytics toDomain(AnalyticsEntity entity) { private Analytics toDomain(AnalyticsEntity entity) {
return Analytics.builder() return Analytics.builder()
.id(entity.getId()) .id(entity.getId())
.storeId(entity.getStoreId()) .storeId(entity.getStoreId())
.totalReviews(entity.getTotalReviews()) .totalReviews(entity.getTotalReviews())
.averageRating(entity.getAverageRating()) .averageRating(entity.getAverageRating())
.sentimentScore(entity.getSentimentScore()) .sentimentScore(entity.getSentimentScore())
.positiveReviewRate(entity.getPositiveReviewRate()) .positiveReviewRate(entity.getPositiveReviewRate())
.negativeReviewRate(entity.getNegativeReviewRate()) .negativeReviewRate(entity.getNegativeReviewRate())
.lastAnalysisDate(entity.getLastAnalysisDate()) .lastAnalysisDate(entity.getLastAnalysisDate())
.createdAt(entity.getCreatedAt()) .createdAt(entity.getCreatedAt())
.updatedAt(entity.getUpdatedAt()) .updatedAt(entity.getUpdatedAt())
.build(); .build();
} }
/** /**
* Analytics Domain을 Entity로 변환 * Analytics Domain을 Entity로 변환
*/ */
private AnalyticsEntity toEntity(Analytics domain) { private AnalyticsEntity toEntity(Analytics domain) {
return AnalyticsEntity.builder() return AnalyticsEntity.builder()
.id(domain.getId()) .id(domain.getId())
.storeId(domain.getStoreId()) .storeId(domain.getStoreId())
.totalReviews(domain.getTotalReviews()) .totalReviews(domain.getTotalReviews())
.averageRating(domain.getAverageRating()) .averageRating(domain.getAverageRating())
.sentimentScore(domain.getSentimentScore()) .sentimentScore(domain.getSentimentScore())
.positiveReviewRate(domain.getPositiveReviewRate()) .positiveReviewRate(domain.getPositiveReviewRate())
.negativeReviewRate(domain.getNegativeReviewRate()) .negativeReviewRate(domain.getNegativeReviewRate())
.lastAnalysisDate(domain.getLastAnalysisDate()) .lastAnalysisDate(domain.getLastAnalysisDate())
.build(); .build();
} }
/** /**
* AiFeedback Entity를 Domain으로 변환 * AiFeedback Entity를 Domain으로 변환
*/ */
private AiFeedback toAiFeedbackDomain(AiFeedbackEntity entity) { private AiFeedback toAiFeedbackDomain(AiFeedbackEntity entity) {
return AiFeedback.builder() return AiFeedback.builder()
.id(entity.getId()) .id(entity.getId())
.storeId(entity.getStoreId()) .storeId(entity.getStoreId())
.summary(entity.getSummary()) .summary(entity.getSummary())
.positivePoints(parseJsonToList(entity.getPositivePointsJson())) .positivePoints(parseJsonToList(entity.getPositivePointsJson()))
.negativePoints(parseJsonToList(entity.getNegativePointsJson())) .negativePoints(parseJsonToList(entity.getNegativePointsJson()))
.improvementPoints(parseJsonToList(entity.getImprovementPointsJson())) .improvementPoints(parseJsonToList(entity.getImprovementPointsJson()))
.recommendations(parseJsonToList(entity.getRecommendationsJson())) .recommendations(parseJsonToList(entity.getRecommendationsJson()))
.sentimentAnalysis(entity.getSentimentAnalysis()) .sentimentAnalysis(entity.getSentimentAnalysis())
.confidenceScore(entity.getConfidenceScore()) .confidenceScore(entity.getConfidenceScore())
.generatedAt(entity.getGeneratedAt()) .generatedAt(entity.getGeneratedAt())
.createdAt(entity.getCreatedAt()) .createdAt(entity.getCreatedAt())
.updatedAt(entity.getUpdatedAt()) .updatedAt(entity.getUpdatedAt())
.build(); .build();
} }
/** /**
* AiFeedback Domain을 Entity로 변환 * AiFeedback Domain을 Entity로 변환
*/ */
private AiFeedbackEntity toAiFeedbackEntity(AiFeedback domain) { private AiFeedbackEntity toAiFeedbackEntity(AiFeedback domain) {
return AiFeedbackEntity.builder() return AiFeedbackEntity.builder()
.id(domain.getId()) .id(domain.getId())
.storeId(domain.getStoreId()) .storeId(domain.getStoreId())
.summary(domain.getSummary()) .summary(domain.getSummary())
.positivePointsJson(parseListToJson(domain.getPositivePoints())) .positivePointsJson(parseListToJson(domain.getPositivePoints()))
.negativePointsJson(parseListToJson(domain.getNegativePoints())) .negativePointsJson(parseListToJson(domain.getNegativePoints()))
.improvementPointsJson(parseListToJson(domain.getImprovementPoints())) .improvementPointsJson(parseListToJson(domain.getImprovementPoints()))
.recommendationsJson(parseListToJson(domain.getRecommendations())) .recommendationsJson(parseListToJson(domain.getRecommendations()))
.sentimentAnalysis(domain.getSentimentAnalysis()) .sentimentAnalysis(domain.getSentimentAnalysis())
.confidenceScore(domain.getConfidenceScore()) .confidenceScore(domain.getConfidenceScore())
.generatedAt(domain.getGeneratedAt()) .generatedAt(domain.getGeneratedAt())
.build(); .build();
} }
/** /**
* JSON 문자열을 List로 변환 * JSON 문자열을 List로 변환
*/ */
private List<String> parseJsonToList(String json) { private List<String> parseJsonToList(String json) {
if (json == null || json.trim().isEmpty()) { if (json == null || json.trim().isEmpty()) {
return List.of(); return List.of();
} }
try { try {
return objectMapper.readValue(json, new TypeReference<List<String>>() {}); return objectMapper.readValue(json, new TypeReference<List<String>>() {});
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
log.warn("JSON 파싱 실패: {}", json, e); log.warn("JSON 파싱 실패: {}", json, e);
return List.of(); return List.of();
} }
} }
/** /**
* List를 JSON 문자열로 변환 * List를 JSON 문자열로 변환
*/ */
private String parseListToJson(List<String> list) { private String parseListToJson(List<String> list) {
if (list == null || list.isEmpty()) { if (list == null || list.isEmpty()) {
return "[]"; return "[]";
} }
try { try {
return objectMapper.writeValueAsString(list); return objectMapper.writeValueAsString(list);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
log.warn("JSON 직렬화 실패: {}", list, e); log.warn("JSON 직렬화 실패: {}", list, e);
return "[]"; return "[]";
} }
} }
} }

View File

@ -1,72 +1,72 @@
package com.ktds.hi.analytics.infra.gateway.entity; package com.ktds.hi.analytics.infra.gateway.entity;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
* AI 피드백 엔티티 * AI 피드백 엔티티
* AI가 생성한 피드백 정보를 저장 * AI가 생성한 피드백 정보를 저장
*/ */
@Entity @Entity
@Table(name = "ai_feedback", @Table(name = "ai_feedback",
indexes = { indexes = {
@Index(name = "idx_ai_feedback_store_id", columnList = "store_id"), @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_generated_at", columnList = "generated_at"),
@Index(name = "idx_ai_feedback_created_at", columnList = "created_at"), @Index(name = "idx_ai_feedback_created_at", columnList = "created_at"),
@Index(name = "idx_ai_feedback_confidence_score", columnList = "confidence_score") @Index(name = "idx_ai_feedback_confidence_score", columnList = "confidence_score")
}) })
@Getter @Getter
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@EntityListeners(AuditingEntityListener.class) @EntityListeners(AuditingEntityListener.class)
public class AiFeedbackEntity { public class AiFeedbackEntity {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Column(name = "store_id", nullable = false) @Column(name = "store_id", nullable = false)
private Long storeId; private Long storeId;
@Column(name = "summary", length = 1000) @Column(name = "summary", length = 1000)
private String summary; private String summary;
@Column(name = "positive_points", columnDefinition = "TEXT") @Column(name = "positive_points", columnDefinition = "TEXT")
private String positivePointsJson; private String positivePointsJson;
@Column(name = "negative_points", columnDefinition = "TEXT") @Column(name = "negative_points", columnDefinition = "TEXT")
private String negativePointsJson; private String negativePointsJson;
@Column(name = "improvement_points", columnDefinition = "TEXT") @Column(name = "improvement_points", columnDefinition = "TEXT")
private String improvementPointsJson; private String improvementPointsJson;
@Column(name = "recommendations", columnDefinition = "TEXT") @Column(name = "recommendations", columnDefinition = "TEXT")
private String recommendationsJson; private String recommendationsJson;
@Column(name = "sentiment_analysis", length = 500) @Column(name = "sentiment_analysis", length = 500)
private String sentimentAnalysis; private String sentimentAnalysis;
@Column(name = "confidence_score") @Column(name = "confidence_score")
private Double confidenceScore; private Double confidenceScore;
@Column(name = "generated_at") @Column(name = "generated_at")
private LocalDateTime generatedAt; private LocalDateTime generatedAt;
@CreatedDate @CreatedDate
@Column(name = "created_at", nullable = false, updatable = false) @Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt; private LocalDateTime createdAt;
@LastModifiedDate @LastModifiedDate
@Column(name = "updated_at") @Column(name = "updated_at")
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
} }

View File

@ -1,33 +1,33 @@
package com.ktds.hi.review.infra.config; package com.ktds.hi.review.infra.config;
import com.azure.messaging.eventhubs.EventHubClientBuilder; import com.azure.messaging.eventhubs.EventHubClientBuilder;
import com.azure.messaging.eventhubs.EventHubConsumerClient; import com.azure.messaging.eventhubs.EventHubConsumerClient;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
/** /**
* Azure Event Hub 설정 클래스 (단일 EntityPath 포함된 connection string 사용) * Azure Event Hub 설정 클래스 (단일 EntityPath 포함된 connection string 사용)
*/ */
@Slf4j @Slf4j
@Configuration @Configuration
public class EventHubConfig { public class EventHubConfig {
@Value("${azure.eventhub.connection-string}") @Value("${azure.eventhub.connection-string}")
private String connectionString; private String connectionString;
@Value("${azure.eventhub.consumer-group:$Default}") @Value("${azure.eventhub.consumer-group:$Default}")
private String consumerGroup; private String consumerGroup;
/** /**
* 외부 리뷰 이벤트 수신용 Consumer * 외부 리뷰 이벤트 수신용 Consumer
*/ */
@Bean("externalReviewEventConsumer") @Bean("externalReviewEventConsumer")
public EventHubConsumerClient externalReviewEventConsumer() { public EventHubConsumerClient externalReviewEventConsumer() {
return new EventHubClientBuilder() return new EventHubClientBuilder()
.connectionString(connectionString) .connectionString(connectionString)
.consumerGroup(consumerGroup) .consumerGroup(consumerGroup)
.buildConsumerClient(); .buildConsumerClient();
} }
} }

View File

@ -1,74 +1,74 @@
package com.ktds.hi.review.infra.gateway.entity; package com.ktds.hi.review.infra.gateway.entity;
import com.ktds.hi.review.biz.domain.ReviewStatus; import com.ktds.hi.review.biz.domain.ReviewStatus;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/** /**
* 리뷰 엔티티 클래스 * 리뷰 엔티티 클래스
* 데이터베이스 reviews 테이블과 매핑되는 JPA 엔티티 * 데이터베이스 reviews 테이블과 매핑되는 JPA 엔티티
*/ */
@Entity @Entity
@Table(name = "reviews") @Table(name = "reviews")
@Getter @Getter
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@EntityListeners(AuditingEntityListener.class) @EntityListeners(AuditingEntityListener.class)
public class ReviewEntity { public class ReviewEntity {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Column(name = "store_id", nullable = false) @Column(name = "store_id", nullable = false)
private Long storeId; private Long storeId;
@Column(name = "member_id", nullable = true) @Column(name = "member_id", nullable = true)
private Long memberId; private Long memberId;
@Column(name = "member_nickname", nullable = false, length = 50) @Column(name = "member_nickname", nullable = false, length = 50)
private String memberNickname; private String memberNickname;
@Column(nullable = false) @Column(nullable = false)
private Integer rating; private Integer rating;
@Column(nullable = false, length = 1000) @Column(nullable = false, length = 1000)
private String content; private String content;
@ElementCollection @ElementCollection
@CollectionTable(name = "review_images", @CollectionTable(name = "review_images",
joinColumns = @JoinColumn(name = "review_id")) joinColumns = @JoinColumn(name = "review_id"))
@Column(name = "image_url") @Column(name = "image_url")
private List<String> imageUrls; private List<String> imageUrls;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(nullable = false) @Column(nullable = false)
@Builder.Default @Builder.Default
private ReviewStatus status = ReviewStatus.ACTIVE; private ReviewStatus status = ReviewStatus.ACTIVE;
@Column(name = "like_count") @Column(name = "like_count")
@Builder.Default @Builder.Default
private Integer likeCount = 0; private Integer likeCount = 0;
@Column(name = "dislike_count") @Column(name = "dislike_count")
@Builder.Default @Builder.Default
private Integer dislikeCount = 0; private Integer dislikeCount = 0;
@CreatedDate @CreatedDate
@Column(name = "created_at", updatable = false) @Column(name = "created_at", updatable = false)
private LocalDateTime createdAt; private LocalDateTime createdAt;
@LastModifiedDate @LastModifiedDate
@Column(name = "updated_at") @Column(name = "updated_at")
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
} }

View File

@ -1,48 +1,48 @@
server: server:
port: ${REVIEW_SERVICE_PORT:8083} port: ${REVIEW_SERVICE_PORT:8083}
spring: spring:
application: application:
name: review-service name: review-service
datasource: datasource:
url: ${REVIEW_DB_URL:jdbc:postgresql://20.214.91.15:5432/hiorder_review} url: ${REVIEW_DB_URL:jdbc:postgresql://20.214.91.15:5432/hiorder_review}
username: ${REVIEW_DB_USERNAME:hiorder_user} username: ${REVIEW_DB_USERNAME:hiorder_user}
password: ${REVIEW_DB_PASSWORD:hiorder_pass} password: ${REVIEW_DB_PASSWORD:hiorder_pass}
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
jpa: jpa:
hibernate: hibernate:
ddl-auto: ${JPA_DDL_AUTO:update} ddl-auto: ${JPA_DDL_AUTO:update}
show-sql: ${JPA_SHOW_SQL:false} show-sql: ${JPA_SHOW_SQL:false}
properties: properties:
hibernate: hibernate:
format_sql: true format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect dialect: org.hibernate.dialect.PostgreSQLDialect
data: data:
redis: redis:
host: ${REDIS_HOST:localhost} host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379} port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:} password: ${REDIS_PASSWORD:}
servlet: servlet:
multipart: multipart:
max-file-size: ${MAX_FILE_SIZE:10MB} max-file-size: ${MAX_FILE_SIZE:10MB}
max-request-size: ${MAX_REQUEST_SIZE:50MB} max-request-size: ${MAX_REQUEST_SIZE:50MB}
azure: azure:
eventhub: eventhub:
connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING} connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING}
consumer-group: $Default consumer-group: $Default
file-storage: file-storage:
base-path: ${FILE_STORAGE_PATH:/var/hiorder/uploads} base-path: ${FILE_STORAGE_PATH:/var/hiorder/uploads}
allowed-extensions: jpg,jpeg,png,gif,webp allowed-extensions: jpg,jpeg,png,gif,webp
max-file-size: 10485760 # 10MB max-file-size: 10485760 # 10MB
springdoc: springdoc:
api-docs: api-docs:
path: /docs/review/api-docs path: /docs/review/api-docs
swagger-ui: swagger-ui:
enabled: true enabled: true
path: /docs/review/swagger-ui.html path: /docs/review/swagger-ui.html

View File

@ -87,6 +87,7 @@ public class StoreService implements StoreUseCase {
.rating(store.getRating()) .rating(store.getRating())
.reviewCount(store.getReviewCount()) .reviewCount(store.getReviewCount())
.status("운영중") .status("운영중")
.imageUrl(store.getImageUrl())
.operatingHours(store.getOperatingHours()) .operatingHours(store.getOperatingHours())
.build()) .build())
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -129,7 +130,7 @@ public class StoreService implements StoreUseCase {
.orElseThrow(() -> new BusinessException("STORE_ACCESS_DENIED", "매장에 대한 권한이 없습니다.")); .orElseThrow(() -> new BusinessException("STORE_ACCESS_DENIED", "매장에 대한 권한이 없습니다."));
store.updateInfo(request.getStoreName(), request.getAddress(), request.getDescription(), store.updateInfo(request.getStoreName(), request.getAddress(), request.getDescription(),
request.getPhone(), request.getOperatingHours()); request.getPhone(), request.getOperatingHours(), request.getImageUrl());
storeJpaRepository.save(store); storeJpaRepository.save(store);

View File

@ -39,4 +39,6 @@ public class MyStoreListResponse {
@Schema(description = "운영시간", example = "월-금 09:00-21:00") @Schema(description = "운영시간", example = "월-금 09:00-21:00")
private String operatingHours; private String operatingHours;
@Schema(description = "매장 이미지")
private String imageUrl;
} }

View File

@ -38,4 +38,7 @@ public class StoreUpdateRequest {
@Schema(description = "매장 태그 목록", example = "[\"맛집\", \"혼밥\", \"가성비\"]") @Schema(description = "매장 태그 목록", example = "[\"맛집\", \"혼밥\", \"가성비\"]")
private List<String> tags; private List<String> tags;
@Schema(description = "매장 이미지")
private String imageUrl;
} }

View File

@ -132,13 +132,14 @@ public class StoreEntity {
* 매장 기본 정보 업데이트 * 매장 기본 정보 업데이트
*/ */
public void updateInfo(String storeName, String address, String description, public void updateInfo(String storeName, String address, String description,
String phone, String operatingHours) { String phone, String operatingHours, String imageUrl) {
this.storeName = storeName; this.storeName = storeName;
this.address = address; this.address = address;
this.description = description; this.description = description;
this.phone = phone; this.phone = phone;
this.operatingHours = operatingHours; this.operatingHours = operatingHours;
this.updatedAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now();
this.imageUrl = imageUrl;
} }
/** /**

View File

@ -1,60 +1,60 @@
server: server:
port: ${STORE_SERVICE_PORT:8082} port: ${STORE_SERVICE_PORT:8082}
spring: spring:
application: application:
name: store-service name: store-service
datasource: datasource:
url: ${STORE_DB_URL:jdbc:postgresql://20.249.154.116:5432/hiorder_store} url: ${STORE_DB_URL:jdbc:postgresql://20.249.154.116:5432/hiorder_store}
username: ${STORE_DB_USERNAME:hiorder_user} username: ${STORE_DB_USERNAME:hiorder_user}
password: ${STORE_DB_PASSWORD:hiorder_pass} password: ${STORE_DB_PASSWORD:hiorder_pass}
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
jpa: jpa:
hibernate: hibernate:
ddl-auto: ${JPA_DDL_AUTO:update} ddl-auto: ${JPA_DDL_AUTO:update}
show-sql: ${JPA_SHOW_SQL:false} show-sql: ${JPA_SHOW_SQL:false}
properties: properties:
hibernate: hibernate:
format_sql: true format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect dialect: org.hibernate.dialect.PostgreSQLDialect
# Azure Event Hub 설정 (추가) # Azure Event Hub 설정 (추가)
azure: azure:
eventhub: eventhub:
connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING} connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING}
data: data:
redis: redis:
host: ${REDIS_HOST:localhost} host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379} port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:} password: ${REDIS_PASSWORD:}
timeout: 2000ms timeout: 2000ms
lettuce: lettuce:
pool: pool:
max-active: 8 max-active: 8
max-wait: -1ms max-wait: -1ms
max-idle: 8 max-idle: 8
min-idle: 0 min-idle: 0
external-api: external-api:
naver: naver:
client-id: ${NAVER_CLIENT_ID:} client-id: ${NAVER_CLIENT_ID:}
client-secret: ${NAVER_CLIENT_SECRET:} client-secret: ${NAVER_CLIENT_SECRET:}
base-url: https://openapi.naver.com base-url: https://openapi.naver.com
kakao: kakao:
api-key: ${KAKAO_API_KEY:} api-key: ${KAKAO_API_KEY:}
base-url: http://kakao-review-api-service.ai-review-ns.svc.cluster.local base-url: http://kakao-review-api-service.ai-review-ns.svc.cluster.local
google: google:
api-key: ${GOOGLE_API_KEY:} api-key: ${GOOGLE_API_KEY:}
base-url: https://maps.googleapis.com base-url: https://maps.googleapis.com
hiorder: hiorder:
api-key: ${HIORDER_API_KEY:} api-key: ${HIORDER_API_KEY:}
base-url: ${HIORDER_BASE_URL:https://api.hiorder.com} base-url: ${HIORDER_BASE_URL:https://api.hiorder.com}
springdoc: springdoc:
api-docs: api-docs:
path: /docs/store/api-docs path: /docs/store/api-docs
swagger-ui: swagger-ui:
path: /docs/store/swagger-ui.html path: /docs/store/swagger-ui.html