AI 서비스 Kafka/Redis 통합 테스트 및 설정 개선
- Gradle 빌드 캐시 파일 제외 (.gitignore 업데이트) - Kafka 통합 테스트 구현 (AIJobConsumerIntegrationTest) - 단위 테스트 추가 (Controller, Service 레이어) - IntelliJ 실행 프로파일 자동 생성 도구 추가 - Kafka 테스트 배치 스크립트 추가 - Redis 캐시 설정 개선 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package com.kt.ai;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
/**
|
||||
@@ -14,7 +15,7 @@ import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@EnableFeignClients
|
||||
@SpringBootApplication
|
||||
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
|
||||
public class AiServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -30,11 +30,10 @@ public interface ClaudeApiClient {
|
||||
* @param request Claude 요청
|
||||
* @return Claude 응답
|
||||
*/
|
||||
@PostMapping
|
||||
@PostMapping(consumes = "application/json", produces = "application/json")
|
||||
ClaudeResponse sendMessage(
|
||||
@RequestHeader("x-api-key") String apiKey,
|
||||
@RequestHeader("anthropic-version") String anthropicVersion,
|
||||
@RequestHeader("content-type") String contentType,
|
||||
@RequestBody ClaudeRequest request
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
package com.kt.ai.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import io.lettuce.core.ClientOptions;
|
||||
import io.lettuce.core.SocketOptions;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Redis 설정
|
||||
* - 작업 상태 및 추천 결과 캐싱
|
||||
@@ -33,6 +41,9 @@ public class RedisConfig {
|
||||
@Value("${spring.data.redis.database}")
|
||||
private int redisDatabase;
|
||||
|
||||
@Value("${spring.data.redis.timeout:3000}")
|
||||
private long redisTimeout;
|
||||
|
||||
/**
|
||||
* Redis 연결 팩토리 설정
|
||||
*/
|
||||
@@ -46,13 +57,46 @@ public class RedisConfig {
|
||||
}
|
||||
config.setDatabase(redisDatabase);
|
||||
|
||||
return new LettuceConnectionFactory(config);
|
||||
// Lettuce Client 설정: Timeout 및 Connection 옵션
|
||||
SocketOptions socketOptions = SocketOptions.builder()
|
||||
.connectTimeout(Duration.ofMillis(redisTimeout))
|
||||
.keepAlive(true)
|
||||
.build();
|
||||
|
||||
ClientOptions clientOptions = ClientOptions.builder()
|
||||
.socketOptions(socketOptions)
|
||||
.autoReconnect(true)
|
||||
.build();
|
||||
|
||||
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
|
||||
.commandTimeout(Duration.ofMillis(redisTimeout))
|
||||
.clientOptions(clientOptions)
|
||||
.build();
|
||||
|
||||
// afterPropertiesSet() 제거: Spring이 자동으로 호출함
|
||||
return new LettuceConnectionFactory(config, clientConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* ObjectMapper for Redis (Java 8 Date/Time 지원)
|
||||
*/
|
||||
@Bean
|
||||
public ObjectMapper redisObjectMapper() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
// Java 8 Date/Time 모듈 등록
|
||||
mapper.registerModule(new JavaTimeModule());
|
||||
|
||||
// Timestamp 대신 ISO-8601 형식으로 직렬화
|
||||
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
|
||||
return mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* RedisTemplate 설정
|
||||
* - Key: String
|
||||
* - Value: JSON (Jackson)
|
||||
* - Value: JSON (Jackson with Java 8 Date/Time support)
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
@@ -63,9 +107,12 @@ public class RedisConfig {
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
|
||||
// Value Serializer: JSON
|
||||
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
// Value Serializer: JSON with Java 8 Date/Time support
|
||||
GenericJackson2JsonRedisSerializer serializer =
|
||||
new GenericJackson2JsonRedisSerializer(redisObjectMapper());
|
||||
|
||||
template.setValueSerializer(serializer);
|
||||
template.setHashValueSerializer(serializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
|
||||
@@ -5,8 +5,8 @@ import com.kt.ai.model.enums.CircuitBreakerState;
|
||||
import com.kt.ai.model.enums.ServiceStatus;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -23,22 +23,27 @@ import java.time.LocalDateTime;
|
||||
@Slf4j
|
||||
@Tag(name = "Health Check", description = "서비스 상태 확인")
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class HealthController {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
@Autowired(required = false)
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 서비스 헬스체크
|
||||
*/
|
||||
@Operation(summary = "서비스 헬스체크", description = "AI Service 상태 및 외부 연동 확인")
|
||||
@GetMapping("/health")
|
||||
@GetMapping("/api/v1/ai-service/health")
|
||||
public ResponseEntity<HealthCheckResponse> healthCheck() {
|
||||
// Redis 상태 확인
|
||||
ServiceStatus redisStatus = checkRedis();
|
||||
|
||||
// 전체 서비스 상태
|
||||
ServiceStatus overallStatus = (redisStatus == ServiceStatus.UP) ? ServiceStatus.UP : ServiceStatus.DEGRADED;
|
||||
// 전체 서비스 상태 (Redis가 DOWN이면 DEGRADED, UNKNOWN이면 UP으로 처리)
|
||||
ServiceStatus overallStatus;
|
||||
if (redisStatus == ServiceStatus.DOWN) {
|
||||
overallStatus = ServiceStatus.DEGRADED;
|
||||
} else {
|
||||
overallStatus = ServiceStatus.UP;
|
||||
}
|
||||
|
||||
HealthCheckResponse.Services services = HealthCheckResponse.Services.builder()
|
||||
.kafka(ServiceStatus.UP) // TODO: 실제 Kafka 상태 확인
|
||||
@@ -61,11 +66,25 @@ public class HealthController {
|
||||
* Redis 연결 상태 확인
|
||||
*/
|
||||
private ServiceStatus checkRedis() {
|
||||
// RedisTemplate이 주입되지 않은 경우 (로컬 환경 등)
|
||||
if (redisTemplate == null) {
|
||||
log.warn("RedisTemplate이 주입되지 않았습니다. Redis 상태를 UNKNOWN으로 표시합니다.");
|
||||
return ServiceStatus.UNKNOWN;
|
||||
}
|
||||
|
||||
try {
|
||||
redisTemplate.getConnectionFactory().getConnection().ping();
|
||||
log.debug("Redis 연결 테스트 시작...");
|
||||
String pong = redisTemplate.getConnectionFactory().getConnection().ping();
|
||||
log.info("✅ Redis 연결 성공! PING 응답: {}", pong);
|
||||
return ServiceStatus.UP;
|
||||
} catch (Exception e) {
|
||||
log.error("Redis 연결 실패", e);
|
||||
log.error("❌ Redis 연결 실패", e);
|
||||
log.error("상세 오류 정보:");
|
||||
log.error(" - 오류 타입: {}", e.getClass().getName());
|
||||
log.error(" - 오류 메시지: {}", e.getMessage());
|
||||
if (e.getCause() != null) {
|
||||
log.error(" - 원인: {}", e.getCause().getMessage());
|
||||
}
|
||||
return ServiceStatus.DOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.kt.ai.controller;
|
||||
|
||||
import com.kt.ai.model.dto.response.JobStatusResponse;
|
||||
import com.kt.ai.model.enums.JobStatus;
|
||||
import com.kt.ai.service.CacheService;
|
||||
import com.kt.ai.service.JobStatusService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -12,6 +14,9 @@ import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Internal Job Controller
|
||||
* Event Service에서 호출하는 내부 API
|
||||
@@ -22,11 +27,12 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@Slf4j
|
||||
@Tag(name = "Internal API", description = "내부 서비스 간 통신용 API")
|
||||
@RestController
|
||||
@RequestMapping("/internal/jobs")
|
||||
@RequestMapping("/api/v1/ai-service/internal/jobs")
|
||||
@RequiredArgsConstructor
|
||||
public class InternalJobController {
|
||||
|
||||
private final JobStatusService jobStatusService;
|
||||
private final CacheService cacheService;
|
||||
|
||||
/**
|
||||
* 작업 상태 조회
|
||||
@@ -38,4 +44,49 @@ public class InternalJobController {
|
||||
JobStatusResponse response = jobStatusService.getJobStatus(jobId);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 디버그: Job 상태 테스트 데이터 생성
|
||||
*/
|
||||
@Operation(summary = "Job 테스트 데이터 생성 (디버그)", description = "Redis에 샘플 Job 상태 데이터 저장")
|
||||
@GetMapping("/debug/create-test-job/{jobId}")
|
||||
public ResponseEntity<Map<String, Object>> createTestJob(@PathVariable String jobId) {
|
||||
log.info("Job 테스트 데이터 생성 요청: jobId={}", jobId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 다양한 상태의 테스트 데이터 생성
|
||||
JobStatus[] statuses = JobStatus.values();
|
||||
|
||||
// 요청된 jobId로 PROCESSING 상태 데이터 생성
|
||||
jobStatusService.updateJobStatus(jobId, JobStatus.PROCESSING, "AI 추천 생성 중 (50%)");
|
||||
|
||||
// 추가 샘플 데이터 생성 (다양한 상태)
|
||||
jobStatusService.updateJobStatus(jobId + "-pending", JobStatus.PENDING, "대기 중");
|
||||
jobStatusService.updateJobStatus(jobId + "-completed", JobStatus.COMPLETED, "AI 추천 완료");
|
||||
jobStatusService.updateJobStatus(jobId + "-failed", JobStatus.FAILED, "AI API 호출 실패");
|
||||
|
||||
// 저장 확인
|
||||
Object saved = cacheService.getJobStatus(jobId);
|
||||
|
||||
result.put("success", true);
|
||||
result.put("jobId", jobId);
|
||||
result.put("saved", saved != null);
|
||||
result.put("data", saved);
|
||||
result.put("additionalSamples", Map.of(
|
||||
"pending", jobId + "-pending",
|
||||
"completed", jobId + "-completed",
|
||||
"failed", jobId + "-failed"
|
||||
));
|
||||
|
||||
log.info("Job 테스트 데이터 생성 완료: jobId={}, saved={}", jobId, saved != null);
|
||||
} catch (Exception e) {
|
||||
log.error("Job 테스트 데이터 생성 실패: jobId={}", jobId, e);
|
||||
result.put("success", false);
|
||||
result.put("error", e.getMessage());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
+224
-1
@@ -1,17 +1,26 @@
|
||||
package com.kt.ai.controller;
|
||||
|
||||
import com.kt.ai.model.dto.response.AIRecommendationResult;
|
||||
import com.kt.ai.model.dto.response.EventRecommendation;
|
||||
import com.kt.ai.model.dto.response.TrendAnalysis;
|
||||
import com.kt.ai.service.AIRecommendationService;
|
||||
import com.kt.ai.service.CacheService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Internal Recommendation Controller
|
||||
* Event Service에서 호출하는 내부 API
|
||||
@@ -22,11 +31,13 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@Slf4j
|
||||
@Tag(name = "Internal API", description = "내부 서비스 간 통신용 API")
|
||||
@RestController
|
||||
@RequestMapping("/internal/recommendations")
|
||||
@RequestMapping("/api/v1/ai-service/internal/recommendations")
|
||||
@RequiredArgsConstructor
|
||||
public class InternalRecommendationController {
|
||||
|
||||
private final AIRecommendationService aiRecommendationService;
|
||||
private final CacheService cacheService;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* AI 추천 결과 조회
|
||||
@@ -38,4 +49,216 @@ public class InternalRecommendationController {
|
||||
AIRecommendationResult response = aiRecommendationService.getRecommendation(eventId);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 디버그: 모든 키 조회
|
||||
*/
|
||||
@Operation(summary = "Redis 키 조회 (디버그)", description = "Redis에 저장된 모든 키 조회")
|
||||
@GetMapping("/debug/redis-keys")
|
||||
public ResponseEntity<Map<String, Object>> debugRedisKeys() {
|
||||
log.info("Redis 키 디버그 요청");
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 모든 ai:* 키 조회
|
||||
Set<String> keys = redisTemplate.keys("ai:*");
|
||||
result.put("totalKeys", keys != null ? keys.size() : 0);
|
||||
result.put("keys", keys);
|
||||
|
||||
// 특정 키의 값 조회
|
||||
if (keys != null && !keys.isEmpty()) {
|
||||
Map<String, Object> values = new HashMap<>();
|
||||
for (String key : keys) {
|
||||
Object value = redisTemplate.opsForValue().get(key);
|
||||
values.put(key, value);
|
||||
}
|
||||
result.put("values", values);
|
||||
}
|
||||
|
||||
log.info("Redis 키 조회 성공: {} 개의 키 발견", keys != null ? keys.size() : 0);
|
||||
} catch (Exception e) {
|
||||
log.error("Redis 키 조회 실패", e);
|
||||
result.put("error", e.getMessage());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 디버그: 특정 키 조회
|
||||
*/
|
||||
@Operation(summary = "Redis 특정 키 조회 (디버그)", description = "Redis에서 특정 키의 값 조회")
|
||||
@GetMapping("/debug/redis-key/{key}")
|
||||
public ResponseEntity<Map<String, Object>> debugRedisKey(@PathVariable String key) {
|
||||
log.info("Redis 특정 키 조회 요청: key={}", key);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("key", key);
|
||||
|
||||
try {
|
||||
Object value = redisTemplate.opsForValue().get(key);
|
||||
result.put("exists", value != null);
|
||||
result.put("value", value);
|
||||
|
||||
log.info("Redis 키 조회: key={}, exists={}", key, value != null);
|
||||
} catch (Exception e) {
|
||||
log.error("Redis 키 조회 실패: key={}", key, e);
|
||||
result.put("error", e.getMessage());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 디버그: 모든 database 검색
|
||||
*/
|
||||
@Operation(summary = "모든 Redis DB 검색 (디버그)", description = "Redis database 0~15에서 ai:* 키 검색")
|
||||
@GetMapping("/debug/search-all-databases")
|
||||
public ResponseEntity<Map<String, Object>> searchAllDatabases() {
|
||||
log.info("모든 Redis database 검색 시작");
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
Map<Integer, Set<String>> databaseKeys = new HashMap<>();
|
||||
|
||||
try {
|
||||
// Redis connection factory를 통해 database 변경하며 검색
|
||||
var connectionFactory = redisTemplate.getConnectionFactory();
|
||||
|
||||
for (int db = 0; db < 16; db++) {
|
||||
try {
|
||||
var connection = connectionFactory.getConnection();
|
||||
connection.select(db);
|
||||
|
||||
Set<byte[]> keyBytes = connection.keys("ai:*".getBytes());
|
||||
if (keyBytes != null && !keyBytes.isEmpty()) {
|
||||
Set<String> keys = new java.util.HashSet<>();
|
||||
for (byte[] keyByte : keyBytes) {
|
||||
keys.add(new String(keyByte));
|
||||
}
|
||||
databaseKeys.put(db, keys);
|
||||
log.info("Database {} 에서 {} 개의 ai:* 키 발견", db, keys.size());
|
||||
}
|
||||
|
||||
connection.close();
|
||||
} catch (Exception e) {
|
||||
log.warn("Database {} 검색 실패: {}", db, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
result.put("databasesWithKeys", databaseKeys);
|
||||
result.put("totalDatabases", databaseKeys.size());
|
||||
|
||||
log.info("모든 database 검색 완료: {} 개의 database에 키 존재", databaseKeys.size());
|
||||
} catch (Exception e) {
|
||||
log.error("모든 database 검색 실패", e);
|
||||
result.put("error", e.getMessage());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 디버그: 테스트 데이터 생성
|
||||
*/
|
||||
@Operation(summary = "테스트 데이터 생성 (디버그)", description = "Redis에 샘플 AI 추천 데이터 저장")
|
||||
@GetMapping("/debug/create-test-data/{eventId}")
|
||||
public ResponseEntity<Map<String, Object>> createTestData(@PathVariable String eventId) {
|
||||
log.info("테스트 데이터 생성 요청: eventId={}", eventId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 샘플 AI 추천 결과 생성
|
||||
AIRecommendationResult testData = AIRecommendationResult.builder()
|
||||
.eventId(eventId)
|
||||
.trendAnalysis(TrendAnalysis.builder()
|
||||
.industryTrends(List.of(
|
||||
TrendAnalysis.TrendKeyword.builder()
|
||||
.keyword("BBQ 고기집")
|
||||
.relevance(0.95)
|
||||
.description("음식점 업종, 고기 구이 인기 트렌드")
|
||||
.build()
|
||||
))
|
||||
.regionalTrends(List.of(
|
||||
TrendAnalysis.TrendKeyword.builder()
|
||||
.keyword("강남 맛집")
|
||||
.relevance(0.90)
|
||||
.description("강남구 지역 외식 인기 증가")
|
||||
.build()
|
||||
))
|
||||
.seasonalTrends(List.of(
|
||||
TrendAnalysis.TrendKeyword.builder()
|
||||
.keyword("봄나들이 외식")
|
||||
.relevance(0.85)
|
||||
.description("봄철 야외 활동 및 외식 증가")
|
||||
.build()
|
||||
))
|
||||
.build())
|
||||
.recommendations(List.of(
|
||||
EventRecommendation.builder()
|
||||
.optionNumber(1)
|
||||
.concept("SNS 이벤트")
|
||||
.title("인스타그램 후기 이벤트")
|
||||
.description("음식 사진을 인스타그램에 올리고 해시태그를 달면 할인 쿠폰 제공")
|
||||
.targetAudience("20-30대 SNS 활동층")
|
||||
.duration(EventRecommendation.Duration.builder()
|
||||
.recommendedDays(14)
|
||||
.recommendedPeriod("2주")
|
||||
.build())
|
||||
.mechanics(EventRecommendation.Mechanics.builder()
|
||||
.type(com.kt.ai.model.enums.EventMechanicsType.DISCOUNT)
|
||||
.details("인스타그램 게시물 작성 시 10% 할인")
|
||||
.build())
|
||||
.promotionChannels(List.of("Instagram", "Facebook", "매장 포스터"))
|
||||
.estimatedCost(EventRecommendation.EstimatedCost.builder()
|
||||
.min(100000)
|
||||
.max(200000)
|
||||
.breakdown(Map.of(
|
||||
"할인비용", 150000,
|
||||
"홍보비", 50000
|
||||
))
|
||||
.build())
|
||||
.expectedMetrics(com.kt.ai.model.dto.response.ExpectedMetrics.builder()
|
||||
.newCustomers(com.kt.ai.model.dto.response.ExpectedMetrics.Range.builder()
|
||||
.min(30.0)
|
||||
.max(50.0)
|
||||
.build())
|
||||
.revenueIncrease(com.kt.ai.model.dto.response.ExpectedMetrics.Range.builder()
|
||||
.min(10.0)
|
||||
.max(20.0)
|
||||
.build())
|
||||
.roi(com.kt.ai.model.dto.response.ExpectedMetrics.Range.builder()
|
||||
.min(100.0)
|
||||
.max(150.0)
|
||||
.build())
|
||||
.build())
|
||||
.differentiator("SNS를 활용한 바이럴 마케팅")
|
||||
.build()
|
||||
))
|
||||
.generatedAt(java.time.LocalDateTime.now())
|
||||
.expiresAt(java.time.LocalDateTime.now().plusDays(1))
|
||||
.aiProvider(com.kt.ai.model.enums.AIProvider.CLAUDE)
|
||||
.build();
|
||||
|
||||
// Redis에 저장
|
||||
cacheService.saveRecommendation(eventId, testData);
|
||||
|
||||
// 저장 확인
|
||||
Object saved = cacheService.getRecommendation(eventId);
|
||||
|
||||
result.put("success", true);
|
||||
result.put("eventId", eventId);
|
||||
result.put("saved", saved != null);
|
||||
result.put("data", saved);
|
||||
|
||||
log.info("테스트 데이터 생성 완료: eventId={}, saved={}", eventId, saved != null);
|
||||
} catch (Exception e) {
|
||||
log.error("테스트 데이터 생성 실패: eventId={}", eventId, e);
|
||||
result.put("success", false);
|
||||
result.put("error", e.getMessage());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
@@ -89,6 +90,29 @@ public class GlobalExceptionHandler {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* 정적 리소스를 찾을 수 없는 예외 처리 (favicon.ico 등)
|
||||
* WARN 레벨로 로깅하여 에러 로그 오염 방지
|
||||
*/
|
||||
@ExceptionHandler(NoResourceFoundException.class)
|
||||
public ResponseEntity<ErrorResponse> handleNoResourceFoundException(NoResourceFoundException ex) {
|
||||
// favicon.ico 등 브라우저가 자동으로 요청하는 리소스는 DEBUG 레벨로 로깅
|
||||
String resourcePath = ex.getResourcePath();
|
||||
if (resourcePath != null && (resourcePath.contains("favicon") || resourcePath.endsWith(".ico"))) {
|
||||
log.debug("Static resource not found (expected): {}", resourcePath);
|
||||
} else {
|
||||
log.warn("Static resource not found: {}", resourcePath);
|
||||
}
|
||||
|
||||
ErrorResponse error = ErrorResponse.builder()
|
||||
.code("RESOURCE_NOT_FOUND")
|
||||
.message("요청하신 리소스를 찾을 수 없습니다")
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일반 예외 처리
|
||||
*/
|
||||
|
||||
@@ -20,5 +20,10 @@ public enum ServiceStatus {
|
||||
/**
|
||||
* 성능 저하
|
||||
*/
|
||||
DEGRADED
|
||||
DEGRADED,
|
||||
|
||||
/**
|
||||
* 상태 알 수 없음 (설정되지 않음)
|
||||
*/
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
@@ -184,7 +184,6 @@ public class AIRecommendationService {
|
||||
ClaudeResponse response = claudeApiClient.sendMessage(
|
||||
apiKey,
|
||||
anthropicVersion,
|
||||
"application/json",
|
||||
request
|
||||
);
|
||||
|
||||
|
||||
@@ -93,7 +93,6 @@ public class TrendAnalysisService {
|
||||
ClaudeResponse response = claudeApiClient.sendMessage(
|
||||
apiKey,
|
||||
anthropicVersion,
|
||||
"application/json",
|
||||
request
|
||||
);
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ spring:
|
||||
# Redis Configuration
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:20.214.210.71}
|
||||
host: ${REDIS_HOST:redis-external} # Production: redis-external, Local: 20.214.210.71
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
database: ${REDIS_DATABASE:3} # AI Service uses database 3
|
||||
database: ${REDIS_DATABASE:0} # AI Service uses database 3
|
||||
timeout: ${REDIS_TIMEOUT:3000}
|
||||
lettuce:
|
||||
pool:
|
||||
@@ -33,26 +33,6 @@ spring:
|
||||
listener:
|
||||
ack-mode: manual
|
||||
|
||||
# JPA Configuration (Not used but included for consistency)
|
||||
jpa:
|
||||
open-in-view: false
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
use_sql_comments: false
|
||||
|
||||
# Database Configuration (Not used but included for consistency)
|
||||
datasource:
|
||||
url: jdbc:postgresql://${DB_HOST:4.230.112.141}:${DB_PORT:5432}/${DB_NAME:aidb}
|
||||
username: ${DB_USERNAME:eventuser}
|
||||
password: ${DB_PASSWORD:}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 10
|
||||
minimum-idle: 2
|
||||
connection-timeout: 30000
|
||||
|
||||
# Server Configuration
|
||||
server:
|
||||
port: ${SERVER_PORT:8083}
|
||||
@@ -119,6 +99,13 @@ logging:
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: ${LOG_FILE:logs/ai-service.log}
|
||||
logback:
|
||||
rollingpolicy:
|
||||
max-file-size: 10MB
|
||||
max-history: 7
|
||||
total-size-cap: 100MB
|
||||
|
||||
# Kafka Topics Configuration
|
||||
kafka:
|
||||
@@ -131,8 +118,10 @@ ai:
|
||||
claude:
|
||||
api-url: ${CLAUDE_API_URL:https://api.anthropic.com/v1/messages}
|
||||
api-key: ${CLAUDE_API_KEY:}
|
||||
anthropic-version: ${CLAUDE_ANTHROPIC_VERSION:2023-06-01}
|
||||
model: ${CLAUDE_MODEL:claude-3-5-sonnet-20241022}
|
||||
max-tokens: ${CLAUDE_MAX_TOKENS:4096}
|
||||
temperature: ${CLAUDE_TEMPERATURE:0.7}
|
||||
timeout: ${CLAUDE_TIMEOUT:300000} # 5 minutes
|
||||
gpt4:
|
||||
api-url: ${GPT4_API_URL:https://api.openai.com/v1/chat/completions}
|
||||
|
||||
Reference in New Issue
Block a user