Compare commits

..

4 Commits

Author SHA1 Message Date
wonho
2314156afe ai-service Redis 설정 수정 및 실행 프로파일 업데이트
주요 변경사항:
- Redis Sentinel 모드에서 Standalone 모드로 변경
- RedisConfig.java: RedisSentinelConfiguration → RedisStandaloneConfiguration
- AiServiceApplication 실행 프로파일 수정:
  * 메인 클래스: AiApplication → AiServiceApplication
  * 포트: 8081 → 8083
  * Claude API 설정 추가 (URL, API Key, Model)
  * Kafka 토픽 설정 추가
- application.yml: Redis 단일 인스턴스 설정으로 통일
- RecommendationGenerationException 클래스 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 11:24:29 +09:00
이선민
5b0d3f5289
Update README.md 2025-10-31 11:22:09 +09:00
kkkd-max
6a49255fbb
Merge pull request #42 from ktds-dg0501/feature/partici23
api경로 수정
2025-10-31 11:06:51 +09:00
jhbkjh
1a8e43fb68 api경로 수정 2025-10-31 11:02:29 +09:00
7 changed files with 71 additions and 56 deletions

23
.github/README.md vendored
View File

@ -1,12 +1,15 @@
# KT Event Marketing - CI/CD Infrastructure # KT 이벤트 파트너
이 디렉토리는 KT Event Marketing 백엔드 서비스의 CI/CD 인프라를 포함합니다. KT 이벤트 파트너는 소상공인이 AI를 활용하여 효과적인 이벤트를 쉽게 기획하고 관리할 수 있도록 지원하는 플랫폼입니다.
매장 정보와 AI 추천을 기반으로 이벤트를 생성하고, SNS 콘텐츠를 자동 생성하며, 다양한 채널로 배포하고, 실시간으로 성과를 분석할 수 있습니다.
이 디렉토리는 KT 이벤트 파트너 서비스의 CI/CD 인프라를 포함합니다.
## 디렉토리 구조 ## 디렉토리 구조
``` ```
.github/ .github/
├── README.md # 이 파일 ├── README.md
├── workflows/ ├── workflows/
│ └── backend-cicd.yaml # GitHub Actions 워크플로우 │ └── backend-cicd.yaml # GitHub Actions 워크플로우
├── kustomize/ # Kubernetes 매니페스트 관리 ├── kustomize/ # Kubernetes 매니페스트 관리
@ -126,13 +129,13 @@ GitHub Actions 워크플로우 정의 파일입니다.
## 서비스 목록 ## 서비스 목록
1. **user-service** (8081) - 사용자 관리 1. **user-service** (8081) - 사용자 및 매장 관리
2. **event-service** (8082) - 이벤트 관리 2. **event-service** (8080) - 이벤트 관리
3. **ai-service** (8083) - AI 기반 콘텐츠 생성 3. **ai-service** (8083) - AI 기반 트렌드 분석 및 이벤트 추천
4. **content-service** (8084) - 콘텐츠 관리 4. **content-service** (8084) - SNS 콘텐츠(이미지) 생성
5. **distribution-service** (8085) - 경품 배포 5. **distribution-service** (8085) - 다중 채 배포
6. **participation-service** (8086) - 이벤트 참여 6. **participation-service** (8084) - 이벤트 참여자 관리
7. **analytics-service** (8087) - 분석 및 통계 7. **analytics-service** (8086) - 성과 분석 및 통계
## 모니터링 ## 모니터링

View File

@ -2,7 +2,7 @@
<configuration default="false" name="AiServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true"> <configuration default="false" name="AiServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
<option name="ACTIVE_PROFILES" /> <option name="ACTIVE_PROFILES" />
<module name="kt-event-marketing.ai-service.main" /> <module name="kt-event-marketing.ai-service.main" />
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.ai.AiApplication" /> <option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.ai.AiServiceApplication" />
<extension name="coverage"> <extension name="coverage">
<pattern> <pattern>
<option name="PATTERN" value="com.kt.ai.*" /> <option name="PATTERN" value="com.kt.ai.*" />
@ -10,19 +10,25 @@
</pattern> </pattern>
</extension> </extension>
<envs> <envs>
<env name="SERVER_PORT" value="8081" /> <env name="SERVER_PORT" value="8083" />
<env name="DB_HOST" value="4.230.112.141" />
<env name="DB_PORT" value="5432" />
<env name="DB_NAME" value="aidb" />
<env name="DB_USERNAME" value="eventuser" />
<env name="DB_PASSWORD" value="Hi5Jessica!" />
<env name="REDIS_HOST" value="20.214.210.71" /> <env name="REDIS_HOST" value="20.214.210.71" />
<env name="REDIS_PORT" value="6379" /> <env name="REDIS_PORT" value="6379" />
<env name="REDIS_PASSWORD" value="Hi5Jessica!" /> <env name="REDIS_PASSWORD" value="Hi5Jessica!" />
<env name="REDIS_DATABASE" value="3" />
<env name="KAFKA_BOOTSTRAP_SERVERS" value="20.249.182.13:9095,4.217.131.59:9095" /> <env name="KAFKA_BOOTSTRAP_SERVERS" value="20.249.182.13:9095,4.217.131.59:9095" />
<env name="KAFKA_CONSUMER_GROUP" value="ai" /> <env name="KAFKA_CONSUMER_GROUP" value="ai-service-consumers" />
<env name="JPA_DDL_AUTO" value="update" /> <env name="KAFKA_TOPICS_AI_JOB" value="ai-event-generation-job" />
<env name="JPA_SHOW_SQL" value="false" /> <env name="KAFKA_TOPICS_AI_JOB_DLQ" value="ai-event-generation-job-dlq" />
<env name="AI_PROVIDER" value="CLAUDE" />
<env name="AI_CLAUDE_API_URL" value="https://api.anthropic.com/v1/messages" />
<env name="AI_CLAUDE_API_KEY" value="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" />
<env name="AI_CLAUDE_ANTHROPIC_VERSION" value="2023-06-01" />
<env name="AI_CLAUDE_MODEL" value="claude-sonnet-4-5-20250929" />
<env name="AI_CLAUDE_MAX_TOKENS" value="4096" />
<env name="AI_CLAUDE_TEMPERATURE" value="0.7" />
<env name="LOG_LEVEL_ROOT" value="INFO" />
<env name="LOG_LEVEL_AI" value="DEBUG" />
<env name="LOG_LEVEL_KAFKA" value="INFO" />
</envs> </envs>
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />

View File

@ -9,7 +9,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
@ -17,7 +17,6 @@ import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSeriali
import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays;
/** /**
* Redis 설정 * Redis 설정
@ -30,11 +29,11 @@ import java.util.Arrays;
@Configuration @Configuration
public class RedisConfig { public class RedisConfig {
@Value("${spring.data.redis.sentinel.master:mymaster}") @Value("${spring.data.redis.host}")
private String sentinelMaster; private String redisHost;
@Value("${spring.data.redis.sentinel.nodes}") @Value("${spring.data.redis.port}")
private String sentinelNodes; private int redisPort;
@Value("${spring.data.redis.password}") @Value("${spring.data.redis.password}")
private String redisPassword; private String redisPassword;
@ -46,26 +45,18 @@ public class RedisConfig {
private long redisTimeout; private long redisTimeout;
/** /**
* Redis 연결 팩토리 설정 (Sentinel 모드) * Redis 연결 팩토리 설정 (Standalone 모드)
*/ */
@Bean @Bean
public RedisConnectionFactory redisConnectionFactory() { public RedisConnectionFactory redisConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
.master(sentinelMaster); redisConfig.setHostName(redisHost);
redisConfig.setPort(redisPort);
// Sentinel 노드 추가 (콤마로 구분된 host:port 형식) redisConfig.setDatabase(redisDatabase);
Arrays.stream(sentinelNodes.split(","))
.map(String::trim)
.forEach(node -> {
String[] parts = node.split(":");
sentinelConfig.sentinel(parts[0], Integer.parseInt(parts[1]));
});
if (redisPassword != null && !redisPassword.isEmpty()) { if (redisPassword != null && !redisPassword.isEmpty()) {
sentinelConfig.setPassword(redisPassword); redisConfig.setPassword(redisPassword);
sentinelConfig.setSentinelPassword(redisPassword);
} }
sentinelConfig.setDatabase(redisDatabase);
// Lettuce Client 설정: Timeout Connection 옵션 // Lettuce Client 설정: Timeout Connection 옵션
SocketOptions socketOptions = SocketOptions.builder() SocketOptions socketOptions = SocketOptions.builder()
@ -83,7 +74,7 @@ public class RedisConfig {
.clientOptions(clientOptions) .clientOptions(clientOptions)
.build(); .build();
return new LettuceConnectionFactory(sentinelConfig, clientConfig); return new LettuceConnectionFactory(redisConfig, clientConfig);
} }
/** /**

View File

@ -0,0 +1,17 @@
package com.kt.ai.exception;
/**
* 추천 생성 발생한 예외
*
* @author AI Service Team
* @since 1.0.0
*/
public class RecommendationGenerationException extends AIServiceException {
public RecommendationGenerationException(String message) {
super("RECOMMENDATION_GENERATION_FAILED", message);
}
public RecommendationGenerationException(String message, Throwable cause) {
super("RECOMMENDATION_GENERATION_FAILED", message, cause);
}
}

View File

@ -2,16 +2,14 @@ spring:
application: application:
name: ai-service name: ai-service
# Redis Configuration with Sentinel # Redis Configuration
data: data:
redis: redis:
host: ${REDIS_HOST:20.214.210.71}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:Hi5Jessica!} password: ${REDIS_PASSWORD:Hi5Jessica!}
database: ${REDIS_DATABASE:3} database: ${REDIS_DATABASE:3}
timeout: ${REDIS_TIMEOUT:3000} timeout: ${REDIS_TIMEOUT:3000}
sentinel:
master: ${REDIS_SENTINEL_MASTER:mymaster}
nodes: ${REDIS_SENTINEL_NODES:redis-node-0.redis-headless.kt-event-marketing.svc.cluster.local:26379,redis-node-1.redis-headless.kt-event-marketing.svc.cluster.local:26379}
password: ${REDIS_PASSWORD:Hi5Jessica!}
lettuce: lettuce:
pool: pool:
max-active: ${REDIS_POOL_MAX:8} max-active: ${REDIS_POOL_MAX:8}

View File

@ -34,9 +34,9 @@ public class ParticipationController {
/** /**
* 이벤트 참여 * 이벤트 참여
* POST /events/{eventId}/participate * POST /{eventId}/participate
*/ */
@PostMapping("/events/{eventId}/participate") @PostMapping("/{eventId}/participate")
public ResponseEntity<ApiResponse<ParticipationResponse>> participate( public ResponseEntity<ApiResponse<ParticipationResponse>> participate(
@PathVariable String eventId, @PathVariable String eventId,
@Valid @RequestBody ParticipationRequest request) { @Valid @RequestBody ParticipationRequest request) {
@ -60,14 +60,14 @@ public class ParticipationController {
/** /**
* 참여자 목록 조회 * 참여자 목록 조회
* GET /events/{eventId}/participants * GET /{eventId}/participants
*/ */
@Operation( @Operation(
summary = "참여자 목록 조회", summary = "참여자 목록 조회",
description = "이벤트의 참여자 목록을 페이징하여 조회합니다. " + description = "이벤트의 참여자 목록을 페이징하여 조회합니다. " +
"정렬 가능한 필드: createdAt(기본값), participantId, name, phoneNumber, bonusEntries, isWinner, wonAt" "정렬 가능한 필드: createdAt(기본값), participantId, name, phoneNumber, bonusEntries, isWinner, wonAt"
) )
@GetMapping({"/events/{eventId}/participants"}) @GetMapping({"/{eventId}/participants"})
public ResponseEntity<ApiResponse<PageResponse<ParticipationResponse>>> getParticipants( public ResponseEntity<ApiResponse<PageResponse<ParticipationResponse>>> getParticipants(
@Parameter(description = "이벤트 ID", example = "evt_20250124_001") @Parameter(description = "이벤트 ID", example = "evt_20250124_001")
@PathVariable String eventId, @PathVariable String eventId,
@ -88,9 +88,9 @@ public class ParticipationController {
/** /**
* 참여자 상세 조회 * 참여자 상세 조회
* GET /events/{eventId}/participants/{participantId} * GET /{eventId}/participants/{participantId}
*/ */
@GetMapping({"/events/{eventId}/participants/{participantId}"}) @GetMapping({"/{eventId}/participants/{participantId}"})
public ResponseEntity<ApiResponse<ParticipationResponse>> getParticipant( public ResponseEntity<ApiResponse<ParticipationResponse>> getParticipant(
@PathVariable String eventId, @PathVariable String eventId,
@PathVariable String participantId) { @PathVariable String participantId) {

View File

@ -35,9 +35,9 @@ public class WinnerController {
/** /**
* 당첨자 추첨 * 당첨자 추첨
* POST /events/{eventId}/draw-winners * POST /{eventId}/draw-winners
*/ */
@PostMapping("/events/{eventId}/draw-winners") @PostMapping("/{eventId}/draw-winners")
public ResponseEntity<ApiResponse<DrawWinnersResponse>> drawWinners( public ResponseEntity<ApiResponse<DrawWinnersResponse>> drawWinners(
@PathVariable String eventId, @PathVariable String eventId,
@Valid @RequestBody DrawWinnersRequest request) { @Valid @RequestBody DrawWinnersRequest request) {
@ -50,14 +50,14 @@ public class WinnerController {
/** /**
* 당첨자 목록 조회 * 당첨자 목록 조회
* GET /participations/{eventId}/winners * GET /{eventId}/winners
*/ */
@Operation( @Operation(
summary = "당첨자 목록 조회", summary = "당첨자 목록 조회",
description = "이벤트의 당첨자 목록을 페이징하여 조회합니다. " + description = "이벤트의 당첨자 목록을 페이징하여 조회합니다. " +
"정렬 가능한 필드: winnerRank(기본값), wonAt, participantId, name, phoneNumber, bonusEntries" "정렬 가능한 필드: winnerRank(기본값), wonAt, participantId, name, phoneNumber, bonusEntries"
) )
@GetMapping("/events/{eventId}/winners") @GetMapping("/{eventId}/winners")
public ResponseEntity<ApiResponse<PageResponse<ParticipationResponse>>> getWinners( public ResponseEntity<ApiResponse<PageResponse<ParticipationResponse>>> getWinners(
@Parameter(description = "이벤트 ID", example = "evt_20250124_001") @Parameter(description = "이벤트 ID", example = "evt_20250124_001")
@PathVariable String eventId, @PathVariable String eventId,