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