Merge branch 'main' of https://github.com/dg04-hi/hi-backend
This commit is contained in:
commit
1daa087bda
@ -21,6 +21,7 @@ public class ActionPlan {
|
|||||||
private Long id;
|
private Long id;
|
||||||
private Long storeId;
|
private Long storeId;
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
private Long feedbackId;
|
||||||
private String title;
|
private String title;
|
||||||
private String description;
|
private String description;
|
||||||
private String period;
|
private String period;
|
||||||
|
|||||||
@ -41,6 +41,7 @@ public class ActionPlanService implements ActionPlanUseCase {
|
|||||||
.map(plan -> ActionPlanListResponse.builder()
|
.map(plan -> ActionPlanListResponse.builder()
|
||||||
.id(plan.getId())
|
.id(plan.getId())
|
||||||
.title(plan.getTitle())
|
.title(plan.getTitle())
|
||||||
|
.description(plan.getDescription())
|
||||||
.status(plan.getStatus())
|
.status(plan.getStatus())
|
||||||
.period(plan.getPeriod())
|
.period(plan.getPeriod())
|
||||||
.createdAt(plan.getCreatedAt())
|
.createdAt(plan.getCreatedAt())
|
||||||
|
|||||||
@ -116,6 +116,7 @@ public class AnalyticsService implements AnalyticsUseCase {
|
|||||||
aiFeedback = Optional.of(generateAIFeedback(storeId));
|
aiFeedback = Optional.of(generateAIFeedback(storeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 3. 응답 생성
|
// 3. 응답 생성
|
||||||
AiFeedbackDetailResponse response = AiFeedbackDetailResponse.builder()
|
AiFeedbackDetailResponse response = AiFeedbackDetailResponse.builder()
|
||||||
.feedbackId(aiFeedback.get().getId())
|
.feedbackId(aiFeedback.get().getId())
|
||||||
@ -129,6 +130,11 @@ public class AnalyticsService implements AnalyticsUseCase {
|
|||||||
.generatedAt(aiFeedback.get().getGeneratedAt())
|
.generatedAt(aiFeedback.get().getGeneratedAt())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
//(추가) 실행계획을 조회해서, 이미 생성된 improvementPoints인지 판단
|
||||||
|
List<String> actionPlanTitleList = actionPlanPort.findActionPlanTitleByFeedbackId(aiFeedback.get().getId());
|
||||||
|
log.info("실행계획 확인 => {}", actionPlanTitleList.toString());
|
||||||
|
response.updateImprovementCheck(actionPlanTitleList); //이미 생성된 실행계획 추가.
|
||||||
|
|
||||||
log.info("AI 피드백 상세 조회 완료: storeId={}", storeId);
|
log.info("AI 피드백 상세 조회 완료: storeId={}", storeId);
|
||||||
return response;
|
return response;
|
||||||
|
|
||||||
@ -444,9 +450,10 @@ public class AnalyticsService implements AnalyticsUseCase {
|
|||||||
|
|
||||||
// 2. 실행계획 생성 (요청 시)
|
// 2. 실행계획 생성 (요청 시)
|
||||||
List<String> actionPlans = null;
|
List<String> actionPlans = null;
|
||||||
if (Boolean.TRUE.equals(request.getGenerateActionPlan())) {
|
//TODO : 추후에 AI 분석후에 바로 실행계획까지 생성해야 한다면 추가.
|
||||||
actionPlans = aiServicePort.generateActionPlan(aiFeedback);
|
// if (Boolean.TRUE.equals(request.getGenerateActionPlan())) {
|
||||||
}
|
// actionPlans = aiServicePort.generateActionPlan(aiFeedback);
|
||||||
|
// }
|
||||||
|
|
||||||
// 3. 응답 생성
|
// 3. 응답 생성
|
||||||
AiAnalysisResponse response = AiAnalysisResponse.builder()
|
AiAnalysisResponse response = AiAnalysisResponse.builder()
|
||||||
@ -459,7 +466,7 @@ public class AnalyticsService implements AnalyticsUseCase {
|
|||||||
.sentimentAnalysis(aiFeedback.getSentimentAnalysis())
|
.sentimentAnalysis(aiFeedback.getSentimentAnalysis())
|
||||||
.confidenceScore(aiFeedback.getConfidenceScore())
|
.confidenceScore(aiFeedback.getConfidenceScore())
|
||||||
.totalReviewsAnalyzed(getTotalReviewsCount(storeId, request.getDays()))
|
.totalReviewsAnalyzed(getTotalReviewsCount(storeId, request.getDays()))
|
||||||
.actionPlans(actionPlans)
|
.actionPlans(actionPlans) //TODO : 사용하는 값은 아니지만 의존성을 위해 그대로 둠, 추후에 변경 필요.
|
||||||
.analyzedAt(aiFeedback.getGeneratedAt())
|
.analyzedAt(aiFeedback.getGeneratedAt())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -474,7 +481,7 @@ public class AnalyticsService implements AnalyticsUseCase {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public List<String> generateActionPlansFromFeedback(Long feedbackId) {
|
public List<String> generateActionPlansFromFeedback(ActionPlanCreateRequest request, Long feedbackId) {
|
||||||
log.info("실행계획 생성: feedbackId={}", feedbackId);
|
log.info("실행계획 생성: feedbackId={}", feedbackId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -487,11 +494,11 @@ public class AnalyticsService implements AnalyticsUseCase {
|
|||||||
|
|
||||||
AiFeedback feedback = aiFeedback.get();
|
AiFeedback feedback = aiFeedback.get();
|
||||||
// 2. 기존 AIServicePort.generateActionPlan 메서드 활용
|
// 2. 기존 AIServicePort.generateActionPlan 메서드 활용
|
||||||
List<String> actionPlans = aiServicePort.generateActionPlan(aiFeedback.get());
|
List<String> actionPlans = aiServicePort.generateActionPlan(request.getActionPlanSelect(), aiFeedback.get());
|
||||||
|
|
||||||
|
|
||||||
// 3. DB에 실행계획 저장
|
// 3. DB에 실행계획 저장
|
||||||
saveGeneratedActionPlansToDatabase(feedback, actionPlans);
|
saveGeneratedActionPlansToDatabase(request.getActionPlanSelect(), feedback, actionPlans);
|
||||||
|
|
||||||
log.info("실행계획 생성 완료: feedbackId={}, planCount={}", feedbackId, actionPlans.size());
|
log.info("실행계획 생성 완료: feedbackId={}, planCount={}", feedbackId, actionPlans.size());
|
||||||
return actionPlans;
|
return actionPlans;
|
||||||
@ -578,7 +585,7 @@ public class AnalyticsService implements AnalyticsUseCase {
|
|||||||
* 생성된 실행계획을 데이터베이스에 저장하는 메서드
|
* 생성된 실행계획을 데이터베이스에 저장하는 메서드
|
||||||
* AI 피드백 기반으로 생성된 실행계획들을 ActionPlan 테이블에 저장
|
* AI 피드백 기반으로 생성된 실행계획들을 ActionPlan 테이블에 저장
|
||||||
*/
|
*/
|
||||||
private void saveGeneratedActionPlansToDatabase(AiFeedback feedback, List<String> actionPlans) {
|
private void saveGeneratedActionPlansToDatabase(List<String> actionPlanSelect, AiFeedback feedback, List<String> actionPlans) {
|
||||||
if (actionPlans.isEmpty()) {
|
if (actionPlans.isEmpty()) {
|
||||||
log.info("저장할 실행계획이 없습니다: storeId={}", feedback.getStoreId());
|
log.info("저장할 실행계획이 없습니다: storeId={}", feedback.getStoreId());
|
||||||
return;
|
return;
|
||||||
@ -593,8 +600,9 @@ public class AnalyticsService implements AnalyticsUseCase {
|
|||||||
// ActionPlan 도메인 객체 생성 (기존 ActionPlanService의 패턴과 동일하게)
|
// ActionPlan 도메인 객체 생성 (기존 ActionPlanService의 패턴과 동일하게)
|
||||||
ActionPlan actionPlan = ActionPlan.builder()
|
ActionPlan actionPlan = ActionPlan.builder()
|
||||||
.storeId(feedback.getStoreId())
|
.storeId(feedback.getStoreId())
|
||||||
.userId(1L) // AI가 생성한 계획이므로 userId는 null
|
.userId(0L) // AI가 생성한 계획이므로 userId는 0
|
||||||
.title("AI 추천 실행계획 " + (i + 1))
|
.feedbackId(feedback.getId())
|
||||||
|
.title(actionPlanSelect.get(i))
|
||||||
.description(planContent)
|
.description(planContent)
|
||||||
.period("1개월") // 기본 실행 기간
|
.period("1개월") // 기본 실행 기간
|
||||||
.status(PlanStatus.PLANNED)
|
.status(PlanStatus.PLANNED)
|
||||||
|
|||||||
@ -44,6 +44,6 @@ public interface AnalyticsUseCase {
|
|||||||
/**
|
/**
|
||||||
* AI 피드백 기반 실행계획 생성
|
* AI 피드백 기반 실행계획 생성
|
||||||
*/
|
*/
|
||||||
List<String> generateActionPlansFromFeedback(Long feedbackId);
|
List<String> generateActionPlansFromFeedback(ActionPlanCreateRequest request,Long feedbackId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,5 +24,5 @@ public interface AIServicePort {
|
|||||||
/**
|
/**
|
||||||
* 실행 계획 생성
|
* 실행 계획 생성
|
||||||
*/
|
*/
|
||||||
List<String> generateActionPlan(AiFeedback feedback);
|
List<String> generateActionPlan(List<String> actionPlanSelect, AiFeedback feedback);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,4 +30,9 @@ public interface ActionPlanPort {
|
|||||||
* 실행 계획 삭제
|
* 실행 계획 삭제
|
||||||
*/
|
*/
|
||||||
void deleteActionPlan(Long planId);
|
void deleteActionPlan(Long planId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 피드백 id로 실행계획 title 조회
|
||||||
|
*/
|
||||||
|
List<String> findActionPlanTitleByFeedbackId(Long feedbackId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -145,14 +145,17 @@ public class AnalyticsController {
|
|||||||
*/
|
*/
|
||||||
@Operation(summary = "실행계획 생성", description = "AI 피드백을 기반으로 실행계획을 생성합니다.")
|
@Operation(summary = "실행계획 생성", description = "AI 피드백을 기반으로 실행계획을 생성합니다.")
|
||||||
@PostMapping("/ai-feedback/{feedbackId}/action-plans")
|
@PostMapping("/ai-feedback/{feedbackId}/action-plans")
|
||||||
public ResponseEntity<SuccessResponse<List<String>>> 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) {
|
||||||
|
|
||||||
log.info("실행계획 생성 요청: feedbackId={}", feedbackId);
|
log.info("실행계획 생성 요청: feedbackId={}", feedbackId);
|
||||||
|
|
||||||
List<String> actionPlans = analyticsUseCase.generateActionPlansFromFeedback(feedbackId);
|
log.info("실행계획 바디 데이터 => {}", request);
|
||||||
|
|
||||||
return ResponseEntity.ok(SuccessResponse.of(actionPlans, "실행계획 생성 완료"));
|
List<String> actionPlans = analyticsUseCase.generateActionPlansFromFeedback(request,feedbackId);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(SuccessResponse.of("실행계획 생성 완료"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.ktds.hi.analytics.infra.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실행계획 생성요청
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@ToString
|
||||||
|
public class ActionPlanCreateRequest {
|
||||||
|
|
||||||
|
private List<String> actionPlanSelect;
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ public class ActionPlanListResponse {
|
|||||||
private Long id;
|
private Long id;
|
||||||
private String title;
|
private String title;
|
||||||
private PlanStatus status;
|
private PlanStatus status;
|
||||||
|
private String description;
|
||||||
private String period;
|
private String period;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime completedAt;
|
private LocalDateTime completedAt;
|
||||||
|
|||||||
@ -7,6 +7,8 @@ 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.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 피드백 상세 응답 DTO
|
* AI 피드백 상세 응답 DTO
|
||||||
@ -22,8 +24,23 @@ public class AiFeedbackDetailResponse {
|
|||||||
private String summary;
|
private String summary;
|
||||||
private List<String> positivePoints;
|
private List<String> positivePoints;
|
||||||
private List<String> improvementPoints;
|
private List<String> improvementPoints;
|
||||||
|
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){
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.ktds.hi.analytics.infra.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class OrderListResponse {
|
||||||
|
private Long storeId;
|
||||||
|
private Integer totalCount;
|
||||||
|
private List<OrderResponse> orders;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.ktds.hi.analytics.infra.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class OrderResponse {
|
||||||
|
private Long id;
|
||||||
|
private Long storeId;
|
||||||
|
private Long menuId;
|
||||||
|
private Integer customerAge;
|
||||||
|
private String customerGender;
|
||||||
|
private BigDecimal orderAmount;
|
||||||
|
private LocalDateTime orderDate;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@ -125,25 +125,30 @@ public class AIServiceAdapter implements AIServicePort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> generateActionPlan(AiFeedback feedback) {
|
public List<String> generateActionPlan(List<String> actionPlanSelect, AiFeedback feedback) {
|
||||||
log.info("OpenAI 실행 계획 생성 시작");
|
log.info("OpenAI 실행 계획 생성 시작");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
StringBuffer planFormat = new StringBuffer();
|
||||||
|
for(int i = 1; i <= actionPlanSelect.size(); i++) {
|
||||||
|
planFormat.append(i).append(" [구체적인 실행 계획 ").append(i).append("]\n");
|
||||||
|
}
|
||||||
String prompt = String.format(
|
String prompt = String.format(
|
||||||
"""
|
"""
|
||||||
다음 AI 피드백을 바탕으로 구체적인 실행 계획 3개를 생성해주세요.
|
다음 AI 피드백을 바탕으로 구체적인 실행 계획 %s개를 생성해주세요.
|
||||||
각 계획은 실행 가능하고 구체적이어야 합니다.
|
각 계획은 실행 가능하고 구체적이어야 합니다.
|
||||||
|
|
||||||
요약: %s
|
요약: %s
|
||||||
개선점: %s
|
개선점: %s
|
||||||
|
|
||||||
|
실행계획 내용은 점주가 반영할 수 있도록 구체적어야 합니다.
|
||||||
실행 계획을 다음 형식으로 작성해주세요:
|
실행 계획을 다음 형식으로 작성해주세요:
|
||||||
1. [구체적인 실행 계획 1]
|
%s
|
||||||
2. [구체적인 실행 계획 2]
|
|
||||||
3. [구체적인 실행 계획 3]
|
|
||||||
""",
|
""",
|
||||||
|
actionPlanSelect.size(),
|
||||||
feedback.getSummary(),
|
feedback.getSummary(),
|
||||||
String.join(", ", feedback.getImprovementPoints())
|
String.join(", ", actionPlanSelect),
|
||||||
|
planFormat
|
||||||
);
|
);
|
||||||
|
|
||||||
String result = callOpenAI(prompt);
|
String result = callOpenAI(prompt);
|
||||||
@ -151,10 +156,9 @@ public class AIServiceAdapter implements AIServicePort {
|
|||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("OpenAI 실행 계획 생성 중 오류 발생", e);
|
log.error("OpenAI 실행 계획 생성 중 오류 발생", e);
|
||||||
|
//TODO : 시연을 위해서 우선은 아래과 같이 처리, 추후에는 실행계획이 실패했다면 Runtime계열의 예외를 던지는게 좋을듯.
|
||||||
return Arrays.asList(
|
return Arrays.asList(
|
||||||
"서비스 품질 개선을 위한 직원 교육 실시",
|
"실행계획 생성 실패. 재생성 필요"
|
||||||
"고객 피드백 수집 체계 구축",
|
|
||||||
"매장 운영 프로세스 개선"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,6 +53,11 @@ public class ActionPlanRepositoryAdapter implements ActionPlanPort {
|
|||||||
actionPlanJpaRepository.deleteById(planId);
|
actionPlanJpaRepository.deleteById(planId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> findActionPlanTitleByFeedbackId(Long feedbackId) {
|
||||||
|
return actionPlanJpaRepository.findActionPlanTitleByFeedbackId(feedbackId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity를 Domain으로 변환
|
* Entity를 Domain으로 변환
|
||||||
*/
|
*/
|
||||||
@ -79,6 +84,7 @@ public class ActionPlanRepositoryAdapter implements ActionPlanPort {
|
|||||||
private ActionPlanEntity toEntity(ActionPlan domain) {
|
private ActionPlanEntity toEntity(ActionPlan domain) {
|
||||||
return ActionPlanEntity.builder()
|
return ActionPlanEntity.builder()
|
||||||
.id(domain.getId())
|
.id(domain.getId())
|
||||||
|
.feedbackId(domain.getFeedbackId())
|
||||||
.storeId(domain.getStoreId())
|
.storeId(domain.getStoreId())
|
||||||
.userId(domain.getUserId())
|
.userId(domain.getUserId())
|
||||||
.title(domain.getTitle())
|
.title(domain.getTitle())
|
||||||
|
|||||||
@ -2,16 +2,27 @@ package com.ktds.hi.analytics.infra.gateway;
|
|||||||
|
|
||||||
import com.ktds.hi.analytics.biz.domain.OrderStatistics;
|
import com.ktds.hi.analytics.biz.domain.OrderStatistics;
|
||||||
import com.ktds.hi.analytics.biz.usecase.out.OrderDataPort;
|
import com.ktds.hi.analytics.biz.usecase.out.OrderDataPort;
|
||||||
|
import com.ktds.hi.analytics.infra.dto.OrderListResponse;
|
||||||
|
import com.ktds.hi.analytics.infra.dto.OrderResponse;
|
||||||
|
import com.ktds.hi.common.dto.SuccessResponse;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
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.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 주문 데이터 어댑터 클래스
|
* 주문 데이터 어댑터 클래스
|
||||||
@ -32,14 +43,32 @@ public class OrderDataAdapter implements OrderDataPort {
|
|||||||
log.info("주문 통계 조회: storeId={}, period={} ~ {}", storeId, startDate, endDate);
|
log.info("주문 통계 조회: storeId={}, period={} ~ {}", storeId, startDate, endDate);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String url = String.format("%s/api/orders/stores/%d/statistics?startDate=%s&endDate=%s",
|
|
||||||
storeServiceUrl, storeId, startDate, endDate);
|
|
||||||
|
|
||||||
OrderStatistics statistics = restTemplate.getForObject(url, OrderStatistics.class);
|
// LocalDate를 LocalDateTime으로 변환 (시작일은 00:00:00, 종료일은 23:59:59)
|
||||||
|
LocalDateTime startDateTime = startDate.atStartOfDay();
|
||||||
|
LocalDateTime endDateTime = endDate.atTime(23, 59, 59);
|
||||||
|
|
||||||
|
String url = String.format("%s/api/stores/orders/store/%d/period?startDate=%s&endDate=%s",
|
||||||
|
storeServiceUrl, storeId, startDateTime, endDateTime);
|
||||||
|
|
||||||
|
|
||||||
|
// ParameterizedTypeReference를 사용하여 제네릭 타입 안전하게 파싱
|
||||||
|
ParameterizedTypeReference<SuccessResponse<OrderListResponse>> responseType =
|
||||||
|
new ParameterizedTypeReference<SuccessResponse<OrderListResponse>>() {};
|
||||||
|
|
||||||
|
ResponseEntity<SuccessResponse<OrderListResponse>> responseEntity =
|
||||||
|
restTemplate.exchange(url, HttpMethod.GET, null, responseType);
|
||||||
|
|
||||||
|
SuccessResponse<OrderListResponse> successResponse = responseEntity.getBody();
|
||||||
|
|
||||||
|
if (successResponse != null && successResponse.isSuccess() && successResponse.getData() != null) {
|
||||||
|
OrderListResponse orderListResponse = successResponse.getData();
|
||||||
|
|
||||||
|
// OrderListResponse를 OrderStatistics로 변환
|
||||||
|
OrderStatistics statistics = convertToOrderStatistics(orderListResponse);
|
||||||
|
|
||||||
if (statistics != null) {
|
|
||||||
log.info("주문 통계 조회 완료: storeId={}, totalOrders={}",
|
log.info("주문 통계 조회 완료: storeId={}, totalOrders={}",
|
||||||
storeId, statistics.getTotalOrders());
|
storeId, statistics.getTotalOrders());
|
||||||
return statistics;
|
return statistics;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +80,125 @@ public class OrderDataAdapter implements OrderDataPort {
|
|||||||
return createDummyOrderStatistics(storeId);
|
return createDummyOrderStatistics(storeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OrderListResponse를 OrderStatistics로 변환
|
||||||
|
*/
|
||||||
|
private OrderStatistics convertToOrderStatistics(OrderListResponse orderListResponse) {
|
||||||
|
List<OrderResponse> orders = orderListResponse.getOrders();
|
||||||
|
|
||||||
|
if (orders == null || orders.isEmpty()) {
|
||||||
|
return createEmptyOrderStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 총 주문 수
|
||||||
|
int totalOrders = orders.size();
|
||||||
|
|
||||||
|
// 총 매출 계산
|
||||||
|
BigDecimal totalRevenue = orders.stream()
|
||||||
|
.map(OrderResponse::getOrderAmount)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
// 평균 주문 금액 계산
|
||||||
|
double averageOrderValue = totalRevenue.doubleValue() / totalOrders;
|
||||||
|
|
||||||
|
// 시간대별 주문 분석 (피크 시간 계산)
|
||||||
|
Map<Integer, Long> hourlyOrderCount = orders.stream()
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
order -> order.getOrderDate().getHour(),
|
||||||
|
Collectors.counting()
|
||||||
|
));
|
||||||
|
|
||||||
|
int peakHour = hourlyOrderCount.entrySet().stream()
|
||||||
|
.max(Map.Entry.comparingByValue())
|
||||||
|
.map(Map.Entry::getKey)
|
||||||
|
.orElse(12); // 기본값 12시
|
||||||
|
|
||||||
|
// 연령대별 분포 계산
|
||||||
|
Map<String, Integer> ageDistribution = calculateCustomerAgeDistribution(orders);
|
||||||
|
|
||||||
|
// 인기 메뉴 계산 (메뉴ID별 주문 횟수) - 실제로는 메뉴명을 가져와야 하지만 임시로 메뉴ID 사용
|
||||||
|
List<String> popularMenus = orders.stream()
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
OrderResponse::getMenuId,
|
||||||
|
Collectors.counting()
|
||||||
|
))
|
||||||
|
.entrySet().stream()
|
||||||
|
.sorted(Map.Entry.<Long, Long>comparingByValue().reversed())
|
||||||
|
.limit(4)
|
||||||
|
.map(entry -> "메뉴" + entry.getKey()) // 실제로는 메뉴명으로 변환 필요
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return OrderStatistics.builder()
|
||||||
|
.totalOrders(totalOrders)
|
||||||
|
.totalRevenue(totalRevenue.longValue())
|
||||||
|
.averageOrderValue(averageOrderValue)
|
||||||
|
.peakHour(peakHour)
|
||||||
|
.popularMenus(popularMenus)
|
||||||
|
.customerAgeDistribution(ageDistribution)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 연령대별 분포 계산
|
||||||
|
*/
|
||||||
|
private Map<String, Integer> calculateCustomerAgeDistribution(List<OrderResponse> orders) {
|
||||||
|
Map<String, Long> ageGroupCount = orders.stream()
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
order -> getAgeGroup(order.getCustomerAge()),
|
||||||
|
Collectors.counting()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Long을 Integer로 변환
|
||||||
|
Map<String, Integer> result = new HashMap<>();
|
||||||
|
ageGroupCount.forEach((key, value) -> result.put(key, value.intValue()));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 연령대별 분포 계산
|
||||||
|
*/
|
||||||
|
private Map<String, Integer> calculateAgeDistribution(List<OrderResponse> orders) {
|
||||||
|
Map<String, Long> ageGroupCount = orders.stream()
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
order -> getAgeGroup(order.getCustomerAge()),
|
||||||
|
Collectors.counting()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Long을 Integer로 변환
|
||||||
|
Map<String, Integer> result = new HashMap<>();
|
||||||
|
ageGroupCount.forEach((key, value) -> result.put(key, value.intValue()));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 나이를 연령대 그룹으로 변환
|
||||||
|
*/
|
||||||
|
private String getAgeGroup(Integer age) {
|
||||||
|
if (age == null) return "미분류";
|
||||||
|
if (age < 20) return "10대";
|
||||||
|
if (age < 30) return "20대";
|
||||||
|
if (age < 40) return "30대";
|
||||||
|
if (age < 50) return "40대";
|
||||||
|
if (age < 60) return "50대";
|
||||||
|
return "60대+";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 빈 주문 통계 생성
|
||||||
|
*/
|
||||||
|
private OrderStatistics createEmptyOrderStatistics() {
|
||||||
|
return OrderStatistics.builder()
|
||||||
|
.totalOrders(0)
|
||||||
|
.totalRevenue(0L)
|
||||||
|
.averageOrderValue(0.0)
|
||||||
|
.peakHour(12)
|
||||||
|
.popularMenus(Arrays.asList())
|
||||||
|
.customerAgeDistribution(new HashMap<>())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer getCurrentOrderCount(Long storeId) {
|
public Integer getCurrentOrderCount(Long storeId) {
|
||||||
log.info("실시간 주문 현황 조회: storeId={}", storeId);
|
log.info("실시간 주문 현황 조회: storeId={}", storeId);
|
||||||
|
|||||||
@ -50,6 +50,9 @@ public class ActionPlanEntity {
|
|||||||
@Column(name = "user_id", nullable = false)
|
@Column(name = "user_id", nullable = false)
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
|
@Column(name = "feedback_id", nullable = true)
|
||||||
|
private Long feedbackId;
|
||||||
|
|
||||||
@Column(name = "title", nullable = false, length = 100)
|
@Column(name = "title", nullable = false, length = 100)
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,13 @@ package com.ktds.hi.analytics.infra.gateway.repository;
|
|||||||
import com.ktds.hi.analytics.biz.domain.PlanStatus;
|
import com.ktds.hi.analytics.biz.domain.PlanStatus;
|
||||||
import com.ktds.hi.analytics.infra.gateway.entity.ActionPlanEntity;
|
import com.ktds.hi.analytics.infra.gateway.entity.ActionPlanEntity;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.lettuce.core.dynamic.annotation.Param;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 실행 계획 JPA 리포지토리 인터페이스
|
* 실행 계획 JPA 리포지토리 인터페이스
|
||||||
* 실행 계획 데이터의 CRUD 작업을 담당
|
* 실행 계획 데이터의 CRUD 작업을 담당
|
||||||
@ -33,4 +36,10 @@ public interface ActionPlanJpaRepository extends JpaRepository<ActionPlanEntity,
|
|||||||
* 매장 ID와 사용자 ID로 실행 계획 목록 조회
|
* 매장 ID와 사용자 ID로 실행 계획 목록 조회
|
||||||
*/
|
*/
|
||||||
List<ActionPlanEntity> findByStoreIdAndUserIdOrderByCreatedAtDesc(Long storeId, Long userId);
|
List<ActionPlanEntity> findByStoreIdAndUserIdOrderByCreatedAtDesc(Long storeId, Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 피드백 id로 실행계획 title 조회
|
||||||
|
*/
|
||||||
|
@Query("SELECT a.title FROM ActionPlanEntity a WHERE a.feedbackId = :feedbackId")
|
||||||
|
List<String> findActionPlanTitleByFeedbackId(@Param("feedbackId")Long feedbackId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
// member/src/main/java/com/ktds/hi/member/repository/jpa/TagRepository.java
|
||||||
|
package com.ktds.hi.member.repository.jpa;
|
||||||
|
|
||||||
|
import com.ktds.hi.member.repository.entity.TagEntity;
|
||||||
|
import com.ktds.hi.member.repository.entity.TagCategory;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 태그 정보 JPA 리포지토리 인터페이스
|
||||||
|
* 태그 정보 데이터의 CRUD 작업을 담당
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface TagRepository extends JpaRepository<TagEntity, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 태그명 목록으로 활성 태그 조회
|
||||||
|
*/
|
||||||
|
List<TagEntity> findByTagNameInAndIsActiveTrue(List<String> tagNames);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 활성 태그 전체 조회 (카테고리, 정렬순서별 정렬)
|
||||||
|
*/
|
||||||
|
List<TagEntity> findByIsActiveTrueOrderByTagCategoryAscSortOrderAsc();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카테고리별 활성 태그 조회 (정렬순서별 정렬)
|
||||||
|
*/
|
||||||
|
List<TagEntity> findByTagCategoryAndIsActiveTrueOrderBySortOrderAsc(TagCategory tagCategory);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 태그명으로 태그 조회
|
||||||
|
*/
|
||||||
|
Optional<TagEntity> findByTagNameAndIsActiveTrue(String tagName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카테고리별 태그 개수 조회
|
||||||
|
*/
|
||||||
|
Long countByTagCategoryAndIsActiveTrue(TagCategory tagCategory);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 활성 태그 존재 여부 확인
|
||||||
|
*/
|
||||||
|
boolean existsByTagNameAndIsActiveTrue(String tagName);
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
// member/src/main/java/com/ktds/hi/member/service/PreferenceServiceImpl.java
|
||||||
|
package com.ktds.hi.member.service;
|
||||||
|
|
||||||
|
import com.ktds.hi.member.dto.PreferenceRequest;
|
||||||
|
import com.ktds.hi.member.dto.TasteTagResponse;
|
||||||
|
import com.ktds.hi.member.domain.TagType;
|
||||||
|
import com.ktds.hi.member.repository.entity.PreferenceEntity;
|
||||||
|
import com.ktds.hi.member.repository.entity.TagEntity;
|
||||||
|
import com.ktds.hi.member.repository.entity.TagCategory;
|
||||||
|
import com.ktds.hi.member.repository.jpa.PreferenceRepository;
|
||||||
|
import com.ktds.hi.member.repository.jpa.TagRepository;
|
||||||
|
import com.ktds.hi.common.exception.BusinessException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 취향 관리 서비스 구현체
|
||||||
|
* 취향 정보 등록/수정 및 태그 관리 기능을 구현
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
@Transactional
|
||||||
|
public class PreferenceServiceImpl implements PreferenceService {
|
||||||
|
|
||||||
|
private final PreferenceRepository preferenceRepository;
|
||||||
|
private final TagRepository tagRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void savePreference(Long memberId, PreferenceRequest request) {
|
||||||
|
// 태그 유효성 검증
|
||||||
|
List<TagEntity> existingTags = tagRepository.findByTagNameInAndIsActiveTrue(request.getTags());
|
||||||
|
if (existingTags.size() != request.getTags().size()) {
|
||||||
|
throw new BusinessException("유효하지 않은 태그가 포함되어 있습니다");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존 취향 정보 조회
|
||||||
|
PreferenceEntity preference = preferenceRepository.findByMemberId(memberId)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (preference != null) {
|
||||||
|
// 기존 정보 업데이트
|
||||||
|
preference.updatePreference(request.getTags(), request.getHealthInfo(), request.getSpicyLevel());
|
||||||
|
} else {
|
||||||
|
// 새로운 취향 정보 생성
|
||||||
|
preference = PreferenceEntity.builder()
|
||||||
|
.memberId(memberId)
|
||||||
|
.tags(request.getTags())
|
||||||
|
.healthInfo(request.getHealthInfo())
|
||||||
|
.spicyLevel(request.getSpicyLevel())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
preferenceRepository.save(preference);
|
||||||
|
|
||||||
|
log.info("취향 정보 저장 완료: memberId={}, tags={}", memberId, request.getTags());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<TasteTagResponse> getAvailableTags() {
|
||||||
|
List<TagEntity> tags = tagRepository.findByIsActiveTrueOrderByTagCategoryAscSortOrderAsc();
|
||||||
|
|
||||||
|
return tags.stream()
|
||||||
|
.map(tag -> TasteTagResponse.builder()
|
||||||
|
.id(tag.getId())
|
||||||
|
.tagName(tag.getTagName())
|
||||||
|
.tagType(convertTagCategoryToTagType(tag.getTagCategory()))
|
||||||
|
.description(tag.getTagCategory().getDescription())
|
||||||
|
.build())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<TasteTagResponse> getTagsByType(TagType tagType) {
|
||||||
|
TagCategory tagCategory = convertTagTypeToTagCategory(tagType);
|
||||||
|
List<TagEntity> tags = tagRepository.findByTagCategoryAndIsActiveTrueOrderBySortOrderAsc(tagCategory);
|
||||||
|
|
||||||
|
return tags.stream()
|
||||||
|
.map(tag -> TasteTagResponse.builder()
|
||||||
|
.id(tag.getId())
|
||||||
|
.tagName(tag.getTagName())
|
||||||
|
.tagType(tagType)
|
||||||
|
.description(tag.getTagCategory().getDescription())
|
||||||
|
.build())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TagCategory를 TagType으로 변환 (기존 호환성을 위해)
|
||||||
|
*/
|
||||||
|
private TagType convertTagCategoryToTagType(TagCategory tagCategory) {
|
||||||
|
// TagType enum이 존재한다면 적절한 매핑 로직 구현
|
||||||
|
// 임시로 기본값 반환
|
||||||
|
return TagType.TASTE; // 실제 매핑 로직 필요
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TagType을 TagCategory로 변환 (기존 호환성을 위해)
|
||||||
|
*/
|
||||||
|
private TagCategory convertTagTypeToTagCategory(TagType tagType) {
|
||||||
|
// TagType에 따른 TagCategory 매핑
|
||||||
|
switch (tagType) {
|
||||||
|
case TASTE:
|
||||||
|
return TagCategory.TASTE;
|
||||||
|
default:
|
||||||
|
return TagCategory.TASTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package com.ktds.hi.store.infra.gateway.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장별 태그 클릭 통계 엔티티
|
||||||
|
* 어떤 매장에서 어떤 태그가 얼마나 클릭되었는지 저장
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "store_tag_clicks")
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class StoreTagClickEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "store_id", nullable = false)
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
|
@Column(name = "tag_id", nullable = false)
|
||||||
|
private Long tagId;
|
||||||
|
|
||||||
|
@Column(name = "click_count")
|
||||||
|
@Builder.Default
|
||||||
|
private Long clickCount = 0L;
|
||||||
|
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 클릭 수 증가
|
||||||
|
*/
|
||||||
|
public void incrementClickCount() {
|
||||||
|
this.clickCount = this.clickCount != null ? this.clickCount + 1 : 1L;
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
package com.ktds.hi.store.infra.gateway.repository;
|
||||||
|
|
||||||
|
|
||||||
|
import com.ktds.hi.store.infra.gateway.entity.StoreTagClickEntity;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface StoreTagClickJpaRepository extends JpaRepository<StoreTagClickEntity, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 매장의 가장 많이 클릭된 태그 5개 조회
|
||||||
|
*/
|
||||||
|
@Query("SELECT stc FROM StoreTagClickEntity stc WHERE stc.storeId = :storeId " +
|
||||||
|
"ORDER BY stc.clickCount DESC")
|
||||||
|
List<StoreTagClickEntity> findTop5TagsByStoreIdOrderByClickCountDesc(
|
||||||
|
@Param("storeId") Long storeId, PageRequest pageRequest);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 매장에서 가장 많이 클릭된 태그 5개 조회
|
||||||
|
*/
|
||||||
|
@Query("SELECT stc.tagId, SUM(stc.clickCount) as totalClicks " +
|
||||||
|
"FROM StoreTagClickEntity stc " +
|
||||||
|
"GROUP BY stc.tagId " +
|
||||||
|
"ORDER BY totalClicks DESC")
|
||||||
|
List<Object[]> findTop5TagsGloballyOrderByClickCount(PageRequest pageRequest);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장별 태그별 클릭 통계 조회
|
||||||
|
*/
|
||||||
|
Optional<StoreTagClickEntity> findByStoreIdAndTagId(Long storeId, Long tagId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 매장의 모든 태그 클릭 통계 조회
|
||||||
|
*/
|
||||||
|
List<StoreTagClickEntity> findByStoreIdOrderByClickCountDesc(Long storeId);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user