diff --git a/.github/workflows/member-ci.yml b/.github/workflows/member-ci.yml index 78339de..e313066 100644 --- a/.github/workflows/member-ci.yml +++ b/.github/workflows/member-ci.yml @@ -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 diff --git a/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java b/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java index 18c40af..2a36644 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java @@ -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 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 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 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 reviews) { + // 실제로는 AI 서비스를 통한 감정 분석 필요 + return (int) (reviews.size() * 0.6); // 60% 가정 + } + + private int countNegativeReviews(List reviews) { + // 실제로는 AI 서비스를 통한 감정 분석 필요 + return (int) (reviews.size() * 0.2); // 20% 가정 } } diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SecurityConfig.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SecurityConfig.java index 1a43aa3..722388e 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SecurityConfig.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SecurityConfig.java @@ -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 관련 경로 모두 허용 diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SwaggerConfig.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SwaggerConfig.java index 3f96b18..e894a51 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SwaggerConfig.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SwaggerConfig.java @@ -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... + """); + } } diff --git a/analytics/src/main/resources/application.yml b/analytics/src/main/resources/application.yml index 751ad1c..f9b1049 100644 --- a/analytics/src/main/resources/application.yml +++ b/analytics/src/main/resources/application.yml @@ -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: @@ -92,4 +92,4 @@ azure: ai-analysis-events: ${AZURE_EVENTHUB_AI_ANALYSIS_EVENTS:ai-analysis-events} storage: 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} \ No newline at end of file + container-name: ${AZURE_STORAGE_CONTAINER_NAME:eventhub-checkpoints} diff --git a/common/src/main/java/com/ktds/hi/common/config/CorsConfig.java b/common/src/main/java/com/ktds/hi/common/config/CorsConfig.java new file mode 100644 index 0000000..013a726 --- /dev/null +++ b/common/src/main/java/com/ktds/hi/common/config/CorsConfig.java @@ -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 origins = Arrays.asList(allowedOrigins.split(",")); + configuration.setAllowedOriginPatterns(origins); + + // Method 설정 + List methods = Arrays.asList(allowedMethods.split(",")); + configuration.setAllowedMethods(methods); + + // Header 설정 + if ("*".equals(allowedHeaders)) { + configuration.addAllowedHeader("*"); + } else { + List headers = Arrays.asList(allowedHeaders.split(",")); + configuration.setAllowedHeaders(headers); + } + + // Exposed Headers 설정 + List 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()); + } +} \ No newline at end of file diff --git a/common/src/main/resources/application-common.yml b/common/src/main/resources/application-common.yml index 06c3902..0d276bd 100644 --- a/common/src/main/resources/application-common.yml +++ b/common/src/main/resources/application-common.yml @@ -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} diff --git a/member/src/main/java/com/ktds/hi/member/config/SecurityConfig.java b/member/src/main/java/com/ktds/hi/member/config/SecurityConfig.java index 8a0e6b0..20dbd85 100644 --- a/member/src/main/java/com/ktds/hi/member/config/SecurityConfig.java +++ b/member/src/main/java/com/ktds/hi/member/config/SecurityConfig.java @@ -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() diff --git a/member/src/main/java/com/ktds/hi/member/config/SwaggerConfig.java b/member/src/main/java/com/ktds/hi/member/config/SwaggerConfig.java index fbd659f..993c465 100644 --- a/member/src/main/java/com/ktds/hi/member/config/SwaggerConfig.java +++ b/member/src/main/java/com/ktds/hi/member/config/SwaggerConfig.java @@ -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 접두사 제외)"); + } } diff --git a/member/src/main/resources/application.yml b/member/src/main/resources/application.yml index 6851aa9..202da20 100644 --- a/member/src/main/resources/application.yml +++ b/member/src/main/resources/application.yml @@ -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: diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SecurityConfig.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SecurityConfig.java new file mode 100644 index 0000000..f1b3a0c --- /dev/null +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SecurityConfig.java @@ -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(); + } +} diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SwaggerConfig.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SwaggerConfig.java index 425511b..f0e4f1d 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SwaggerConfig.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SwaggerConfig.java @@ -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... + """); + } } diff --git a/recommend/src/main/resources/application.yml b/recommend/src/main/resources/application.yml index 5a8062c..6f8573a 100644 --- a/recommend/src/main/resources/application.yml +++ b/recommend/src/main/resources/application.yml @@ -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 diff --git a/review/src/main/java/com/ktds/hi/review/biz/service/ReviewInteractor.java b/review/src/main/java/com/ktds/hi/review/biz/service/ReviewInteractor.java index d0ca3b3..b69a463 100644 --- a/review/src/main/java/com/ktds/hi/review/biz/service/ReviewInteractor.java +++ b/review/src/main/java/com/ktds/hi/review/biz/service/ReviewInteractor.java @@ -65,9 +65,8 @@ public class ReviewInteractor implements CreateReviewUseCase, DeleteReviewUseCas public ReviewDeleteResponse deleteReview(Long reviewId, Long memberId) { 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); diff --git a/review/src/main/java/com/ktds/hi/review/infra/config/SecurityConfig.java b/review/src/main/java/com/ktds/hi/review/infra/config/SecurityConfig.java index 92a1a7f..6c66b1a 100644 --- a/review/src/main/java/com/ktds/hi/review/infra/config/SecurityConfig.java +++ b/review/src/main/java/com/ktds/hi/review/infra/config/SecurityConfig.java @@ -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 관련 경로 모두 허용 diff --git a/review/src/main/java/com/ktds/hi/review/infra/config/SwaggerConfig.java b/review/src/main/java/com/ktds/hi/review/infra/config/SwaggerConfig.java index 051b36d..ff1f042 100644 --- a/review/src/main/java/com/ktds/hi/review/infra/config/SwaggerConfig.java +++ b/review/src/main/java/com/ktds/hi/review/infra/config/SwaggerConfig.java @@ -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"))); } -} - +} \ No newline at end of file diff --git a/review/src/main/resources/application.yml b/review/src/main/resources/application.yml index c84841a..f20d8a6 100644 --- a/review/src/main/resources/application.yml +++ b/review/src/main/resources/application.yml @@ -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: diff --git a/store/src/main/java/com/ktds/hi/store/config/SecurityConfig.java b/store/src/main/java/com/ktds/hi/store/config/SecurityConfig.java index 1beeacd..1c1d436 100644 --- a/store/src/main/java/com/ktds/hi/store/config/SecurityConfig.java +++ b/store/src/main/java/com/ktds/hi/store/config/SecurityConfig.java @@ -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 관련 경로 모두 허용 diff --git a/store/src/main/resources/application.yml b/store/src/main/resources/application.yml index fedfdbe..cb7b868 100644 --- a/store/src/main/resources/application.yml +++ b/store/src/main/resources/application.yml @@ -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: