diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/config/RedisConfig.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/config/RedisConfig.java index 5c52449..9c89b1c 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/config/RedisConfig.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/config/RedisConfig.java @@ -31,13 +31,13 @@ import java.util.Map; @RequiredArgsConstructor public class RedisConfig { - @Value("${spring.redis.host}") + @Value("${spring.data.redis.host}") private String redisHost; - @Value("${spring.redis.port}") + @Value("${spring.data.redis.port}") private int redisPort; - @Value("${spring.redis.password:}") + @Value("${spring.data.redis.password:}") private String redisPassword; private final ObjectMapper objectMapper; diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/exception/ActionPlanNotFoundException.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/exception/ActionPlanNotFoundException.java index 09ea2d3..df9cd1b 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/exception/ActionPlanNotFoundException.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/exception/ActionPlanNotFoundException.java @@ -8,4 +8,8 @@ public class ActionPlanNotFoundException extends AnalyticsException { public ActionPlanNotFoundException(Long planId) { super("ACTION_PLAN_NOT_FOUND", "실행 계획을 찾을 수 없습니다: " + planId); } + + public ActionPlanNotFoundException(String message) { + super("ACTION_PLAN_NOT_FOUND", message); + } } diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/exception/GlobalExceptionHandler.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/exception/GlobalExceptionHandler.java index 5beddba..1696906 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/exception/GlobalExceptionHandler.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/exception/GlobalExceptionHandler.java @@ -1,6 +1,9 @@ package com.ktds.hi.analytics.infra.exception; import com.ktds.hi.common.dto.ErrorResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -11,226 +14,223 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import jakarta.validation.*; - -import java.time.LocalDateTime; +import java.util.List; import java.util.stream.Collectors; /** - * 글로벌 예외 처리 핸들러 + * 글로벌 예외 처리 핸들러 (수정 완료) * 모든 컨트롤러에서 발생하는 예외를 중앙에서 처리 + * 새로운 ErrorResponse 필드 구조에 맞게 수정 */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { - + /** * 분석 서비스 커스텀 예외 처리 */ @ExceptionHandler(AnalyticsException.class) - public ResponseEntity handleAnalyticsException(AnalyticsException ex) { + public ResponseEntity> handleAnalyticsException( + AnalyticsException ex, HttpServletRequest request) { log.error("Analytics Exception: {}", ex.getMessage(), ex); - - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.BAD_REQUEST.value()) - .error("Analytics Error") - .message(ex.getMessage()) - .path("/api/analytics") - .build(); - + + ErrorResponse errorResponse = ErrorResponse.of( + ex.getErrorCode(), + ex.getMessage(), + request.getRequestURI() + ); + return ResponseEntity.badRequest().body(errorResponse); } - + /** * 매장 정보 없음 예외 처리 */ @ExceptionHandler(StoreNotFoundException.class) - public ResponseEntity handleStoreNotFoundException(StoreNotFoundException ex) { + public ResponseEntity> handleStoreNotFoundException( + StoreNotFoundException ex, HttpServletRequest request) { log.error("Store Not Found: {}", ex.getMessage()); - - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.NOT_FOUND.value()) - .error("Store Not Found") - .message(ex.getMessage()) - .path("/api/analytics") - .build(); - + + ErrorResponse errorResponse = ErrorResponse.of( + "STORE_NOT_FOUND", + ex.getMessage(), + request.getRequestURI() + ); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); } - + /** * 실행 계획 없음 예외 처리 */ @ExceptionHandler(ActionPlanNotFoundException.class) - public ResponseEntity handleActionPlanNotFoundException(ActionPlanNotFoundException ex) { + public ResponseEntity> handleActionPlanNotFoundException( + ActionPlanNotFoundException ex, HttpServletRequest request) { log.error("Action Plan Not Found: {}", ex.getMessage()); - - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.NOT_FOUND.value()) - .error("Action Plan Not Found") - .message(ex.getMessage()) - .path("/api/action-plans") - .build(); - + + ErrorResponse errorResponse = ErrorResponse.of( + "ACTION_PLAN_NOT_FOUND", + ex.getMessage(), + request.getRequestURI() + ); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); } - + /** * AI 서비스 예외 처리 */ @ExceptionHandler(AIServiceException.class) - public ResponseEntity handleAIServiceException(AIServiceException ex) { + public ResponseEntity> handleAIServiceException( + AIServiceException ex, HttpServletRequest request) { log.error("AI Service Exception: {}", ex.getMessage(), ex); - - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.SERVICE_UNAVAILABLE.value()) - .error("AI Service Error") - .message("AI 서비스 연동 중 오류가 발생했습니다.") - .path("/api/analytics") - .build(); - + + ErrorResponse errorResponse = ErrorResponse.of( + "AI_SERVICE_ERROR", + "AI 서비스 연동 중 오류가 발생했습니다.", + request.getRequestURI() + ); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(errorResponse); } - + /** * 외부 서비스 예외 처리 */ @ExceptionHandler(ExternalServiceException.class) - public ResponseEntity handleExternalServiceException(ExternalServiceException ex) { + public ResponseEntity> handleExternalServiceException( + ExternalServiceException ex, HttpServletRequest request) { log.error("External Service Exception: {}", ex.getMessage(), ex); - - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.SERVICE_UNAVAILABLE.value()) - .error("External Service Error") - .message("외부 서비스 연동 중 오류가 발생했습니다.") - .path("/api/analytics") - .build(); - + + ErrorResponse errorResponse = ErrorResponse.of( + ex.getErrorCode(), + "외부 서비스 연동 중 오류가 발생했습니다.", + request.getRequestURI() + ); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(errorResponse); } - + /** * 입력 값 검증 예외 처리 */ @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleValidationException(MethodArgumentNotValidException ex) { + public ResponseEntity> handleValidationException( + MethodArgumentNotValidException ex, HttpServletRequest request) { log.error("Validation Exception: {}", ex.getMessage()); - - String errorMessage = ex.getBindingResult().getFieldErrors().stream() - .map(FieldError::getDefaultMessage) - .collect(Collectors.joining(", ")); - - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.BAD_REQUEST.value()) - .error("Validation Error") - .message(errorMessage) - .path("/api/analytics") - .build(); - + + List validationErrors = ex.getBindingResult() + .getFieldErrors() + .stream() + .map(error -> ErrorResponse.ValidationError.builder() + .field(error.getField()) + .rejectedValue(error.getRejectedValue()) + .message(error.getDefaultMessage()) + .build()) + .collect(Collectors.toList()); + + ErrorResponse errorResponse = ErrorResponse.ofValidation( + "입력값 검증 실패", + request.getRequestURI(), + validationErrors + ); + return ResponseEntity.badRequest().body(errorResponse); } - + /** * 바인딩 예외 처리 */ @ExceptionHandler(BindException.class) - public ResponseEntity handleBindException(BindException ex) { + public ResponseEntity> handleBindException( + BindException ex, HttpServletRequest request) { log.error("Bind Exception: {}", ex.getMessage()); - - String errorMessage = ex.getFieldErrors().stream() - .map(FieldError::getDefaultMessage) - .collect(Collectors.joining(", ")); - - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.BAD_REQUEST.value()) - .error("Binding Error") - .message(errorMessage) - .path("/api/analytics") - .build(); - + + List validationErrors = ex.getFieldErrors() + .stream() + .map(error -> ErrorResponse.ValidationError.builder() + .field(error.getField()) + .rejectedValue(error.getRejectedValue()) + .message(error.getDefaultMessage()) + .build()) + .collect(Collectors.toList()); + + ErrorResponse errorResponse = ErrorResponse.ofValidation( + "바인딩 실패", + request.getRequestURI(), + validationErrors + ); + return ResponseEntity.badRequest().body(errorResponse); } - + /** * 제약 조건 위반 예외 처리 */ @ExceptionHandler(ConstraintViolationException.class) - public ResponseEntity handleConstraintViolationException(ConstraintViolationException ex) { + public ResponseEntity> handleConstraintViolationException( + ConstraintViolationException ex, HttpServletRequest request) { log.error("Constraint Violation Exception: {}", ex.getMessage()); - + String errorMessage = ex.getConstraintViolations().stream() - .map(ConstraintViolation::getMessage) - .collect(Collectors.joining(", ")); - - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.BAD_REQUEST.value()) - .error("Constraint Violation") - .message(errorMessage) - .path("/api/analytics") - .build(); - + .map(ConstraintViolation::getMessage) + .collect(Collectors.joining(", ")); + + ErrorResponse errorResponse = ErrorResponse.of( + "CONSTRAINT_VIOLATION", + errorMessage, + request.getRequestURI() + ); + return ResponseEntity.badRequest().body(errorResponse); } - + /** * 타입 불일치 예외 처리 */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity handleTypeMismatchException(MethodArgumentTypeMismatchException ex) { + public ResponseEntity> handleTypeMismatchException( + MethodArgumentTypeMismatchException ex, HttpServletRequest request) { log.error("Type Mismatch Exception: {}", ex.getMessage()); - - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.BAD_REQUEST.value()) - .error("Type Mismatch") - .message("잘못된 파라미터 타입입니다: " + ex.getName()) - .path("/api/analytics") - .build(); - + + ErrorResponse errorResponse = ErrorResponse.of( + "TYPE_MISMATCH", + "잘못된 파라미터 타입입니다: " + ex.getName(), + request.getRequestURI() + ); + return ResponseEntity.badRequest().body(errorResponse); } - + /** * 일반적인 RuntimeException 처리 */ @ExceptionHandler(RuntimeException.class) - public ResponseEntity handleRuntimeException(RuntimeException ex) { + public ResponseEntity> handleRuntimeException( + RuntimeException ex, HttpServletRequest request) { log.error("Runtime Exception: {}", ex.getMessage(), ex); - - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) - .error("Internal Server Error") - .message("내부 서버 오류가 발생했습니다.") - .path("/api/analytics") - .build(); - + + ErrorResponse errorResponse = ErrorResponse.of( + "RUNTIME_ERROR", + "내부 서버 오류가 발생했습니다.", + request.getRequestURI() + ); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); } - + /** * 모든 예외의 최종 처리 */ @ExceptionHandler(Exception.class) - public ResponseEntity handleAllExceptions(Exception ex) { + public ResponseEntity> handleAllExceptions( + Exception ex, HttpServletRequest request) { log.error("Unexpected Exception: {}", ex.getMessage(), ex); - - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) - .error("Unexpected Error") - .message("예상치 못한 오류가 발생했습니다.") - .path("/api/analytics") - .build(); - + + ErrorResponse errorResponse = ErrorResponse.ofInternalError( + "예상치 못한 오류가 발생했습니다." + ); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); } -} +} \ No newline at end of file diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/exception/StoreNotFoundException.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/exception/StoreNotFoundException.java index cdb25a3..99e55e4 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/exception/StoreNotFoundException.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/exception/StoreNotFoundException.java @@ -8,4 +8,8 @@ public class StoreNotFoundException extends AnalyticsException { public StoreNotFoundException(Long storeId) { super("STORE_NOT_FOUND", "매장을 찾을 수 없습니다: " + storeId); } + + public StoreNotFoundException(String message) { + super("STORE_NOT_FOUND", message); + } } diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/AIServiceAdapter.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/AIServiceAdapter.java index f9280c0..f16f0f3 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/AIServiceAdapter.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/AIServiceAdapter.java @@ -1,9 +1,12 @@ package com.ktds.hi.analytics.infra.gateway; +import static com.azure.ai.textanalytics.models.TextSentiment.*; + import com.azure.ai.textanalytics.TextAnalyticsClient; import com.azure.ai.textanalytics.TextAnalyticsClientBuilder; import com.azure.ai.textanalytics.models.AnalyzeSentimentResult; import com.azure.ai.textanalytics.models.DocumentSentiment; +import com.azure.ai.textanalytics.models.TextSentiment; import com.azure.core.credential.AzureKeyCredential; import com.ktds.hi.analytics.biz.domain.AiFeedback; import com.ktds.hi.analytics.biz.domain.SentimentType; @@ -97,16 +100,19 @@ public class AIServiceAdapter implements AIServicePort { @Override public SentimentType analyzeSentiment(String content) { try { - AnalyzeSentimentResult result = textAnalyticsClient.analyzeSentiment(content); - DocumentSentiment sentiment = result.getDocumentSentiment(); - - switch (sentiment) { - case POSITIVE: - return SentimentType.POSITIVE; - case NEGATIVE: - return SentimentType.NEGATIVE; - default: - return SentimentType.NEUTRAL; + DocumentSentiment documentSentiment = textAnalyticsClient.analyzeSentiment(content); + TextSentiment sentiment = documentSentiment.getSentiment(); + + if (sentiment == TextSentiment.POSITIVE) { + return SentimentType.POSITIVE; + } else if (sentiment == TextSentiment.NEGATIVE) { + return SentimentType.NEGATIVE; + } else if (sentiment == TextSentiment.NEUTRAL) { + return SentimentType.NEUTRAL; + } else if (sentiment == TextSentiment.MIXED) { + return SentimentType.NEUTRAL; // MIXED는 NEUTRAL로 처리 + } else { + return SentimentType.NEUTRAL; } } catch (Exception e) { diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/EventHubAdapter.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/EventHubAdapter.java index 4af64f2..e3eceb4 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/EventHubAdapter.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/EventHubAdapter.java @@ -31,7 +31,7 @@ import java.util.concurrent.Executors; @Slf4j @Component @RequiredArgsConstructor -public class EventHubAdapter implements EventPort { +public class EventHubAdapter { @Qualifier("reviewEventConsumer") private final EventHubConsumerClient reviewEventConsumer; @@ -60,8 +60,7 @@ public class EventHubAdapter implements EventPort { reviewEventConsumer.close(); aiAnalysisEventProducer.close(); } - - @Override + public void publishAnalysisCompletedEvent(Long storeId, AnalysisType analysisType) { try { Map eventData = new HashMap<>(); @@ -84,7 +83,7 @@ public class EventHubAdapter implements EventPort { } } - @Override + public void publishActionPlanCreatedEvent(ActionPlan actionPlan) { try { Map eventData = new HashMap<>(); diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/entity/ActionPlanEntity.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/entity/ActionPlanEntity.java index 096f0c8..f3a253b 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/entity/ActionPlanEntity.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/entity/ActionPlanEntity.java @@ -26,7 +26,13 @@ import java.time.LocalDateTime; * 점주의 개선 실행 계획을 저장 */ @Entity -@Table(name = "action_plan") +@Table(name = "action_plan", + indexes = { + @Index(name = "idx_action_plan_store_id", columnList = "store_id"), + @Index(name = "idx_action_plan_user_id", columnList = "user_id"), + @Index(name = "idx_action_plan_status", columnList = "status"), + @Index(name = "idx_action_plan_created_at", columnList = "created_at") + }) @Getter @Builder @NoArgsConstructor @@ -73,10 +79,4 @@ public class ActionPlanEntity { @LastModifiedDate @Column(name = "updated_at") private LocalDateTime updatedAt; - - @Index(name = "idx_action_plan_store_id", columnList = "store_id") - @Index(name = "idx_action_plan_user_id", columnList = "user_id") - @Index(name = "idx_action_plan_status", columnList = "status") - public static class Indexes { - } } diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/entity/AiFeedbackEntity.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/entity/AiFeedbackEntity.java index 596e492..6219c9e 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/entity/AiFeedbackEntity.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/entity/AiFeedbackEntity.java @@ -16,7 +16,13 @@ import java.time.LocalDateTime; * AI가 생성한 피드백 정보를 저장 */ @Entity -@Table(name = "ai_feedback") +@Table(name = "ai_feedback", + indexes = { + @Index(name = "idx_ai_feedback_store_id", columnList = "store_id"), + @Index(name = "idx_ai_feedback_generated_at", columnList = "generated_at"), + @Index(name = "idx_ai_feedback_created_at", columnList = "created_at"), + @Index(name = "idx_ai_feedback_confidence_score", columnList = "confidence_score") + }) @Getter @Builder @NoArgsConstructor @@ -59,8 +65,5 @@ public class AiFeedbackEntity { @LastModifiedDate @Column(name = "updated_at") private LocalDateTime updatedAt; - - @Index(name = "idx_ai_feedback_store_id", columnList = "store_id") - public static class Indexes { - } + } diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/entity/AnalyticsEntity.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/entity/AnalyticsEntity.java index 4444755..8d5e687 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/entity/AnalyticsEntity.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/entity/AnalyticsEntity.java @@ -16,7 +16,13 @@ import java.time.LocalDateTime; * 매장의 분석 정보를 저장 */ @Entity -@Table(name = "analytics") +@Table(name = "analytics", + indexes = { + @Index(name = "idx_analytics_store_id", columnList = "store_id"), + @Index(name = "idx_analytics_last_analysis_date", columnList = "last_analysis_date"), + @Index(name = "idx_analytics_created_at", columnList = "created_at"), + @Index(name = "idx_analytics_average_rating", columnList = "average_rating") + }) @Getter @Builder @NoArgsConstructor @@ -56,8 +62,5 @@ public class AnalyticsEntity { @LastModifiedDate @Column(name = "updated_at") private LocalDateTime updatedAt; - - @Index(name = "idx_analytics_store_id", columnList = "store_id") - public static class Indexes { - } + } diff --git a/analytics/src/main/resources/application.yml b/analytics/src/main/resources/application.yml index 3747394..20cdc5d 100644 --- a/analytics/src/main/resources/application.yml +++ b/analytics/src/main/resources/application.yml @@ -13,7 +13,7 @@ spring: jpa: hibernate: - ddl-auto: ${JPA_DDL_AUTO:validate} + ddl-auto: ${JPA_DDL_AUTO:create} show-sql: ${JPA_SHOW_SQL:false} properties: hibernate: @@ -44,6 +44,13 @@ ai-api: # api-key: ${CLAUDE_API_KEY:} # base-url: https://api.anthropic.com +# 외부 서비스 설정 +external: + services: + review: ${EXTERNAL_SERVICES_REVIEW:http://localhost:8082} + store: ${EXTERNAL_SERVICES_STORE:http://localhost:8081} + member: ${EXTERNAL_SERVICES_MEMBER:http://localhost:8080} + springdoc: api-docs: path: /api-docs @@ -54,4 +61,25 @@ management: endpoints: web: exposure: - include: health,info,metrics \ No newline at end of file + include: health,info,metrics + +# AI 서비스 설정 +ai: + azure: + cognitive: + endpoint: ${AI_AZURE_COGNITIVE_ENDPOINT:https://your-cognitive-service.cognitiveservices.azure.com} + key: ${AI_AZURE_COGNITIVE_KEY:your-cognitive-service-key} + openai: + api-key: ${AI_OPENAI_API_KEY:your-openai-api-key} + +# Azure Event Hub 설정 +azure: + eventhub: + connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING:Endpoint=sb://your-eventhub.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=your-key} + consumer-group: ${AZURE_EVENTHUB_CONSUMER_GROUP:analytics-consumer} + event-hubs: + review-events: ${AZURE_EVENTHUB_REVIEW_EVENTS:review-events} + 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 diff --git a/build.gradle b/build.gradle index 967144f..0208fb7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,13 @@ plugins { id 'java' id 'org.springframework.boot' version '3.4.0' apply false - id 'io.spring.dependency-management' version '1.1.6' apply false + id 'io.spring.dependency-management' version '1.1.4' apply false } allprojects { group = 'com.ktds.hi' version = '1.0.0' - + repositories { mavenCentral() } @@ -17,37 +17,50 @@ subprojects { apply plugin: 'java' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' - + java { - sourceCompatibility = '21' - } - - configurations { - compileOnly { - extendsFrom annotationProcessor + toolchain { + languageVersion = JavaLanguageVersion.of(21) } } - - // 공통 의존성 + dependencies { - // Lombok (모든 서브 프로젝트에서 사용) + // 공통 의존성 + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' + + // Database + runtimeOnly 'org.postgresql:postgresql' + + // Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' - - // 테스트 (모든 서브 프로젝트에서 사용) + + // MapStruct + implementation 'org.mapstruct:mapstruct:1.5.5.Final' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' + + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:postgresql' } - - // 컴파일 옵션 - tasks.withType(JavaCompile) { - options.encoding = 'UTF-8' - options.compilerArgs += ['-parameters'] - } - - // 테스트 설정 + tasks.named('test') { useJUnitPlatform() - systemProperty 'spring.profiles.active', 'test' } -} \ No newline at end of file +} diff --git a/common/src/build.gradle b/common/src/build.gradle index 714c145..cf52352 100644 --- a/common/src/build.gradle +++ b/common/src/build.gradle @@ -5,11 +5,14 @@ plugins { description = 'Common utilities and shared components' dependencies { + + implementation 'org.springframework.boot:spring-boot-starter-data-redis' // Spring Boot Starters api 'org.springframework.boot:spring-boot-starter-web' api 'org.springframework.boot:spring-boot-starter-data-jpa' api 'org.springframework.boot:spring-boot-starter-validation' api 'org.springframework.boot:spring-boot-starter-security' + api 'org.springframework.data:spring-data-commons' // AOP (AspectJ) - 명시적 의존성 추가 api 'org.springframework.boot:spring-boot-starter-aop' @@ -36,6 +39,10 @@ dependencies { compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + // Database + runtimeOnly 'org.postgresql:postgresql' + testRuntimeOnly 'com.h2database:h2' + // 테스트 testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' diff --git a/common/src/main/java/com/ktds/hi/common/CommonModuleConfiguration.java b/common/src/main/java/com/ktds/hi/common/CommonModuleConfiguration.java index 73da2c1..04e52d1 100644 --- a/common/src/main/java/com/ktds/hi/common/CommonModuleConfiguration.java +++ b/common/src/main/java/com/ktds/hi/common/CommonModuleConfiguration.java @@ -11,7 +11,10 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories; */ @Configuration @ComponentScan(basePackages = "com.ktds.hi.common") -@EntityScan(basePackages = "com.ktds.hi.common.entity") +@EntityScan(basePackages = { + "com.ktds.hi.common.entity", + "com.ktds.hi.common.audit" +}) @EnableJpaRepositories(basePackages = "com.ktds.hi.common.repository") public class CommonModuleConfiguration { // 설정 클래스는 어노테이션만으로도 충분 diff --git a/common/src/main/java/com/ktds/hi/common/audit/AuditLog.java b/common/src/main/java/com/ktds/hi/common/audit/AuditLog.java index 23c4edd..638ed75 100644 --- a/common/src/main/java/com/ktds/hi/common/audit/AuditLog.java +++ b/common/src/main/java/com/ktds/hi/common/audit/AuditLog.java @@ -1,5 +1,8 @@ package com.ktds.hi.common.audit; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -7,15 +10,20 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + /** * 감사 로그 엔티티 */ @Getter +@Entity @Builder @NoArgsConstructor @AllArgsConstructor +@EntityListeners(AuditingEntityListener.class) public class AuditLog { + @Id private Long id; private String entityType; private String entityId; @@ -26,4 +34,8 @@ public class AuditLog { private String userAgent; private String ipAddress; private LocalDateTime timestamp; + private LocalDateTime createdAt; + + + } \ No newline at end of file diff --git a/common/src/main/java/com/ktds/hi/common/audit/AuditLogRepository.java b/common/src/main/java/com/ktds/hi/common/audit/AuditLogRepository.java deleted file mode 100644 index 4eb36d5..0000000 --- a/common/src/main/java/com/ktds/hi/common/audit/AuditLogRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.ktds.hi.common.audit; - -/** - * 감사 로그 리포지토리 인터페이스 - */ -public interface AuditLogRepository { - - void save(AuditLog auditLog); - - AuditLog findById(Long id); -} \ No newline at end of file diff --git a/common/src/main/java/com/ktds/hi/common/audit/AuditLogger.java b/common/src/main/java/com/ktds/hi/common/audit/AuditLogger.java index f89e1f6..f095d14 100644 --- a/common/src/main/java/com/ktds/hi/common/audit/AuditLogger.java +++ b/common/src/main/java/com/ktds/hi/common/audit/AuditLogger.java @@ -29,7 +29,7 @@ public class AuditLogger { */ public void logCreate(Object entity) { try { - AuditLog auditLog = AuditLog.builder() + AuditLog auditLog = AuditLog.builder() .entityType(entity.getClass().getSimpleName()) .entityId(extractEntityId(entity)) .action(AuditAction.CREATE) diff --git a/common/src/main/java/com/ktds/hi/common/audit/CustomAuditorAware.java b/common/src/main/java/com/ktds/hi/common/audit/CustomAuditorAware.java index 0c03842..dd8066f 100644 --- a/common/src/main/java/com/ktds/hi/common/audit/CustomAuditorAware.java +++ b/common/src/main/java/com/ktds/hi/common/audit/CustomAuditorAware.java @@ -1,5 +1,6 @@ package com.ktds.hi.common.audit; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.data.domain.AuditorAware; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -11,6 +12,7 @@ import java.util.Optional; * JPA Auditing을 위한 사용자 정보 제공자 */ @Component +@ConditionalOnClass(AuditorAware.class) // 👈 이 어노테이션 추가 public class CustomAuditorAware implements AuditorAware { @Override 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 e8a099e..791569f 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 @@ -3,6 +3,7 @@ package com.ktds.hi.member.config; import com.ktds.hi.member.service.JwtTokenProvider; import com.ktds.hi.member.service.AuthService; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -23,7 +24,8 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { - + + @Qualifier("memberJwtTokenProvider") private final JwtTokenProvider jwtTokenProvider; private final AuthService authService; diff --git a/member/src/main/java/com/ktds/hi/member/service/AuthServiceImpl.java b/member/src/main/java/com/ktds/hi/member/service/AuthServiceImpl.java index 88b3633..7026d6d 100644 --- a/member/src/main/java/com/ktds/hi/member/service/AuthServiceImpl.java +++ b/member/src/main/java/com/ktds/hi/member/service/AuthServiceImpl.java @@ -6,6 +6,7 @@ import com.ktds.hi.member.repository.jpa.MemberRepository; import com.ktds.hi.common.exception.BusinessException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -23,6 +24,7 @@ public class AuthServiceImpl implements AuthService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; + @Qualifier("memberJwtTokenProvider") private final JwtTokenProvider jwtTokenProvider; private final SmsService smsService; private final RedisTemplate redisTemplate; diff --git a/member/src/main/java/com/ktds/hi/member/service/JwtTokenProvider.java b/member/src/main/java/com/ktds/hi/member/service/JwtTokenProvider.java index f0c3878..ac58680 100644 --- a/member/src/main/java/com/ktds/hi/member/service/JwtTokenProvider.java +++ b/member/src/main/java/com/ktds/hi/member/service/JwtTokenProvider.java @@ -17,7 +17,7 @@ import java.util.Date; * JWT 토큰 프로바이더 클래스 * JWT 토큰 생성, 검증, 파싱 기능을 제공 */ -@Component +@Component("memberJwtTokenProvider") // 기존: @Component @Slf4j public class JwtTokenProvider { diff --git a/store/build.gradle b/store/build.gradle index b59d106..239c9b6 100644 --- a/store/build.gradle +++ b/store/build.gradle @@ -2,5 +2,7 @@ dependencies { implementation project(':common') // External API Integration + implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-webflux' + } diff --git a/store/src/main/java/com/ktds/hi/store/StoreApplication.java b/store/src/main/java/com/ktds/hi/store/StoreApplication.java new file mode 100644 index 0000000..b119d24 --- /dev/null +++ b/store/src/main/java/com/ktds/hi/store/StoreApplication.java @@ -0,0 +1,33 @@ +package com.ktds.hi.store; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.web.client.RestTemplate; + +/** + * 추천 서비스 메인 애플리케이션 클래스 + * 가게 추천, 취향 분석 기능을 제공 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +@SpringBootApplication(scanBasePackages = { + "com.ktds.hi.store", + "com.ktds.hi.common" +}) + +@EnableJpaAuditing +public class StoreApplication { + + public static void main(String[] args) { + SpringApplication.run(StoreApplication.class, args); + } + + // 👈 이 부분만 추가 + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/store/src/main/resources/application.yml b/store/src/main/resources/application.yml index 361abb3..fedfdbe 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:validate} + ddl-auto: ${JPA_DDL_AUTO:create} show-sql: ${JPA_SHOW_SQL:false} properties: hibernate: