Merge remote-tracking branch 'origin/main'

This commit is contained in:
정유빈 2025-06-13 16:40:16 +09:00
commit b25c2edcc0
19 changed files with 528 additions and 36 deletions

View File

@ -23,6 +23,7 @@ env:
MANIFEST_REPO: dg04-hi/hi-manifest MANIFEST_REPO: dg04-hi/hi-manifest
MANIFEST_FILE_PATH: member/deployment.yml MANIFEST_FILE_PATH: member/deployment.yml
jobs: jobs:
build-and-push: build-and-push:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -12,6 +12,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -119,32 +120,283 @@ public class AnalyticsService implements AnalyticsUseCase {
@Override @Override
public StoreStatisticsResponse getStoreStatistics(Long storeId, LocalDate startDate, LocalDate endDate) { public StoreStatisticsResponse getStoreStatistics(Long storeId, LocalDate startDate, LocalDate endDate) {
// 이전 구현과 동일 log.info("매장 통계 조회 시작: storeId={}, startDate={}, endDate={}", storeId, startDate, endDate);
return null; // 구현 생략
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 @Override
public AiFeedbackSummaryResponse getAIFeedbackSummary(Long storeId) { public AiFeedbackSummaryResponse getAIFeedbackSummary(Long storeId) {
// 이전 구현과 동일 log.info("AI 피드백 요약 조회 시작: storeId={}", storeId);
return null; // 구현 생략
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 @Override
public ReviewAnalysisResponse getReviewAnalysis(Long storeId) { public ReviewAnalysisResponse getReviewAnalysis(Long storeId) {
// 이전 구현과 동일 log.info("리뷰 분석 조회 시작: storeId={}", storeId);
return null; // 구현 생략
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 메서드들 // private 메서드들
@Transactional @Transactional
private Analytics generateNewAnalytics(Long storeId) { public Analytics generateNewAnalytics(Long storeId) {
// 이전 구현과 동일 log.info("새로운 분석 데이터 생성 시작: storeId={}", storeId);
return null; // 구현 생략
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 @Transactional
private AiFeedback generateAIFeedback(Long storeId) { public AiFeedback generateAIFeedback(Long storeId) {
// 이전 구현과 동일 log.info("AI 피드백 생성 시작: storeId={}", storeId);
return null; // 구현 생략
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% 가정
} }
} }

View File

@ -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.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfigurationSource;
import lombok.RequiredArgsConstructor;
/** /**
* Analytics 서비스 보안 설정 클래스 * Analytics 서비스 보안 설정 클래스
@ -14,12 +17,17 @@ import org.springframework.security.web.SecurityFilterChain;
*/ */
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig { public class SecurityConfig {
private final CorsConfigurationSource corsConfigurationSource;
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http http
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
// Swagger 관련 경로 모두 허용 // Swagger 관련 경로 모두 허용

View File

@ -23,5 +23,23 @@ public class SwaggerConfig {
.description("하이오더 분석 서비스 API 문서") .description("하이오더 분석 서비스 API 문서")
.version("1.0.0")); .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...
""");
}
} }

View File

@ -18,7 +18,7 @@ spring:
jpa: jpa:
hibernate: hibernate:
ddl-auto: ${JPA_DDL_AUTO:create} ddl-auto: ${JPA_DDL_AUTO:update}
show-sql: ${JPA_SHOW_SQL:false} show-sql: ${JPA_SHOW_SQL:false}
properties: properties:
hibernate: hibernate:
@ -92,4 +92,4 @@ azure:
ai-analysis-events: ${AZURE_EVENTHUB_AI_ANALYSIS_EVENTS:ai-analysis-events} ai-analysis-events: ${AZURE_EVENTHUB_AI_ANALYSIS_EVENTS:ai-analysis-events}
storage: storage:
connection-string: ${AZURE_STORAGE_CONNECTION_STRING:DefaultEndpointsProtocol=https;AccountName=yourstorageaccount;AccountKey=your-storage-key;EndpointSuffix=core.windows.net} connection-string: ${AZURE_STORAGE_CONNECTION_STRING:DefaultEndpointsProtocol=https;AccountName=yourstorageaccount;AccountKey=your-storage-key;EndpointSuffix=core.windows.net}
container-name: ${AZURE_STORAGE_CONTAINER_NAME:eventhub-checkpoints} container-name: ${AZURE_STORAGE_CONTAINER_NAME:eventhub-checkpoints}

View 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());
}
}

View File

@ -2,7 +2,7 @@ spring:
# JPA 설정 # JPA 설정
jpa: jpa:
hibernate: hibernate:
ddl-auto: ${JPA_DDL_AUTO:validate} ddl-auto: ${JPA_DDL_AUTO:update}
naming: naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl 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} 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시간 access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600000} # 1시간
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800000} # 7일 refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800000} # 7일
# CORS 설정 # CORS 설정
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-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS}
allowed-headers: ${CORS_ALLOWED_HEADERS:*} 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} allow-credentials: ${CORS_ALLOW_CREDENTIALS:true}
max-age: ${CORS_MAX_AGE:3600} max-age: ${CORS_MAX_AGE:3600}

View File

@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.ktds.hi.common.security.JwtTokenProvider; import com.ktds.hi.common.security.JwtTokenProvider;
import com.ktds.hi.common.security.JwtAuthenticationFilter; import com.ktds.hi.common.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpoint;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager; 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.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfigurationSource;
/** /**
* Spring Security 설정 클래스 * Spring Security 설정 클래스
@ -27,6 +30,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
public class SecurityConfig { public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider; private final JwtTokenProvider jwtTokenProvider;
private final CorsConfigurationSource corsConfigurationSource;
/** /**
* 보안 필터 체인 설정 * 보안 필터 체인 설정
@ -36,9 +40,10 @@ public class SecurityConfig {
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http http
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz .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-ui.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/swagger-resources/**", "/webjars/**").permitAll() .requestMatchers("/swagger-resources/**", "/webjars/**").permitAll()
.requestMatchers("/actuator/**").permitAll() .requestMatchers("/actuator/**").permitAll()

View File

@ -2,6 +2,7 @@ package com.ktds.hi.member.config;
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info; 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 io.swagger.v3.oas.models.servers.Server;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -22,4 +23,17 @@ public class SwaggerConfig {
.description("회원 가입, 로그인, 취향 관리 등 회원 관련 기능을 제공하는 API") .description("회원 가입, 로그인, 취향 관리 등 회원 관련 기능을 제공하는 API")
.version("1.0.0")); .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 접두사 제외)");
}
} }

View File

@ -13,7 +13,7 @@ spring:
jpa: jpa:
hibernate: hibernate:
ddl-auto: ${JPA_DDL_AUTO:create} ddl-auto: ${JPA_DDL_AUTO:update}
show-sql: ${JPA_SHOW_SQL:false} show-sql: ${JPA_SHOW_SQL:false}
properties: properties:
hibernate: hibernate:

View File

@ -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();
}
}

View File

@ -2,6 +2,7 @@ package com.ktds.hi.recommend.infra.config;
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info; 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 io.swagger.v3.oas.models.servers.Server;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -23,4 +24,24 @@ public class SwaggerConfig {
.description("사용자 취향 기반 매장 추천 및 취향 분석 관련 기능을 제공하는 API") .description("사용자 취향 기반 매장 추천 및 취향 분석 관련 기능을 제공하는 API")
.version("1.0.0")); .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...
""");
}
} }

View File

@ -30,7 +30,7 @@ spring:
# JPA 설정 # JPA 설정
jpa: jpa:
hibernate: hibernate:
ddl-auto: ${JPA_DDL_AUTO:create} ddl-auto: ${JPA_DDL_AUTO:update}
show-sql: ${JPA_SHOW_SQL:false} show-sql: ${JPA_SHOW_SQL:false}
properties: properties:
hibernate: hibernate:
@ -132,7 +132,6 @@ management:
springdoc: springdoc:
api-docs: api-docs:
path: /api-docs path: /api-docs
enabled: true
swagger-ui: swagger-ui:
path: /swagger-ui.html path: /swagger-ui.html
tags-sorter: alpha tags-sorter: alpha

View File

@ -65,9 +65,8 @@ public class ReviewInteractor implements CreateReviewUseCase, DeleteReviewUseCas
public ReviewDeleteResponse deleteReview(Long reviewId, Long memberId) { public ReviewDeleteResponse deleteReview(Long reviewId, Long memberId) {
Review review = reviewRepository.findReviewByIdAndMemberId(reviewId, memberId) Review review = reviewRepository.findReviewByIdAndMemberId(reviewId, memberId)
.orElseThrow(() -> new BusinessException("리뷰를 찾을 수 없거나 권한이 없습니다")); .orElseThrow(() -> new BusinessException("리뷰를 찾을 수 없거나 권한이 없습니다"));
Review deletedReview = review.updateStatus(ReviewStatus.DELETED); reviewRepository.deleteReview(reviewId);
reviewRepository.saveReview(deletedReview);
log.info("리뷰 삭제 완료: reviewId={}, memberId={}", reviewId, memberId); log.info("리뷰 삭제 완료: reviewId={}, memberId={}", reviewId, memberId);

View File

@ -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.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfigurationSource;
import lombok.RequiredArgsConstructor;
/** /**
* Analytics 서비스 보안 설정 클래스 * Analytics 서비스 보안 설정 클래스
@ -14,12 +17,16 @@ import org.springframework.security.web.SecurityFilterChain;
*/ */
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig { public class SecurityConfig {
private final CorsConfigurationSource corsConfigurationSource;
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http http
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
// Swagger 관련 경로 모두 허용 // Swagger 관련 경로 모두 허용

View File

@ -1,26 +1,34 @@
package com.ktds.hi.review.infra.config; 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.OpenAPI;
import io.swagger.v3.oas.models.info.Info; 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 io.swagger.v3.oas.models.servers.Server;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
/**
* Swagger 설정 클래스
* API 문서화를 위한 OpenAPI 설정
*/
@Configuration @Configuration
public class SwaggerConfig { public class SwaggerConfig {
@Bean @Bean
public OpenAPI openAPI() { public OpenAPI openAPI() {
final String securitySchemeName = "Bearer Authentication";
return new OpenAPI() return new OpenAPI()
.addServersItem(new Server().url("/")) .addServersItem(new Server().url("/"))
.info(new Info() .info(new Info()
.title("하이오더 리뷰 관리 서비스 API") .title("하이오더 리뷰 관리 서비스 API")
.description("리뷰 작성, 조회, 삭제, 반응, 댓글 등 리뷰 관련 기능을 제공하는 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")));
} }
} }

View File

@ -13,7 +13,7 @@ spring:
jpa: jpa:
hibernate: hibernate:
ddl-auto: ${JPA_DDL_AUTO:create} ddl-auto: ${JPA_DDL_AUTO:update}
show-sql: ${JPA_SHOW_SQL:false} show-sql: ${JPA_SHOW_SQL:false}
properties: properties:
hibernate: hibernate:

View File

@ -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.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfigurationSource;
import lombok.RequiredArgsConstructor;
/** /**
* Analytics 서비스 보안 설정 클래스 * Analytics 서비스 보안 설정 클래스
@ -14,12 +17,16 @@ import org.springframework.security.web.SecurityFilterChain;
*/ */
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig { public class SecurityConfig {
private final CorsConfigurationSource corsConfigurationSource;
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http http
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
// Swagger 관련 경로 모두 허용 // Swagger 관련 경로 모두 허용

View File

@ -13,7 +13,7 @@ spring:
jpa: jpa:
hibernate: hibernate:
ddl-auto: ${JPA_DDL_AUTO:create} ddl-auto: ${JPA_DDL_AUTO:update}
show-sql: ${JPA_SHOW_SQL:false} show-sql: ${JPA_SHOW_SQL:false}
properties: properties:
hibernate: hibernate: