Merge branch 'main' of https://github.com/dg04-hi/hi-backend
This commit is contained in:
commit
2373a07772
@ -399,4 +399,138 @@ public class AnalyticsService implements AnalyticsUseCase {
|
||||
// 실제로는 AI 서비스를 통한 감정 분석 필요
|
||||
return (int) (reviews.size() * 0.2); // 20% 가정
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public AiAnalysisResponse generateAIAnalysis(Long storeId, AiAnalysisRequest request) {
|
||||
log.info("AI 분석 시작: storeId={}, days={}", storeId, request.getDays());
|
||||
|
||||
try {
|
||||
// 1. 기존 generateAIFeedback 메서드를 실제 AI 호출로 수정하여 사용
|
||||
AiFeedback aiFeedback = generateRealAIFeedback(storeId, request.getDays());
|
||||
|
||||
// 2. 실행계획 생성 (요청 시)
|
||||
List<String> actionPlans = null;
|
||||
if (Boolean.TRUE.equals(request.getGenerateActionPlan())) {
|
||||
actionPlans = aiServicePort.generateActionPlan(aiFeedback);
|
||||
}
|
||||
|
||||
// 3. 응답 생성
|
||||
AiAnalysisResponse response = AiAnalysisResponse.builder()
|
||||
.storeId(storeId)
|
||||
.feedbackId(aiFeedback.getId())
|
||||
.summary(aiFeedback.getSummary())
|
||||
.positivePoints(aiFeedback.getPositivePoints())
|
||||
.improvementPoints(aiFeedback.getImprovementPoints())
|
||||
.recommendations(aiFeedback.getRecommendations())
|
||||
.sentimentAnalysis(aiFeedback.getSentimentAnalysis())
|
||||
.confidenceScore(aiFeedback.getConfidenceScore())
|
||||
.totalReviewsAnalyzed(getTotalReviewsCount(storeId, request.getDays()))
|
||||
.actionPlans(actionPlans)
|
||||
.analyzedAt(aiFeedback.getGeneratedAt())
|
||||
.build();
|
||||
|
||||
log.info("AI 분석 완료: storeId={}, feedbackId={}", storeId, aiFeedback.getId());
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI 분석 중 오류 발생: storeId={}", storeId, e);
|
||||
return createErrorAnalysisResponse(storeId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> generateActionPlansFromFeedback(Long feedbackId) {
|
||||
log.info("실행계획 생성: feedbackId={}", feedbackId);
|
||||
|
||||
try {
|
||||
// 1. AI 피드백 조회
|
||||
var aiFeedback = analyticsPort.findAIFeedbackByStoreId(feedbackId); // 실제로는 feedbackId로 조회하는 메서드 필요
|
||||
|
||||
if (aiFeedback.isEmpty()) {
|
||||
throw new RuntimeException("AI 피드백을 찾을 수 없습니다: " + feedbackId);
|
||||
}
|
||||
|
||||
// 2. 기존 AIServicePort.generateActionPlan 메서드 활용
|
||||
List<String> actionPlans = aiServicePort.generateActionPlan(aiFeedback.get());
|
||||
|
||||
log.info("실행계획 생성 완료: feedbackId={}, planCount={}", feedbackId, actionPlans.size());
|
||||
return actionPlans;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("실행계획 생성 중 오류 발생: feedbackId={}", feedbackId, e);
|
||||
return List.of("서비스 개선을 위한 기본 실행계획을 수립하세요.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제 AI를 호출하는 개선된 피드백 생성 메서드
|
||||
* 기존 generateAIFeedback()의 하드코딩 부분을 실제 AI 호출로 수정
|
||||
*/
|
||||
@Transactional
|
||||
public AiFeedback generateRealAIFeedback(Long storeId, Integer days) {
|
||||
log.info("실제 AI 피드백 생성: storeId={}, days={}", storeId, days);
|
||||
|
||||
try {
|
||||
// 1. 리뷰 데이터 수집
|
||||
List<String> reviewData = externalReviewPort.getRecentReviews(storeId, days);
|
||||
|
||||
if (reviewData.isEmpty()) {
|
||||
log.warn("AI 피드백 생성을 위한 리뷰 데이터가 없습니다: storeId={}", storeId);
|
||||
return createDefaultAIFeedback(storeId);
|
||||
}
|
||||
|
||||
// 2. 실제 AI 서비스 호출 (기존 하드코딩 부분을 수정)
|
||||
AiFeedback aiFeedback = aiServicePort.generateFeedback(reviewData);
|
||||
|
||||
// 3. 도메인 객체 속성 설정
|
||||
AiFeedback completeAiFeedback = AiFeedback.builder()
|
||||
.storeId(storeId)
|
||||
.summary(aiFeedback.getSummary())
|
||||
.positivePoints(aiFeedback.getPositivePoints())
|
||||
.improvementPoints(aiFeedback.getImprovementPoints())
|
||||
.recommendations(aiFeedback.getRecommendations())
|
||||
.sentimentAnalysis(aiFeedback.getSentimentAnalysis())
|
||||
.confidenceScore(aiFeedback.getConfidenceScore())
|
||||
.generatedAt(LocalDateTime.now())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 4. 데이터베이스에 저장
|
||||
AiFeedback saved = analyticsPort.saveAIFeedback(completeAiFeedback);
|
||||
|
||||
log.info("실제 AI 피드백 생성 완료: storeId={}, reviewCount={}", storeId, reviewData.size());
|
||||
return saved;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("실제 AI 피드백 생성 중 오류 발생: storeId={}", storeId, e);
|
||||
return createDefaultAIFeedback(storeId);
|
||||
}
|
||||
}
|
||||
|
||||
private Integer getTotalReviewsCount(Long storeId, Integer days) {
|
||||
try {
|
||||
return externalReviewPort.getRecentReviews(storeId, days).size();
|
||||
} catch (Exception e) {
|
||||
log.warn("리뷰 수 조회 실패: storeId={}", storeId, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private AiAnalysisResponse createErrorAnalysisResponse(Long storeId) {
|
||||
return AiAnalysisResponse.builder()
|
||||
.storeId(storeId)
|
||||
.summary("분석 중 오류가 발생했습니다.")
|
||||
.positivePoints(List.of("현재 분석이 불가능합니다"))
|
||||
.improvementPoints(List.of("시스템 안정화 후 재시도 필요"))
|
||||
.recommendations(List.of("잠시 후 다시 분석을 요청해주세요"))
|
||||
.sentimentAnalysis("분석 실패")
|
||||
.confidenceScore(0.0)
|
||||
.totalReviewsAnalyzed(0)
|
||||
.actionPlans(List.of("기본 실행계획을 수립하세요"))
|
||||
.analyzedAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package com.ktds.hi.analytics.biz.usecase.in;
|
||||
import com.ktds.hi.analytics.infra.dto.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 분석 서비스 UseCase 인터페이스
|
||||
@ -34,4 +35,15 @@ public interface AnalyticsUseCase {
|
||||
* 리뷰 분석 조회
|
||||
*/
|
||||
ReviewAnalysisResponse getReviewAnalysis(Long storeId);
|
||||
|
||||
/**
|
||||
* AI 리뷰 분석 및 실행계획 생성
|
||||
*/
|
||||
AiAnalysisResponse generateAIAnalysis(Long storeId, AiAnalysisRequest request);
|
||||
|
||||
/**
|
||||
* AI 피드백 기반 실행계획 생성
|
||||
*/
|
||||
List<String> generateActionPlansFromFeedback(Long feedbackId);
|
||||
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ 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.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 분석 서비스 컨트롤러 클래스
|
||||
@ -113,4 +115,44 @@ public class AnalyticsController {
|
||||
|
||||
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<List<String>>> generateActionPlans(
|
||||
@Parameter(description = "AI 피드백 ID", required = true)
|
||||
@PathVariable @NotNull Long feedbackId) {
|
||||
|
||||
log.info("실행계획 생성 요청: feedbackId={}", feedbackId);
|
||||
|
||||
List<String> actionPlans = analyticsUseCase.generateActionPlansFromFeedback(feedbackId);
|
||||
|
||||
return ResponseEntity.ok(SuccessResponse.of(actionPlans, "실행계획 생성 완료"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
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 jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
|
||||
/**
|
||||
* AI 분석 요청 DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "AI 리뷰 분석 요청")
|
||||
public class AiAnalysisRequest {
|
||||
|
||||
@Schema(description = "분석할 최근 일수", example = "30")
|
||||
@Min(value = 1, message = "분석 기간은 최소 1일 이상이어야 합니다")
|
||||
@Max(value = 365, message = "분석 기간은 최대 365일까지 가능합니다")
|
||||
@Builder.Default
|
||||
private Integer days = 30;
|
||||
|
||||
@Schema(description = "실행계획 자동 생성 여부", example = "true")
|
||||
@Builder.Default
|
||||
private Boolean generateActionPlan = true;
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
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> 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;
|
||||
}
|
||||
@ -18,6 +18,7 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@ -96,6 +97,9 @@ public class AIServiceAdapter implements AIServicePort {
|
||||
throw new RuntimeException("AI 피드백 생성에 실패했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public SentimentType analyzeSentiment(String content) {
|
||||
@ -175,40 +179,70 @@ public class AIServiceAdapter implements AIServicePort {
|
||||
* 긍정적 요소 생성
|
||||
*/
|
||||
private List<String> generatePositivePoints(List<String> reviewData, List<SentimentType> sentiments) {
|
||||
// 실제로는 자연어 처리를 통해 긍정적 키워드 추출
|
||||
return Arrays.asList(
|
||||
"음식 맛에 대한 긍정적 평가가 많습니다",
|
||||
"직원 서비스에 대한 만족도가 높습니다",
|
||||
"가격 대비 만족도가 좋습니다"
|
||||
);
|
||||
List<String> positivePoints = new ArrayList<>();
|
||||
|
||||
long positiveCount = sentiments.stream().mapToLong(s -> s == SentimentType.POSITIVE ? 1 : 0).sum();
|
||||
double positiveRate = (double) positiveCount / reviewData.size() * 100;
|
||||
|
||||
if (positiveRate > 70) {
|
||||
positivePoints.add("고객 만족도가 매우 높습니다");
|
||||
positivePoints.add("전반적으로 긍정적인 평가를 받고 있습니다");
|
||||
positivePoints.add("재방문 의향이 높은 고객들이 많습니다");
|
||||
} else if (positiveRate > 50) {
|
||||
positivePoints.add("평균 이상의 고객 만족도를 보입니다");
|
||||
positivePoints.add("많은 고객들이 만족하고 있습니다");
|
||||
} else {
|
||||
positivePoints.add("일부 고객들이 긍정적으로 평가하고 있습니다");
|
||||
positivePoints.add("개선의 여지가 있습니다");
|
||||
}
|
||||
|
||||
return positivePoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 개선점 생성
|
||||
*/
|
||||
private List<String> generateImprovementPoints(List<String> reviewData, List<SentimentType> sentiments) {
|
||||
// 실제로는 자연어 처리를 통해 부정적 키워드 추출
|
||||
return Arrays.asList(
|
||||
"배달 시간 단축이 필요합니다",
|
||||
"음식 포장 상태 개선이 필요합니다",
|
||||
"메뉴 다양성 확대를 고려해보세요"
|
||||
);
|
||||
List<String> improvementPoints = new ArrayList<>();
|
||||
|
||||
long negativeCount = sentiments.stream().mapToLong(s -> s == SentimentType.NEGATIVE ? 1 : 0).sum();
|
||||
double negativeRate = (double) negativeCount / reviewData.size() * 100;
|
||||
|
||||
if (negativeRate > 30) {
|
||||
improvementPoints.add("고객 서비스 품질 개선이 시급합니다");
|
||||
improvementPoints.add("부정적 피드백에 대한 체계적 대응이 필요합니다");
|
||||
improvementPoints.add("근본적인 서비스 개선 방안을 마련해야 합니다");
|
||||
} else if (negativeRate > 15) {
|
||||
improvementPoints.add("일부 서비스 영역에서 개선이 필요합니다");
|
||||
improvementPoints.add("고객 만족도 향상을 위한 노력이 필요합니다");
|
||||
} else {
|
||||
improvementPoints.add("현재 서비스 수준을 유지하며 세부 개선점을 찾아보세요");
|
||||
improvementPoints.add("더 높은 고객 만족을 위한 차별화 요소를 개발하세요");
|
||||
}
|
||||
|
||||
return improvementPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 추천사항 생성
|
||||
*/
|
||||
private List<String> generateRecommendations(double positiveRate, double negativeRate) {
|
||||
List<String> recommendations = Arrays.asList(
|
||||
"고객 피드백을 정기적으로 모니터링하고 대응하세요",
|
||||
"리뷰에 적극적으로 댓글을 달아 고객과 소통하세요",
|
||||
"메뉴 품질 관리 체계를 강화하세요"
|
||||
);
|
||||
|
||||
if (negativeRate > 30) {
|
||||
recommendations.add("긴급히 서비스 품질 개선 계획을 수립하세요");
|
||||
List<String> recommendations = new ArrayList<>();
|
||||
|
||||
if (positiveRate > 70) {
|
||||
recommendations.add("현재의 우수한 서비스를 유지하면서 브랜드 가치를 높이세요");
|
||||
recommendations.add("긍정적 리뷰를 마케팅 자료로 활용하세요");
|
||||
recommendations.add("고객 충성도 프로그램을 도입하세요");
|
||||
} else if (negativeRate > 30) {
|
||||
recommendations.add("고객 불만사항에 대한 즉각적인 대응 체계를 구축하세요");
|
||||
recommendations.add("직원 교육을 통한 서비스 품질 향상에 집중하세요");
|
||||
recommendations.add("고객 피드백 수집 및 분석 프로세스를 강화하세요");
|
||||
} else {
|
||||
recommendations.add("지속적인 품질 관리와 고객 만족도 모니터링을 실시하세요");
|
||||
recommendations.add("차별화된 서비스 제공을 통해 경쟁력을 강화하세요");
|
||||
recommendations.add("고객과의 소통을 늘려 관계를 강화하세요");
|
||||
}
|
||||
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
@ -216,25 +250,34 @@ public class AIServiceAdapter implements AIServicePort {
|
||||
* 신뢰도 점수 계산
|
||||
*/
|
||||
private double calculateConfidenceScore(int reviewCount) {
|
||||
if (reviewCount >= 100) return 0.95;
|
||||
if (reviewCount >= 50) return 0.85;
|
||||
if (reviewCount >= 20) return 0.75;
|
||||
if (reviewCount >= 10) return 0.65;
|
||||
return 0.5;
|
||||
if (reviewCount >= 50) {
|
||||
return 0.9;
|
||||
} else if (reviewCount >= 20) {
|
||||
return 0.75;
|
||||
} else if (reviewCount >= 10) {
|
||||
return 0.6;
|
||||
} else if (reviewCount >= 5) {
|
||||
return 0.4;
|
||||
} else {
|
||||
return 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 개선점을 실행 계획으로 변환
|
||||
*/
|
||||
private String convertToActionPlan(String improvementPoint) {
|
||||
// 개선점을 구체적인 실행 계획으로 변환하는 로직
|
||||
if (improvementPoint.contains("배달 시간")) {
|
||||
return "배달 시간 단축을 위한 배달 경로 최적화 및 인력 증원 검토";
|
||||
} else if (improvementPoint.contains("포장")) {
|
||||
return "음식 포장 재료 교체 및 포장 방법 개선";
|
||||
// 개선점을 구체적인 실행계획으로 변환
|
||||
if (improvementPoint.contains("서비스 품질")) {
|
||||
return "직원 서비스 교육 프로그램 실시 (월 1회, 2시간)";
|
||||
} else if (improvementPoint.contains("대기시간")) {
|
||||
return "주문 처리 시스템 개선 및 대기열 관리 체계 도입";
|
||||
} else if (improvementPoint.contains("가격")) {
|
||||
return "경쟁사 가격 분석 및 합리적 가격 정책 수립";
|
||||
} else if (improvementPoint.contains("메뉴")) {
|
||||
return "고객 선호도 조사를 통한 신메뉴 개발 계획 수립";
|
||||
return "고객 선호도 조사를 통한 메뉴 다양화 방안 검토";
|
||||
} else {
|
||||
return "고객 피드백 기반 서비스 개선 계획 수립";
|
||||
}
|
||||
return improvementPoint + "을 위한 구체적인 실행 방안 수립";
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,9 +52,9 @@ ai-api:
|
||||
# 외부 서비스 설정
|
||||
external:
|
||||
services:
|
||||
review: ${EXTERNAL_SERVICES_REVIEW:http://localhost:8082}
|
||||
store: ${EXTERNAL_SERVICES_STORE:http://localhost:8081}
|
||||
member: ${EXTERNAL_SERVICES_MEMBER:http://localhost:8080}
|
||||
review: ${EXTERNAL_SERVICES_REVIEW:http://localhost:8083}
|
||||
store: ${EXTERNAL_SERVICES_STORE:http://localhost:8082}
|
||||
member: ${EXTERNAL_SERVICES_MEMBER:http://localhost:8081}
|
||||
|
||||
#springdoc:
|
||||
# api-docs:
|
||||
@ -83,6 +83,9 @@ ai:
|
||||
key: ${AI_AZURE_COGNITIVE_KEY:your-cognitive-service-key}
|
||||
openai:
|
||||
api-key: ${AI_OPENAI_API_KEY:your-openai-api-key}
|
||||
claude:
|
||||
api-key: ${AI_CLAUDE_KEY:your-claude-key}
|
||||
endpoint: ${AI_CLAUDE_ENDPOINT:https://api.anthropic.com}
|
||||
|
||||
# Azure Event Hub 설정
|
||||
azure:
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// common/src/main/java/com/ktds/hi/common/exception/BusinessException.java
|
||||
package com.ktds.hi.common.exception;
|
||||
|
||||
import com.ktds.hi.common.dto.ResponseCode;
|
||||
|
||||
/**
|
||||
* 비즈니스 로직 예외의 기본 클래스
|
||||
* 모든 커스텀 예외의 부모 클래스
|
||||
@ -10,6 +12,22 @@ public class BusinessException extends RuntimeException {
|
||||
private String errorCode;
|
||||
private Object[] args;
|
||||
|
||||
/**
|
||||
* ResponseCode와 메시지로 예외 생성
|
||||
*/
|
||||
public BusinessException(ResponseCode responseCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = responseCode.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* ResponseCode로 예외 생성 (기본 메시지 사용)
|
||||
*/
|
||||
public BusinessException(ResponseCode responseCode) {
|
||||
super(responseCode.getMessage());
|
||||
this.errorCode = responseCode.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 메시지만으로 예외 생성
|
||||
*/
|
||||
|
||||
@ -3,6 +3,9 @@ package com.ktds.hi.common.security;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import com.ktds.hi.common.exception.BusinessException;
|
||||
import com.ktds.hi.common.constants.SecurityConstants;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import com.ktds.hi.common.exception.BusinessException;
|
||||
import com.ktds.hi.common.dto.ResponseCode;
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -134,6 +137,52 @@ public class JwtTokenProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HttpServletRequest에서 점주 ID 추출
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @return 점주 ID
|
||||
*/
|
||||
public Long extractOwnerIdFromRequest(HttpServletRequest request) {
|
||||
try {
|
||||
String token = getJwtFromRequest(request);
|
||||
if (token == null) {
|
||||
throw new BusinessException(ResponseCode.UNAUTHORIZED, "토큰이 필요합니다.");
|
||||
}
|
||||
|
||||
if (!validateToken(token)) {
|
||||
throw new BusinessException(ResponseCode.INVALID_TOKEN, "유효하지 않은 토큰입니다.");
|
||||
}
|
||||
|
||||
String userId = getUserIdFromToken(token);
|
||||
if (userId == null || userId.trim().isEmpty()) {
|
||||
throw new BusinessException(ResponseCode.INVALID_TOKEN, "토큰에서 사용자 정보를 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
return Long.parseLong(userId);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BusinessException(ResponseCode.INVALID_TOKEN, "유효하지 않은 사용자 ID 형식입니다.");
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("토큰에서 점주 ID 추출 실패", e);
|
||||
throw new BusinessException(ResponseCode.UNAUTHORIZED, "인증에 실패했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청에서 JWT 토큰 추출
|
||||
*/
|
||||
private String getJwtFromRequest(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
|
||||
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
|
||||
return bearerToken.substring(7);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 액세스 토큰 생성
|
||||
*/
|
||||
|
||||
@ -0,0 +1,247 @@
|
||||
// store/src/main/java/com/ktds/hi/store/biz/service/MenuService.java
|
||||
package com.ktds.hi.store.biz.service;
|
||||
|
||||
import com.ktds.hi.common.dto.ResponseCode;
|
||||
import com.ktds.hi.common.exception.BusinessException;
|
||||
import com.ktds.hi.store.domain.Menu;
|
||||
import com.ktds.hi.store.domain.Store;
|
||||
import com.ktds.hi.store.biz.usecase.in.MenuUseCase;
|
||||
import com.ktds.hi.store.biz.usecase.out.MenuRepositoryPort;
|
||||
import com.ktds.hi.store.biz.usecase.out.StoreRepositoryPort;
|
||||
import com.ktds.hi.store.infra.dto.request.MenuCreateRequest;
|
||||
import com.ktds.hi.store.infra.dto.request.MenuUpdateRequest;
|
||||
import com.ktds.hi.store.infra.dto.response.MenuCreateResponse;
|
||||
import com.ktds.hi.store.infra.dto.response.MenuDetailResponse;
|
||||
import com.ktds.hi.store.infra.dto.response.MenuUpdateResponse;
|
||||
import com.ktds.hi.store.infra.dto.response.StoreMenuListResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 메뉴 서비스 구현체
|
||||
* 메뉴 관련 비즈니스 로직을 처리
|
||||
*
|
||||
* @author 하이오더 개발팀
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
//@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class MenuService implements MenuUseCase {
|
||||
|
||||
private final MenuRepositoryPort menuRepositoryPort;
|
||||
private final StoreRepositoryPort storeRepositoryPort;
|
||||
|
||||
public MenuService(@Qualifier("menuJpaAdapter") MenuRepositoryPort menuRepositoryPort,
|
||||
StoreRepositoryPort storeRepositoryPort) {
|
||||
this.menuRepositoryPort = menuRepositoryPort;
|
||||
this.storeRepositoryPort = storeRepositoryPort;
|
||||
}
|
||||
@Override
|
||||
public List<StoreMenuListResponse> getStoreMenus(Long storeId) {
|
||||
log.info("매장 메뉴 목록 조회 시작 - storeId: {}", storeId);
|
||||
|
||||
// 매장 존재 여부 확인
|
||||
validateStoreExists(storeId);
|
||||
|
||||
List<Menu> menus = menuRepositoryPort.findMenusByStoreId(storeId);
|
||||
|
||||
return menus.stream()
|
||||
.map(this::mapToStoreMenuListResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public MenuDetailResponse getMenuDetail(Long menuId) {
|
||||
log.info("메뉴 상세 조회 시작 - menuId: {}", menuId);
|
||||
|
||||
Menu menu = menuRepositoryPort.findMenuById(menuId)
|
||||
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, "메뉴를 찾을 수 없습니다."));
|
||||
|
||||
return mapToMenuDetailResponse(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public MenuCreateResponse createMenu(Long ownerId, Long storeId, MenuCreateRequest request) {
|
||||
log.info("메뉴 등록 시작 - ownerId: {}, storeId: {}, menuName: {}", ownerId, storeId, request.getMenuName());
|
||||
|
||||
// 매장 소유권 확인
|
||||
validateStoreOwnership(ownerId, storeId);
|
||||
|
||||
// 메뉴 생성
|
||||
Menu menu = Menu.builder()
|
||||
.storeId(storeId)
|
||||
.menuName(request.getMenuName())
|
||||
.description(request.getDescription())
|
||||
.price(request.getPrice())
|
||||
.category(request.getCategory())
|
||||
.imageUrl(request.getImageUrl())
|
||||
.available(request.getIsAvailable() != null ? request.getIsAvailable() : true)
|
||||
.orderCount(0)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 메뉴 유효성 검증
|
||||
if (!menu.isValid()) {
|
||||
throw new BusinessException(ResponseCode.INVALID_INPUT, "메뉴 정보가 올바르지 않습니다.");
|
||||
}
|
||||
|
||||
Menu savedMenu = menuRepositoryPort.saveMenu(menu);
|
||||
|
||||
log.info("메뉴 등록 완료 - menuId: {}", savedMenu.getId());
|
||||
|
||||
return MenuCreateResponse.builder()
|
||||
.menuId(savedMenu.getId())
|
||||
.message("메뉴가 성공적으로 등록되었습니다.")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public MenuUpdateResponse updateMenu(Long ownerId, Long menuId, MenuUpdateRequest request) {
|
||||
log.info("메뉴 수정 시작 - ownerId: {}, menuId: {}", ownerId, menuId);
|
||||
|
||||
// 메뉴 조회 및 소유권 확인
|
||||
Menu existingMenu = menuRepositoryPort.findMenuById(menuId)
|
||||
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, "메뉴를 찾을 수 없습니다."));
|
||||
|
||||
validateStoreOwnership(ownerId, existingMenu.getStoreId());
|
||||
|
||||
// 메뉴 정보 업데이트
|
||||
Menu updatedMenu = existingMenu.updateInfo(
|
||||
request.getMenuName() != null ? request.getMenuName() : existingMenu.getMenuName(),
|
||||
request.getDescription() != null ? request.getDescription() : existingMenu.getDescription(),
|
||||
request.getPrice() != null ? request.getPrice() : existingMenu.getPrice()
|
||||
);
|
||||
|
||||
if (request.getCategory() != null) {
|
||||
updatedMenu = updatedMenu.updateCategory(request.getCategory());
|
||||
}
|
||||
|
||||
if (request.getImageUrl() != null) {
|
||||
updatedMenu = updatedMenu.updateImage(request.getImageUrl());
|
||||
}
|
||||
|
||||
if (request.getIsAvailable() != null) {
|
||||
updatedMenu = updatedMenu.setAvailable(request.getIsAvailable());
|
||||
}
|
||||
|
||||
Menu savedMenu = menuRepositoryPort.saveMenu(updatedMenu);
|
||||
|
||||
log.info("메뉴 수정 완료 - menuId: {}", savedMenu.getId());
|
||||
|
||||
return MenuUpdateResponse.builder()
|
||||
.menuId(savedMenu.getId())
|
||||
.message("메뉴가 성공적으로 수정되었습니다.")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteMenu(Long ownerId, Long menuId) {
|
||||
log.info("메뉴 삭제 시작 - ownerId: {}, menuId: {}", ownerId, menuId);
|
||||
|
||||
// 메뉴 조회 및 소유권 확인
|
||||
Menu existingMenu = menuRepositoryPort.findMenuById(menuId)
|
||||
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, "메뉴를 찾을 수 없습니다."));
|
||||
|
||||
validateStoreOwnership(ownerId, existingMenu.getStoreId());
|
||||
|
||||
menuRepositoryPort.deleteMenu(menuId);
|
||||
|
||||
log.info("메뉴 삭제 완료 - menuId: {}", menuId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateMenuAvailability(Long ownerId, Long menuId, Boolean isAvailable) {
|
||||
log.info("메뉴 가용성 변경 시작 - ownerId: {}, menuId: {}, isAvailable: {}", ownerId, menuId, isAvailable);
|
||||
|
||||
// 메뉴 조회 및 소유권 확인
|
||||
Menu existingMenu = menuRepositoryPort.findMenuById(menuId)
|
||||
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, "메뉴를 찾을 수 없습니다."));
|
||||
|
||||
validateStoreOwnership(ownerId, existingMenu.getStoreId());
|
||||
|
||||
Menu updatedMenu = existingMenu.setAvailable(isAvailable);
|
||||
menuRepositoryPort.saveMenu(updatedMenu);
|
||||
|
||||
log.info("메뉴 가용성 변경 완료 - menuId: {}, isAvailable: {}", menuId, isAvailable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StoreMenuListResponse> getMenusByCategory(Long storeId, String category) {
|
||||
log.info("카테고리별 메뉴 조회 시작 - storeId: {}, category: {}", storeId, category);
|
||||
|
||||
validateStoreExists(storeId);
|
||||
|
||||
List<Menu> menus = menuRepositoryPort.findMenusByStoreIdAndCategory(storeId, category);
|
||||
|
||||
return menus.stream()
|
||||
.map(this::mapToStoreMenuListResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 매장 존재 여부 확인
|
||||
*/
|
||||
private void validateStoreExists(Long storeId) {
|
||||
if (!storeRepositoryPort.findStoreById(storeId).isPresent()) {
|
||||
throw new BusinessException(ResponseCode.STORE_NOT_FOUND, "매장을 찾을 수 없습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 매장 소유권 확인
|
||||
*/
|
||||
private void validateStoreOwnership(Long ownerId, Long storeId) {
|
||||
Store store = storeRepositoryPort.findStoreByIdAndOwnerId(storeId, ownerId)
|
||||
.orElseThrow(() -> new BusinessException(ResponseCode.ACCESS_DENIED, "해당 매장에 대한 권한이 없습니다."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu를 StoreMenuListResponse로 변환
|
||||
*/
|
||||
private StoreMenuListResponse mapToStoreMenuListResponse(Menu menu) {
|
||||
return StoreMenuListResponse.builder()
|
||||
.menuId(menu.getId())
|
||||
.menuName(menu.getMenuName())
|
||||
.description(menu.getDescription())
|
||||
.price(menu.getPrice())
|
||||
.category(menu.getCategory())
|
||||
.imageUrl(menu.getImageUrl())
|
||||
.isAvailable(menu.getAvailable())
|
||||
.orderCount(menu.getOrderCount())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu를 MenuDetailResponse로 변환
|
||||
*/
|
||||
private MenuDetailResponse mapToMenuDetailResponse(Menu menu) {
|
||||
return MenuDetailResponse.builder()
|
||||
.menuId(menu.getId())
|
||||
.storeId(menu.getStoreId())
|
||||
.menuName(menu.getMenuName())
|
||||
.description(menu.getDescription())
|
||||
.price(menu.getPrice())
|
||||
.category(menu.getCategory())
|
||||
.imageUrl(menu.getImageUrl())
|
||||
.isAvailable(menu.getAvailable())
|
||||
.orderCount(menu.getOrderCount())
|
||||
.createdAt(menu.getCreatedAt())
|
||||
.updatedAt(menu.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package com.ktds.hi.store.biz.usecase.in;
|
||||
|
||||
import com.ktds.hi.store.infra.dto.response.StoreMenuListResponse;
|
||||
import java.util.List;
|
||||
import com.ktds.hi.store.infra.dto.request.MenuCreateRequest;
|
||||
import com.ktds.hi.store.infra.dto.request.MenuUpdateRequest;
|
||||
import com.ktds.hi.store.infra.dto.response.MenuCreateResponse;
|
||||
import com.ktds.hi.store.infra.dto.response.MenuDetailResponse;
|
||||
import com.ktds.hi.store.infra.dto.response.MenuUpdateResponse;
|
||||
|
||||
public interface MenuUseCase {
|
||||
|
||||
/**
|
||||
* 매장 메뉴 목록 조회
|
||||
*
|
||||
* @param storeId 매장 ID
|
||||
* @return 메뉴 목록 응답
|
||||
*/
|
||||
List<StoreMenuListResponse> getStoreMenus(Long storeId);
|
||||
|
||||
|
||||
/**
|
||||
* 메뉴 상세 조회
|
||||
*
|
||||
* @param menuId 메뉴 ID
|
||||
* @return 메뉴 상세 정보
|
||||
*/
|
||||
MenuDetailResponse getMenuDetail(Long menuId);
|
||||
|
||||
/**
|
||||
* 메뉴 등록
|
||||
*
|
||||
* @param ownerId 점주 ID
|
||||
* @param storeId 매장 ID
|
||||
* @param request 메뉴 등록 요청
|
||||
* @return 메뉴 등록 응답
|
||||
*/
|
||||
MenuCreateResponse createMenu(Long ownerId, Long storeId, MenuCreateRequest request);
|
||||
|
||||
/**
|
||||
* 메뉴 수정
|
||||
*
|
||||
* @param ownerId 점주 ID
|
||||
* @param menuId 메뉴 ID
|
||||
* @param request 메뉴 수정 요청
|
||||
* @return 메뉴 수정 응답
|
||||
*/
|
||||
MenuUpdateResponse updateMenu(Long ownerId, Long menuId, MenuUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 메뉴 삭제
|
||||
*
|
||||
* @param ownerId 점주 ID
|
||||
* @param menuId 메뉴 ID
|
||||
*/
|
||||
void deleteMenu(Long ownerId, Long menuId);
|
||||
|
||||
/**
|
||||
* 메뉴 가용성 변경
|
||||
*
|
||||
* @param ownerId 점주 ID
|
||||
* @param menuId 메뉴 ID
|
||||
* @param isAvailable 가용성 여부
|
||||
*/
|
||||
void updateMenuAvailability(Long ownerId, Long menuId, Boolean isAvailable);
|
||||
|
||||
/**
|
||||
* 메뉴 카테고리별 조회
|
||||
*
|
||||
* @param storeId 매장 ID
|
||||
* @param category 카테고리
|
||||
* @return 카테고리별 메뉴 목록
|
||||
*/
|
||||
List<StoreMenuListResponse> getMenusByCategory(Long storeId, String category);
|
||||
}
|
||||
@ -21,6 +21,8 @@ public class Menu {
|
||||
private Boolean available;
|
||||
private LocalDateTime createdAt; // 추가
|
||||
private LocalDateTime updatedAt; // 추가
|
||||
private Integer orderCount;
|
||||
|
||||
|
||||
/**
|
||||
* 메뉴 정보 업데이트
|
||||
@ -35,16 +37,93 @@ public class Menu {
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 판매 상태 변경
|
||||
* 메뉴 카테고리 업데이트
|
||||
*/
|
||||
public void setAvailable(Boolean available) {
|
||||
this.available = available;
|
||||
public Menu updateCategory(String category) {
|
||||
return Menu.builder()
|
||||
.id(this.id)
|
||||
.storeId(this.storeId)
|
||||
.menuName(this.menuName)
|
||||
.description(this.description)
|
||||
.price(this.price)
|
||||
.category(category)
|
||||
.imageUrl(this.imageUrl)
|
||||
.available(this.available)
|
||||
.orderCount(this.orderCount)
|
||||
.createdAt(this.createdAt)
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 이미지 업데이트
|
||||
*/
|
||||
public Menu updateImage(String imageUrl) {
|
||||
return Menu.builder()
|
||||
.id(this.id)
|
||||
.storeId(this.storeId)
|
||||
.menuName(this.menuName)
|
||||
.description(this.description)
|
||||
.price(this.price)
|
||||
.category(this.category)
|
||||
.imageUrl(imageUrl)
|
||||
.available(this.available)
|
||||
.orderCount(this.orderCount)
|
||||
.createdAt(this.createdAt)
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 정보 업데이트 (불변 객체 반환)
|
||||
*/
|
||||
public Menu updateInfo(String menuName, String description, Integer price) {
|
||||
return Menu.builder()
|
||||
.id(this.id)
|
||||
.storeId(this.storeId)
|
||||
.menuName(menuName)
|
||||
.description(description)
|
||||
.price(price)
|
||||
.category(this.category)
|
||||
.imageUrl(this.imageUrl)
|
||||
.available(this.available)
|
||||
.orderCount(this.orderCount)
|
||||
.createdAt(this.createdAt)
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 판매 상태 변경 (불변 객체 반환)
|
||||
*/
|
||||
public Menu setAvailable(Boolean available) {
|
||||
return Menu.builder()
|
||||
.id(this.id)
|
||||
.storeId(this.storeId)
|
||||
.menuName(this.menuName)
|
||||
.description(this.description)
|
||||
.price(this.price)
|
||||
.category(this.category)
|
||||
.imageUrl(this.imageUrl)
|
||||
.available(available)
|
||||
.orderCount(this.orderCount)
|
||||
.createdAt(this.createdAt)
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
/**
|
||||
* 메뉴 이용 가능 여부 확인
|
||||
*/
|
||||
public boolean isAvailable() {
|
||||
return this.available != null && this.available;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴가 유효한지 확인
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return this.menuName != null && !this.menuName.trim().isEmpty() &&
|
||||
this.price != null && this.price > 0 &&
|
||||
this.storeId != null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
package com.ktds.hi.store.infra.controller;
|
||||
|
||||
import com.ktds.hi.common.dto.ApiResponse;
|
||||
import com.ktds.hi.common.security.JwtTokenProvider;
|
||||
import com.ktds.hi.store.biz.usecase.in.MenuUseCase;
|
||||
import com.ktds.hi.store.infra.dto.request.MenuCreateRequest;
|
||||
import com.ktds.hi.store.infra.dto.request.MenuUpdateRequest;
|
||||
import com.ktds.hi.store.infra.dto.request.MenuAvailabilityRequest;
|
||||
import com.ktds.hi.store.infra.dto.response.MenuCreateResponse;
|
||||
import com.ktds.hi.store.infra.dto.response.MenuDetailResponse;
|
||||
import com.ktds.hi.store.infra.dto.response.MenuUpdateResponse;
|
||||
import com.ktds.hi.store.infra.dto.response.StoreMenuListResponse;
|
||||
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.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 메뉴 관리 컨트롤러
|
||||
* 메뉴 관련 REST API 엔드포인트를 제공
|
||||
*
|
||||
* @author 하이오더 개발팀
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/menus")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "메뉴 관리", description = "메뉴 등록, 조회, 수정, 삭제 API")
|
||||
public class MenuController {
|
||||
|
||||
private final MenuUseCase menuUseCase;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@GetMapping("/stores/{storeId}")
|
||||
@Operation(summary = "매장 메뉴 목록 조회", description = "특정 매장의 모든 메뉴를 조회합니다.")
|
||||
public ResponseEntity<ApiResponse<List<StoreMenuListResponse>>> getStoreMenus(
|
||||
@Parameter(description = "매장 ID") @PathVariable Long storeId) {
|
||||
|
||||
log.info("매장 메뉴 목록 조회 요청 - storeId: {}", storeId);
|
||||
|
||||
List<StoreMenuListResponse> response = menuUseCase.getStoreMenus(storeId);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
@GetMapping("/{menuId}")
|
||||
@Operation(summary = "메뉴 상세 조회", description = "특정 메뉴의 상세 정보를 조회합니다.")
|
||||
public ResponseEntity<ApiResponse<MenuDetailResponse>> getMenuDetail(
|
||||
@Parameter(description = "메뉴 ID") @PathVariable Long menuId) {
|
||||
|
||||
log.info("메뉴 상세 조회 요청 - menuId: {}", menuId);
|
||||
|
||||
MenuDetailResponse response = menuUseCase.getMenuDetail(menuId);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
@PostMapping("/stores/{storeId}")
|
||||
@Operation(summary = "메뉴 등록", description = "새로운 메뉴를 등록합니다.")
|
||||
public ResponseEntity<ApiResponse<MenuCreateResponse>> createMenu(
|
||||
@Parameter(description = "매장 ID") @PathVariable Long storeId,
|
||||
@Parameter(description = "메뉴 등록 정보") @Valid @RequestBody MenuCreateRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long ownerId = jwtTokenProvider.extractOwnerIdFromRequest(httpRequest);
|
||||
log.info("메뉴 등록 요청 - ownerId: {}, storeId: {}, menuName: {}", ownerId, storeId, request.getMenuName());
|
||||
|
||||
MenuCreateResponse response = menuUseCase.createMenu(ownerId, storeId, request);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
@PutMapping("/{menuId}")
|
||||
@Operation(summary = "메뉴 수정", description = "기존 메뉴 정보를 수정합니다.")
|
||||
public ResponseEntity<ApiResponse<MenuUpdateResponse>> updateMenu(
|
||||
@Parameter(description = "메뉴 ID") @PathVariable Long menuId,
|
||||
@Parameter(description = "메뉴 수정 정보") @Valid @RequestBody MenuUpdateRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long ownerId = jwtTokenProvider.extractOwnerIdFromRequest(httpRequest);
|
||||
log.info("메뉴 수정 요청 - ownerId: {}, menuId: {}", ownerId, menuId);
|
||||
|
||||
MenuUpdateResponse response = menuUseCase.updateMenu(ownerId, menuId, request);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{menuId}")
|
||||
@Operation(summary = "메뉴 삭제", description = "메뉴를 삭제합니다.")
|
||||
public ResponseEntity<ApiResponse<Void>> deleteMenu(
|
||||
@Parameter(description = "메뉴 ID") @PathVariable Long menuId,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long ownerId = jwtTokenProvider.extractOwnerIdFromRequest(httpRequest);
|
||||
log.info("메뉴 삭제 요청 - ownerId: {}, menuId: {}", ownerId, menuId);
|
||||
|
||||
menuUseCase.deleteMenu(ownerId, menuId);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(null));
|
||||
}
|
||||
|
||||
@PatchMapping("/{menuId}/availability")
|
||||
@Operation(summary = "메뉴 가용성 변경", description = "메뉴의 판매 가능 여부를 변경합니다.")
|
||||
public ResponseEntity<ApiResponse<Void>> updateMenuAvailability(
|
||||
@Parameter(description = "메뉴 ID") @PathVariable Long menuId,
|
||||
@Parameter(description = "가용성 변경 정보") @Valid @RequestBody MenuAvailabilityRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long ownerId = jwtTokenProvider.extractOwnerIdFromRequest(httpRequest);
|
||||
log.info("메뉴 가용성 변경 요청 - ownerId: {}, menuId: {}, isAvailable: {}", ownerId, menuId, request.getIsAvailable());
|
||||
|
||||
menuUseCase.updateMenuAvailability(ownerId, menuId, request.getIsAvailable());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(null));
|
||||
}
|
||||
|
||||
@GetMapping("/stores/{storeId}/categories/{category}")
|
||||
@Operation(summary = "카테고리별 메뉴 조회", description = "특정 매장의 카테고리별 메뉴를 조회합니다.")
|
||||
public ResponseEntity<ApiResponse<List<StoreMenuListResponse>>> getMenusByCategory(
|
||||
@Parameter(description = "매장 ID") @PathVariable Long storeId,
|
||||
@Parameter(description = "메뉴 카테고리") @PathVariable String category) {
|
||||
|
||||
log.info("카테고리별 메뉴 조회 요청 - storeId: {}, category: {}", storeId, category);
|
||||
|
||||
List<StoreMenuListResponse> response = menuUseCase.getMenusByCategory(storeId, category);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,12 @@
|
||||
// store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java
|
||||
package com.ktds.hi.store.infra.controller;
|
||||
|
||||
import com.ktds.hi.store.biz.usecase.in.MenuUseCase;
|
||||
import com.ktds.hi.store.biz.usecase.in.StoreUseCase;
|
||||
import com.ktds.hi.store.infra.dto.*;
|
||||
import com.ktds.hi.common.dto.ApiResponse;
|
||||
import com.ktds.hi.common.security.JwtTokenProvider;
|
||||
import com.ktds.hi.store.infra.dto.response.StoreMenuListResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@ -37,6 +39,8 @@ public class StoreController {
|
||||
|
||||
private final StoreUseCase storeUseCase;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final MenuUseCase menuUseCase;
|
||||
|
||||
|
||||
@Operation(summary = "매장 등록", description = "새로운 매장을 등록합니다.")
|
||||
@PostMapping
|
||||
@ -117,4 +121,26 @@ public class StoreController {
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(responses, "매장 검색 완료"));
|
||||
}
|
||||
|
||||
@GetMapping("/{storeId}/menus")
|
||||
@Operation(summary = "매장 메뉴 목록 조회 (매장 상세에서)", description = "매장 상세 정보 조회 시 메뉴 목록을 함께 제공")
|
||||
public ResponseEntity<ApiResponse<List<StoreMenuListResponse>>> getStoreMenusFromStore(
|
||||
@Parameter(description = "매장 ID") @PathVariable Long storeId) {
|
||||
|
||||
List<StoreMenuListResponse> response = menuUseCase.getStoreMenus(storeId);
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@GetMapping("/{storeId}/menus/popular")
|
||||
@Operation(summary = "매장 인기 메뉴 조회", description = "매장의 인기 메뉴(주문 횟수 기준)를 조회합니다.")
|
||||
public ResponseEntity<ApiResponse<List<StoreMenuListResponse>>> getPopularMenus(
|
||||
@Parameter(description = "매장 ID") @PathVariable Long storeId,
|
||||
@Parameter(description = "조회할 메뉴 개수") @RequestParam(defaultValue = "5") int limit) {
|
||||
|
||||
List<StoreMenuListResponse> response = menuUseCase.getStoreMenus(storeId);
|
||||
// 인기 메뉴 조회 로직 추가 필요
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.ktds.hi.store.infra.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "메뉴 가용성 변경 요청")
|
||||
public class MenuAvailabilityRequest {
|
||||
|
||||
@NotNull(message = "가용성 여부는 필수입니다.")
|
||||
@Schema(description = "판매 가능 여부", example = "true", required = true)
|
||||
private Boolean isAvailable;
|
||||
|
||||
@Schema(description = "변경 사유", example = "재료 소진으로 인한 일시 판매 중단")
|
||||
private String reason;
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.ktds.hi.store.infra.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 메뉴 등록 요청 DTO
|
||||
*
|
||||
* @author 하이오더 개발팀
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "메뉴 등록 요청")
|
||||
public class MenuCreateRequest {
|
||||
|
||||
@NotBlank(message = "메뉴명은 필수입니다.")
|
||||
@Schema(description = "메뉴명", example = "치킨버거")
|
||||
private String menuName;
|
||||
|
||||
@Schema(description = "메뉴 설명", example = "바삭한 치킨과 신선한 야채가 들어간 버거")
|
||||
private String description;
|
||||
|
||||
@NotNull(message = "가격은 필수입니다.")
|
||||
@Min(value = 0, message = "가격은 0원 이상이어야 합니다.")
|
||||
@Schema(description = "가격", example = "8500")
|
||||
private Integer price;
|
||||
|
||||
@Schema(description = "카테고리", example = "메인메뉴")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "이미지 URL", example = "/images/chicken-burger.jpg")
|
||||
private String imageUrl;
|
||||
|
||||
@Schema(description = "판매 가능 여부", example = "true")
|
||||
private Boolean isAvailable;
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package com.ktds.hi.store.infra.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 메뉴 수정 요청 DTO
|
||||
*
|
||||
* @author 하이오더 개발팀
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "메뉴 수정 요청")
|
||||
public class MenuUpdateRequest {
|
||||
|
||||
@Schema(description = "메뉴명", example = "치킨버거")
|
||||
private String menuName;
|
||||
|
||||
@Schema(description = "메뉴 설명", example = "바삭한 치킨과 신선한 야채가 들어간 버거")
|
||||
private String description;
|
||||
|
||||
@Min(value = 0, message = "가격은 0원 이상이어야 합니다.")
|
||||
@Schema(description = "가격", example = "8500")
|
||||
private Integer price;
|
||||
|
||||
@Schema(description = "카테고리", example = "메인메뉴")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "이미지 URL", example = "/images/chicken-burger.jpg")
|
||||
private String imageUrl;
|
||||
|
||||
@Schema(description = "판매 가능 여부", example = "true")
|
||||
private Boolean isAvailable;
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package com.ktds.hi.store.infra.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 메뉴 등록 요청 DTO
|
||||
*
|
||||
* @author 하이오더 개발팀
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "메뉴 등록 응답")
|
||||
public class MenuCreateResponse {
|
||||
|
||||
@Schema(description = "생성된 메뉴 ID", example = "1")
|
||||
private Long menuId;
|
||||
|
||||
@Schema(description = "응답 메시지", example = "메뉴가 성공적으로 등록되었습니다.")
|
||||
private String message;
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.ktds.hi.store.infra.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 메뉴 상세 응답 DTO
|
||||
*
|
||||
* @author 하이오더 개발팀
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "메뉴 상세 정보")
|
||||
public class MenuDetailResponse {
|
||||
|
||||
@Schema(description = "메뉴 ID", example = "1")
|
||||
private Long menuId;
|
||||
|
||||
@Schema(description = "매장 ID", example = "1")
|
||||
private Long storeId;
|
||||
|
||||
@Schema(description = "메뉴명", example = "치킨버거")
|
||||
private String menuName;
|
||||
|
||||
@Schema(description = "메뉴 설명", example = "바삭한 치킨과 신선한 야채가 들어간 버거")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "가격", example = "8500")
|
||||
private Integer price;
|
||||
|
||||
@Schema(description = "카테고리", example = "메인메뉴")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "이미지 URL", example = "/images/chicken-burger.jpg")
|
||||
private String imageUrl;
|
||||
|
||||
@Schema(description = "판매 가능 여부", example = "true")
|
||||
private Boolean isAvailable;
|
||||
|
||||
@Schema(description = "주문 횟수", example = "25")
|
||||
private Integer orderCount;
|
||||
|
||||
@Schema(description = "생성일시", example = "2024-01-15T10:30:00")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "수정일시", example = "2024-01-15T10:30:00")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.ktds.hi.store.infra.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 메뉴 수정 응답 DTO
|
||||
*
|
||||
* @author 하이오더 개발팀
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "메뉴 수정 응답")
|
||||
public class MenuUpdateResponse {
|
||||
|
||||
@Schema(description = "수정된 메뉴 ID", example = "1")
|
||||
private Long menuId;
|
||||
|
||||
@Schema(description = "응답 메시지", example = "메뉴가 성공적으로 수정되었습니다.")
|
||||
private String message;
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
// store/src/main/java/com/ktds/hi/store/infra/dto/response/StoreMenuListResponse.java
|
||||
package com.ktds.hi.store.infra.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 매장 메뉴 목록 응답 DTO
|
||||
* 매장의 메뉴 목록을 조회할 때 사용
|
||||
*
|
||||
* @author 하이오더 개발팀
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "매장 메뉴 목록 응답")
|
||||
public class StoreMenuListResponse {
|
||||
|
||||
@Schema(description = "메뉴 ID", example = "1")
|
||||
private Long menuId;
|
||||
|
||||
@Schema(description = "메뉴명", example = "치킨버거")
|
||||
private String menuName;
|
||||
|
||||
@Schema(description = "메뉴 설명", example = "바삭한 치킨과 신선한 야채가 들어간 버거")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "가격", example = "8500")
|
||||
private Integer price;
|
||||
|
||||
@Schema(description = "카테고리", example = "메인메뉴")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "이미지 URL", example = "/images/chicken-burger.jpg")
|
||||
private String imageUrl;
|
||||
|
||||
@Schema(description = "판매 가능 여부", example = "true")
|
||||
private Boolean isAvailable;
|
||||
|
||||
@Schema(description = "주문 횟수", example = "25")
|
||||
private Integer orderCount;
|
||||
|
||||
/**
|
||||
* 메뉴가 판매 가능한지 확인
|
||||
*/
|
||||
public boolean isMenuAvailable() {
|
||||
return isAvailable != null && isAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* 가격을 포맷된 문자열로 반환
|
||||
*/
|
||||
public String getFormattedPrice() {
|
||||
if (price == null) {
|
||||
return "0원";
|
||||
}
|
||||
return String.format("%,d원", price);
|
||||
}
|
||||
|
||||
/**
|
||||
* 인기 메뉴 여부 확인 (주문 횟수 기준)
|
||||
*/
|
||||
public boolean isPopularMenu() {
|
||||
return orderCount != null && orderCount >= 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 상태 텍스트 반환
|
||||
*/
|
||||
public String getStatusText() {
|
||||
if (isMenuAvailable()) {
|
||||
return "판매중";
|
||||
} else {
|
||||
return "품절";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 빌더 패턴을 위한 정적 메서드
|
||||
*/
|
||||
public static StoreMenuListResponse of(Long menuId, String menuName, String description,
|
||||
Integer price, String category, String imageUrl,
|
||||
Boolean isAvailable, Integer orderCount) {
|
||||
return StoreMenuListResponse.builder()
|
||||
.menuId(menuId)
|
||||
.menuName(menuName)
|
||||
.description(description)
|
||||
.price(price)
|
||||
.category(category)
|
||||
.imageUrl(imageUrl)
|
||||
.isAvailable(isAvailable)
|
||||
.orderCount(orderCount)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
package com.ktds.hi.store.infra.gateway;
|
||||
|
||||
import com.ktds.hi.store.domain.Menu;
|
||||
import com.ktds.hi.store.biz.usecase.out.MenuRepositoryPort;
|
||||
import com.ktds.hi.store.infra.gateway.entity.MenuEntity;
|
||||
import com.ktds.hi.store.infra.gateway.repository.MenuJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 메뉴 JPA 어댑터
|
||||
* 메뉴 리포지토리 포트의 JPA 구현체
|
||||
*
|
||||
* @author 하이오더 개발팀
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MenuJpaAdapter implements MenuRepositoryPort {
|
||||
|
||||
private final MenuJpaRepository menuJpaRepository;
|
||||
|
||||
@Override
|
||||
public List<Menu> findMenusByStoreId(Long storeId) {
|
||||
List<MenuEntity> entities = menuJpaRepository.findByStoreId(storeId);
|
||||
return entities.stream()
|
||||
.map(MenuEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Menu> findMenuById(Long menuId) {
|
||||
return menuJpaRepository.findById(menuId)
|
||||
.map(MenuEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Menu saveMenu(Menu menu) {
|
||||
MenuEntity entity = MenuEntity.fromDomain(menu);
|
||||
MenuEntity savedEntity = menuJpaRepository.save(entity);
|
||||
return savedEntity.toDomain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMenu(Long menuId) {
|
||||
menuJpaRepository.deleteById(menuId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Menu> findAvailableMenusByStoreId(Long storeId) {
|
||||
List<MenuEntity> entities = menuJpaRepository.findByStoreIdAndIsAvailableTrue(storeId);
|
||||
return entities.stream()
|
||||
.map(MenuEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Menu> findMenusByStoreIdAndCategory(Long storeId, String category) {
|
||||
List<MenuEntity> entities = menuJpaRepository.findByStoreIdAndCategoryAndIsAvailableTrue(storeId, category);
|
||||
return entities.stream()
|
||||
.map(MenuEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Menu> saveMenus(List<Menu> menus) {
|
||||
List<MenuEntity> entities = menus.stream()
|
||||
.map(MenuEntity::fromDomain)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<MenuEntity> savedEntities = menuJpaRepository.saveAll(entities);
|
||||
|
||||
return savedEntities.stream()
|
||||
.map(MenuEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMenusByStoreId(Long storeId) {
|
||||
menuJpaRepository.deleteByStoreId(storeId);
|
||||
}
|
||||
}
|
||||
@ -1,61 +1,106 @@
|
||||
package com.ktds.hi.store.infra.gateway.entity;
|
||||
|
||||
import com.ktds.hi.store.domain.Menu;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Data;
|
||||
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.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 메뉴 엔티티 클래스
|
||||
* 데이터베이스 menus 테이블과 매핑되는 JPA 엔티티
|
||||
* 메뉴 엔티티
|
||||
* 메뉴 정보를 데이터베이스에 저장하기 위한 JPA 엔티티
|
||||
*
|
||||
* @author 하이오더 개발팀
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "menus")
|
||||
@Getter
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class MenuEntity {
|
||||
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "menu_id")
|
||||
private Long id;
|
||||
|
||||
|
||||
@Column(name = "store_id", nullable = false)
|
||||
private Long storeId;
|
||||
|
||||
|
||||
@Column(name = "menu_name", nullable = false, length = 100)
|
||||
private String menuName;
|
||||
|
||||
@Column(length = 500)
|
||||
|
||||
@Column(name = "description", columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(nullable = false)
|
||||
|
||||
@Column(name = "price", nullable = false)
|
||||
private Integer price;
|
||||
|
||||
@Column(length = 50)
|
||||
|
||||
@Column(name = "category", length = 50)
|
||||
private String category;
|
||||
|
||||
|
||||
@Column(name = "image_url", length = 500)
|
||||
private String imageUrl;
|
||||
|
||||
@Column(name = "is_available")
|
||||
|
||||
@Column(name = "is_available", nullable = false)
|
||||
private Boolean isAvailable;
|
||||
|
||||
@Column(name = "order_count", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean isAvailable = true;
|
||||
|
||||
private Integer orderCount = 0;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", updatable = false)
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "updated_at")
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 엔티티를 도메인 객체로 변환
|
||||
*/
|
||||
public Menu toDomain() {
|
||||
return Menu.builder()
|
||||
.id(this.id)
|
||||
.storeId(this.storeId)
|
||||
.menuName(this.menuName)
|
||||
.description(this.description)
|
||||
.price(this.price)
|
||||
.category(this.category)
|
||||
.imageUrl(this.imageUrl)
|
||||
.available(this.isAvailable)
|
||||
.orderCount(this.orderCount)
|
||||
.createdAt(this.createdAt)
|
||||
.updatedAt(this.updatedAt)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 도메인 객체를 엔티티로 변환
|
||||
*/
|
||||
public static MenuEntity fromDomain(Menu menu) {
|
||||
return MenuEntity.builder()
|
||||
.id(menu.getId())
|
||||
.storeId(menu.getStoreId())
|
||||
.menuName(menu.getMenuName())
|
||||
.description(menu.getDescription())
|
||||
.price(menu.getPrice())
|
||||
.category(menu.getCategory())
|
||||
.imageUrl(menu.getImageUrl())
|
||||
.isAvailable(menu.getAvailable())
|
||||
.orderCount(menu.getOrderCount())
|
||||
.createdAt(menu.getCreatedAt())
|
||||
.updatedAt(menu.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@ package com.ktds.hi.store.infra.gateway.repository;
|
||||
|
||||
import com.ktds.hi.store.infra.gateway.entity.MenuEntity;
|
||||
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;
|
||||
@ -12,24 +14,47 @@ import java.util.List;
|
||||
*/
|
||||
@Repository
|
||||
public interface MenuJpaRepository extends JpaRepository<MenuEntity, Long> {
|
||||
|
||||
|
||||
/**
|
||||
* 매장 ID로 이용 가능한 메뉴 목록 조회
|
||||
*/
|
||||
List<MenuEntity> findByStoreIdAndIsAvailableTrue(Long storeId);
|
||||
|
||||
|
||||
/**
|
||||
* 매장 ID로 모든 메뉴 목록 조회
|
||||
*/
|
||||
List<MenuEntity> findByStoreId(Long storeId);
|
||||
|
||||
|
||||
/**
|
||||
* 매장 ID로 메뉴 삭제
|
||||
*/
|
||||
void deleteByStoreId(Long storeId);
|
||||
|
||||
|
||||
/**
|
||||
* 카테고리별 메뉴 조회
|
||||
* 카테고리별 이용 가능한 메뉴 조회
|
||||
*/
|
||||
List<MenuEntity> findByStoreIdAndCategoryAndIsAvailableTrue(Long storeId, String category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 매장 ID와 카테고리로 모든 메뉴 조회 (가용성 무관)
|
||||
*/
|
||||
List<MenuEntity> findByStoreIdAndCategory(Long storeId, String category);
|
||||
|
||||
/**
|
||||
* 매장별 메뉴 개수 조회
|
||||
*/
|
||||
@Query("SELECT COUNT(m) FROM MenuEntity m WHERE m.storeId = :storeId")
|
||||
Long countByStoreId(@Param("storeId") Long storeId);
|
||||
|
||||
/**
|
||||
* 매장별 이용 가능한 메뉴 개수 조회
|
||||
*/
|
||||
@Query("SELECT COUNT(m) FROM MenuEntity m WHERE m.storeId = :storeId AND m.isAvailable = true")
|
||||
Long countByStoreIdAndIsAvailableTrue(@Param("storeId") Long storeId);
|
||||
|
||||
/**
|
||||
* 인기 메뉴 조회 (주문 횟수 기준 상위 N개)
|
||||
*/
|
||||
@Query("SELECT m FROM MenuEntity m WHERE m.storeId = :storeId AND m.isAvailable = true ORDER BY m.orderCount DESC")
|
||||
List<MenuEntity> findPopularMenusByStoreId(@Param("storeId") Long storeId);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user