Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
b25c2edcc0
1
.github/workflows/member-ci.yml
vendored
1
.github/workflows/member-ci.yml
vendored
@ -23,6 +23,7 @@ env:
|
||||
MANIFEST_REPO: dg04-hi/hi-manifest
|
||||
MANIFEST_FILE_PATH: member/deployment.yml
|
||||
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@ -12,6 +12,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@ -119,32 +120,283 @@ public class AnalyticsService implements AnalyticsUseCase {
|
||||
|
||||
@Override
|
||||
public StoreStatisticsResponse getStoreStatistics(Long storeId, LocalDate startDate, LocalDate endDate) {
|
||||
// 이전 구현과 동일
|
||||
return null; // 구현 생략
|
||||
log.info("매장 통계 조회 시작: storeId={}, startDate={}, endDate={}", storeId, startDate, endDate);
|
||||
|
||||
try {
|
||||
// 1. 캐시 키 생성
|
||||
String cacheKey = String.format("statistics:store:%d:%s:%s", storeId, startDate, endDate);
|
||||
var cachedResult = cachePort.getAnalyticsCache(cacheKey);
|
||||
if (cachedResult.isPresent()) {
|
||||
log.info("캐시에서 통계 데이터 반환: storeId={}", storeId);
|
||||
return (StoreStatisticsResponse) cachedResult.get();
|
||||
}
|
||||
|
||||
// 2. 주문 통계 데이터 조회 (실제 OrderStatistics 도메인 필드 사용)
|
||||
var orderStatistics = orderDataPort.getOrderStatistics(storeId, startDate, endDate);
|
||||
|
||||
// 3. 응답 생성
|
||||
StoreStatisticsResponse response = StoreStatisticsResponse.builder()
|
||||
.storeId(storeId)
|
||||
.startDate(startDate)
|
||||
.endDate(endDate)
|
||||
.totalOrders(orderStatistics.getTotalOrders())
|
||||
.totalRevenue(orderStatistics.getTotalRevenue())
|
||||
.averageOrderValue(orderStatistics.getAverageOrderValue())
|
||||
.peakHour(orderStatistics.getPeakHour())
|
||||
.popularMenus(orderStatistics.getPopularMenus())
|
||||
.customerAgeDistribution(orderStatistics.getCustomerAgeDistribution())
|
||||
.build();
|
||||
|
||||
// 4. 캐시에 저장
|
||||
cachePort.putAnalyticsCache(cacheKey, response, java.time.Duration.ofMinutes(30));
|
||||
|
||||
log.info("매장 통계 조회 완료: storeId={}", storeId);
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("매장 통계 조회 중 오류 발생: storeId={}", storeId, e);
|
||||
throw new RuntimeException("매장 통계 조회에 실패했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiFeedbackSummaryResponse getAIFeedbackSummary(Long storeId) {
|
||||
// 이전 구현과 동일
|
||||
return null; // 구현 생략
|
||||
log.info("AI 피드백 요약 조회 시작: storeId={}", storeId);
|
||||
|
||||
try {
|
||||
// 1. 캐시에서 확인
|
||||
String cacheKey = "ai_feedback_summary:store:" + storeId;
|
||||
var cachedResult = cachePort.getAnalyticsCache(cacheKey);
|
||||
if (cachedResult.isPresent()) {
|
||||
return (AiFeedbackSummaryResponse) cachedResult.get();
|
||||
}
|
||||
|
||||
// 2. AI 피드백 조회
|
||||
var aiFeedback = analyticsPort.findAIFeedbackByStoreId(storeId);
|
||||
|
||||
if (aiFeedback.isEmpty()) {
|
||||
// 3. 피드백이 없으면 기본 응답 생성
|
||||
AiFeedbackSummaryResponse emptyResponse = AiFeedbackSummaryResponse.builder()
|
||||
.storeId(storeId)
|
||||
.hasData(false)
|
||||
.message("분석할 데이터가 부족합니다.")
|
||||
.lastUpdated(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
cachePort.putAnalyticsCache(cacheKey, emptyResponse, java.time.Duration.ofHours(1));
|
||||
return emptyResponse;
|
||||
}
|
||||
|
||||
// 4. 응답 생성
|
||||
AiFeedbackSummaryResponse response = AiFeedbackSummaryResponse.builder()
|
||||
.storeId(storeId)
|
||||
.hasData(true)
|
||||
.message("AI 분석이 완료되었습니다.")
|
||||
.overallScore(aiFeedback.get().getConfidenceScore())
|
||||
.keyInsight(aiFeedback.get().getSummary())
|
||||
.priorityRecommendation(getFirstRecommendation(aiFeedback.get()))
|
||||
.lastUpdated(aiFeedback.get().getUpdatedAt())
|
||||
.build();
|
||||
|
||||
// 5. 캐시에 저장
|
||||
cachePort.putAnalyticsCache(cacheKey, response, java.time.Duration.ofHours(2));
|
||||
|
||||
log.info("AI 피드백 요약 조회 완료: storeId={}", storeId);
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI 피드백 요약 조회 중 오류 발생: storeId={}", storeId, e);
|
||||
throw new RuntimeException("AI 피드백 요약 조회에 실패했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReviewAnalysisResponse getReviewAnalysis(Long storeId) {
|
||||
// 이전 구현과 동일
|
||||
return null; // 구현 생략
|
||||
log.info("리뷰 분석 조회 시작: storeId={}", storeId);
|
||||
|
||||
try {
|
||||
// 1. 캐시에서 확인
|
||||
String cacheKey = "review_analysis:store:" + storeId;
|
||||
var cachedResult = cachePort.getAnalyticsCache(cacheKey);
|
||||
if (cachedResult.isPresent()) {
|
||||
return (ReviewAnalysisResponse) cachedResult.get();
|
||||
}
|
||||
|
||||
// 2. 최근 리뷰 데이터 조회 (30일)
|
||||
List<String> recentReviews = externalReviewPort.getRecentReviews(storeId, 30);
|
||||
|
||||
if (recentReviews.isEmpty()) {
|
||||
ReviewAnalysisResponse emptyResponse = ReviewAnalysisResponse.builder()
|
||||
.storeId(storeId)
|
||||
.totalReviews(0)
|
||||
.positiveReviewCount(0)
|
||||
.negativeReviewCount(0)
|
||||
.positiveRate(0.0)
|
||||
.negativeRate(0.0)
|
||||
.analysisDate(LocalDate.now())
|
||||
.build();
|
||||
|
||||
cachePort.putAnalyticsCache(cacheKey, emptyResponse, java.time.Duration.ofHours(1));
|
||||
return emptyResponse;
|
||||
}
|
||||
|
||||
// 3. 응답 생성
|
||||
int positiveCount = countPositiveReviews(recentReviews);
|
||||
int negativeCount = countNegativeReviews(recentReviews);
|
||||
int totalCount = recentReviews.size();
|
||||
|
||||
ReviewAnalysisResponse response = ReviewAnalysisResponse.builder()
|
||||
.storeId(storeId)
|
||||
.totalReviews(totalCount)
|
||||
.positiveReviewCount(positiveCount)
|
||||
.negativeReviewCount(negativeCount)
|
||||
.positiveRate((double) positiveCount / totalCount * 100)
|
||||
.negativeRate((double) negativeCount / totalCount * 100)
|
||||
.analysisDate(LocalDate.now())
|
||||
.build();
|
||||
|
||||
// 4. 캐시에 저장
|
||||
cachePort.putAnalyticsCache(cacheKey, response, java.time.Duration.ofHours(4));
|
||||
|
||||
log.info("리뷰 분석 조회 완료: storeId={}", storeId);
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("리뷰 분석 중 오류 발생: storeId={}", storeId, e);
|
||||
throw new RuntimeException("리뷰 분석에 실패했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// private 메서드들
|
||||
@Transactional
|
||||
private Analytics generateNewAnalytics(Long storeId) {
|
||||
// 이전 구현과 동일
|
||||
return null; // 구현 생략
|
||||
public Analytics generateNewAnalytics(Long storeId) {
|
||||
log.info("새로운 분석 데이터 생성 시작: storeId={}", storeId);
|
||||
|
||||
try {
|
||||
// 1. 리뷰 데이터 수집
|
||||
List<String> reviewData = externalReviewPort.getReviewData(storeId);
|
||||
int totalReviews = reviewData.size();
|
||||
|
||||
if (totalReviews == 0) {
|
||||
log.warn("리뷰 데이터가 없어 기본값으로 분석 데이터 생성: storeId={}", storeId);
|
||||
return createDefaultAnalytics(storeId);
|
||||
}
|
||||
|
||||
// 2. 기본 통계 계산
|
||||
double averageRating = 4.0; // 기본값
|
||||
double sentimentScore = 0.5; // 중립
|
||||
double positiveRate = 60.0;
|
||||
double negativeRate = 20.0;
|
||||
|
||||
// 3. Analytics 도메인 객체 생성
|
||||
Analytics analytics = Analytics.builder()
|
||||
.storeId(storeId)
|
||||
.totalReviews(totalReviews)
|
||||
.averageRating(averageRating)
|
||||
.sentimentScore(sentimentScore)
|
||||
.positiveReviewRate(positiveRate)
|
||||
.negativeReviewRate(negativeRate)
|
||||
.lastAnalysisDate(LocalDateTime.now())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 4. 데이터베이스에 저장
|
||||
Analytics saved = analyticsPort.saveAnalytics(analytics);
|
||||
|
||||
log.info("새로운 분석 데이터 생성 완료: storeId={}", storeId);
|
||||
return saved;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("분석 데이터 생성 중 오류 발생: storeId={}", storeId, e);
|
||||
return createDefaultAnalytics(storeId);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
private AiFeedback generateAIFeedback(Long storeId) {
|
||||
// 이전 구현과 동일
|
||||
return null; // 구현 생략
|
||||
public AiFeedback generateAIFeedback(Long storeId) {
|
||||
log.info("AI 피드백 생성 시작: storeId={}", storeId);
|
||||
|
||||
try {
|
||||
// 1. 최근 30일 리뷰 데이터 수집
|
||||
List<String> reviewData = externalReviewPort.getRecentReviews(storeId, 30);
|
||||
|
||||
if (reviewData.isEmpty()) {
|
||||
log.warn("AI 피드백 생성을 위한 리뷰 데이터가 없습니다: storeId={}", storeId);
|
||||
return createDefaultAIFeedback(storeId);
|
||||
}
|
||||
|
||||
// 2. AI 피드백 생성 (실제로는 AI 서비스 호출)
|
||||
AiFeedback aiFeedback = AiFeedback.builder()
|
||||
.storeId(storeId)
|
||||
.summary("고객들의 전반적인 만족도가 높습니다.")
|
||||
.positivePoints(List.of("맛이 좋다", "서비스가 친절하다", "분위기가 좋다"))
|
||||
.improvementPoints(List.of("대기시간 단축", "가격 경쟁력", "메뉴 다양성"))
|
||||
.recommendations(List.of("특별 메뉴 개발", "예약 시스템 도입", "고객 서비스 교육"))
|
||||
.sentimentAnalysis("POSITIVE")
|
||||
.confidenceScore(0.85)
|
||||
.generatedAt(LocalDateTime.now())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 3. 데이터베이스에 저장
|
||||
AiFeedback saved = analyticsPort.saveAIFeedback(aiFeedback);
|
||||
|
||||
log.info("AI 피드백 생성 완료: storeId={}", storeId);
|
||||
return saved;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI 피드백 생성 중 오류 발생: storeId={}", storeId, e);
|
||||
return createDefaultAIFeedback(storeId);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private Analytics createDefaultAnalytics(Long storeId) {
|
||||
return Analytics.builder()
|
||||
.storeId(storeId)
|
||||
.totalReviews(0)
|
||||
.averageRating(0.0)
|
||||
.sentimentScore(0.0)
|
||||
.positiveReviewRate(0.0)
|
||||
.negativeReviewRate(0.0)
|
||||
.lastAnalysisDate(LocalDateTime.now())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
private AiFeedback createDefaultAIFeedback(Long storeId) {
|
||||
return AiFeedback.builder()
|
||||
.storeId(storeId)
|
||||
.summary("분석할 리뷰 데이터가 부족합니다.")
|
||||
.positivePoints(List.of("데이터 부족으로 분석 불가"))
|
||||
.improvementPoints(List.of("리뷰 데이터 수집 필요"))
|
||||
.recommendations(List.of("고객들의 리뷰 작성을 유도해보세요"))
|
||||
.sentimentAnalysis("NEUTRAL")
|
||||
.confidenceScore(0.0)
|
||||
.generatedAt(LocalDateTime.now())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
private String getFirstRecommendation(AiFeedback feedback) {
|
||||
if (feedback.getRecommendations() != null && !feedback.getRecommendations().isEmpty()) {
|
||||
return feedback.getRecommendations().get(0);
|
||||
}
|
||||
return "추천사항이 없습니다.";
|
||||
}
|
||||
|
||||
private int countPositiveReviews(List<String> reviews) {
|
||||
// 실제로는 AI 서비스를 통한 감정 분석 필요
|
||||
return (int) (reviews.size() * 0.6); // 60% 가정
|
||||
}
|
||||
|
||||
private int countNegativeReviews(List<String> reviews) {
|
||||
// 실제로는 AI 서비스를 통한 감정 분석 필요
|
||||
return (int) (reviews.size() * 0.2); // 20% 가정
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Analytics 서비스 보안 설정 클래스
|
||||
@ -14,12 +17,17 @@ import org.springframework.security.web.SecurityFilterChain;
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
|
||||
private final CorsConfigurationSource corsConfigurationSource;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Swagger 관련 경로 모두 허용
|
||||
|
||||
@ -23,5 +23,23 @@ public class SwaggerConfig {
|
||||
.description("하이오더 분석 서비스 API 문서")
|
||||
.version("1.0.0"));
|
||||
}
|
||||
/**
|
||||
* JWT Bearer 토큰을 위한 Security Scheme 생성
|
||||
*/
|
||||
private SecurityScheme createAPIKeyScheme() {
|
||||
return new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.name("Authorization")
|
||||
.description("""
|
||||
JWT 토큰을 입력하세요
|
||||
|
||||
사용법:
|
||||
1. 로그인 API로 토큰 발급
|
||||
2. Bearer 접두사 없이 토큰만 입력
|
||||
3. 예: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOi...
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ spring:
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:create}
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
|
||||
102
common/src/main/java/com/ktds/hi/common/config/CorsConfig.java
Normal file
102
common/src/main/java/com/ktds/hi/common/config/CorsConfig.java
Normal file
@ -0,0 +1,102 @@
|
||||
package com.ktds.hi.common.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 전체 서비스 통합 CORS 설정 클래스
|
||||
* 모든 마이크로서비스에서 공통으로 사용되는 CORS 정책을 정의
|
||||
*/
|
||||
@Configuration
|
||||
public class CorsConfig implements WebMvcConfigurer {
|
||||
|
||||
@Value("${app.cors.allowed-origins:http://20.214.126.84,http://localhost:3000}")
|
||||
private String allowedOrigins;
|
||||
|
||||
@Value("${app.cors.allowed-methods:GET,POST,PUT,DELETE,OPTIONS}")
|
||||
private String allowedMethods;
|
||||
|
||||
@Value("${app.cors.allowed-headers:*}")
|
||||
private String allowedHeaders;
|
||||
|
||||
@Value("${app.cors.exposed-headers:Authorization,X-Total-Count}")
|
||||
private String exposedHeaders;
|
||||
|
||||
@Value("${app.cors.allow-credentials:true}")
|
||||
private boolean allowCredentials;
|
||||
|
||||
@Value("${app.cors.max-age:3600}")
|
||||
private long maxAge;
|
||||
|
||||
/**
|
||||
* WebMvcConfigurer를 통한 CORS 설정
|
||||
*/
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns(allowedOrigins.split(","))
|
||||
.allowedMethods(allowedMethods.split(","))
|
||||
.allowedHeaders(allowedHeaders.split(","))
|
||||
.exposedHeaders(exposedHeaders.split(","))
|
||||
.allowCredentials(allowCredentials)
|
||||
.maxAge(maxAge);
|
||||
}
|
||||
|
||||
/**
|
||||
* CorsConfigurationSource Bean 생성
|
||||
* Spring Security와 함께 사용되는 CORS 설정
|
||||
*/
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
|
||||
// Origin 설정
|
||||
List<String> origins = Arrays.asList(allowedOrigins.split(","));
|
||||
configuration.setAllowedOriginPatterns(origins);
|
||||
|
||||
// Method 설정
|
||||
List<String> methods = Arrays.asList(allowedMethods.split(","));
|
||||
configuration.setAllowedMethods(methods);
|
||||
|
||||
// Header 설정
|
||||
if ("*".equals(allowedHeaders)) {
|
||||
configuration.addAllowedHeader("*");
|
||||
} else {
|
||||
List<String> headers = Arrays.asList(allowedHeaders.split(","));
|
||||
configuration.setAllowedHeaders(headers);
|
||||
}
|
||||
|
||||
// Exposed Headers 설정
|
||||
List<String> exposed = Arrays.asList(exposedHeaders.split(","));
|
||||
configuration.setExposedHeaders(exposed);
|
||||
|
||||
// Credentials 설정
|
||||
configuration.setAllowCredentials(allowCredentials);
|
||||
|
||||
// Max Age 설정
|
||||
configuration.setMaxAge(maxAge);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* CorsFilter Bean 생성
|
||||
* 글로벌 CORS 필터로 사용
|
||||
*/
|
||||
@Bean
|
||||
public CorsFilter corsFilter() {
|
||||
return new CorsFilter(corsConfigurationSource());
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@ spring:
|
||||
# JPA 설정
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:validate}
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
naming:
|
||||
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
|
||||
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
|
||||
@ -51,13 +51,12 @@ app:
|
||||
secret-key: ${JWT_SECRET_KEY:hiorder-secret-key-for-jwt-token-generation-2024-very-long-secret-key}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600000} # 1시간
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800000} # 7일
|
||||
|
||||
# CORS 설정
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:8080}
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://20.214.126.84,http://localhost:8080}
|
||||
allowed-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS}
|
||||
allowed-headers: ${CORS_ALLOWED_HEADERS:*}
|
||||
exposed-headers: ${CORS_EXPOSED_HEADERS:Authorization}
|
||||
exposed-headers: ${CORS_EXPOSED_HEADERS:Authorization, X-Total-Count}
|
||||
allow-credentials: ${CORS_ALLOW_CREDENTIALS:true}
|
||||
max-age: ${CORS_MAX_AGE:3600}
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ktds.hi.common.security.JwtTokenProvider;
|
||||
import com.ktds.hi.common.security.JwtAuthenticationFilter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpoint;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
@ -16,6 +18,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
|
||||
/**
|
||||
* Spring Security 설정 클래스
|
||||
@ -27,6 +30,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final CorsConfigurationSource corsConfigurationSource;
|
||||
|
||||
/**
|
||||
* 보안 필터 체인 설정
|
||||
@ -36,9 +40,10 @@ public class SecurityConfig {
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
.requestMatchers("/api/auth/**", "/api/members/register").permitAll()
|
||||
.requestMatchers("/api/auth/**", "/api/members/register", "/api/auth/login").permitAll()
|
||||
.requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||
.requestMatchers("/swagger-resources/**", "/webjars/**").permitAll()
|
||||
.requestMatchers("/actuator/**").permitAll()
|
||||
|
||||
@ -2,6 +2,7 @@ package com.ktds.hi.member.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@ -22,4 +23,17 @@ public class SwaggerConfig {
|
||||
.description("회원 가입, 로그인, 취향 관리 등 회원 관련 기능을 제공하는 API")
|
||||
.version("1.0.0"));
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT Bearer 토큰을 위한 Security Scheme 생성
|
||||
*/
|
||||
private SecurityScheme createAPIKeyScheme() {
|
||||
return new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.name("Authorization")
|
||||
.description("JWT 토큰을 입력하세요 (Bearer 접두사 제외)");
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ spring:
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:create}
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
package com.ktds.hi.recommend.infra.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Analytics 서비스 보안 설정 클래스
|
||||
* 테스트를 위해 모든 엔드포인트를 인증 없이 접근 가능하도록 설정
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final CorsConfigurationSource corsConfigurationSource;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
|
||||
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Swagger 관련 경로 모두 허용
|
||||
.requestMatchers("/swagger-ui.html","/swagger-ui/**", "/swagger-ui.html").permitAll()
|
||||
.requestMatchers("/api-docs/**", "/v3/api-docs/**").permitAll()
|
||||
.requestMatchers("/swagger-resources/**", "/webjars/**").permitAll()
|
||||
|
||||
// Analytics API 모두 허용 (테스트용)
|
||||
.requestMatchers("/api/analytics/**").permitAll()
|
||||
.requestMatchers("/api/action-plans/**").permitAll()
|
||||
|
||||
// Actuator 엔드포인트 허용
|
||||
.requestMatchers("/actuator/**").permitAll()
|
||||
|
||||
// 기타 모든 요청 허용 (테스트용)
|
||||
.anyRequest().permitAll()
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ package com.ktds.hi.recommend.infra.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@ -23,4 +24,24 @@ public class SwaggerConfig {
|
||||
.description("사용자 취향 기반 매장 추천 및 취향 분석 관련 기능을 제공하는 API")
|
||||
.version("1.0.0"));
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT Bearer 토큰을 위한 Security Scheme 생성
|
||||
*/
|
||||
private SecurityScheme createAPIKeyScheme() {
|
||||
return new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.name("Authorization")
|
||||
.description("""
|
||||
JWT 토큰을 입력하세요
|
||||
|
||||
사용법:
|
||||
1. 로그인 API로 토큰 발급
|
||||
2. Bearer 접두사 없이 토큰만 입력
|
||||
3. 예: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOi...
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ spring:
|
||||
# JPA 설정
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:create}
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
@ -132,7 +132,6 @@ management:
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
enabled: true
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
tags-sorter: alpha
|
||||
|
||||
@ -66,8 +66,7 @@ public class ReviewInteractor implements CreateReviewUseCase, DeleteReviewUseCas
|
||||
Review review = reviewRepository.findReviewByIdAndMemberId(reviewId, memberId)
|
||||
.orElseThrow(() -> new BusinessException("리뷰를 찾을 수 없거나 권한이 없습니다"));
|
||||
|
||||
Review deletedReview = review.updateStatus(ReviewStatus.DELETED);
|
||||
reviewRepository.saveReview(deletedReview);
|
||||
reviewRepository.deleteReview(reviewId);
|
||||
|
||||
log.info("리뷰 삭제 완료: reviewId={}, memberId={}", reviewId, memberId);
|
||||
|
||||
|
||||
@ -7,6 +7,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Analytics 서비스 보안 설정 클래스
|
||||
@ -14,12 +17,16 @@ import org.springframework.security.web.SecurityFilterChain;
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final CorsConfigurationSource corsConfigurationSource;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Swagger 관련 경로 모두 허용
|
||||
|
||||
@ -1,26 +1,34 @@
|
||||
package com.ktds.hi.review.infra.config;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Swagger 설정 클래스
|
||||
* API 문서화를 위한 OpenAPI 설정
|
||||
*/
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
final String securitySchemeName = "Bearer Authentication";
|
||||
|
||||
return new OpenAPI()
|
||||
.addServersItem(new Server().url("/"))
|
||||
.info(new Info()
|
||||
.title("하이오더 리뷰 관리 서비스 API")
|
||||
.description("리뷰 작성, 조회, 삭제, 반응, 댓글 등 리뷰 관련 기능을 제공하는 API")
|
||||
.version("1.0.0"));
|
||||
.version("1.0.0"))
|
||||
.addSecurityItem(new SecurityRequirement()
|
||||
.addList(securitySchemeName))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes(securitySchemeName, new SecurityScheme()
|
||||
.name(securitySchemeName)
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ spring:
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:create}
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
|
||||
@ -7,6 +7,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Analytics 서비스 보안 설정 클래스
|
||||
@ -14,12 +17,16 @@ import org.springframework.security.web.SecurityFilterChain;
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final CorsConfigurationSource corsConfigurationSource;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Swagger 관련 경로 모두 허용
|
||||
|
||||
@ -13,7 +13,7 @@ spring:
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:create}
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user