Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d14a7349bc | |||
| cf379407e8 | |||
| f13bfe6a6e | |||
| c6dfc74bda | |||
| 027ab86e8d | |||
| c95c47d630 | |||
| b92307d564 | |||
| 2663baf615 | |||
| 349b644617 | |||
| ea4d551d3e | |||
| d81c5be90d | |||
| e080acbcb9 | |||
| 29285d8576 | |||
| f2e8f7499f | |||
| 52b63fb0f0 | |||
| a23b4eb505 | |||
| c6b33885e0 | |||
| ac7fcbd2fe | |||
| 97f50fd751 | |||
| c53cbdf4f8 | |||
| 7dc039361f | |||
| 48c76db83a | |||
| 72728841db | |||
| 9e2d0a3889 | |||
| 14823a17c4 | |||
| a3781a279a | |||
| f80418f5ee | |||
| 5c365fe899 | |||
| a3381cc540 | |||
| 7ed2465d57 | |||
| 5cac8ccc12 | |||
| acd827b226 | |||
| ea53bd13a8 | |||
| 6948b48498 | |||
| aa8db3bf2f | |||
| 3afee053d0 | |||
| 27a3111dd8 | |||
| 3465a35827 | |||
| 8ff79ca1ab | |||
| 336d811f55 | |||
| ee941e4910 | |||
| b71d27aa8b | |||
| 108ee10293 | |||
| 20e0d24930 | |||
| 640e94bf17 | |||
| 98ed508a6f | |||
| e8d0a1d4b4 | |||
| 857fa5501c | |||
| ab39c76585 | |||
| 1e38d52967 | |||
| 34291e1613 | |||
| 6205a98ca0 | |||
| ebd7ae12b6 | |||
| 2cd1ba76f5 | |||
| a41e431daf | |||
| 3da9303091 | |||
| 3075a5d49f | |||
| 2bce7cfb24 | |||
| bcfbb6c7f9 | |||
| da173d79e9 | |||
| 7711f2d527 | |||
| edcb519800 | |||
| e7ffdcfe44 | |||
| 95a419f104 | |||
| 1b73d2880b | |||
| 5a93205f30 | |||
| df04f85346 | |||
| 23265b5849 | |||
| 8fef09df02 | |||
| 63ba449f93 | |||
| 64aec0fda5 | |||
| 436c0bf2b8 | |||
| 828a76b630 | |||
| 074be336d6 | |||
| b0d8a6d10e | |||
| 9f50c7feaa | |||
| 28a7a91ca2 |
@@ -0,0 +1,186 @@
|
|||||||
|
# KT Event Marketing - CI/CD Infrastructure
|
||||||
|
|
||||||
|
이 디렉토리는 KT Event Marketing 백엔드 서비스의 CI/CD 인프라를 포함합니다.
|
||||||
|
|
||||||
|
## 디렉토리 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
.github/
|
||||||
|
├── README.md # 이 파일
|
||||||
|
├── workflows/
|
||||||
|
│ └── backend-cicd.yaml # GitHub Actions 워크플로우
|
||||||
|
├── kustomize/ # Kubernetes 매니페스트 관리
|
||||||
|
│ ├── base/ # 기본 리소스 정의
|
||||||
|
│ │ ├── kustomization.yaml
|
||||||
|
│ │ ├── cm-common.yaml
|
||||||
|
│ │ ├── secret-common.yaml
|
||||||
|
│ │ ├── secret-imagepull.yaml
|
||||||
|
│ │ ├── ingress.yaml
|
||||||
|
│ │ └── {service}-*.yaml # 각 서비스별 리소스
|
||||||
|
│ └── overlays/ # 환경별 설정
|
||||||
|
│ ├── dev/
|
||||||
|
│ │ ├── kustomization.yaml
|
||||||
|
│ │ └── *-patch.yaml # 1 replica, 256Mi-1024Mi
|
||||||
|
│ ├── staging/
|
||||||
|
│ │ ├── kustomization.yaml
|
||||||
|
│ │ └── *-patch.yaml # 2 replicas, 512Mi-2048Mi
|
||||||
|
│ └── prod/
|
||||||
|
│ ├── kustomization.yaml
|
||||||
|
│ └── *-patch.yaml # 3 replicas, 1024Mi-4096Mi
|
||||||
|
├── config/
|
||||||
|
│ ├── deploy_env_vars_dev # Dev 환경 변수
|
||||||
|
│ ├── deploy_env_vars_staging # Staging 환경 변수
|
||||||
|
│ └── deploy_env_vars_prod # Prod 환경 변수
|
||||||
|
└── scripts/
|
||||||
|
├── deploy.sh # 수동 배포 스크립트
|
||||||
|
├── generate-patches.sh # 패치 파일 생성 스크립트
|
||||||
|
└── copy-manifests-to-base.py # 매니페스트 복사 스크립트
|
||||||
|
```
|
||||||
|
|
||||||
|
## 주요 파일 설명
|
||||||
|
|
||||||
|
### workflows/backend-cicd.yaml
|
||||||
|
GitHub Actions 워크플로우 정의 파일입니다.
|
||||||
|
|
||||||
|
**트리거**:
|
||||||
|
- develop 브랜치 push → dev 환경 배포
|
||||||
|
- main 브랜치 push → prod 환경 배포
|
||||||
|
- Manual workflow dispatch → 원하는 환경과 서비스 선택
|
||||||
|
|
||||||
|
**Jobs**:
|
||||||
|
1. `detect-changes`: 변경된 서비스 감지
|
||||||
|
2. `build-and-push`: 서비스 빌드 및 ACR 푸시
|
||||||
|
3. `deploy`: AKS에 배포
|
||||||
|
4. `notify`: 배포 결과 알림
|
||||||
|
|
||||||
|
### kustomize/base/kustomization.yaml
|
||||||
|
모든 환경에서 공통으로 사용하는 기본 리소스를 정의합니다.
|
||||||
|
|
||||||
|
**포함 리소스**:
|
||||||
|
- Common ConfigMaps and Secrets
|
||||||
|
- Ingress
|
||||||
|
- 7개 서비스의 Deployment, Service, ConfigMap, Secret
|
||||||
|
|
||||||
|
### kustomize/overlays/{env}/kustomization.yaml
|
||||||
|
환경별 설정을 오버라이드합니다.
|
||||||
|
|
||||||
|
**주요 차이점**:
|
||||||
|
- 이미지 태그 (dev/staging/prod)
|
||||||
|
- Replica 수 (1/2/3)
|
||||||
|
- 리소스 할당량 (작음/중간/큼)
|
||||||
|
|
||||||
|
### scripts/deploy.sh
|
||||||
|
로컬에서 수동 배포를 위한 스크립트입니다.
|
||||||
|
|
||||||
|
**사용법**:
|
||||||
|
```bash
|
||||||
|
# 모든 서비스를 dev 환경에 배포
|
||||||
|
./scripts/deploy.sh dev
|
||||||
|
|
||||||
|
# 특정 서비스만 prod 환경에 배포
|
||||||
|
./scripts/deploy.sh prod user-service
|
||||||
|
```
|
||||||
|
|
||||||
|
## 배포 프로세스
|
||||||
|
|
||||||
|
### 자동 배포 (GitHub Actions)
|
||||||
|
|
||||||
|
1. **Dev 환경**:
|
||||||
|
```bash
|
||||||
|
git checkout develop
|
||||||
|
git push origin develop
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Prod 환경**:
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git merge develop
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **수동 배포**:
|
||||||
|
- GitHub Actions UI → Run workflow
|
||||||
|
- Environment 선택 (dev/staging/prod)
|
||||||
|
- Service 선택 (all 또는 특정 서비스)
|
||||||
|
|
||||||
|
### 수동 배포 (로컬)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 사전 요구사항: Azure CLI, kubectl, kustomize 설치
|
||||||
|
# Azure 로그인 필요
|
||||||
|
|
||||||
|
# Dev 환경에 모든 서비스 배포
|
||||||
|
./.github/scripts/deploy.sh dev
|
||||||
|
|
||||||
|
# Prod 환경에 user-service만 배포
|
||||||
|
./.github/scripts/deploy.sh prod user-service
|
||||||
|
```
|
||||||
|
|
||||||
|
## 환경별 설정
|
||||||
|
|
||||||
|
| 환경 | 브랜치 | 이미지 태그 | Replicas | CPU Request | Memory Request |
|
||||||
|
|------|--------|-------------|----------|-------------|----------------|
|
||||||
|
| Dev | develop | dev | 1 | 256m | 256Mi |
|
||||||
|
| Staging | manual | staging | 2 | 512m | 512Mi |
|
||||||
|
| Prod | main | prod | 3 | 1024m | 1024Mi |
|
||||||
|
|
||||||
|
## 서비스 목록
|
||||||
|
|
||||||
|
1. **user-service** (8081) - 사용자 관리
|
||||||
|
2. **event-service** (8082) - 이벤트 관리
|
||||||
|
3. **ai-service** (8083) - AI 기반 콘텐츠 생성
|
||||||
|
4. **content-service** (8084) - 콘텐츠 관리
|
||||||
|
5. **distribution-service** (8085) - 경품 배포
|
||||||
|
6. **participation-service** (8086) - 이벤트 참여
|
||||||
|
7. **analytics-service** (8087) - 분석 및 통계
|
||||||
|
|
||||||
|
## 모니터링
|
||||||
|
|
||||||
|
### Pod 상태 확인
|
||||||
|
```bash
|
||||||
|
kubectl get pods -n kt-event-marketing
|
||||||
|
```
|
||||||
|
|
||||||
|
### 로그 확인
|
||||||
|
```bash
|
||||||
|
# 실시간 로그
|
||||||
|
kubectl logs -n kt-event-marketing -l app=user-service -f
|
||||||
|
|
||||||
|
# 이전 컨테이너 로그
|
||||||
|
kubectl logs -n kt-event-marketing <pod-name> --previous
|
||||||
|
```
|
||||||
|
|
||||||
|
### 리소스 사용량
|
||||||
|
```bash
|
||||||
|
# Pod 리소스
|
||||||
|
kubectl top pods -n kt-event-marketing
|
||||||
|
|
||||||
|
# Node 리소스
|
||||||
|
kubectl top nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
## 트러블슈팅
|
||||||
|
|
||||||
|
상세한 트러블슈팅 가이드는 [deployment/cicd/CICD-GUIDE.md](../../deployment/cicd/CICD-GUIDE.md)를 참조하세요.
|
||||||
|
|
||||||
|
**주요 문제 해결**:
|
||||||
|
- ImagePullBackOff → ACR Secret 확인
|
||||||
|
- CrashLoopBackOff → 로그 확인 및 환경 변수 검증
|
||||||
|
- Readiness Probe Failed → Context Path 및 Actuator 경로 확인
|
||||||
|
|
||||||
|
## 롤백
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 이전 버전으로 롤백
|
||||||
|
kubectl rollout undo deployment/user-service -n kt-event-marketing
|
||||||
|
|
||||||
|
# 특정 리비전으로 롤백
|
||||||
|
kubectl rollout undo deployment/user-service --to-revision=2 -n kt-event-marketing
|
||||||
|
```
|
||||||
|
|
||||||
|
## 참고 자료
|
||||||
|
|
||||||
|
- [CI/CD 가이드 (한글)](../../deployment/cicd/CICD-GUIDE.md)
|
||||||
|
- [GitHub Actions 공식 문서](https://docs.github.com/en/actions)
|
||||||
|
- [Kustomize 공식 문서](https://kustomize.io/)
|
||||||
|
- [Azure AKS 공식 문서](https://docs.microsoft.com/en-us/azure/aks/)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Development Environment Variables
|
||||||
|
ENVIRONMENT=dev
|
||||||
|
ACR_NAME=acrdigitalgarage01
|
||||||
|
RESOURCE_GROUP=rg-digitalgarage-01
|
||||||
|
AKS_CLUSTER=aks-digitalgarage-01
|
||||||
|
NAMESPACE=kt-event-marketing
|
||||||
|
REPLICAS=1
|
||||||
|
CPU_REQUEST=256m
|
||||||
|
MEMORY_REQUEST=256Mi
|
||||||
|
CPU_LIMIT=1024m
|
||||||
|
MEMORY_LIMIT=1024Mi
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Production Environment Variables
|
||||||
|
ENVIRONMENT=prod
|
||||||
|
ACR_NAME=acrdigitalgarage01
|
||||||
|
RESOURCE_GROUP=rg-digitalgarage-01
|
||||||
|
AKS_CLUSTER=aks-digitalgarage-01
|
||||||
|
NAMESPACE=kt-event-marketing
|
||||||
|
REPLICAS=3
|
||||||
|
CPU_REQUEST=1024m
|
||||||
|
MEMORY_REQUEST=1024Mi
|
||||||
|
CPU_LIMIT=4096m
|
||||||
|
MEMORY_LIMIT=4096Mi
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Staging Environment Variables
|
||||||
|
ENVIRONMENT=staging
|
||||||
|
ACR_NAME=acrdigitalgarage01
|
||||||
|
RESOURCE_GROUP=rg-digitalgarage-01
|
||||||
|
AKS_CLUSTER=aks-digitalgarage-01
|
||||||
|
NAMESPACE=kt-event-marketing
|
||||||
|
REPLICAS=2
|
||||||
|
CPU_REQUEST=512m
|
||||||
|
MEMORY_REQUEST=512Mi
|
||||||
|
CPU_LIMIT=2048m
|
||||||
|
MEMORY_LIMIT=2048Mi
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: cm-ai-service
|
||||||
|
data:
|
||||||
|
# Server Configuration
|
||||||
|
SERVER_PORT: "8083"
|
||||||
|
|
||||||
|
# Redis Configuration (service-specific)
|
||||||
|
REDIS_DATABASE: "3"
|
||||||
|
REDIS_TIMEOUT: "3000"
|
||||||
|
REDIS_POOL_MIN: "2"
|
||||||
|
|
||||||
|
# Kafka Configuration (service-specific)
|
||||||
|
KAFKA_CONSUMER_GROUP: "ai-service-consumers"
|
||||||
|
|
||||||
|
# Kafka Topics Configuration
|
||||||
|
KAFKA_TOPICS_AI_JOB: "ai-event-generation-job"
|
||||||
|
KAFKA_TOPICS_AI_JOB_DLQ: "ai-event-generation-job-dlq"
|
||||||
|
|
||||||
|
# AI Provider Configuration
|
||||||
|
AI_PROVIDER: "CLAUDE"
|
||||||
|
AI_CLAUDE_API_URL: "https://api.anthropic.com/v1/messages"
|
||||||
|
AI_CLAUDE_ANTHROPIC_VERSION: "2023-06-01"
|
||||||
|
AI_CLAUDE_MODEL: "claude-sonnet-4-5-20250929"
|
||||||
|
AI_CLAUDE_MAX_TOKENS: "4096"
|
||||||
|
AI_CLAUDE_TEMPERATURE: "0.7"
|
||||||
|
AI_CLAUDE_TIMEOUT: "300000"
|
||||||
|
|
||||||
|
# Circuit Breaker Configuration
|
||||||
|
RESILIENCE4J_CIRCUITBREAKER_FAILURE_RATE_THRESHOLD: "50"
|
||||||
|
RESILIENCE4J_CIRCUITBREAKER_SLOW_CALL_RATE_THRESHOLD: "50"
|
||||||
|
RESILIENCE4J_CIRCUITBREAKER_SLOW_CALL_DURATION_THRESHOLD: "60s"
|
||||||
|
RESILIENCE4J_CIRCUITBREAKER_PERMITTED_CALLS_HALF_OPEN: "3"
|
||||||
|
RESILIENCE4J_CIRCUITBREAKER_SLIDING_WINDOW_SIZE: "10"
|
||||||
|
RESILIENCE4J_CIRCUITBREAKER_MINIMUM_CALLS: "5"
|
||||||
|
RESILIENCE4J_CIRCUITBREAKER_WAIT_DURATION_OPEN: "60s"
|
||||||
|
RESILIENCE4J_TIMELIMITER_TIMEOUT_DURATION: "300s"
|
||||||
|
|
||||||
|
# Redis Cache TTL Configuration (seconds)
|
||||||
|
CACHE_TTL_RECOMMENDATION: "86400"
|
||||||
|
CACHE_TTL_JOB_STATUS: "86400"
|
||||||
|
CACHE_TTL_TREND: "3600"
|
||||||
|
CACHE_TTL_FALLBACK: "604800"
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
LOG_LEVEL_ROOT: "INFO"
|
||||||
|
LOG_LEVEL_AI: "DEBUG"
|
||||||
|
LOG_LEVEL_KAFKA: "INFO"
|
||||||
|
LOG_LEVEL_REDIS: "INFO"
|
||||||
|
LOG_LEVEL_RESILIENCE4J: "DEBUG"
|
||||||
|
LOG_FILE_NAME: "logs/ai-service.log"
|
||||||
|
LOG_FILE_MAX_SIZE: "10MB"
|
||||||
|
LOG_FILE_MAX_HISTORY: "7"
|
||||||
|
LOG_FILE_TOTAL_CAP: "100MB"
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: ai-service
|
||||||
|
labels:
|
||||||
|
app: ai-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: ai-service
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: ai-service
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: kt-event-marketing
|
||||||
|
containers:
|
||||||
|
- name: ai-service
|
||||||
|
image: acrdigitalgarage01.azurecr.io/kt-event-marketing/ai-service:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8083
|
||||||
|
name: http
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-common
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-ai-service
|
||||||
|
- secretRef:
|
||||||
|
name: secret-common
|
||||||
|
- secretRef:
|
||||||
|
name: secret-ai-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health
|
||||||
|
port: 8083
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 30
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/readiness
|
||||||
|
port: 8083
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/liveness
|
||||||
|
port: 8083
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 3
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: secret-ai-service
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
# Claude API Key
|
||||||
|
AI_CLAUDE_API_KEY: "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"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: ai-service
|
||||||
|
labels:
|
||||||
|
app: ai-service
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 8083
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: ai-service
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: cm-analytics-service
|
||||||
|
data:
|
||||||
|
# Server Configuration
|
||||||
|
SERVER_PORT: "8086"
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_HOST: "analytic-postgresql"
|
||||||
|
DB_PORT: "5432"
|
||||||
|
DB_NAME: "analytics_db"
|
||||||
|
DB_USERNAME: "eventuser"
|
||||||
|
|
||||||
|
# Redis Configuration (service-specific)
|
||||||
|
REDIS_DATABASE: "5"
|
||||||
|
|
||||||
|
# Kafka Configuration (service-specific)
|
||||||
|
KAFKA_ENABLED: "true"
|
||||||
|
KAFKA_CONSUMER_GROUP_ID: "analytics-service"
|
||||||
|
|
||||||
|
# Sample Data Configuration (MVP only)
|
||||||
|
SAMPLE_DATA_ENABLED: "true"
|
||||||
|
|
||||||
|
# Batch Scheduler Configuration
|
||||||
|
BATCH_REFRESH_INTERVAL: "300000" # 5분 (밀리초)
|
||||||
|
BATCH_INITIAL_DELAY: "30000" # 30초 (밀리초)
|
||||||
|
BATCH_ENABLED: "true"
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
LOG_LEVEL_APP: "INFO"
|
||||||
|
LOG_LEVEL_WEB: "INFO"
|
||||||
|
LOG_LEVEL_SQL: "WARN"
|
||||||
|
LOG_LEVEL_SQL_TYPE: "WARN"
|
||||||
|
SHOW_SQL: "false"
|
||||||
|
DDL_AUTO: "update"
|
||||||
|
LOG_FILE: "logs/analytics-service.log"
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: analytics-service
|
||||||
|
labels:
|
||||||
|
app: analytics-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: analytics-service
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: analytics-service
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: kt-event-marketing
|
||||||
|
containers:
|
||||||
|
- name: analytics-service
|
||||||
|
image: acrdigitalgarage01.azurecr.io/kt-event-marketing/analytics-service:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8086
|
||||||
|
name: http
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-common
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-analytics-service
|
||||||
|
- secretRef:
|
||||||
|
name: secret-common
|
||||||
|
- secretRef:
|
||||||
|
name: secret-analytics-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/liveness
|
||||||
|
port: 8086
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 30
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/liveness
|
||||||
|
port: 8086
|
||||||
|
initialDelaySeconds: 0
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 3
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/readiness
|
||||||
|
port: 8086
|
||||||
|
initialDelaySeconds: 0
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 3
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: secret-analytics-service
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
DB_PASSWORD: "Hi5Jessica!"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: analytics-service
|
||||||
|
labels:
|
||||||
|
app: analytics-service
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 8086
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: analytics-service
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: cm-common
|
||||||
|
data:
|
||||||
|
# Redis Configuration
|
||||||
|
REDIS_ENABLED: "true"
|
||||||
|
REDIS_HOST: "redis"
|
||||||
|
REDIS_PORT: "6379"
|
||||||
|
REDIS_TIMEOUT: "2000ms"
|
||||||
|
REDIS_POOL_MAX: "8"
|
||||||
|
REDIS_POOL_IDLE: "8"
|
||||||
|
REDIS_POOL_MIN: "0"
|
||||||
|
REDIS_POOL_WAIT: "-1ms"
|
||||||
|
|
||||||
|
# Kafka Configuration
|
||||||
|
KAFKA_BOOTSTRAP_SERVERS: "20.249.182.13:9095,4.217.131.59:9095"
|
||||||
|
EXCLUDE_KAFKA: ""
|
||||||
|
EXCLUDE_REDIS: ""
|
||||||
|
|
||||||
|
# CORS Configuration
|
||||||
|
CORS_ALLOWED_ORIGINS: "http://localhost:8081,http://localhost:8082,http://localhost:8083,http://localhost:8084,http://kt-event-marketing.20.214.196.128.nip.io"
|
||||||
|
CORS_ALLOWED_METHODS: "GET,POST,PUT,DELETE,OPTIONS,PATCH"
|
||||||
|
CORS_ALLOWED_HEADERS: "*"
|
||||||
|
CORS_ALLOW_CREDENTIALS: "true"
|
||||||
|
CORS_MAX_AGE: "3600"
|
||||||
|
|
||||||
|
# JWT Configuration
|
||||||
|
JWT_ACCESS_TOKEN_VALIDITY: "604800000"
|
||||||
|
JWT_REFRESH_TOKEN_VALIDITY: "86400000"
|
||||||
|
|
||||||
|
# JPA Configuration
|
||||||
|
DDL_AUTO: "update"
|
||||||
|
SHOW_SQL: "false"
|
||||||
|
JPA_DIALECT: "org.hibernate.dialect.PostgreSQLDialect"
|
||||||
|
H2_CONSOLE_ENABLED: "false"
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
LOG_LEVEL_APP: "INFO"
|
||||||
|
LOG_LEVEL_WEB: "INFO"
|
||||||
|
LOG_LEVEL_SQL: "WARN"
|
||||||
|
LOG_LEVEL_SQL_TYPE: "WARN"
|
||||||
|
LOG_LEVEL_ROOT: "INFO"
|
||||||
|
LOG_FILE_MAX_SIZE: "10MB"
|
||||||
|
LOG_FILE_MAX_HISTORY: "7"
|
||||||
|
LOG_FILE_TOTAL_CAP: "100MB"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: cm-content-service
|
||||||
|
data:
|
||||||
|
# Server Configuration
|
||||||
|
SERVER_PORT: "8084"
|
||||||
|
|
||||||
|
# Redis Configuration (service-specific)
|
||||||
|
REDIS_DATABASE: "1"
|
||||||
|
|
||||||
|
# Replicate API Configuration (Stable Diffusion)
|
||||||
|
REPLICATE_API_URL: "https://api.replicate.com"
|
||||||
|
REPLICATE_MODEL_VERSION: "stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b"
|
||||||
|
|
||||||
|
# HuggingFace API Configuration
|
||||||
|
HUGGINGFACE_API_URL: "https://api-inference.huggingface.co"
|
||||||
|
HUGGINGFACE_MODEL: "runwayml/stable-diffusion-v1-5"
|
||||||
|
|
||||||
|
# Azure Blob Storage Configuration
|
||||||
|
AZURE_CONTAINER_NAME: "content-images"
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
LOG_FILE_PATH: "logs/content-service.log"
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: content-service
|
||||||
|
labels:
|
||||||
|
app: content-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: content-service
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: content-service
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: kt-event-marketing
|
||||||
|
containers:
|
||||||
|
- name: content-service
|
||||||
|
image: acrdigitalgarage01.azurecr.io/kt-event-marketing/content-service:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8084
|
||||||
|
name: http
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-common
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-content-service
|
||||||
|
- secretRef:
|
||||||
|
name: secret-common
|
||||||
|
- secretRef:
|
||||||
|
name: secret-content-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/v1/content/actuator/health
|
||||||
|
port: 8084
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 30
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/v1/content/actuator/health/readiness
|
||||||
|
port: 8084
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/v1/content/actuator/health/liveness
|
||||||
|
port: 8084
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 3
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: secret-content-service
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
# Azure Blob Storage Connection String
|
||||||
|
AZURE_STORAGE_CONNECTION_STRING: "DefaultEndpointsProtocol=https;AccountName=blobkteventstorage;AccountKey=tcBN7mAfojbl0uGsOpU7RNuKNhHnzmwDiWjN31liSMVSrWaEK+HHnYKZrjBXXAC6ZPsuxUDlsf8x+AStd++QYg==;EndpointSuffix=core.windows.net"
|
||||||
|
|
||||||
|
# Replicate API Token
|
||||||
|
REPLICATE_API_TOKEN: "r8_BsGCJtAg5U5kkMBXSe3pgMkPufSKnUR4NY9gJ"
|
||||||
|
|
||||||
|
# HuggingFace API Token
|
||||||
|
HUGGINGFACE_API_TOKEN: ""
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: content-service
|
||||||
|
labels:
|
||||||
|
app: content-service
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 8084
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: content-service
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: cm-distribution-service
|
||||||
|
data:
|
||||||
|
# Server Configuration
|
||||||
|
SERVER_PORT: "8085"
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_HOST: "distribution-postgresql"
|
||||||
|
DB_PORT: "5432"
|
||||||
|
DB_NAME: "distributiondb"
|
||||||
|
DB_USERNAME: "eventuser"
|
||||||
|
|
||||||
|
# Kafka Configuration
|
||||||
|
KAFKA_ENABLED: "true"
|
||||||
|
KAFKA_CONSUMER_GROUP: "distribution-service"
|
||||||
|
|
||||||
|
# External Channel APIs
|
||||||
|
URIDONGNETV_API_URL: "http://localhost:9001/api/uridongnetv"
|
||||||
|
RINGOBIZ_API_URL: "http://localhost:9002/api/ringobiz"
|
||||||
|
GINITV_API_URL: "http://localhost:9003/api/ginitv"
|
||||||
|
INSTAGRAM_API_URL: "http://localhost:9004/api/instagram"
|
||||||
|
NAVER_API_URL: "http://localhost:9005/api/naver"
|
||||||
|
KAKAO_API_URL: "http://localhost:9006/api/kakao"
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
LOG_FILE: "logs/distribution-service.log"
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: distribution-service
|
||||||
|
labels:
|
||||||
|
app: distribution-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: distribution-service
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: distribution-service
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: kt-event-marketing
|
||||||
|
containers:
|
||||||
|
- name: distribution-service
|
||||||
|
image: acrdigitalgarage01.azurecr.io/kt-event-marketing/distribution-service:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8085
|
||||||
|
name: http
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-common
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-distribution-service
|
||||||
|
- secretRef:
|
||||||
|
name: secret-common
|
||||||
|
- secretRef:
|
||||||
|
name: secret-distribution-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/v1/distribution/actuator/health
|
||||||
|
port: 8085
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 30
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/v1/distribution/actuator/health/readiness
|
||||||
|
port: 8085
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/v1/distribution/actuator/health/liveness
|
||||||
|
port: 8085
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 3
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: secret-distribution-service
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
DB_PASSWORD: "Hi5Jessica!"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: distribution-service
|
||||||
|
labels:
|
||||||
|
app: distribution-service
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: distribution-service
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 8085
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: cm-event-service
|
||||||
|
data:
|
||||||
|
# Server Configuration
|
||||||
|
SERVER_PORT: "8080"
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_HOST: "event-postgresql"
|
||||||
|
DB_PORT: "5432"
|
||||||
|
DB_NAME: "eventdb"
|
||||||
|
DB_USERNAME: "eventuser"
|
||||||
|
|
||||||
|
# Redis Configuration (service-specific)
|
||||||
|
REDIS_DATABASE: "2"
|
||||||
|
|
||||||
|
# Kafka Configuration (service-specific)
|
||||||
|
KAFKA_CONSUMER_GROUP: "event-service-consumers"
|
||||||
|
|
||||||
|
# Service URLs
|
||||||
|
CONTENT_SERVICE_URL: "http://content-service"
|
||||||
|
DISTRIBUTION_SERVICE_URL: "http://distribution-service"
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
LOG_LEVEL: "INFO"
|
||||||
|
SQL_LOG_LEVEL: "WARN"
|
||||||
|
LOG_FILE: "logs/event-service.log"
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: event-service
|
||||||
|
labels:
|
||||||
|
app: event-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: event-service
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: event-service
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: kt-event-marketing
|
||||||
|
containers:
|
||||||
|
- name: event-service
|
||||||
|
image: acrdigitalgarage01.azurecr.io/kt-event-marketing/event-service:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
name: http
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-common
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-event-service
|
||||||
|
- secretRef:
|
||||||
|
name: secret-common
|
||||||
|
- secretRef:
|
||||||
|
name: secret-event-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 30
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/readiness
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/liveness
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 3
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: secret-event-service
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
# Database Password
|
||||||
|
DB_PASSWORD: "Hi5Jessica!"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: event-service
|
||||||
|
labels:
|
||||||
|
app: event-service
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: event-service
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: kt-event-marketing
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "false"
|
||||||
|
nginx.ingress.kubernetes.io/use-regex: "true"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: kt-event-marketing-api.20.214.196.128.nip.io
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
# User Service
|
||||||
|
- path: /api/v1/users
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: user-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
# Content Service
|
||||||
|
- path: /api/v1/content
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: content-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
# Event Service
|
||||||
|
- path: /api/v1/events
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: event-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
- path: /api/v1/jobs
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: event-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
- path: /api/v1/redis-test
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: event-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
# AI Service
|
||||||
|
- path: /api/v1/ai-service
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: ai-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
# Participation Service
|
||||||
|
- path: /api/v1/participations
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: participation-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
- path: /api/v1/winners
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: participation-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
- path: /debug
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: participation-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
# Analytics Service - Event Analytics
|
||||||
|
- path: /api/v1/events/([0-9]+)/analytics
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: analytics-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
# Analytics Service - User Analytics
|
||||||
|
- path: /api/v1/users/([0-9]+)/analytics
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: analytics-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
# Distribution Service
|
||||||
|
- path: /distribution
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: distribution-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
# Common resources
|
||||||
|
resources:
|
||||||
|
# Common ConfigMaps and Secrets
|
||||||
|
- cm-common.yaml
|
||||||
|
- secret-common.yaml
|
||||||
|
- secret-imagepull.yaml
|
||||||
|
|
||||||
|
# Ingress
|
||||||
|
- ingress.yaml
|
||||||
|
|
||||||
|
# user-service
|
||||||
|
- user-service-deployment.yaml
|
||||||
|
- user-service-service.yaml
|
||||||
|
- user-service-cm-user-service.yaml
|
||||||
|
- user-service-secret-user-service.yaml
|
||||||
|
|
||||||
|
# event-service
|
||||||
|
- event-service-deployment.yaml
|
||||||
|
- event-service-service.yaml
|
||||||
|
- event-service-cm-event-service.yaml
|
||||||
|
- event-service-secret-event-service.yaml
|
||||||
|
|
||||||
|
# ai-service
|
||||||
|
- ai-service-deployment.yaml
|
||||||
|
- ai-service-service.yaml
|
||||||
|
- ai-service-cm-ai-service.yaml
|
||||||
|
- ai-service-secret-ai-service.yaml
|
||||||
|
|
||||||
|
# content-service
|
||||||
|
- content-service-deployment.yaml
|
||||||
|
- content-service-service.yaml
|
||||||
|
- content-service-cm-content-service.yaml
|
||||||
|
- content-service-secret-content-service.yaml
|
||||||
|
|
||||||
|
# distribution-service
|
||||||
|
- distribution-service-deployment.yaml
|
||||||
|
- distribution-service-service.yaml
|
||||||
|
- distribution-service-cm-distribution-service.yaml
|
||||||
|
- distribution-service-secret-distribution-service.yaml
|
||||||
|
|
||||||
|
# participation-service
|
||||||
|
- participation-service-deployment.yaml
|
||||||
|
- participation-service-service.yaml
|
||||||
|
- participation-service-cm-participation-service.yaml
|
||||||
|
- participation-service-secret-participation-service.yaml
|
||||||
|
|
||||||
|
# analytics-service
|
||||||
|
- analytics-service-deployment.yaml
|
||||||
|
- analytics-service-service.yaml
|
||||||
|
- analytics-service-cm-analytics-service.yaml
|
||||||
|
- analytics-service-secret-analytics-service.yaml
|
||||||
|
|
||||||
|
# Image tag replacement (will be overridden by overlays)
|
||||||
|
images:
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/user-service
|
||||||
|
newTag: latest
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/event-service
|
||||||
|
newTag: latest
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/ai-service
|
||||||
|
newTag: latest
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/content-service
|
||||||
|
newTag: latest
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/distribution-service
|
||||||
|
newTag: latest
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/participation-service
|
||||||
|
newTag: latest
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/analytics-service
|
||||||
|
newTag: latest
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: cm-participation-service
|
||||||
|
data:
|
||||||
|
# Server Configuration
|
||||||
|
SERVER_PORT: "8084"
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_HOST: "participation-postgresql"
|
||||||
|
DB_PORT: "5432"
|
||||||
|
DB_NAME: "participationdb"
|
||||||
|
DB_USERNAME: "eventuser"
|
||||||
|
|
||||||
|
# Redis Configuration (service-specific)
|
||||||
|
REDIS_DATABASE: "4"
|
||||||
|
|
||||||
|
# Kafka Configuration (service-specific)
|
||||||
|
KAFKA_CONSUMER_GROUP: "participation-service-consumers"
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
LOG_LEVEL: "INFO"
|
||||||
|
SHOW_SQL: "false"
|
||||||
|
LOG_FILE: "logs/participation-service.log"
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: participation-service
|
||||||
|
labels:
|
||||||
|
app: participation-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: participation-service
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: participation-service
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: kt-event-marketing
|
||||||
|
containers:
|
||||||
|
- name: participation-service
|
||||||
|
image: acrdigitalgarage01.azurecr.io/kt-event-marketing/participation-service:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8084
|
||||||
|
name: http
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-common
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-participation-service
|
||||||
|
- secretRef:
|
||||||
|
name: secret-common
|
||||||
|
- secretRef:
|
||||||
|
name: secret-participation-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/liveness
|
||||||
|
port: 8084
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 30
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/liveness
|
||||||
|
port: 8084
|
||||||
|
initialDelaySeconds: 0
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 3
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/readiness
|
||||||
|
port: 8084
|
||||||
|
initialDelaySeconds: 0
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 3
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: secret-participation-service
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
DB_PASSWORD: "Hi5Jessica!"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: participation-service
|
||||||
|
labels:
|
||||||
|
app: participation-service
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 8084
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: participation-service
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: secret-common
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
# Redis Password
|
||||||
|
REDIS_PASSWORD: "Hi5Jessica!"
|
||||||
|
|
||||||
|
# JWT Secret
|
||||||
|
JWT_SECRET: "QL0czzXckz18kHnxpaTDoWFkq+3qKO7VQXeNvf2bOoU="
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: kt-event-marketing
|
||||||
|
type: kubernetes.io/dockerconfigjson
|
||||||
|
stringData:
|
||||||
|
.dockerconfigjson: |
|
||||||
|
{
|
||||||
|
"auths": {
|
||||||
|
"acrdigitalgarage01.azurecr.io": {
|
||||||
|
"username": "acrdigitalgarage01",
|
||||||
|
"password": "+OY+rmOagorjWvQe/tTk6oqvnZI8SmNbY/Y2o5EDcY+ACRDCDbYk",
|
||||||
|
"auth": "YWNyZGlnaXRhbGdhcmFnZTAxOitPWStybU9hZ29yald2UWUvdFRrNm9xdm5aSThTbU5iWS9ZMm81RURjWStBQ1JEQ0RiWWs="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: cm-user-service
|
||||||
|
data:
|
||||||
|
# Server Configuration
|
||||||
|
SERVER_PORT: "8081"
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_URL: "jdbc:postgresql://user-postgresql:5432/userdb"
|
||||||
|
DB_HOST: "user-postgresql"
|
||||||
|
DB_PORT: "5432"
|
||||||
|
DB_NAME: "userdb"
|
||||||
|
DB_USERNAME: "eventuser"
|
||||||
|
DB_DRIVER: "org.postgresql.Driver"
|
||||||
|
DB_KIND: "postgresql"
|
||||||
|
DB_POOL_MAX: "20"
|
||||||
|
DB_POOL_MIN: "5"
|
||||||
|
DB_CONN_TIMEOUT: "30000"
|
||||||
|
DB_IDLE_TIMEOUT: "600000"
|
||||||
|
DB_MAX_LIFETIME: "1800000"
|
||||||
|
DB_LEAK_THRESHOLD: "60000"
|
||||||
|
|
||||||
|
# Redis Configuration (service-specific)
|
||||||
|
REDIS_DATABASE: "0"
|
||||||
|
|
||||||
|
# Kafka Configuration (service-specific)
|
||||||
|
KAFKA_CONSUMER_GROUP: "user-service-consumers"
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
LOG_FILE_PATH: "logs/user-service.log"
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: user-service
|
||||||
|
labels:
|
||||||
|
app: user-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: user-service
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: user-service
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: kt-event-marketing
|
||||||
|
containers:
|
||||||
|
- name: user-service
|
||||||
|
image: acrdigitalgarage01.azurecr.io/kt-event-marketing/user-service:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8081
|
||||||
|
name: http
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-common
|
||||||
|
- configMapRef:
|
||||||
|
name: cm-user-service
|
||||||
|
- secretRef:
|
||||||
|
name: secret-common
|
||||||
|
- secretRef:
|
||||||
|
name: secret-user-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health
|
||||||
|
port: 8081
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 30
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/readiness
|
||||||
|
port: 8081
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /actuator/health/liveness
|
||||||
|
port: 8081
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 3
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: secret-user-service
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
# Database Password
|
||||||
|
DB_PASSWORD: "Hi5Jessica!"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: user-service
|
||||||
|
labels:
|
||||||
|
app: user-service
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 8081
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: user-service
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: ai-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: ai-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: analytics-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: analytics-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: content-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: content-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: distribution-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: distribution-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: event-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: event-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: kt-event-marketing
|
||||||
|
|
||||||
|
bases:
|
||||||
|
- ../../base
|
||||||
|
|
||||||
|
# Environment-specific patches
|
||||||
|
patchesStrategicMerge:
|
||||||
|
- user-service-patch.yaml
|
||||||
|
- event-service-patch.yaml
|
||||||
|
- ai-service-patch.yaml
|
||||||
|
- content-service-patch.yaml
|
||||||
|
- distribution-service-patch.yaml
|
||||||
|
- participation-service-patch.yaml
|
||||||
|
- analytics-service-patch.yaml
|
||||||
|
|
||||||
|
# Override image tags for dev environment
|
||||||
|
images:
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/user-service
|
||||||
|
newTag: dev
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/event-service
|
||||||
|
newTag: dev
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/ai-service
|
||||||
|
newTag: dev
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/content-service
|
||||||
|
newTag: dev
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/distribution-service
|
||||||
|
newTag: dev
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/participation-service
|
||||||
|
newTag: dev
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/analytics-service
|
||||||
|
newTag: dev
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: participation-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: participation-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: user-service
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: user-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "256m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: ai-service
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: ai-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "4096m"
|
||||||
|
memory: "4096Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: analytics-service
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: analytics-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "4096m"
|
||||||
|
memory: "4096Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: content-service
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: content-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "4096m"
|
||||||
|
memory: "4096Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: distribution-service
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: distribution-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "4096m"
|
||||||
|
memory: "4096Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: event-service
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: event-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "4096m"
|
||||||
|
memory: "4096Mi"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: kt-event-marketing
|
||||||
|
|
||||||
|
bases:
|
||||||
|
- ../../base
|
||||||
|
|
||||||
|
# Environment-specific labels
|
||||||
|
commonLabels:
|
||||||
|
environment: prod
|
||||||
|
|
||||||
|
# Environment-specific patches
|
||||||
|
patchesStrategicMerge:
|
||||||
|
- user-service-patch.yaml
|
||||||
|
- event-service-patch.yaml
|
||||||
|
- ai-service-patch.yaml
|
||||||
|
- content-service-patch.yaml
|
||||||
|
- distribution-service-patch.yaml
|
||||||
|
- participation-service-patch.yaml
|
||||||
|
- analytics-service-patch.yaml
|
||||||
|
|
||||||
|
# Override image tags for prod environment
|
||||||
|
images:
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/user-service
|
||||||
|
newTag: prod
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/event-service
|
||||||
|
newTag: prod
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/ai-service
|
||||||
|
newTag: prod
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/content-service
|
||||||
|
newTag: prod
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/distribution-service
|
||||||
|
newTag: prod
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/participation-service
|
||||||
|
newTag: prod
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/analytics-service
|
||||||
|
newTag: prod
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: participation-service
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: participation-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "4096m"
|
||||||
|
memory: "4096Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: user-service
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: user-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "4096m"
|
||||||
|
memory: "4096Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: ai-service
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: ai-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "512m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "2048m"
|
||||||
|
memory: "2048Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: analytics-service
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: analytics-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "512m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "2048m"
|
||||||
|
memory: "2048Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: content-service
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: content-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "512m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "2048m"
|
||||||
|
memory: "2048Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: distribution-service
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: distribution-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "512m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "2048m"
|
||||||
|
memory: "2048Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: event-service
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: event-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "512m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "2048m"
|
||||||
|
memory: "2048Mi"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: kt-event-marketing
|
||||||
|
|
||||||
|
bases:
|
||||||
|
- ../../base
|
||||||
|
|
||||||
|
# Environment-specific labels
|
||||||
|
commonLabels:
|
||||||
|
environment: staging
|
||||||
|
|
||||||
|
# Environment-specific patches
|
||||||
|
patchesStrategicMerge:
|
||||||
|
- user-service-patch.yaml
|
||||||
|
- event-service-patch.yaml
|
||||||
|
- ai-service-patch.yaml
|
||||||
|
- content-service-patch.yaml
|
||||||
|
- distribution-service-patch.yaml
|
||||||
|
- participation-service-patch.yaml
|
||||||
|
- analytics-service-patch.yaml
|
||||||
|
|
||||||
|
# Override image tags for staging environment
|
||||||
|
images:
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/user-service
|
||||||
|
newTag: staging
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/event-service
|
||||||
|
newTag: staging
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/ai-service
|
||||||
|
newTag: staging
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/content-service
|
||||||
|
newTag: staging
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/distribution-service
|
||||||
|
newTag: staging
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/participation-service
|
||||||
|
newTag: staging
|
||||||
|
- name: acrdigitalgarage01.azurecr.io/kt-event-marketing/analytics-service
|
||||||
|
newTag: staging
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: participation-service
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: participation-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "512m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "2048m"
|
||||||
|
memory: "2048Mi"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: user-service
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: user-service
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "512m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "2048m"
|
||||||
|
memory: "2048Mi"
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Copy K8s manifests to Kustomize base directory and remove namespace declarations
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Service names
|
||||||
|
SERVICES = [
|
||||||
|
'user-service',
|
||||||
|
'event-service',
|
||||||
|
'ai-service',
|
||||||
|
'content-service',
|
||||||
|
'distribution-service',
|
||||||
|
'participation-service',
|
||||||
|
'analytics-service'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Base directories
|
||||||
|
SOURCE_DIR = Path('deployment/k8s')
|
||||||
|
BASE_DIR = Path('.github/kustomize/base')
|
||||||
|
|
||||||
|
def remove_namespace_from_yaml(content):
|
||||||
|
"""Remove namespace field from YAML content"""
|
||||||
|
docs = list(yaml.safe_load_all(content))
|
||||||
|
|
||||||
|
for doc in docs:
|
||||||
|
if doc and isinstance(doc, dict):
|
||||||
|
if 'metadata' in doc and 'namespace' in doc['metadata']:
|
||||||
|
del doc['metadata']['namespace']
|
||||||
|
|
||||||
|
return yaml.dump_all(docs, default_flow_style=False, sort_keys=False)
|
||||||
|
|
||||||
|
def copy_and_process_file(source_path, dest_path):
|
||||||
|
"""Copy file and remove namespace declaration"""
|
||||||
|
with open(source_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Remove namespace from YAML
|
||||||
|
processed_content = remove_namespace_from_yaml(content)
|
||||||
|
|
||||||
|
# Write to destination
|
||||||
|
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(dest_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(processed_content)
|
||||||
|
|
||||||
|
print(f"✓ Copied and processed: {source_path} -> {dest_path}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Starting manifest copy to Kustomize base...")
|
||||||
|
|
||||||
|
# Copy common resources
|
||||||
|
print("\n[Common Resources]")
|
||||||
|
common_dir = SOURCE_DIR / 'common'
|
||||||
|
for file in ['cm-common.yaml', 'secret-common.yaml', 'secret-imagepull.yaml', 'ingress.yaml']:
|
||||||
|
source = common_dir / file
|
||||||
|
if source.exists():
|
||||||
|
dest = BASE_DIR / file
|
||||||
|
copy_and_process_file(source, dest)
|
||||||
|
|
||||||
|
# Copy service-specific resources
|
||||||
|
print("\n[Service Resources]")
|
||||||
|
for service in SERVICES:
|
||||||
|
service_dir = SOURCE_DIR / service
|
||||||
|
if not service_dir.exists():
|
||||||
|
print(f"⚠ Service directory not found: {service_dir}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\nProcessing {service}...")
|
||||||
|
for file in service_dir.glob('*.yaml'):
|
||||||
|
dest = BASE_DIR / f"{service}-{file.name}"
|
||||||
|
copy_and_process_file(file, dest)
|
||||||
|
|
||||||
|
print("\n✅ All manifests copied to base directory!")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Backend Services Deployment Script for AKS
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./deploy.sh <environment> [service-name]
|
||||||
|
#
|
||||||
|
# Arguments:
|
||||||
|
# environment - Target environment (dev, staging, prod)
|
||||||
|
# service-name - Specific service to deploy (optional, deploys all if not specified)
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# ./deploy.sh dev # Deploy all services to dev
|
||||||
|
# ./deploy.sh prod user-service # Deploy only user-service to prod
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Color output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
log_info() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate arguments
|
||||||
|
if [ $# -lt 1 ]; then
|
||||||
|
log_error "Usage: $0 <environment> [service-name]"
|
||||||
|
log_error "Environment must be one of: dev, staging, prod"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ENVIRONMENT=$1
|
||||||
|
SERVICE=${2:-all}
|
||||||
|
|
||||||
|
# Validate environment
|
||||||
|
if [[ ! "$ENVIRONMENT" =~ ^(dev|staging|prod)$ ]]; then
|
||||||
|
log_error "Invalid environment: $ENVIRONMENT"
|
||||||
|
log_error "Must be one of: dev, staging, prod"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
ENV_FILE=".github/config/deploy_env_vars_${ENVIRONMENT}"
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
log_error "Environment file not found: $ENV_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
source "$ENV_FILE"
|
||||||
|
log_info "Loaded environment configuration: $ENVIRONMENT"
|
||||||
|
|
||||||
|
# Service list
|
||||||
|
SERVICES=(
|
||||||
|
"user-service"
|
||||||
|
"event-service"
|
||||||
|
"ai-service"
|
||||||
|
"content-service"
|
||||||
|
"distribution-service"
|
||||||
|
"participation-service"
|
||||||
|
"analytics-service"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate service if specified
|
||||||
|
if [ "$SERVICE" != "all" ]; then
|
||||||
|
if [[ ! " ${SERVICES[@]} " =~ " ${SERVICE} " ]]; then
|
||||||
|
log_error "Invalid service: $SERVICE"
|
||||||
|
log_error "Must be one of: ${SERVICES[*]}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
SERVICES=("$SERVICE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Services to deploy: ${SERVICES[*]}"
|
||||||
|
|
||||||
|
# Check prerequisites
|
||||||
|
log_info "Checking prerequisites..."
|
||||||
|
|
||||||
|
if ! command -v az &> /dev/null; then
|
||||||
|
log_error "Azure CLI not found. Please install Azure CLI."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v kubectl &> /dev/null; then
|
||||||
|
log_error "kubectl not found. Please install kubectl."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v kustomize &> /dev/null; then
|
||||||
|
log_error "kustomize not found. Please install kustomize."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Azure login check
|
||||||
|
log_info "Checking Azure authentication..."
|
||||||
|
if ! az account show &> /dev/null; then
|
||||||
|
log_error "Not logged in to Azure. Please run 'az login'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get AKS credentials
|
||||||
|
log_info "Getting AKS credentials..."
|
||||||
|
az aks get-credentials \
|
||||||
|
--resource-group "$RESOURCE_GROUP" \
|
||||||
|
--name "$AKS_CLUSTER" \
|
||||||
|
--overwrite-existing
|
||||||
|
|
||||||
|
# Check namespace
|
||||||
|
log_info "Checking namespace: $NAMESPACE"
|
||||||
|
if ! kubectl get namespace "$NAMESPACE" &> /dev/null; then
|
||||||
|
log_warn "Namespace $NAMESPACE does not exist. Creating..."
|
||||||
|
kubectl create namespace "$NAMESPACE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build and deploy with Kustomize
|
||||||
|
OVERLAY_DIR=".github/kustomize/overlays/${ENVIRONMENT}"
|
||||||
|
if [ ! -d "$OVERLAY_DIR" ]; then
|
||||||
|
log_error "Kustomize overlay directory not found: $OVERLAY_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Building Kustomize manifests for $ENVIRONMENT..."
|
||||||
|
cd "$OVERLAY_DIR"
|
||||||
|
|
||||||
|
# Update image tags
|
||||||
|
log_info "Updating image tags to: $ENVIRONMENT"
|
||||||
|
kustomize edit set image \
|
||||||
|
${ACR_NAME}.azurecr.io/kt-event-marketing/user-service:${ENVIRONMENT} \
|
||||||
|
${ACR_NAME}.azurecr.io/kt-event-marketing/event-service:${ENVIRONMENT} \
|
||||||
|
${ACR_NAME}.azurecr.io/kt-event-marketing/ai-service:${ENVIRONMENT} \
|
||||||
|
${ACR_NAME}.azurecr.io/kt-event-marketing/content-service:${ENVIRONMENT} \
|
||||||
|
${ACR_NAME}.azurecr.io/kt-event-marketing/distribution-service:${ENVIRONMENT} \
|
||||||
|
${ACR_NAME}.azurecr.io/kt-event-marketing/participation-service:${ENVIRONMENT} \
|
||||||
|
${ACR_NAME}.azurecr.io/kt-event-marketing/analytics-service:${ENVIRONMENT}
|
||||||
|
|
||||||
|
# Apply manifests
|
||||||
|
log_info "Applying manifests to AKS..."
|
||||||
|
kustomize build . | kubectl apply -f -
|
||||||
|
|
||||||
|
cd - > /dev/null
|
||||||
|
|
||||||
|
# Wait for deployments
|
||||||
|
log_info "Waiting for deployments to be ready..."
|
||||||
|
for service in "${SERVICES[@]}"; do
|
||||||
|
log_info "Waiting for $service deployment..."
|
||||||
|
if ! kubectl rollout status deployment/"$service" -n "$NAMESPACE" --timeout=5m; then
|
||||||
|
log_error "Deployment of $service failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_info "✓ $service is ready"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Verify deployment
|
||||||
|
log_info "Verifying deployment..."
|
||||||
|
echo ""
|
||||||
|
echo "=== Pods Status ==="
|
||||||
|
kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/part-of=kt-event-marketing
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Services ==="
|
||||||
|
kubectl get svc -n "$NAMESPACE"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Ingress ==="
|
||||||
|
kubectl get ingress -n "$NAMESPACE"
|
||||||
|
|
||||||
|
log_info "Deployment completed successfully!"
|
||||||
|
log_info "Environment: $ENVIRONMENT"
|
||||||
|
log_info "Services: ${SERVICES[*]}"
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SERVICES=(user-service event-service ai-service content-service distribution-service participation-service analytics-service)
|
||||||
|
|
||||||
|
# Staging patches (2 replicas, increased resources)
|
||||||
|
for service in "${SERVICES[@]}"; do
|
||||||
|
cat > ".github/kustomize/overlays/staging/${service}-patch.yaml" << YAML
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: ${service}
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: ${service}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "512m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "2048m"
|
||||||
|
memory: "2048Mi"
|
||||||
|
YAML
|
||||||
|
done
|
||||||
|
|
||||||
|
# Prod patches (3 replicas, maximum resources)
|
||||||
|
for service in "${SERVICES[@]}"; do
|
||||||
|
cat > ".github/kustomize/overlays/prod/${service}-patch.yaml" << YAML
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: ${service}
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: ${service}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "1024m"
|
||||||
|
memory: "1024Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "4096m"
|
||||||
|
memory: "4096Mi"
|
||||||
|
YAML
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✅ Generated all patch files for staging and prod"
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
name: Backend CI/CD Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
# push:
|
||||||
|
# branches:
|
||||||
|
# - develop
|
||||||
|
# - main
|
||||||
|
# paths:
|
||||||
|
# - '*-service/**'
|
||||||
|
# - '.github/workflows/backend-cicd.yaml'
|
||||||
|
# - '.github/kustomize/**'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '*-service/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: 'Target environment'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- dev
|
||||||
|
- staging
|
||||||
|
- prod
|
||||||
|
service:
|
||||||
|
description: 'Service to deploy (all for all services)'
|
||||||
|
required: true
|
||||||
|
default: 'all'
|
||||||
|
|
||||||
|
env:
|
||||||
|
ACR_NAME: acrdigitalgarage01
|
||||||
|
RESOURCE_GROUP: rg-digitalgarage-01
|
||||||
|
AKS_CLUSTER: aks-digitalgarage-01
|
||||||
|
NAMESPACE: kt-event-marketing
|
||||||
|
JDK_VERSION: '21'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect-changes:
|
||||||
|
name: Detect Changed Services
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
services: ${{ steps.detect.outputs.services }}
|
||||||
|
environment: ${{ steps.env.outputs.environment }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Determine environment
|
||||||
|
id: env
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT
|
||||||
|
elif [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||||
|
echo "environment=prod" >> $GITHUB_OUTPUT
|
||||||
|
elif [ "${{ github.ref }}" = "refs/heads/develop" ]; then
|
||||||
|
echo "environment=dev" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "environment=dev" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Detect changed services
|
||||||
|
id: detect
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ github.event.inputs.service }}" != "all" ]; then
|
||||||
|
echo "services=[\"${{ github.event.inputs.service }}\"]" >> $GITHUB_OUTPUT
|
||||||
|
elif [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ github.event.inputs.service }}" = "all" ]; then
|
||||||
|
echo "services=[\"user-service\",\"event-service\",\"ai-service\",\"content-service\",\"distribution-service\",\"participation-service\",\"analytics-service\"]" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
CHANGED_SERVICES=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | \
|
||||||
|
grep -E '^(user|event|ai|content|distribution|participation|analytics)-service/' | \
|
||||||
|
cut -d'/' -f1 | sort -u | \
|
||||||
|
jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||||
|
|
||||||
|
if [ "$CHANGED_SERVICES" = "[]" ] || [ -z "$CHANGED_SERVICES" ]; then
|
||||||
|
echo "services=[\"user-service\",\"event-service\",\"ai-service\",\"content-service\",\"distribution-service\",\"participation-service\",\"analytics-service\"]" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "services=$CHANGED_SERVICES" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
build-and-push:
|
||||||
|
name: Build and Push - ${{ matrix.service }}
|
||||||
|
needs: detect-changes
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
service: ${{ fromJson(needs.detect-changes.outputs.services) }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up JDK ${{ env.JDK_VERSION }}
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: ${{ env.JDK_VERSION }}
|
||||||
|
distribution: 'temurin'
|
||||||
|
cache: 'gradle'
|
||||||
|
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew ${{ matrix.service }}:build -x test
|
||||||
|
|
||||||
|
# - name: Run tests
|
||||||
|
# run: ./gradlew ${{ matrix.service }}:test
|
||||||
|
|
||||||
|
- name: Build JAR
|
||||||
|
run: ./gradlew ${{ matrix.service }}:bootJar
|
||||||
|
|
||||||
|
- name: Log in to Azure Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.ACR_NAME }}.azurecr.io
|
||||||
|
username: ${{ secrets.ACR_USERNAME }}
|
||||||
|
password: ${{ secrets.ACR_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./${{ matrix.service }}
|
||||||
|
file: ./${{ matrix.service }}/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.ACR_NAME }}.azurecr.io/kt-event-marketing/${{ matrix.service }}:${{ needs.detect-changes.outputs.environment }}
|
||||||
|
${{ env.ACR_NAME }}.azurecr.io/kt-event-marketing/${{ matrix.service }}:${{ github.sha }}
|
||||||
|
${{ env.ACR_NAME }}.azurecr.io/kt-event-marketing/${{ matrix.service }}:latest
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy to AKS - ${{ needs.detect-changes.outputs.environment }}
|
||||||
|
needs: [detect-changes, build-and-push]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: ${{ needs.detect-changes.outputs.environment }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Azure login
|
||||||
|
uses: azure/login@v1
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Get AKS credentials
|
||||||
|
run: |
|
||||||
|
az aks get-credentials \
|
||||||
|
--resource-group ${{ env.RESOURCE_GROUP }} \
|
||||||
|
--name ${{ env.AKS_CLUSTER }} \
|
||||||
|
--overwrite-existing
|
||||||
|
|
||||||
|
- name: Setup Kustomize
|
||||||
|
uses: imranismail/setup-kustomize@v2
|
||||||
|
|
||||||
|
- name: Deploy with Kustomize
|
||||||
|
run: |
|
||||||
|
cd .github/kustomize/overlays/${{ needs.detect-changes.outputs.environment }}
|
||||||
|
kustomize edit set image \
|
||||||
|
acrdigitalgarage01.azurecr.io/kt-event-marketing/user-service:${{ needs.detect-changes.outputs.environment }} \
|
||||||
|
acrdigitalgarage01.azurecr.io/kt-event-marketing/event-service:${{ needs.detect-changes.outputs.environment }} \
|
||||||
|
acrdigitalgarage01.azurecr.io/kt-event-marketing/ai-service:${{ needs.detect-changes.outputs.environment }} \
|
||||||
|
acrdigitalgarage01.azurecr.io/kt-event-marketing/content-service:${{ needs.detect-changes.outputs.environment }} \
|
||||||
|
acrdigitalgarage01.azurecr.io/kt-event-marketing/distribution-service:${{ needs.detect-changes.outputs.environment }} \
|
||||||
|
acrdigitalgarage01.azurecr.io/kt-event-marketing/participation-service:${{ needs.detect-changes.outputs.environment }} \
|
||||||
|
acrdigitalgarage01.azurecr.io/kt-event-marketing/analytics-service:${{ needs.detect-changes.outputs.environment }}
|
||||||
|
|
||||||
|
kustomize build . | kubectl apply -f -
|
||||||
|
|
||||||
|
- name: Wait for deployment rollout
|
||||||
|
run: |
|
||||||
|
for service in $(echo '${{ needs.detect-changes.outputs.services }}' | jq -r '.[]'); do
|
||||||
|
echo "Waiting for ${service} deployment..."
|
||||||
|
kubectl rollout status deployment/${service} -n ${{ env.NAMESPACE }} --timeout=5m
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Verify deployment
|
||||||
|
run: |
|
||||||
|
echo "=== Pods Status ==="
|
||||||
|
kubectl get pods -n ${{ env.NAMESPACE }} -l app.kubernetes.io/part-of=kt-event-marketing
|
||||||
|
|
||||||
|
echo "=== Services ==="
|
||||||
|
kubectl get svc -n ${{ env.NAMESPACE }}
|
||||||
|
|
||||||
|
echo "=== Ingress ==="
|
||||||
|
kubectl get ingress -n ${{ env.NAMESPACE }}
|
||||||
|
|
||||||
|
notify:
|
||||||
|
name: Notify Deployment Result
|
||||||
|
needs: [detect-changes, deploy]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- name: Deployment Success
|
||||||
|
if: needs.deploy.result == 'success'
|
||||||
|
run: |
|
||||||
|
echo "✅ Deployment to ${{ needs.detect-changes.outputs.environment }} succeeded!"
|
||||||
|
echo "Services: ${{ needs.detect-changes.outputs.services }}"
|
||||||
|
|
||||||
|
- name: Deployment Failure
|
||||||
|
if: needs.deploy.result == 'failure'
|
||||||
|
run: |
|
||||||
|
echo "❌ Deployment to ${{ needs.detect-changes.outputs.environment }} failed!"
|
||||||
|
echo "Services: ${{ needs.detect-changes.outputs.services }}"
|
||||||
|
exit 1
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
@@ -0,0 +1,31 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="AiServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||||
|
<option name="ACTIVE_PROFILES" />
|
||||||
|
<module name="kt-event-marketing.ai-service.main" />
|
||||||
|
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.ai.AiApplication" />
|
||||||
|
<extension name="coverage">
|
||||||
|
<pattern>
|
||||||
|
<option name="PATTERN" value="com.kt.ai.*" />
|
||||||
|
<option name="ENABLED" value="true" />
|
||||||
|
</pattern>
|
||||||
|
</extension>
|
||||||
|
<envs>
|
||||||
|
<env name="SERVER_PORT" value="8081" />
|
||||||
|
<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_PORT" value="6379" />
|
||||||
|
<env name="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||||
|
<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="JPA_DDL_AUTO" value="update" />
|
||||||
|
<env name="JPA_SHOW_SQL" value="false" />
|
||||||
|
</envs>
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="AnalyticsServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||||
|
<option name="ACTIVE_PROFILES" />
|
||||||
|
<module name="kt-event-marketing.analytics-service.main" />
|
||||||
|
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.analytics.AnalyticsApplication" />
|
||||||
|
<extension name="coverage">
|
||||||
|
<pattern>
|
||||||
|
<option name="PATTERN" value="com.kt.analytics.*" />
|
||||||
|
<option name="ENABLED" value="true" />
|
||||||
|
</pattern>
|
||||||
|
</extension>
|
||||||
|
<envs>
|
||||||
|
<env name="SERVER_PORT" value="8087" />
|
||||||
|
<env name="DB_HOST" value="4.230.49.9" />
|
||||||
|
<env name="DB_PORT" value="5432" />
|
||||||
|
<env name="DB_NAME" value="analyticdb" />
|
||||||
|
<env name="DB_USERNAME" value="eventuser" />
|
||||||
|
<env name="DB_PASSWORD" value="Hi5Jessica!" />
|
||||||
|
<env name="REDIS_HOST" value="20.214.210.71" />
|
||||||
|
<env name="REDIS_PORT" value="6379" />
|
||||||
|
<env name="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||||
|
<env name="KAFKA_BOOTSTRAP_SERVERS" value="4.230.50.63:9092" />
|
||||||
|
<env name="KAFKA_CONSUMER_GROUP" value="analytic" />
|
||||||
|
<env name="JPA_DDL_AUTO" value="update" />
|
||||||
|
<env name="JPA_SHOW_SQL" value="false" />
|
||||||
|
</envs>
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="ContentServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||||
|
<option name="ACTIVE_PROFILES" />
|
||||||
|
<module name="kt-event-marketing.content-service.main" />
|
||||||
|
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.content.ContentApplication" />
|
||||||
|
<extension name="coverage">
|
||||||
|
<pattern>
|
||||||
|
<option name="PATTERN" value="com.kt.content.*" />
|
||||||
|
<option name="ENABLED" value="true" />
|
||||||
|
</pattern>
|
||||||
|
</extension>
|
||||||
|
<envs>
|
||||||
|
<env name="SERVER_PORT" value="8084" />
|
||||||
|
<env name="DB_HOST" value="4.217.131.139" />
|
||||||
|
<env name="DB_PORT" value="5432" />
|
||||||
|
<env name="DB_NAME" value="contentdb" />
|
||||||
|
<env name="DB_USERNAME" value="eventuser" />
|
||||||
|
<env name="DB_PASSWORD" value="Hi5Jessica!" />
|
||||||
|
<env name="REDIS_HOST" value="20.214.210.71" />
|
||||||
|
<env name="REDIS_PORT" value="6379" />
|
||||||
|
<env name="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||||
|
<env name="JPA_DDL_AUTO" value="update" />
|
||||||
|
<env name="JPA_SHOW_SQL" value="false" />
|
||||||
|
<env name="REPLICATE_API_TOKEN" value="r8_cqE8IzQr9DZ8Dr72ozbomiXe6IFPL0005Vuq9" />
|
||||||
|
<env name="REPLICATE_MOCK_ENABLED" value="true" />
|
||||||
|
</envs>
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="DistributionServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||||
|
<option name="ACTIVE_PROFILES" />
|
||||||
|
<module name="kt-event-marketing.distribution-service.main" />
|
||||||
|
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.distribution.DistributionApplication" />
|
||||||
|
<extension name="coverage">
|
||||||
|
<pattern>
|
||||||
|
<option name="PATTERN" value="com.kt.distribution.*" />
|
||||||
|
<option name="ENABLED" value="true" />
|
||||||
|
</pattern>
|
||||||
|
</extension>
|
||||||
|
<envs>
|
||||||
|
<env name="SERVER_PORT" value="8085" />
|
||||||
|
<env name="DB_HOST" value="4.217.133.59" />
|
||||||
|
<env name="DB_PORT" value="5432" />
|
||||||
|
<env name="DB_NAME" value="distributiondb" />
|
||||||
|
<env name="DB_USERNAME" value="eventuser" />
|
||||||
|
<env name="DB_PASSWORD" value="Hi5Jessica!" />
|
||||||
|
<env name="REDIS_HOST" value="20.214.210.71" />
|
||||||
|
<env name="REDIS_PORT" value="6379" />
|
||||||
|
<env name="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||||
|
<env name="KAFKA_BOOTSTRAP_SERVERS" value="4.230.50.63:9092" />
|
||||||
|
<env name="KAFKA_CONSUMER_GROUP" value="distribution-service" />
|
||||||
|
<env name="JPA_DDL_AUTO" value="update" />
|
||||||
|
<env name="JPA_SHOW_SQL" value="false" />
|
||||||
|
</envs>
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="EventServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||||
|
<option name="ACTIVE_PROFILES" />
|
||||||
|
<module name="kt-event-marketing.event-service.main" />
|
||||||
|
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.event.EventApplication" />
|
||||||
|
<extension name="coverage">
|
||||||
|
<pattern>
|
||||||
|
<option name="PATTERN" value="com.kt.event.*" />
|
||||||
|
<option name="ENABLED" value="true" />
|
||||||
|
</pattern>
|
||||||
|
</extension>
|
||||||
|
<envs>
|
||||||
|
<env name="SERVER_PORT" value="8082" />
|
||||||
|
<env name="DB_HOST" value="20.249.177.232" />
|
||||||
|
<env name="DB_PORT" value="5432" />
|
||||||
|
<env name="DB_NAME" value="eventdb" />
|
||||||
|
<env name="DB_USERNAME" value="eventuser" />
|
||||||
|
<env name="DB_PASSWORD" value="Hi5Jessica!" />
|
||||||
|
<env name="REDIS_HOST" value="20.214.210.71" />
|
||||||
|
<env name="REDIS_PORT" value="6379" />
|
||||||
|
<env name="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||||
|
<env name="KAFKA_BOOTSTRAP_SERVERS" value="4.230.50.63:9092" />
|
||||||
|
<env name="DISTRIBUTION_SERVICE_URL" value="http://localhost:8085" />
|
||||||
|
<env name="JPA_DDL_AUTO" value="update" />
|
||||||
|
<env name="JPA_SHOW_SQL" value="false" />
|
||||||
|
</envs>
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="UserServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||||
|
<option name="ACTIVE_PROFILES" />
|
||||||
|
<module name="kt-event-marketing.user-service.main" />
|
||||||
|
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.user.UserApplication" />
|
||||||
|
<extension name="coverage">
|
||||||
|
<pattern>
|
||||||
|
<option name="PATTERN" value="com.kt.user.*" />
|
||||||
|
<option name="ENABLED" value="true" />
|
||||||
|
</pattern>
|
||||||
|
</extension>
|
||||||
|
<envs>
|
||||||
|
<env name="SERVER_PORT" value="8083" />
|
||||||
|
<env name="DB_HOST" value="20.249.125.115" />
|
||||||
|
<env name="DB_PORT" value="5432" />
|
||||||
|
<env name="DB_NAME" value="userdb" />
|
||||||
|
<env name="DB_USERNAME" value="eventuser" />
|
||||||
|
<env name="DB_PASSWORD" value="Hi5Jessica!" />
|
||||||
|
<env name="REDIS_HOST" value="20.214.210.71" />
|
||||||
|
<env name="REDIS_PORT" value="6379" />
|
||||||
|
<env name="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||||
|
<env name="JPA_DDL_AUTO" value="update" />
|
||||||
|
<env name="JPA_SHOW_SQL" value="false" />
|
||||||
|
</envs>
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<!-- Kafka Configuration (원격 서버) -->
|
<!-- Kafka Configuration (원격 서버) -->
|
||||||
<entry key="KAFKA_ENABLED" value="true" />
|
<entry key="KAFKA_ENABLED" value="true" />
|
||||||
<entry key="KAFKA_BOOTSTRAP_SERVERS" value="20.249.182.13:9095,4.217.131.59:9095" />
|
<entry key="KAFKA_BOOTSTRAP_SERVERS" value="20.249.182.13:9095,4.217.131.59:9095" />
|
||||||
<entry key="KAFKA_CONSUMER_GROUP_ID" value="analytics-service-consumers" />
|
<entry key="KAFKA_CONSUMER_GROUP_ID" value="analytics-service-consumers-v3" />
|
||||||
|
|
||||||
<!-- Sample Data Configuration (MVP Only) -->
|
<!-- Sample Data Configuration (MVP Only) -->
|
||||||
<!-- ⚠️ Kafka Producer로 이벤트 발행 (Consumer가 처리) -->
|
<!-- ⚠️ Kafka Producer로 이벤트 발행 (Consumer가 처리) -->
|
||||||
|
|||||||
@@ -0,0 +1,620 @@
|
|||||||
|
# Develop 브랜치 변경사항 요약
|
||||||
|
|
||||||
|
**업데이트 일시**: 2025-10-30
|
||||||
|
**머지 브랜치**: feature/event → develop
|
||||||
|
**머지 커밋**: 3465a35
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 변경사항 통계
|
||||||
|
|
||||||
|
```
|
||||||
|
60개 파일 변경
|
||||||
|
+2,795 줄 추가
|
||||||
|
-222 줄 삭제
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 주요 변경사항
|
||||||
|
|
||||||
|
### 1. 비즈니스 친화적 ID 생성 시스템 구현
|
||||||
|
|
||||||
|
#### EventId 생성 로직
|
||||||
|
**파일**: `event-service/.../EventIdGenerator.java` (신규)
|
||||||
|
|
||||||
|
**ID 포맷**: `EVT-{store_id}-{timestamp}-{random}`
|
||||||
|
```
|
||||||
|
예시: EVT-str_dev_test_001-20251030001311-70eea424
|
||||||
|
```
|
||||||
|
|
||||||
|
**특징**:
|
||||||
|
- ✅ 비즈니스 의미를 담은 접두사 (EVT)
|
||||||
|
- ✅ 매장 식별자 포함 (store_id)
|
||||||
|
- ✅ 타임스탬프 기반 시간 추적 가능
|
||||||
|
- ✅ 랜덤 해시로 유일성 보장
|
||||||
|
- ✅ 사람이 읽기 쉬운 형식
|
||||||
|
|
||||||
|
**구현 내역**:
|
||||||
|
```java
|
||||||
|
public class EventIdGenerator {
|
||||||
|
private static final String PREFIX = "EVT";
|
||||||
|
|
||||||
|
public static String generate(String storeId) {
|
||||||
|
String cleanStoreId = sanitizeStoreId(storeId);
|
||||||
|
String timestamp = LocalDateTime.now()
|
||||||
|
.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
|
||||||
|
String randomHash = UUID.randomUUID().toString()
|
||||||
|
.substring(0, 8);
|
||||||
|
|
||||||
|
return String.format("%s-%s-%s-%s",
|
||||||
|
PREFIX, cleanStoreId, timestamp, randomHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JobId 생성 로직
|
||||||
|
**파일**: `event-service/.../JobIdGenerator.java` (신규)
|
||||||
|
|
||||||
|
**ID 포맷**: `JOB-{type}-{timestamp}-{random}`
|
||||||
|
```
|
||||||
|
예시: JOB-IMG-1761750847428-b88d2f54
|
||||||
|
```
|
||||||
|
|
||||||
|
**타입 코드**:
|
||||||
|
- `IMG`: 이미지 생성 작업
|
||||||
|
- `AI`: AI 추천 작업
|
||||||
|
- `REG`: 이미지 재생성 작업
|
||||||
|
|
||||||
|
**특징**:
|
||||||
|
- ✅ 작업 타입 식별 가능
|
||||||
|
- ✅ 타임스탬프로 작업 시간 추적
|
||||||
|
- ✅ UUID 기반 유일성 보장
|
||||||
|
- ✅ 로그 분석 및 디버깅 용이
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Kafka 메시지 구조 개선
|
||||||
|
|
||||||
|
#### 필드명 표준화 (snake_case → camelCase)
|
||||||
|
|
||||||
|
**변경 파일**:
|
||||||
|
- `AIEventGenerationJobMessage.java`
|
||||||
|
- `EventCreatedMessage.java`
|
||||||
|
- `ImageJobKafkaProducer.java`
|
||||||
|
- `AIJobKafkaProducer.java`
|
||||||
|
- 관련 Consumer 클래스들
|
||||||
|
|
||||||
|
**Before**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"job_id": "...",
|
||||||
|
"event_id": "...",
|
||||||
|
"store_id": "...",
|
||||||
|
"store_name": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jobId": "...",
|
||||||
|
"eventId": "...",
|
||||||
|
"storeId": "...",
|
||||||
|
"storeName": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**이점**:
|
||||||
|
- ✅ Java 네이밍 컨벤션 준수
|
||||||
|
- ✅ JSON 직렬화/역직렬화 간소화
|
||||||
|
- ✅ 프론트엔드와 일관된 필드명
|
||||||
|
- ✅ 코드 가독성 향상
|
||||||
|
|
||||||
|
**영향받는 메시지**:
|
||||||
|
1. **이미지 생성 작업 메시지** (`image-generation-job`)
|
||||||
|
- jobId, eventId, prompt, styles, platforms 등
|
||||||
|
|
||||||
|
2. **AI 이벤트 생성 작업 메시지** (`ai-event-generation-job`)
|
||||||
|
- jobId, eventId, objective, storeInfo 등
|
||||||
|
|
||||||
|
3. **이벤트 생성 완료 메시지** (`event-created`)
|
||||||
|
- eventId, storeId, storeName, objective 등
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 데이터베이스 스키마 및 마이그레이션
|
||||||
|
|
||||||
|
#### 신규 스키마 파일
|
||||||
|
**파일**: `develop/database/schema/create_event_tables.sql`
|
||||||
|
|
||||||
|
**테이블 구조**:
|
||||||
|
```sql
|
||||||
|
-- events 테이블
|
||||||
|
CREATE TABLE events (
|
||||||
|
id VARCHAR(100) PRIMARY KEY, -- EVT-{store_id}-{timestamp}-{hash}
|
||||||
|
user_id VARCHAR(50) NOT NULL,
|
||||||
|
store_id VARCHAR(50) NOT NULL,
|
||||||
|
store_name VARCHAR(200),
|
||||||
|
objective VARCHAR(50),
|
||||||
|
status VARCHAR(20),
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- jobs 테이블
|
||||||
|
CREATE TABLE jobs (
|
||||||
|
id VARCHAR(100) PRIMARY KEY, -- JOB-{type}-{timestamp}-{hash}
|
||||||
|
event_id VARCHAR(100),
|
||||||
|
job_type VARCHAR(50),
|
||||||
|
status VARCHAR(20),
|
||||||
|
progress INTEGER,
|
||||||
|
result_message TEXT,
|
||||||
|
error_message TEXT,
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ai_recommendations 테이블
|
||||||
|
CREATE TABLE ai_recommendations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
event_id VARCHAR(100),
|
||||||
|
recommendation_text TEXT,
|
||||||
|
-- ... 기타 필드
|
||||||
|
);
|
||||||
|
|
||||||
|
-- generated_images 테이블
|
||||||
|
CREATE TABLE generated_images (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
event_id VARCHAR(100),
|
||||||
|
image_url TEXT,
|
||||||
|
style VARCHAR(50),
|
||||||
|
platform VARCHAR(50),
|
||||||
|
-- ... 기타 필드
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 마이그레이션 스크립트
|
||||||
|
**파일**: `develop/database/migration/alter_event_id_to_varchar.sql`
|
||||||
|
|
||||||
|
**목적**: 기존 BIGINT 타입의 ID를 VARCHAR로 변경
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Step 1: 백업 테이블 생성
|
||||||
|
CREATE TABLE events_backup AS SELECT * FROM events;
|
||||||
|
CREATE TABLE jobs_backup AS SELECT * FROM jobs;
|
||||||
|
|
||||||
|
-- Step 2: 기존 테이블 삭제
|
||||||
|
DROP TABLE IF EXISTS events CASCADE;
|
||||||
|
DROP TABLE IF EXISTS jobs CASCADE;
|
||||||
|
|
||||||
|
-- Step 3: 새 스키마로 테이블 재생성
|
||||||
|
-- (create_event_tables.sql 실행)
|
||||||
|
|
||||||
|
-- Step 4: 데이터 마이그레이션
|
||||||
|
-- (필요시 기존 데이터를 새 형식으로 변환하여 삽입)
|
||||||
|
```
|
||||||
|
|
||||||
|
**주의사항**:
|
||||||
|
- ⚠️ 프로덕션 환경에서는 반드시 백업 후 실행
|
||||||
|
- ⚠️ 외래 키 제약조건 재설정 필요
|
||||||
|
- ⚠️ 애플리케이션 코드와 동시 배포 필요
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Content Service 통합 및 개선
|
||||||
|
|
||||||
|
#### Content Service 설정 업데이트
|
||||||
|
**파일**: `content-service/src/main/resources/application.yml`
|
||||||
|
|
||||||
|
**변경사항**:
|
||||||
|
```yaml
|
||||||
|
# JWT 설정 추가
|
||||||
|
jwt:
|
||||||
|
secret: ${JWT_SECRET:kt-event-marketing-jwt-secret...}
|
||||||
|
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600000}
|
||||||
|
|
||||||
|
# Azure Blob Storage 설정 추가
|
||||||
|
azure:
|
||||||
|
storage:
|
||||||
|
connection-string: ${AZURE_STORAGE_CONNECTION_STRING:...}
|
||||||
|
container-name: ${AZURE_CONTAINER_NAME:content-images}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 서비스 개선사항
|
||||||
|
**파일**: `content-service/.../RegenerateImageService.java`, `StableDiffusionImageGenerator.java`
|
||||||
|
|
||||||
|
**주요 개선**:
|
||||||
|
- ✅ 이미지 재생성 로직 추가 (28줄)
|
||||||
|
- ✅ Stable Diffusion 통합 개선 (28줄)
|
||||||
|
- ✅ Mock Mode 개선 (개발 환경)
|
||||||
|
- ✅ 에러 처리 강화
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Event Service 리팩토링
|
||||||
|
|
||||||
|
#### DTO 구조 개선
|
||||||
|
**변경 파일**:
|
||||||
|
- Request DTO: `AiRecommendationRequest`, `SelectImageRequest` 등
|
||||||
|
- Response DTO: `EventCreatedResponse`, `EventDetailResponse` 등
|
||||||
|
- Kafka DTO: 모든 메시지 클래스
|
||||||
|
|
||||||
|
**주요 변경**:
|
||||||
|
1. **필드명 표준화**: snake_case → camelCase
|
||||||
|
2. **ID 타입 변경**: Long → String
|
||||||
|
3. **Nullable 필드 명시**: @Nullable 어노테이션 추가
|
||||||
|
4. **Validation 강화**: @NotNull, @NotBlank 등
|
||||||
|
|
||||||
|
#### Service Layer 개선
|
||||||
|
**파일**: `EventService.java`, `JobService.java`
|
||||||
|
|
||||||
|
**Before**:
|
||||||
|
```java
|
||||||
|
public EventCreatedResponse createEvent(CreateEventRequest request) {
|
||||||
|
Event event = new Event();
|
||||||
|
event.setId(generateSequentialId()); // Long 타입
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After**:
|
||||||
|
```java
|
||||||
|
public EventCreatedResponse createEvent(CreateEventRequest request) {
|
||||||
|
String eventId = EventIdGenerator.generate(request.getStoreId());
|
||||||
|
Event event = Event.builder()
|
||||||
|
.id(eventId) // String 타입
|
||||||
|
.storeId(request.getStoreId())
|
||||||
|
// ...
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**개선사항**:
|
||||||
|
- ✅ EventIdGenerator 사용
|
||||||
|
- ✅ Builder 패턴 적용
|
||||||
|
- ✅ 비즈니스 로직 분리
|
||||||
|
- ✅ 에러 처리 개선
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Kafka 연동 개선
|
||||||
|
|
||||||
|
#### Producer 개선
|
||||||
|
**파일**: `AIJobKafkaProducer.java`, `ImageJobKafkaProducer.java`
|
||||||
|
|
||||||
|
**주요 개선**:
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class ImageJobKafkaProducer {
|
||||||
|
|
||||||
|
public void sendImageGenerationJob(ImageGenerationJobMessage message) {
|
||||||
|
log.info("이미지 생성 작업 메시지 발행 시작 - JobId: {}",
|
||||||
|
message.getJobId());
|
||||||
|
|
||||||
|
kafkaTemplate.send(topicName, message.getJobId(), message)
|
||||||
|
.whenComplete((result, ex) -> {
|
||||||
|
if (ex != null) {
|
||||||
|
log.error("메시지 발행 실패: {}", ex.getMessage());
|
||||||
|
} else {
|
||||||
|
log.info("메시지 발행 성공 - Offset: {}",
|
||||||
|
result.getRecordMetadata().offset());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**개선사항**:
|
||||||
|
- ✅ 상세한 로깅 추가
|
||||||
|
- ✅ 비동기 콜백 처리
|
||||||
|
- ✅ 에러 핸들링 강화
|
||||||
|
- ✅ 메시지 키 설정 (jobId)
|
||||||
|
|
||||||
|
#### Consumer 개선
|
||||||
|
**파일**: `ImageJobKafkaConsumer.java`, `AIJobKafkaConsumer.java`
|
||||||
|
|
||||||
|
**주요 개선**:
|
||||||
|
```java
|
||||||
|
@KafkaListener(
|
||||||
|
topics = "${app.kafka.topics.image-generation-job}",
|
||||||
|
groupId = "${spring.kafka.consumer.group-id}"
|
||||||
|
)
|
||||||
|
public void consumeImageJob(
|
||||||
|
@Payload ImageGenerationJobMessage message,
|
||||||
|
Acknowledgment ack
|
||||||
|
) {
|
||||||
|
log.info("이미지 작업 메시지 수신 - JobId: {}", message.getJobId());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 메시지 처리
|
||||||
|
processImageJob(message);
|
||||||
|
|
||||||
|
// Manual Acknowledgment
|
||||||
|
ack.acknowledge();
|
||||||
|
log.info("메시지 처리 완료 - JobId: {}", message.getJobId());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("메시지 처리 실패: {}", e.getMessage());
|
||||||
|
// 재시도 로직 또는 DLQ 전송
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**개선사항**:
|
||||||
|
- ✅ Manual Acknowledgment 패턴
|
||||||
|
- ✅ 상세한 로깅
|
||||||
|
- ✅ 예외 처리 강화
|
||||||
|
- ✅ 메시지 재시도 메커니즘
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 보안 및 인증 개선
|
||||||
|
|
||||||
|
#### JWT 토큰 처리 개선
|
||||||
|
**파일**: `common/security/JwtTokenProvider.java`, `UserPrincipal.java`
|
||||||
|
|
||||||
|
**주요 변경**:
|
||||||
|
```java
|
||||||
|
public class JwtTokenProvider {
|
||||||
|
|
||||||
|
public String getUserId(String token) {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
return claims.get("userId", String.class); // 명시적 타입 변환
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStoreId(String token) {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
return claims.get("storeId", String.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**개선사항**:
|
||||||
|
- ✅ 타입 안전성 향상
|
||||||
|
- ✅ null 처리 개선
|
||||||
|
- ✅ 토큰 파싱 로직 강화
|
||||||
|
- ✅ 에러 메시지 개선
|
||||||
|
|
||||||
|
#### 개발 환경 인증 필터
|
||||||
|
**파일**: `event-service/.../DevAuthenticationFilter.java`
|
||||||
|
|
||||||
|
**개선사항**:
|
||||||
|
- ✅ 개발 환경용 Mock 인증
|
||||||
|
- ✅ JWT 토큰 파싱 개선
|
||||||
|
- ✅ 로깅 추가
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. 테스트 및 문서화
|
||||||
|
|
||||||
|
#### 통합 테스트 보고서
|
||||||
|
**파일**: `test/content-service-integration-test-results.md` (신규, 673줄)
|
||||||
|
|
||||||
|
**내용**:
|
||||||
|
- ✅ 9개 테스트 시나리오 실행 결과
|
||||||
|
- ✅ 성공률: 100% (9/9)
|
||||||
|
- ✅ HTTP 통신 검증
|
||||||
|
- ✅ Job 관리 메커니즘 검증
|
||||||
|
- ✅ EventId 기반 조회 검증
|
||||||
|
- ✅ 이미지 재생성 기능 검증
|
||||||
|
- ✅ 성능 분석 (평균 응답 시간 < 150ms)
|
||||||
|
|
||||||
|
#### 아키텍처 분석 문서
|
||||||
|
**파일**: `test/content-service-integration-analysis.md` (신규, 504줄)
|
||||||
|
|
||||||
|
**내용**:
|
||||||
|
- ✅ content-service API 구조 분석
|
||||||
|
- ✅ Redis 기반 Job 관리 메커니즘
|
||||||
|
- ✅ Kafka 연동 현황 분석
|
||||||
|
- ✅ 서비스 간 통신 구조
|
||||||
|
- ✅ 권장사항 및 개선 방향
|
||||||
|
|
||||||
|
#### Kafka 연동 테스트 보고서
|
||||||
|
**파일**: `test/test-kafka-integration-results.md` (신규, 348줄)
|
||||||
|
|
||||||
|
**내용**:
|
||||||
|
- ✅ event-service Kafka Producer/Consumer 검증
|
||||||
|
- ✅ Kafka 브로커 연결 테스트
|
||||||
|
- ✅ 메시지 발행/수신 검증
|
||||||
|
- ✅ Manual Acknowledgment 패턴 검증
|
||||||
|
- ✅ content-service Kafka Consumer 미구현 확인
|
||||||
|
|
||||||
|
#### API 테스트 결과
|
||||||
|
**파일**: `test/API-TEST-RESULT.md` (이동)
|
||||||
|
|
||||||
|
**내용**:
|
||||||
|
- ✅ 기존 API 테스트 결과
|
||||||
|
- ✅ test/ 폴더로 이동하여 정리
|
||||||
|
|
||||||
|
#### 테스트 자동화 스크립트
|
||||||
|
**파일**:
|
||||||
|
- `test-content-service.sh` (신규, 82줄)
|
||||||
|
- `run-content-service.sh` (신규, 80줄)
|
||||||
|
- `run-content-service.bat` (신규, 81줄)
|
||||||
|
|
||||||
|
**기능**:
|
||||||
|
- ✅ content-service 자동 테스트
|
||||||
|
- ✅ 서버 실행 스크립트 (Linux/Windows)
|
||||||
|
- ✅ 7가지 테스트 시나리오 자동 실행
|
||||||
|
- ✅ Health Check 및 API 검증
|
||||||
|
|
||||||
|
#### 테스트 데이터
|
||||||
|
**파일**:
|
||||||
|
- `test-integration-event.json`
|
||||||
|
- `test-integration-objective.json`
|
||||||
|
- `test-integration-ai-request.json`
|
||||||
|
- `test-image-generation.json`
|
||||||
|
- `test-ai-recommendation.json`
|
||||||
|
|
||||||
|
**목적**:
|
||||||
|
- ✅ 통합 테스트용 샘플 데이터
|
||||||
|
- ✅ API 테스트 자동화
|
||||||
|
- ✅ 재현 가능한 테스트 환경
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. 실행 환경 설정
|
||||||
|
|
||||||
|
#### IntelliJ 실행 프로파일 업데이트
|
||||||
|
**파일**:
|
||||||
|
- `.run/ContentServiceApplication.run.xml`
|
||||||
|
- `.run/AiServiceApplication.run.xml`
|
||||||
|
|
||||||
|
**변경사항**:
|
||||||
|
```xml
|
||||||
|
<envs>
|
||||||
|
<env name="SERVER_PORT" value="8084" />
|
||||||
|
<env name="REDIS_HOST" value="20.214.210.71" />
|
||||||
|
<env name="REDIS_PORT" value="6379" />
|
||||||
|
<env name="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||||
|
<env name="DB_HOST" value="4.217.131.139" />
|
||||||
|
<env name="DB_PORT" value="5432" />
|
||||||
|
<env name="REPLICATE_MOCK_ENABLED" value="true" />
|
||||||
|
<!-- JWT, Azure 설정 추가 -->
|
||||||
|
</envs>
|
||||||
|
```
|
||||||
|
|
||||||
|
**개선사항**:
|
||||||
|
- ✅ 환경 변수 명시적 설정
|
||||||
|
- ✅ Mock Mode 설정 추가
|
||||||
|
- ✅ 데이터베이스 연결 정보 명시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Kafka 아키텍처 현황
|
||||||
|
|
||||||
|
### 현재 구현된 아키텍처
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ event-service │
|
||||||
|
│ (Port 8081) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
├─── Kafka Producer ───→ Kafka Topic (image-generation-job)
|
||||||
|
│ │
|
||||||
|
│ │ (event-service Consumer가 수신)
|
||||||
|
│ ↓
|
||||||
|
│ ┌──────────────┐
|
||||||
|
│ │ event-service│
|
||||||
|
│ │ Consumer │
|
||||||
|
│ └──────────────┘
|
||||||
|
│
|
||||||
|
└─── Redis Job Data ───→ Redis Cache
|
||||||
|
↑
|
||||||
|
│
|
||||||
|
┌───────┴────────┐
|
||||||
|
│ content-service│
|
||||||
|
│ (Port 8084) │
|
||||||
|
└────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 주요 발견사항
|
||||||
|
- ⚠️ **content-service에는 Kafka Consumer 미구현**
|
||||||
|
- ✅ Redis 기반 Job 관리로 서비스 간 통신
|
||||||
|
- ✅ event-service에서 Producer/Consumer 모두 구현
|
||||||
|
- ⚠️ 논리 아키텍처 설계와 실제 구현 불일치
|
||||||
|
|
||||||
|
### 권장사항
|
||||||
|
1. **단기**: 설계 문서를 실제 구현에 맞춰 업데이트
|
||||||
|
2. **중기**: API 문서 자동화 (Swagger/OpenAPI)
|
||||||
|
3. **장기**: content-service에 Kafka Consumer 추가 구현
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 성능 및 품질 지표
|
||||||
|
|
||||||
|
### API 응답 시간
|
||||||
|
```
|
||||||
|
Health Check: < 50ms
|
||||||
|
GET 요청: 50-100ms
|
||||||
|
POST 요청: 100-150ms
|
||||||
|
```
|
||||||
|
|
||||||
|
### Job 처리 시간 (Mock Mode)
|
||||||
|
```
|
||||||
|
이미지 4개 생성: ~0.2초
|
||||||
|
이미지 1개 재생성: ~0.1초
|
||||||
|
```
|
||||||
|
|
||||||
|
### 테스트 성공률
|
||||||
|
```
|
||||||
|
통합 테스트: 100% (9/9 성공)
|
||||||
|
Kafka 연동: 100% (event-service)
|
||||||
|
API 엔드포인트: 100% (전체 정상)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 코드 품질
|
||||||
|
```
|
||||||
|
추가된 코드: 2,795줄
|
||||||
|
제거된 코드: 222줄
|
||||||
|
순 증가: 2,573줄
|
||||||
|
변경된 파일: 60개
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 배포 준비 상태
|
||||||
|
|
||||||
|
### ✅ 완료된 작업
|
||||||
|
- [x] EventId/JobId 생성 로직 구현
|
||||||
|
- [x] Kafka 메시지 구조 개선
|
||||||
|
- [x] 데이터베이스 스키마 정의
|
||||||
|
- [x] content-service 통합 테스트 완료
|
||||||
|
- [x] API 문서화 및 테스트 보고서 작성
|
||||||
|
- [x] 테스트 자동화 스크립트 작성
|
||||||
|
|
||||||
|
### ⏳ 진행 예정 작업
|
||||||
|
- [ ] content-service Kafka Consumer 구현 (옵션)
|
||||||
|
- [ ] 프로덕션 환경 데이터베이스 마이그레이션
|
||||||
|
- [ ] Swagger/OpenAPI 문서 자동화
|
||||||
|
- [ ] 성능 모니터링 도구 설정
|
||||||
|
- [ ] 로그 수집 및 분석 시스템 구축
|
||||||
|
|
||||||
|
### ⚠️ 주의사항
|
||||||
|
1. **데이터베이스 마이그레이션**: 프로덕션 배포 전 백업 필수
|
||||||
|
2. **Kafka 메시지 호환성**: 기존 Consumer가 있다면 메시지 형식 변경 영향 확인
|
||||||
|
3. **ID 형식 변경**: 기존 데이터와의 호환성 검토 필요
|
||||||
|
4. **환경 변수**: 모든 환경에서 필요한 환경 변수 설정 확인
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 주요 커밋 히스토리
|
||||||
|
|
||||||
|
```
|
||||||
|
3465a35 Merge branch 'feature/event' into develop
|
||||||
|
8ff79ca 테스트 결과 파일들을 test/ 폴더로 이동
|
||||||
|
336d811 content-service 통합 테스트 완료 및 보고서 작성
|
||||||
|
ee941e4 Event-AI Kafka 연동 개선 및 메시지 필드명 camelCase 변경
|
||||||
|
b71d27a 비즈니스 친화적 eventId 및 jobId 생성 로직 구현
|
||||||
|
34291e1 백엔드 서비스 구조 개선 및 데이터베이스 스키마 추가
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 관련 문서
|
||||||
|
|
||||||
|
1. **테스트 보고서**
|
||||||
|
- `test/content-service-integration-test-results.md`
|
||||||
|
- `test/test-kafka-integration-results.md`
|
||||||
|
- `test/API-TEST-RESULT.md`
|
||||||
|
|
||||||
|
2. **아키텍처 문서**
|
||||||
|
- `test/content-service-integration-analysis.md`
|
||||||
|
|
||||||
|
3. **데이터베이스**
|
||||||
|
- `develop/database/schema/create_event_tables.sql`
|
||||||
|
- `develop/database/migration/alter_event_id_to_varchar.sql`
|
||||||
|
|
||||||
|
4. **테스트 스크립트**
|
||||||
|
- `test-content-service.sh`
|
||||||
|
- `run-content-service.sh`
|
||||||
|
- `run-content-service.bat`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**작성자**: Backend Developer
|
||||||
|
**검토자**: System Architect
|
||||||
|
**최종 업데이트**: 2025-10-30 01:40
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Multi-stage build for Spring Boot application
|
||||||
|
FROM eclipse-temurin:21-jre-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY build/libs/*.jar app.jar
|
||||||
|
RUN java -Djarmode=layertools -jar app.jar extract
|
||||||
|
|
||||||
|
FROM eclipse-temurin:21-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -S spring && adduser -S spring -G spring
|
||||||
|
USER spring:spring
|
||||||
|
|
||||||
|
# Copy layers from builder
|
||||||
|
COPY --from=builder /app/dependencies/ ./
|
||||||
|
COPY --from=builder /app/spring-boot-loader/ ./
|
||||||
|
COPY --from=builder /app/snapshot-dependencies/ ./
|
||||||
|
COPY --from=builder /app/application/ ./
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8083/api/v1/ai-service/actuator/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
bootJar {
|
||||||
|
archiveFileName = 'ai-service.jar'
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Kafka Consumer
|
// Kafka Consumer
|
||||||
implementation 'org.springframework.kafka:spring-kafka'
|
implementation 'org.springframework.kafka:spring-kafka'
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
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.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
@@ -27,21 +28,22 @@ import java.util.List;
|
|||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
/**
|
|
||||||
* Security Filter Chain 설정
|
|
||||||
* - 모든 요청 허용 (내부 API)
|
|
||||||
* - CSRF 비활성화
|
|
||||||
* - Stateless 세션
|
|
||||||
*/
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
|
// CSRF 비활성화 (REST API는 CSRF 불필요)
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
|
||||||
|
// CORS 설정
|
||||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
||||||
|
// 세션 사용 안 함 (JWT 기반 인증)
|
||||||
|
.sessionManagement(session ->
|
||||||
|
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 모든 요청 허용 (테스트용)
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/health", "/actuator/**", "/v3/api-docs/**", "/swagger-ui/**").permitAll()
|
|
||||||
.requestMatchers("/internal/**").permitAll() // Internal API
|
|
||||||
.anyRequest().permitAll()
|
.anyRequest().permitAll()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -50,11 +52,14 @@ public class SecurityConfig {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* CORS 설정
|
* CORS 설정
|
||||||
|
* - 모든 Origin 허용 (Swagger UI 테스트를 위해)
|
||||||
|
* - 모든 HTTP Method 허용
|
||||||
|
* - 모든 Header 허용
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
CorsConfiguration configuration = new CorsConfiguration();
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080"));
|
configuration.setAllowedOriginPatterns(List.of("*")); // 모든 Origin 허용
|
||||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
|
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
|
||||||
configuration.setAllowedHeaders(List.of("*"));
|
configuration.setAllowedHeaders(List.of("*"));
|
||||||
configuration.setAllowCredentials(true);
|
configuration.setAllowCredentials(true);
|
||||||
@@ -64,4 +69,13 @@ public class SecurityConfig {
|
|||||||
source.registerCorsConfiguration("/**", configuration);
|
source.registerCorsConfiguration("/**", configuration);
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chrome DevTools 요청 등 정적 리소스 요청을 Spring Security에서 제외
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||||
|
return (web) -> web.ignoring()
|
||||||
|
.requestMatchers("/.well-known/**");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ public class SwaggerConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public OpenAPI openAPI() {
|
public OpenAPI openAPI() {
|
||||||
|
Server vmServer = new Server();
|
||||||
|
vmServer.setUrl("http://kt-event-marketing-api.20.214.196.128.nip.io/api/v1/ai");
|
||||||
|
vmServer.setDescription("VM Development Server");
|
||||||
|
|
||||||
Server localServer = new Server();
|
Server localServer = new Server();
|
||||||
localServer.setUrl("http://localhost:8083");
|
localServer.setUrl("http://localhost:8083");
|
||||||
localServer.setDescription("Local Development Server");
|
localServer.setDescription("Local Development Server");
|
||||||
@@ -59,6 +63,6 @@ public class SwaggerConfig {
|
|||||||
|
|
||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
.info(info)
|
.info(info)
|
||||||
.servers(List.of(localServer, devServer, prodServer));
|
.servers(List.of(vmServer, localServer, devServer, prodServer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public class HealthController {
|
|||||||
* 서비스 헬스체크
|
* 서비스 헬스체크
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "서비스 헬스체크", description = "AI Service 상태 및 외부 연동 확인")
|
@Operation(summary = "서비스 헬스체크", description = "AI Service 상태 및 외부 연동 확인")
|
||||||
@GetMapping("/api/v1/ai-service/health")
|
@GetMapping("/health")
|
||||||
public ResponseEntity<HealthCheckResponse> healthCheck() {
|
public ResponseEntity<HealthCheckResponse> healthCheck() {
|
||||||
// Redis 상태 확인
|
// Redis 상태 확인
|
||||||
ServiceStatus redisStatus = checkRedis();
|
ServiceStatus redisStatus = checkRedis();
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import java.util.Map;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Tag(name = "Internal API", description = "내부 서비스 간 통신용 API")
|
@Tag(name = "Internal API", description = "내부 서비스 간 통신용 API")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/ai-service/internal/jobs")
|
@RequestMapping("/jobs")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class InternalJobController {
|
public class InternalJobController {
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import java.util.Set;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Tag(name = "Internal API", description = "내부 서비스 간 통신용 API")
|
@Tag(name = "Internal API", description = "내부 서비스 간 통신용 API")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/ai-service/internal/recommendations")
|
@RequestMapping("/recommendations")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class InternalRecommendationController {
|
public class InternalRecommendationController {
|
||||||
|
|
||||||
|
|||||||
@@ -5,29 +5,31 @@ spring:
|
|||||||
# Redis Configuration
|
# Redis Configuration
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
host: 20.214.210.71
|
host: ${REDIS_HOST:20.214.210.71}
|
||||||
port: 6379
|
port: ${REDIS_PORT:6379}
|
||||||
password: Hi5Jessica!
|
password: ${REDIS_PASSWORD:Hi5Jessica!}
|
||||||
database: 3
|
database: ${REDIS_DATABASE:3}
|
||||||
timeout: 3000
|
timeout: ${REDIS_TIMEOUT:3000}
|
||||||
lettuce:
|
lettuce:
|
||||||
pool:
|
pool:
|
||||||
max-active: 8
|
max-active: ${REDIS_POOL_MAX:8}
|
||||||
max-idle: 8
|
max-idle: ${REDIS_POOL_IDLE:8}
|
||||||
min-idle: 2
|
min-idle: ${REDIS_POOL_MIN:2}
|
||||||
max-wait: -1ms
|
max-wait: ${REDIS_POOL_WAIT:-1ms}
|
||||||
|
|
||||||
# Kafka Consumer Configuration
|
# Kafka Consumer Configuration
|
||||||
kafka:
|
kafka:
|
||||||
bootstrap-servers: 4.230.50.63:9092
|
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:20.249.182.13:9095,4.217.131.59:9095}
|
||||||
consumer:
|
consumer:
|
||||||
group-id: ai-service-consumers
|
group-id: ${KAFKA_CONSUMER_GROUP:ai-service-consumers}
|
||||||
auto-offset-reset: earliest
|
auto-offset-reset: earliest
|
||||||
enable-auto-commit: false
|
enable-auto-commit: false
|
||||||
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||||
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
|
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
|
||||||
properties:
|
properties:
|
||||||
spring.json.trusted.packages: "*"
|
spring.json.trusted.packages: "*"
|
||||||
|
spring.json.use.type.headers: false
|
||||||
|
spring.json.value.default.type: com.kt.ai.kafka.message.AIJobMessage
|
||||||
max.poll.records: 10
|
max.poll.records: 10
|
||||||
session.timeout.ms: 30000
|
session.timeout.ms: 30000
|
||||||
listener:
|
listener:
|
||||||
@@ -35,9 +37,9 @@ spring:
|
|||||||
|
|
||||||
# Server Configuration
|
# Server Configuration
|
||||||
server:
|
server:
|
||||||
port: 8083
|
port: ${SERVER_PORT:8083}
|
||||||
servlet:
|
servlet:
|
||||||
context-path: /
|
context-path: /api/v1/ai
|
||||||
encoding:
|
encoding:
|
||||||
charset: UTF-8
|
charset: UTF-8
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -45,17 +47,17 @@ server:
|
|||||||
|
|
||||||
# JWT Configuration
|
# JWT Configuration
|
||||||
jwt:
|
jwt:
|
||||||
secret: kt-event-marketing-secret-key-for-development-only-please-change-in-production
|
secret: ${JWT_SECRET:kt-event-marketing-secret-key-for-development-only-please-change-in-production}
|
||||||
access-token-validity: 604800000
|
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:604800000}
|
||||||
refresh-token-validity: 86400
|
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:86400}
|
||||||
|
|
||||||
# CORS Configuration
|
# CORS Configuration
|
||||||
cors:
|
cors:
|
||||||
allowed-origins: http://localhost:*
|
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*,http://kt-event-marketing.20.214.196.128.nip.io}
|
||||||
allowed-methods: GET,POST,PUT,DELETE,OPTIONS,PATCH
|
allowed-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS,PATCH}
|
||||||
allowed-headers: "*"
|
allowed-headers: ${CORS_ALLOWED_HEADERS:*}
|
||||||
allow-credentials: true
|
allow-credentials: ${CORS_ALLOW_CREDENTIALS:true}
|
||||||
max-age: 3600
|
max-age: ${CORS_MAX_AGE:3600}
|
||||||
|
|
||||||
# Actuator Configuration
|
# Actuator Configuration
|
||||||
management:
|
management:
|
||||||
@@ -91,39 +93,39 @@ springdoc:
|
|||||||
# Logging Configuration
|
# Logging Configuration
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
root: INFO
|
root: ${LOG_LEVEL_ROOT:INFO}
|
||||||
com.kt.ai: DEBUG
|
com.kt.ai: ${LOG_LEVEL_AI:DEBUG}
|
||||||
org.springframework.kafka: INFO
|
org.springframework.kafka: ${LOG_LEVEL_KAFKA:INFO}
|
||||||
org.springframework.data.redis: INFO
|
org.springframework.data.redis: ${LOG_LEVEL_REDIS:INFO}
|
||||||
io.github.resilience4j: DEBUG
|
io.github.resilience4j: ${LOG_LEVEL_RESILIENCE4J:DEBUG}
|
||||||
pattern:
|
pattern:
|
||||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
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: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||||
file:
|
file:
|
||||||
name: logs/ai-service.log
|
name: ${LOG_FILE_NAME:logs/ai-service.log}
|
||||||
logback:
|
logback:
|
||||||
rollingpolicy:
|
rollingpolicy:
|
||||||
max-file-size: 10MB
|
max-file-size: ${LOG_FILE_MAX_SIZE:10MB}
|
||||||
max-history: 7
|
max-history: ${LOG_FILE_MAX_HISTORY:7}
|
||||||
total-size-cap: 100MB
|
total-size-cap: ${LOG_FILE_TOTAL_CAP:100MB}
|
||||||
|
|
||||||
# Kafka Topics Configuration
|
# Kafka Topics Configuration
|
||||||
kafka:
|
kafka:
|
||||||
topics:
|
topics:
|
||||||
ai-job: ai-event-generation-job
|
ai-job: ${KAFKA_TOPICS_AI_JOB:ai-event-generation-job}
|
||||||
ai-job-dlq: ai-event-generation-job-dlq
|
ai-job-dlq: ${KAFKA_TOPICS_AI_JOB_DLQ:ai-event-generation-job-dlq}
|
||||||
|
|
||||||
# AI API Configuration (실제 API 사용)
|
# AI API Configuration (실제 API 사용)
|
||||||
ai:
|
ai:
|
||||||
provider: CLAUDE
|
provider: ${AI_PROVIDER:CLAUDE}
|
||||||
claude:
|
claude:
|
||||||
api-url: https://api.anthropic.com/v1/messages
|
api-url: ${AI_CLAUDE_API_URL:https://api.anthropic.com/v1/messages}
|
||||||
api-key: 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
|
api-key: ${AI_CLAUDE_API_KEY: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}
|
||||||
anthropic-version: 2023-06-01
|
anthropic-version: ${AI_CLAUDE_ANTHROPIC_VERSION:2023-06-01}
|
||||||
model: claude-sonnet-4-5-20250929
|
model: ${AI_CLAUDE_MODEL:claude-sonnet-4-5-20250929}
|
||||||
max-tokens: 4096
|
max-tokens: ${AI_CLAUDE_MAX_TOKENS:4096}
|
||||||
temperature: 0.7
|
temperature: ${AI_CLAUDE_TEMPERATURE:0.7}
|
||||||
timeout: 300000
|
timeout: ${AI_CLAUDE_TIMEOUT:300000}
|
||||||
|
|
||||||
# Circuit Breaker Configuration
|
# Circuit Breaker Configuration
|
||||||
resilience4j:
|
resilience4j:
|
||||||
@@ -162,7 +164,7 @@ resilience4j:
|
|||||||
# Redis Cache TTL Configuration (seconds)
|
# Redis Cache TTL Configuration (seconds)
|
||||||
cache:
|
cache:
|
||||||
ttl:
|
ttl:
|
||||||
recommendation: 86400 # 24 hours
|
recommendation: ${CACHE_TTL_RECOMMENDATION:86400} # 24 hours
|
||||||
job-status: 86400 # 24 hours
|
job-status: ${CACHE_TTL_JOB_STATUS:86400} # 24 hours
|
||||||
trend: 3600 # 1 hour
|
trend: ${CACHE_TTL_TREND:3600} # 1 hour
|
||||||
fallback: 604800 # 7 days
|
fallback: ${CACHE_TTL_FALLBACK:604800} # 7 days
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<!-- Kafka Configuration (원격 서버) -->
|
<!-- Kafka Configuration (원격 서버) -->
|
||||||
<entry key="KAFKA_ENABLED" value="true" />
|
<entry key="KAFKA_ENABLED" value="true" />
|
||||||
<entry key="KAFKA_BOOTSTRAP_SERVERS" value="20.249.182.13:9095,4.217.131.59:9095" />
|
<entry key="KAFKA_BOOTSTRAP_SERVERS" value="20.249.182.13:9095,4.217.131.59:9095" />
|
||||||
<entry key="KAFKA_CONSUMER_GROUP_ID" value="analytics-service-consumers" />
|
<entry key="KAFKA_CONSUMER_GROUP_ID" value="analytics-service-consumers-v3" />
|
||||||
|
|
||||||
<!-- Sample Data Configuration (MVP Only) -->
|
<!-- Sample Data Configuration (MVP Only) -->
|
||||||
<!-- ⚠️ Kafka Producer로 이벤트 발행 (Consumer가 처리) -->
|
<!-- ⚠️ Kafka Producer로 이벤트 발행 (Consumer가 처리) -->
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<entry key="JWT_REFRESH_TOKEN_VALIDITY" value="86400" />
|
<entry key="JWT_REFRESH_TOKEN_VALIDITY" value="86400" />
|
||||||
|
|
||||||
<!-- CORS Configuration -->
|
<!-- CORS Configuration -->
|
||||||
<entry key="CORS_ALLOWED_ORIGINS" value="http://localhost:*" />
|
<entry key="CORS_ALLOWED_ORIGINS" value="http://localhost:*,http://*.nip.io:*" />
|
||||||
|
|
||||||
<!-- Logging Configuration -->
|
<!-- Logging Configuration -->
|
||||||
<entry key="LOG_FILE" value="logs/analytics-service.log" />
|
<entry key="LOG_FILE" value="logs/analytics-service.log" />
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Multi-stage build for Spring Boot application
|
||||||
|
FROM eclipse-temurin:21-jre-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY analytics-service/build/libs/*.jar app.jar
|
||||||
|
RUN java -Djarmode=layertools -jar app.jar extract
|
||||||
|
|
||||||
|
FROM eclipse-temurin:21-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -S spring && adduser -S spring -G spring
|
||||||
|
USER spring:spring
|
||||||
|
|
||||||
|
# Copy layers from builder
|
||||||
|
COPY --from=builder /app/dependencies/ ./
|
||||||
|
COPY --from=builder /app/spring-boot-loader/ ./
|
||||||
|
COPY --from=builder /app/snapshot-dependencies/ ./
|
||||||
|
COPY --from=builder /app/application/ ./
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8086/api/v1/analytics/actuator/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
bootJar {
|
||||||
|
archiveFileName = 'analytics-service.jar'
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Kafka Consumer
|
// Kafka Consumer
|
||||||
implementation 'org.springframework.kafka:spring-kafka'
|
implementation 'org.springframework.kafka:spring-kafka'
|
||||||
|
|||||||
+2
-2
@@ -63,7 +63,7 @@ public class AnalyticsBatchScheduler {
|
|||||||
event.getEventId(), event.getEventTitle());
|
event.getEventId(), event.getEventTitle());
|
||||||
|
|
||||||
// refresh=true로 호출하여 캐시 갱신 및 외부 API 호출
|
// refresh=true로 호출하여 캐시 갱신 및 외부 API 호출
|
||||||
analyticsService.getDashboardData(event.getEventId(), null, null, true);
|
analyticsService.getDashboardData(event.getEventId(), true);
|
||||||
|
|
||||||
successCount++;
|
successCount++;
|
||||||
log.info("✅ 배치 갱신 완료: eventId={}", event.getEventId());
|
log.info("✅ 배치 갱신 완료: eventId={}", event.getEventId());
|
||||||
@@ -99,7 +99,7 @@ public class AnalyticsBatchScheduler {
|
|||||||
|
|
||||||
for (EventStats event : allEvents) {
|
for (EventStats event : allEvents) {
|
||||||
try {
|
try {
|
||||||
analyticsService.getDashboardData(event.getEventId(), null, null, true);
|
analyticsService.getDashboardData(event.getEventId(), true);
|
||||||
log.debug("초기 데이터 로딩 완료: eventId={}", event.getEventId());
|
log.debug("초기 데이터 로딩 완료: eventId={}", event.getEventId());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("초기 데이터 로딩 실패: eventId={}, error={}",
|
log.warn("초기 데이터 로딩 실패: eventId={}, error={}",
|
||||||
|
|||||||
+2
-2
@@ -17,13 +17,13 @@ import java.util.Map;
|
|||||||
* Kafka Consumer 설정
|
* Kafka Consumer 설정
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnProperty(name = "spring.kafka.enabled", havingValue = "true", matchIfMissing = true)
|
@ConditionalOnProperty(name = "spring.kafka.enabled", havingValue = "true", matchIfMissing = false)
|
||||||
public class KafkaConsumerConfig {
|
public class KafkaConsumerConfig {
|
||||||
|
|
||||||
@Value("${spring.kafka.bootstrap-servers}")
|
@Value("${spring.kafka.bootstrap-servers}")
|
||||||
private String bootstrapServers;
|
private String bootstrapServers;
|
||||||
|
|
||||||
@Value("${spring.kafka.consumer.group-id:analytics-service}")
|
@Value("${spring.kafka.consumer.group-id:analytics-service-consumers-v3}")
|
||||||
private String groupId;
|
private String groupId;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
package com.kt.event.analytics.config;
|
||||||
|
|
||||||
|
import org.apache.kafka.clients.producer.ProducerConfig;
|
||||||
|
import org.apache.kafka.common.serialization.StringSerializer;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
|
||||||
|
import org.springframework.kafka.core.KafkaTemplate;
|
||||||
|
import org.springframework.kafka.core.ProducerFactory;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kafka Producer 설정
|
||||||
|
*
|
||||||
|
* ⚠️ MVP 전용: SampleDataLoader가 Kafka 이벤트를 발행하기 위해 필요
|
||||||
|
* ⚠️ 실제 운영: Analytics Service는 순수 Consumer 역할만 수행하므로 Producer 불필요
|
||||||
|
*
|
||||||
|
* String 직렬화 방식 사용 (SampleDataLoader가 JSON 문자열을 직접 발행)
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnProperty(name = "spring.kafka.enabled", havingValue = "true", matchIfMissing = false)
|
||||||
|
public class KafkaProducerConfig {
|
||||||
|
|
||||||
|
@Value("${spring.kafka.bootstrap-servers}")
|
||||||
|
private String bootstrapServers;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ProducerFactory<String, String> producerFactory() {
|
||||||
|
Map<String, Object> configProps = new HashMap<>();
|
||||||
|
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
|
||||||
|
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
|
||||||
|
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
|
||||||
|
configProps.put(ProducerConfig.ACKS_CONFIG, "all");
|
||||||
|
configProps.put(ProducerConfig.RETRIES_CONFIG, 3);
|
||||||
|
return new DefaultKafkaProducerFactory<>(configProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public KafkaTemplate<String, String> kafkaTemplate() {
|
||||||
|
return new KafkaTemplate<>(producerFactory());
|
||||||
|
}
|
||||||
|
}
|
||||||
+211
-61
@@ -11,19 +11,23 @@ import jakarta.annotation.PreDestroy;
|
|||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.kafka.clients.admin.AdminClient;
|
||||||
|
import org.apache.kafka.clients.admin.DeleteConsumerGroupOffsetsResult;
|
||||||
|
import org.apache.kafka.clients.admin.ListConsumerGroupOffsetsResult;
|
||||||
|
import org.apache.kafka.common.TopicPartition;
|
||||||
import org.springframework.boot.ApplicationArguments;
|
import org.springframework.boot.ApplicationArguments;
|
||||||
import org.springframework.boot.ApplicationRunner;
|
import org.springframework.boot.ApplicationRunner;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.kafka.core.KafkaAdmin;
|
||||||
import org.springframework.kafka.core.KafkaTemplate;
|
import org.springframework.kafka.core.KafkaTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.Random;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 샘플 데이터 로더 (Kafka Producer 방식)
|
* 샘플 데이터 로더 (Kafka Producer 방식)
|
||||||
@@ -47,6 +51,7 @@ import java.util.UUID;
|
|||||||
public class SampleDataLoader implements ApplicationRunner {
|
public class SampleDataLoader implements ApplicationRunner {
|
||||||
|
|
||||||
private final KafkaTemplate<String, String> kafkaTemplate;
|
private final KafkaTemplate<String, String> kafkaTemplate;
|
||||||
|
private final KafkaAdmin kafkaAdmin;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final EventStatsRepository eventStatsRepository;
|
private final EventStatsRepository eventStatsRepository;
|
||||||
private final ChannelStatsRepository channelStatsRepository;
|
private final ChannelStatsRepository channelStatsRepository;
|
||||||
@@ -56,6 +61,9 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
|
|
||||||
private final Random random = new Random();
|
private final Random random = new Random();
|
||||||
|
|
||||||
|
@Value("${spring.kafka.consumer.group-id}")
|
||||||
|
private String consumerGroupId;
|
||||||
|
|
||||||
// Kafka Topic Names (MVP용 샘플 토픽)
|
// Kafka Topic Names (MVP용 샘플 토픽)
|
||||||
private static final String EVENT_CREATED_TOPIC = "sample.event.created";
|
private static final String EVENT_CREATED_TOPIC = "sample.event.created";
|
||||||
private static final String PARTICIPANT_REGISTERED_TOPIC = "sample.participant.registered";
|
private static final String PARTICIPANT_REGISTERED_TOPIC = "sample.participant.registered";
|
||||||
@@ -85,10 +93,15 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
|
|
||||||
// Redis 멱등성 키 삭제 (새로운 이벤트 처리를 위해)
|
// Redis 멱등성 키 삭제 (새로운 이벤트 처리를 위해)
|
||||||
log.info("Redis 멱등성 키 삭제 중...");
|
log.info("Redis 멱등성 키 삭제 중...");
|
||||||
redisTemplate.delete("processed_events");
|
try {
|
||||||
redisTemplate.delete("distribution_completed");
|
redisTemplate.delete("processed_events_v2");
|
||||||
redisTemplate.delete("processed_participants");
|
redisTemplate.delete("distribution_completed_v2");
|
||||||
log.info("✅ Redis 멱등성 키 삭제 완료");
|
redisTemplate.delete("processed_participants_v2");
|
||||||
|
log.info("✅ Redis 멱등성 키 삭제 완료");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("⚠️ Redis 삭제 실패 (read-only replica일 수 있음): {}", e.getMessage());
|
||||||
|
log.info("→ Redis 삭제 건너뛰고 계속 진행...");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. EventCreated 이벤트 발행 (3개 이벤트)
|
// 1. EventCreated 이벤트 발행 (3개 이벤트)
|
||||||
@@ -103,6 +116,8 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
|
|
||||||
// 3. ParticipantRegistered 이벤트 발행 (각 이벤트당 다수 참여자)
|
// 3. ParticipantRegistered 이벤트 발행 (각 이벤트당 다수 참여자)
|
||||||
publishParticipantRegisteredEvents();
|
publishParticipantRegisteredEvents();
|
||||||
|
log.info("⏳ 참여자 등록 이벤트 처리 대기 중... (20초)");
|
||||||
|
Thread.sleep(20000); // ParticipantRegisteredConsumer가 180개 이벤트 처리할 시간 (비관적 락 고려)
|
||||||
|
|
||||||
log.info("========================================");
|
log.info("========================================");
|
||||||
log.info("🎉 Kafka 이벤트 발행 완료! (Consumer가 처리 중...)");
|
log.info("🎉 Kafka 이벤트 발행 완료! (Consumer가 처리 중...)");
|
||||||
@@ -127,16 +142,17 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 서비스 종료 시 전체 데이터 삭제
|
* 서비스 종료 시 전체 데이터 삭제 및 Consumer Offset 리셋
|
||||||
*/
|
*/
|
||||||
@PreDestroy
|
@PreDestroy
|
||||||
@Transactional
|
@Transactional
|
||||||
public void onShutdown() {
|
public void onShutdown() {
|
||||||
log.info("========================================");
|
log.info("========================================");
|
||||||
log.info("🛑 서비스 종료: PostgreSQL 전체 데이터 삭제");
|
log.info("🛑 서비스 종료: PostgreSQL 전체 데이터 삭제 + Kafka Consumer Offset 리셋");
|
||||||
log.info("========================================");
|
log.info("========================================");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 1. PostgreSQL 데이터 삭제
|
||||||
long timelineCount = timelineDataRepository.count();
|
long timelineCount = timelineDataRepository.count();
|
||||||
long channelCount = channelStatsRepository.count();
|
long channelCount = channelStatsRepository.count();
|
||||||
long eventCount = eventStatsRepository.count();
|
long eventCount = eventStatsRepository.count();
|
||||||
@@ -153,6 +169,10 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
entityManager.clear();
|
entityManager.clear();
|
||||||
|
|
||||||
log.info("✅ 모든 샘플 데이터 삭제 완료!");
|
log.info("✅ 모든 샘플 데이터 삭제 완료!");
|
||||||
|
|
||||||
|
// 2. Kafka Consumer Offset 리셋 (다음 시작 시 처음부터 읽도록)
|
||||||
|
resetConsumerOffsets();
|
||||||
|
|
||||||
log.info("========================================");
|
log.info("========================================");
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -160,37 +180,85 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kafka Consumer Group Offset 리셋
|
||||||
|
*
|
||||||
|
* 서비스 종료 시 Consumer offset을 삭제하여 다음 시작 시
|
||||||
|
* auto.offset.reset=earliest 설정에 따라 처음부터 읽도록 함
|
||||||
|
*/
|
||||||
|
private void resetConsumerOffsets() {
|
||||||
|
try (AdminClient adminClient = AdminClient.create(kafkaAdmin.getConfigurationProperties())) {
|
||||||
|
log.info("🔄 Kafka Consumer Offset 리셋 시작: group={}", consumerGroupId);
|
||||||
|
|
||||||
|
// 모든 토픽의 offset 삭제
|
||||||
|
Set<TopicPartition> partitions = new HashSet<>();
|
||||||
|
|
||||||
|
// 토픽별 파티션 추가 (설계서상 각 토픽은 3개 파티션)
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
partitions.add(new TopicPartition(EVENT_CREATED_TOPIC, i));
|
||||||
|
partitions.add(new TopicPartition(PARTICIPANT_REGISTERED_TOPIC, i));
|
||||||
|
partitions.add(new TopicPartition(DISTRIBUTION_COMPLETED_TOPIC, i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consumer Group Offset 삭제
|
||||||
|
DeleteConsumerGroupOffsetsResult result = adminClient.deleteConsumerGroupOffsets(
|
||||||
|
consumerGroupId,
|
||||||
|
partitions
|
||||||
|
);
|
||||||
|
|
||||||
|
// 완료 대기 (최대 10초)
|
||||||
|
result.all().get(10, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
log.info("✅ Kafka Consumer Offset 리셋 완료!");
|
||||||
|
log.info(" → 다음 시작 시 처음부터(earliest) 메시지를 읽습니다.");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Offset 리셋 실패는 치명적이지 않으므로 경고만 출력
|
||||||
|
log.warn("⚠️ Kafka Consumer Offset 리셋 실패 (무시 가능): {}", e.getMessage());
|
||||||
|
log.warn(" → 수동으로 Consumer Group ID를 변경하거나, Kafka 도구로 offset을 삭제하세요.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EventCreated 이벤트 발행
|
* EventCreated 이벤트 발행
|
||||||
*/
|
*/
|
||||||
private void publishEventCreatedEvents() throws Exception {
|
private void publishEventCreatedEvents() throws Exception {
|
||||||
// 이벤트 1: 신년맞이 할인 이벤트 (진행중, 높은 성과)
|
// 이벤트 1: 신년맞이 할인 이벤트 (진행중, 높은 성과 - ROI 200%)
|
||||||
EventCreatedEvent event1 = EventCreatedEvent.builder()
|
EventCreatedEvent event1 = EventCreatedEvent.builder()
|
||||||
.eventId("evt_2025012301")
|
.eventId("1")
|
||||||
.eventTitle("신년맞이 20% 할인 이벤트")
|
.eventTitle("신년맞이 20% 할인 이벤트")
|
||||||
.storeId("store_001")
|
.storeId("store_001")
|
||||||
.totalInvestment(new BigDecimal("5000000"))
|
.totalInvestment(new BigDecimal("5000000"))
|
||||||
|
.expectedRevenue(new BigDecimal("15000000")) // 투자 대비 3배 수익
|
||||||
.status("ACTIVE")
|
.status("ACTIVE")
|
||||||
|
.startDate(java.time.LocalDateTime.of(2025, 1, 23, 0, 0)) // 2025-01-23 시작
|
||||||
|
.endDate(null) // 진행중
|
||||||
.build();
|
.build();
|
||||||
publishEvent(EVENT_CREATED_TOPIC, event1);
|
publishEvent(EVENT_CREATED_TOPIC, event1);
|
||||||
|
|
||||||
// 이벤트 2: 설날 특가 이벤트 (진행중, 중간 성과)
|
// 이벤트 2: 설날 특가 이벤트 (진행중, 중간 성과 - ROI 100%)
|
||||||
EventCreatedEvent event2 = EventCreatedEvent.builder()
|
EventCreatedEvent event2 = EventCreatedEvent.builder()
|
||||||
.eventId("evt_2025020101")
|
.eventId("2")
|
||||||
.eventTitle("설날 특가 선물세트 이벤트")
|
.eventTitle("설날 특가 선물세트 이벤트")
|
||||||
.storeId("store_001")
|
.storeId("store_001")
|
||||||
.totalInvestment(new BigDecimal("3500000"))
|
.totalInvestment(new BigDecimal("3500000"))
|
||||||
|
.expectedRevenue(new BigDecimal("7000000")) // 투자 대비 2배 수익
|
||||||
.status("ACTIVE")
|
.status("ACTIVE")
|
||||||
|
.startDate(java.time.LocalDateTime.of(2025, 2, 1, 0, 0)) // 2025-02-01 시작
|
||||||
|
.endDate(null) // 진행중
|
||||||
.build();
|
.build();
|
||||||
publishEvent(EVENT_CREATED_TOPIC, event2);
|
publishEvent(EVENT_CREATED_TOPIC, event2);
|
||||||
|
|
||||||
// 이벤트 3: 겨울 신메뉴 런칭 이벤트 (종료, 저조한 성과)
|
// 이벤트 3: 겨울 신메뉴 런칭 이벤트 (종료, 저조한 성과 - ROI 50%)
|
||||||
EventCreatedEvent event3 = EventCreatedEvent.builder()
|
EventCreatedEvent event3 = EventCreatedEvent.builder()
|
||||||
.eventId("evt_2025011501")
|
.eventId("3")
|
||||||
.eventTitle("겨울 신메뉴 런칭 이벤트")
|
.eventTitle("겨울 신메뉴 런칭 이벤트")
|
||||||
.storeId("store_001")
|
.storeId("store_001")
|
||||||
.totalInvestment(new BigDecimal("2000000"))
|
.totalInvestment(new BigDecimal("2000000"))
|
||||||
|
.expectedRevenue(new BigDecimal("3000000")) // 투자 대비 1.5배 수익
|
||||||
.status("COMPLETED")
|
.status("COMPLETED")
|
||||||
|
.startDate(java.time.LocalDateTime.of(2025, 1, 15, 0, 0)) // 2025-01-15 시작
|
||||||
|
.endDate(java.time.LocalDateTime.of(2025, 1, 31, 23, 59)) // 2025-01-31 종료
|
||||||
.build();
|
.build();
|
||||||
publishEvent(EVENT_CREATED_TOPIC, event3);
|
publishEvent(EVENT_CREATED_TOPIC, event3);
|
||||||
|
|
||||||
@@ -201,49 +269,70 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
* DistributionCompleted 이벤트 발행 (설계서 기준 - 이벤트당 1번 발행, 여러 채널 배열)
|
* DistributionCompleted 이벤트 발행 (설계서 기준 - 이벤트당 1번 발행, 여러 채널 배열)
|
||||||
*/
|
*/
|
||||||
private void publishDistributionCompletedEvents() throws Exception {
|
private void publishDistributionCompletedEvents() throws Exception {
|
||||||
String[] eventIds = {"evt_2025012301", "evt_2025020101", "evt_2025011501"};
|
String[] eventIds = {"1", "2", "3"};
|
||||||
int[][] expectedViews = {
|
int[][] expectedViews = {
|
||||||
{5000, 10000, 3000, 2000}, // 이벤트1: 우리동네TV, 지니TV, 링고비즈, SNS
|
{5000, 10000, 3000, 2000}, // 이벤트1: 우리동네TV, 지니TV, 링고비즈, SNS
|
||||||
{3500, 7000, 2000, 1500}, // 이벤트2
|
{3500, 7000, 2000, 1500}, // 이벤트2
|
||||||
{1500, 3000, 1000, 500} // 이벤트3
|
{1500, 3000, 1000, 500} // 이벤트3
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 각 이벤트의 총 투자 금액
|
||||||
|
BigDecimal[] totalInvestments = {
|
||||||
|
new BigDecimal("5000000"), // 이벤트1: 500만원
|
||||||
|
new BigDecimal("3500000"), // 이벤트2: 350만원
|
||||||
|
new BigDecimal("2000000") // 이벤트3: 200만원
|
||||||
|
};
|
||||||
|
|
||||||
|
// 채널 배포는 총 투자의 50%만 사용 (나머지는 경품/콘텐츠/운영비용)
|
||||||
|
double channelBudgetRatio = 0.50;
|
||||||
|
|
||||||
|
// 채널별 비용 비율 (채널 예산 내에서: 우리동네TV 30%, 지니TV 30%, 링고비즈 25%, SNS 15%)
|
||||||
|
double[] costRatios = {0.30, 0.30, 0.25, 0.15};
|
||||||
|
|
||||||
for (int i = 0; i < eventIds.length; i++) {
|
for (int i = 0; i < eventIds.length; i++) {
|
||||||
String eventId = eventIds[i];
|
String eventId = eventIds[i];
|
||||||
|
BigDecimal totalInvestment = totalInvestments[i];
|
||||||
|
|
||||||
|
// 채널 배포 예산: 총 투자의 50%
|
||||||
|
BigDecimal channelBudget = totalInvestment.multiply(BigDecimal.valueOf(channelBudgetRatio));
|
||||||
|
|
||||||
// 4개 채널을 배열로 구성
|
// 4개 채널을 배열로 구성
|
||||||
List<DistributionCompletedEvent.ChannelDistribution> channels = new ArrayList<>();
|
List<DistributionCompletedEvent.ChannelDistribution> channels = new ArrayList<>();
|
||||||
|
|
||||||
// 1. 우리동네TV (TV)
|
// 1. 우리동네TV (TV) - 채널 예산의 30%
|
||||||
channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
|
channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
|
||||||
.channel("우리동네TV")
|
.channel("우리동네TV")
|
||||||
.channelType("TV")
|
.channelType("TV")
|
||||||
.status("SUCCESS")
|
.status("SUCCESS")
|
||||||
.expectedViews(expectedViews[i][0])
|
.expectedViews(expectedViews[i][0])
|
||||||
|
.distributionCost(channelBudget.multiply(BigDecimal.valueOf(costRatios[0])))
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
// 2. 지니TV (TV)
|
// 2. 지니TV (TV) - 채널 예산의 30%
|
||||||
channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
|
channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
|
||||||
.channel("지니TV")
|
.channel("지니TV")
|
||||||
.channelType("TV")
|
.channelType("TV")
|
||||||
.status("SUCCESS")
|
.status("SUCCESS")
|
||||||
.expectedViews(expectedViews[i][1])
|
.expectedViews(expectedViews[i][1])
|
||||||
|
.distributionCost(channelBudget.multiply(BigDecimal.valueOf(costRatios[1])))
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
// 3. 링고비즈 (CALL)
|
// 3. 링고비즈 (CALL) - 채널 예산의 25%
|
||||||
channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
|
channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
|
||||||
.channel("링고비즈")
|
.channel("링고비즈")
|
||||||
.channelType("CALL")
|
.channelType("CALL")
|
||||||
.status("SUCCESS")
|
.status("SUCCESS")
|
||||||
.expectedViews(expectedViews[i][2])
|
.expectedViews(expectedViews[i][2])
|
||||||
|
.distributionCost(channelBudget.multiply(BigDecimal.valueOf(costRatios[2])))
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
// 4. SNS (SNS)
|
// 4. SNS (SNS) - 채널 예산의 15%
|
||||||
channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
|
channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
|
||||||
.channel("SNS")
|
.channel("SNS")
|
||||||
.channelType("SNS")
|
.channelType("SNS")
|
||||||
.status("SUCCESS")
|
.status("SUCCESS")
|
||||||
.expectedViews(expectedViews[i][3])
|
.expectedViews(expectedViews[i][3])
|
||||||
|
.distributionCost(channelBudget.multiply(BigDecimal.valueOf(costRatios[3])))
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
// 이벤트 발행 (채널 배열 포함)
|
// 이벤트 발행 (채널 배열 포함)
|
||||||
@@ -261,22 +350,53 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* ParticipantRegistered 이벤트 발행
|
* ParticipantRegistered 이벤트 발행
|
||||||
|
*
|
||||||
|
* 현실적인 참여 패턴 반영:
|
||||||
|
* - 총 120명의 고유 참여자 풀 생성
|
||||||
|
* - 일부 참여자는 여러 이벤트에 중복 참여
|
||||||
|
* - 이벤트1: 100명 (user001~user100)
|
||||||
|
* - 이벤트2: 50명 (user051~user100) → 50명이 이벤트1과 중복
|
||||||
|
* - 이벤트3: 30명 (user071~user100) → 30명이 이전 이벤트들과 중복
|
||||||
*/
|
*/
|
||||||
private void publishParticipantRegisteredEvents() throws Exception {
|
private void publishParticipantRegisteredEvents() throws Exception {
|
||||||
String[] eventIds = {"evt_2025012301", "evt_2025020101", "evt_2025011501"};
|
String[] eventIds = {"1", "2", "3"};
|
||||||
int[] totalParticipants = {100, 50, 30}; // MVP 테스트용 샘플 데이터 (총 180명)
|
|
||||||
String[] channels = {"우리동네TV", "지니TV", "링고비즈", "SNS"};
|
String[] channels = {"우리동네TV", "지니TV", "링고비즈", "SNS"};
|
||||||
|
|
||||||
|
// 이벤트별 참여자 범위 (중복 참여 반영)
|
||||||
|
int[][] participantRanges = {
|
||||||
|
{1, 100}, // 이벤트1: user001~user100 (100명)
|
||||||
|
{51, 100}, // 이벤트2: user051~user100 (50명, 이벤트1과 50명 중복)
|
||||||
|
{71, 100} // 이벤트3: user071~user100 (30명, 모두 중복)
|
||||||
|
};
|
||||||
|
|
||||||
int totalPublished = 0;
|
int totalPublished = 0;
|
||||||
|
|
||||||
for (int i = 0; i < eventIds.length; i++) {
|
for (int i = 0; i < eventIds.length; i++) {
|
||||||
String eventId = eventIds[i];
|
String eventId = eventIds[i];
|
||||||
int participants = totalParticipants[i];
|
int startUser = participantRanges[i][0];
|
||||||
|
int endUser = participantRanges[i][1];
|
||||||
|
int eventParticipants = endUser - startUser + 1;
|
||||||
|
|
||||||
// 각 이벤트에 대해 참여자 수만큼 ParticipantRegistered 이벤트 발행
|
log.info("이벤트 {} 참여자 발행 시작: user{:03d}~user{:03d} ({}명)",
|
||||||
for (int j = 0; j < participants; j++) {
|
eventId, startUser, endUser, eventParticipants);
|
||||||
String participantId = UUID.randomUUID().toString();
|
|
||||||
String channel = channels[j % channels.length]; // 채널 순환 배정
|
// 각 참여자에 대해 ParticipantRegistered 이벤트 발행
|
||||||
|
for (int userId = startUser; userId <= endUser; userId++) {
|
||||||
|
String participantId = String.format("user%03d", userId); // user001, user002, ...
|
||||||
|
|
||||||
|
// 채널별 가중치 기반 랜덤 배정
|
||||||
|
// SNS: 45%, 우리동네TV: 25%, 지니TV: 20%, 링고비즈: 10%
|
||||||
|
int randomValue = random.nextInt(100);
|
||||||
|
String channel;
|
||||||
|
if (randomValue < 45) {
|
||||||
|
channel = "SNS"; // 0~44: 45%
|
||||||
|
} else if (randomValue < 70) {
|
||||||
|
channel = "우리동네TV"; // 45~69: 25%
|
||||||
|
} else if (randomValue < 90) {
|
||||||
|
channel = "지니TV"; // 70~89: 20%
|
||||||
|
} else {
|
||||||
|
channel = "링고비즈"; // 90~99: 10%
|
||||||
|
}
|
||||||
|
|
||||||
ParticipantRegisteredEvent event = ParticipantRegisteredEvent.builder()
|
ParticipantRegisteredEvent event = ParticipantRegisteredEvent.builder()
|
||||||
.eventId(eventId)
|
.eventId(eventId)
|
||||||
@@ -288,72 +408,102 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
totalPublished++;
|
totalPublished++;
|
||||||
|
|
||||||
// 동시성 충돌 방지: 10개마다 100ms 대기
|
// 동시성 충돌 방지: 10개마다 100ms 대기
|
||||||
if ((j + 1) % 10 == 0) {
|
if (totalPublished % 10 == 0) {
|
||||||
Thread.sleep(100);
|
Thread.sleep(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("✅ 이벤트 {} 참여자 발행 완료: {}명", eventId, eventParticipants);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("========================================");
|
||||||
log.info("✅ ParticipantRegistered 이벤트 {}건 발행 완료", totalPublished);
|
log.info("✅ ParticipantRegistered 이벤트 {}건 발행 완료", totalPublished);
|
||||||
|
log.info("📊 참여 패턴:");
|
||||||
|
log.info(" - 총 고유 참여자: 100명 (user001~user100)");
|
||||||
|
log.info(" - 이벤트1 참여: 100명");
|
||||||
|
log.info(" - 이벤트2 참여: 50명 (이벤트1과 50명 중복)");
|
||||||
|
log.info(" - 이벤트3 참여: 30명 (이벤트1,2와 모두 중복)");
|
||||||
|
log.info(" - 3개 이벤트 모두 참여: 30명");
|
||||||
|
log.info(" - 2개 이벤트 참여: 20명");
|
||||||
|
log.info(" - 1개 이벤트만 참여: 50명");
|
||||||
|
log.info("📺 채널별 참여 비율 (가중치):");
|
||||||
|
log.info(" - SNS: 45% (가장 높음)");
|
||||||
|
log.info(" - 우리동네TV: 25%");
|
||||||
|
log.info(" - 지니TV: 20%");
|
||||||
|
log.info(" - 링고비즈: 10%");
|
||||||
|
log.info("========================================");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TimelineData 생성 (시간대별 샘플 데이터)
|
* TimelineData 생성 (시간대별 샘플 데이터)
|
||||||
*
|
*
|
||||||
* - 각 이벤트마다 30일 치 daily 데이터 생성
|
* - 각 이벤트마다 30일 × 24시간 = 720시간 치 hourly 데이터 생성
|
||||||
|
* - interval=hourly: 시간별 표시 (최근 7일 적합)
|
||||||
|
* - interval=daily: 일별 자동 집계 (30일 전체)
|
||||||
* - 참여자 수, 조회수, 참여행동, 전환수, 누적 참여자 수
|
* - 참여자 수, 조회수, 참여행동, 전환수, 누적 참여자 수
|
||||||
*/
|
*/
|
||||||
private void createTimelineData() {
|
private void createTimelineData() {
|
||||||
log.info("📊 TimelineData 생성 시작...");
|
log.info("📊 TimelineData 생성 시작...");
|
||||||
|
|
||||||
String[] eventIds = {"evt_2025012301", "evt_2025020101", "evt_2025011501"};
|
String[] eventIds = {"evt_2025012301", "evt_2025012302", "evt_2025012303"};
|
||||||
|
|
||||||
// 각 이벤트별 기준 참여자 수 (이벤트 성과에 따라 다름)
|
// 각 이벤트별 시간당 기준 참여자 수 (이벤트 성과에 따라 다름)
|
||||||
int[] baseParticipants = {20, 12, 5}; // 이벤트1(높음), 이벤트2(중간), 이벤트3(낮음)
|
int[] baseParticipantsPerHour = {4, 2, 1}; // 이벤트1(높음), 이벤트2(중간), 이벤트3(낮음)
|
||||||
|
|
||||||
for (int eventIndex = 0; eventIndex < eventIds.length; eventIndex++) {
|
for (int eventIndex = 0; eventIndex < eventIds.length; eventIndex++) {
|
||||||
String eventId = eventIds[eventIndex];
|
String eventId = eventIds[eventIndex];
|
||||||
int baseParticipant = baseParticipants[eventIndex];
|
int baseParticipant = baseParticipantsPerHour[eventIndex];
|
||||||
int cumulativeParticipants = 0;
|
int cumulativeParticipants = 0;
|
||||||
|
|
||||||
// 30일 치 데이터 생성 (2024-09-24부터)
|
// 이벤트 ID에서 날짜 파싱 (evt_2025012301 → 2025-01-23)
|
||||||
java.time.LocalDateTime startDate = java.time.LocalDateTime.of(2024, 9, 24, 0, 0);
|
String dateStr = eventId.substring(4); // "2025012301"
|
||||||
|
int year = Integer.parseInt(dateStr.substring(0, 4)); // 2025
|
||||||
|
int month = Integer.parseInt(dateStr.substring(4, 6)); // 01
|
||||||
|
int day = Integer.parseInt(dateStr.substring(6, 8)); // 23
|
||||||
|
|
||||||
for (int day = 0; day < 30; day++) {
|
// 이벤트 시작일부터 30일 치 hourly 데이터 생성
|
||||||
java.time.LocalDateTime timestamp = startDate.plusDays(day);
|
java.time.LocalDateTime startDate = java.time.LocalDateTime.of(year, month, day, 0, 0);
|
||||||
|
|
||||||
// 랜덤한 참여자 수 생성 (기준값 ± 50%)
|
for (int dayOffset = 0; dayOffset < 30; dayOffset++) {
|
||||||
int dailyParticipants = baseParticipant + random.nextInt(baseParticipant + 1);
|
for (int hour = 0; hour < 24; hour++) {
|
||||||
cumulativeParticipants += dailyParticipants;
|
java.time.LocalDateTime timestamp = startDate.plusDays(dayOffset).plusHours(hour);
|
||||||
|
|
||||||
// 조회수는 참여자의 3~5배
|
// 시간대별 참여자 수 변화 (낮 시간대 12~20시에 더 많음)
|
||||||
int dailyViews = dailyParticipants * (3 + random.nextInt(3));
|
int hourMultiplier = (hour >= 12 && hour <= 20) ? 2 : 1;
|
||||||
|
int hourlyParticipants = (baseParticipant * hourMultiplier) + random.nextInt(baseParticipant + 1);
|
||||||
|
|
||||||
// 참여행동은 참여자의 1~2배
|
cumulativeParticipants += hourlyParticipants;
|
||||||
int dailyEngagement = dailyParticipants * (1 + random.nextInt(2));
|
|
||||||
|
|
||||||
// 전환수는 참여자의 50~80%
|
// 조회수는 참여자의 3~5배
|
||||||
int dailyConversions = (int) (dailyParticipants * (0.5 + random.nextDouble() * 0.3));
|
int hourlyViews = hourlyParticipants * (3 + random.nextInt(3));
|
||||||
|
|
||||||
// TimelineData 생성
|
// 참여행동은 참여자의 1~2배
|
||||||
com.kt.event.analytics.entity.TimelineData timelineData =
|
int hourlyEngagement = hourlyParticipants * (1 + random.nextInt(2));
|
||||||
com.kt.event.analytics.entity.TimelineData.builder()
|
|
||||||
.eventId(eventId)
|
|
||||||
.timestamp(timestamp)
|
|
||||||
.participants(dailyParticipants)
|
|
||||||
.views(dailyViews)
|
|
||||||
.engagement(dailyEngagement)
|
|
||||||
.conversions(dailyConversions)
|
|
||||||
.cumulativeParticipants(cumulativeParticipants)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
timelineDataRepository.save(timelineData);
|
// 전환수는 참여자의 50~80%
|
||||||
|
int hourlyConversions = (int) (hourlyParticipants * (0.5 + random.nextDouble() * 0.3));
|
||||||
|
|
||||||
|
// TimelineData 생성
|
||||||
|
com.kt.event.analytics.entity.TimelineData timelineData =
|
||||||
|
com.kt.event.analytics.entity.TimelineData.builder()
|
||||||
|
.eventId(eventId)
|
||||||
|
.timestamp(timestamp)
|
||||||
|
.participants(hourlyParticipants)
|
||||||
|
.views(hourlyViews)
|
||||||
|
.engagement(hourlyEngagement)
|
||||||
|
.conversions(hourlyConversions)
|
||||||
|
.cumulativeParticipants(cumulativeParticipants)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
timelineDataRepository.save(timelineData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("✅ TimelineData 생성 완료: eventId={}, 30일 데이터", eventId);
|
log.info("✅ TimelineData 생성 완료: eventId={}, 시작일={}-{:02d}-{:02d}, 30일 × 24시간 = 720건",
|
||||||
|
eventId, year, month, day);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("✅ 전체 TimelineData 생성 완료: 3개 이벤트 × 30일 = 90건");
|
log.info("✅ 전체 TimelineData 생성 완료: 3개 이벤트 × 30일 × 24시간 = 2,160건");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,16 +39,7 @@ public class SecurityConfig {
|
|||||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
// Actuator endpoints
|
.anyRequest().permitAll()
|
||||||
.requestMatchers("/actuator/**").permitAll()
|
|
||||||
// Swagger UI endpoints
|
|
||||||
.requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**").permitAll()
|
|
||||||
// Health check
|
|
||||||
.requestMatchers("/health").permitAll()
|
|
||||||
// Analytics API endpoints (테스트 및 개발 용도로 공개)
|
|
||||||
.requestMatchers("/api/**").permitAll()
|
|
||||||
// All other requests require authentication
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
)
|
)
|
||||||
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
|
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
|
||||||
UsernamePasswordAuthenticationFilter.class)
|
UsernamePasswordAuthenticationFilter.class)
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ public class SwaggerConfig {
|
|||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
.info(apiInfo())
|
.info(apiInfo())
|
||||||
.addServersItem(new Server()
|
.addServersItem(new Server()
|
||||||
.url("http://localhost:8086")
|
.url("http://localhost:8086/api/v1/analytics")
|
||||||
.description("Local Development"))
|
.description("Local Development"))
|
||||||
|
.addServersItem(new Server()
|
||||||
|
.url("http://kt-event-marketing-api.20.214.196.128.nip.io/api/v1/analytics")
|
||||||
|
.description("AKS Development"))
|
||||||
.addServersItem(new Server()
|
.addServersItem(new Server()
|
||||||
.url("{protocol}://{host}:{port}")
|
.url("{protocol}://{host}:{port}")
|
||||||
.description("Custom Server")
|
.description("Custom Server")
|
||||||
|
|||||||
+6
-18
@@ -22,7 +22,7 @@ import java.time.LocalDateTime;
|
|||||||
@Tag(name = "Analytics", description = "이벤트 성과 분석 및 대시보드 API")
|
@Tag(name = "Analytics", description = "이벤트 성과 분석 및 대시보드 API")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/events")
|
@RequestMapping("/events")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AnalyticsDashboardController {
|
public class AnalyticsDashboardController {
|
||||||
|
|
||||||
@@ -31,31 +31,19 @@ public class AnalyticsDashboardController {
|
|||||||
/**
|
/**
|
||||||
* 성과 대시보드 조회
|
* 성과 대시보드 조회
|
||||||
*
|
*
|
||||||
* @param eventId 이벤트 ID
|
* @param eventId 이벤트 ID
|
||||||
* @param startDate 조회 시작 날짜
|
* @param refresh 캐시 갱신 여부
|
||||||
* @param endDate 조회 종료 날짜
|
* @return 성과 대시보드 (이벤트 시작일 ~ 현재까지)
|
||||||
* @param refresh 캐시 갱신 여부
|
|
||||||
* @return 성과 대시보드
|
|
||||||
*/
|
*/
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "성과 대시보드 조회",
|
summary = "성과 대시보드 조회",
|
||||||
description = "이벤트의 전체 성과를 통합하여 조회합니다."
|
description = "이벤트의 전체 성과를 통합하여 조회합니다. (이벤트 시작일 ~ 현재까지)"
|
||||||
)
|
)
|
||||||
@GetMapping("/{eventId}/analytics")
|
@GetMapping("/{eventId}/analytics")
|
||||||
public ResponseEntity<ApiResponse<AnalyticsDashboardResponse>> getEventAnalytics(
|
public ResponseEntity<ApiResponse<AnalyticsDashboardResponse>> getEventAnalytics(
|
||||||
@Parameter(description = "이벤트 ID", required = true)
|
@Parameter(description = "이벤트 ID", required = true)
|
||||||
@PathVariable String eventId,
|
@PathVariable String eventId,
|
||||||
|
|
||||||
@Parameter(description = "조회 시작 날짜 (ISO 8601 format)")
|
|
||||||
@RequestParam(required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
|
||||||
LocalDateTime startDate,
|
|
||||||
|
|
||||||
@Parameter(description = "조회 종료 날짜 (ISO 8601 format)")
|
|
||||||
@RequestParam(required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
|
||||||
LocalDateTime endDate,
|
|
||||||
|
|
||||||
@Parameter(description = "캐시 갱신 여부 (true인 경우 외부 API 호출)")
|
@Parameter(description = "캐시 갱신 여부 (true인 경우 외부 API 호출)")
|
||||||
@RequestParam(required = false, defaultValue = "false")
|
@RequestParam(required = false, defaultValue = "false")
|
||||||
Boolean refresh
|
Boolean refresh
|
||||||
@@ -63,7 +51,7 @@ public class AnalyticsDashboardController {
|
|||||||
log.info("성과 대시보드 조회 API 호출: eventId={}, refresh={}", eventId, refresh);
|
log.info("성과 대시보드 조회 API 호출: eventId={}, refresh={}", eventId, refresh);
|
||||||
|
|
||||||
AnalyticsDashboardResponse response = analyticsService.getDashboardData(
|
AnalyticsDashboardResponse response = analyticsService.getDashboardData(
|
||||||
eventId, startDate, endDate, refresh
|
eventId, refresh
|
||||||
);
|
);
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ import java.util.List;
|
|||||||
@Tag(name = "Channels", description = "채널별 성과 분석 API")
|
@Tag(name = "Channels", description = "채널별 성과 분석 API")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/events")
|
@RequestMapping("/events")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ChannelAnalyticsController {
|
public class ChannelAnalyticsController {
|
||||||
|
|
||||||
|
|||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
package com.kt.event.analytics.controller;
|
||||||
|
|
||||||
|
import com.kt.event.analytics.config.SampleDataLoader;
|
||||||
|
import com.kt.event.common.dto.ApiResponse;
|
||||||
|
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.boot.ApplicationArguments;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 디버그 컨트롤러
|
||||||
|
*
|
||||||
|
* ⚠️ 개발/테스트 전용
|
||||||
|
*/
|
||||||
|
@Tag(name = "Debug", description = "디버그 API (개발/테스트 전용)")
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/debug")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DebugController {
|
||||||
|
|
||||||
|
private final SampleDataLoader sampleDataLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 샘플 데이터 수동 생성
|
||||||
|
*/
|
||||||
|
@Operation(
|
||||||
|
summary = "샘플 데이터 수동 생성",
|
||||||
|
description = "SampleDataLoader를 수동으로 실행하여 샘플 데이터를 생성합니다."
|
||||||
|
)
|
||||||
|
@PostMapping("/reload-sample-data")
|
||||||
|
public ResponseEntity<ApiResponse<String>> reloadSampleData() {
|
||||||
|
try {
|
||||||
|
log.info("🔧 수동으로 샘플 데이터 생성 요청");
|
||||||
|
|
||||||
|
// SampleDataLoader 실행
|
||||||
|
sampleDataLoader.run(new ApplicationArguments() {
|
||||||
|
@Override
|
||||||
|
public String[] getSourceArgs() {
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.Set<String> getOptionNames() {
|
||||||
|
return java.util.Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsOption(String name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.List<String> getOptionValues(String name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.List<String> getNonOptionArgs() {
|
||||||
|
return java.util.Collections.emptyList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success("샘플 데이터 생성 완료"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ 샘플 데이터 생성 실패", e);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success("샘플 데이터 생성 실패: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
@Tag(name = "ROI", description = "투자 대비 수익률 분석 API")
|
@Tag(name = "ROI", description = "투자 대비 수익률 분석 API")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/events")
|
@RequestMapping("/events")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class RoiAnalyticsController {
|
public class RoiAnalyticsController {
|
||||||
|
|
||||||
|
|||||||
+7
-19
@@ -24,7 +24,7 @@ import java.util.List;
|
|||||||
@Tag(name = "Timeline", description = "시간대별 분석 API")
|
@Tag(name = "Timeline", description = "시간대별 분석 API")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/events")
|
@RequestMapping("/events")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class TimelineAnalyticsController {
|
public class TimelineAnalyticsController {
|
||||||
|
|
||||||
@@ -33,16 +33,14 @@ public class TimelineAnalyticsController {
|
|||||||
/**
|
/**
|
||||||
* 시간대별 참여 추이
|
* 시간대별 참여 추이
|
||||||
*
|
*
|
||||||
* @param eventId 이벤트 ID
|
* @param eventId 이벤트 ID
|
||||||
* @param interval 시간 간격 단위
|
* @param interval 시간 간격 단위
|
||||||
* @param startDate 조회 시작 날짜
|
* @param metrics 조회할 지표 목록
|
||||||
* @param endDate 조회 종료 날짜
|
* @return 시간대별 참여 추이 (이벤트 시작일 ~ 현재까지)
|
||||||
* @param metrics 조회할 지표 목록
|
|
||||||
* @return 시간대별 참여 추이
|
|
||||||
*/
|
*/
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "시간대별 참여 추이",
|
summary = "시간대별 참여 추이",
|
||||||
description = "이벤트 기간 동안의 시간대별 참여 추이를 분석합니다."
|
description = "이벤트 기간 동안의 시간대별 참여 추이를 분석합니다. (이벤트 시작일 ~ 현재까지)"
|
||||||
)
|
)
|
||||||
@GetMapping("/{eventId}/analytics/timeline")
|
@GetMapping("/{eventId}/analytics/timeline")
|
||||||
public ResponseEntity<ApiResponse<TimelineAnalyticsResponse>> getTimelineAnalytics(
|
public ResponseEntity<ApiResponse<TimelineAnalyticsResponse>> getTimelineAnalytics(
|
||||||
@@ -53,16 +51,6 @@ public class TimelineAnalyticsController {
|
|||||||
@RequestParam(required = false, defaultValue = "daily")
|
@RequestParam(required = false, defaultValue = "daily")
|
||||||
String interval,
|
String interval,
|
||||||
|
|
||||||
@Parameter(description = "조회 시작 날짜 (ISO 8601 format)")
|
|
||||||
@RequestParam(required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
|
||||||
LocalDateTime startDate,
|
|
||||||
|
|
||||||
@Parameter(description = "조회 종료 날짜 (ISO 8601 format)")
|
|
||||||
@RequestParam(required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
|
||||||
LocalDateTime endDate,
|
|
||||||
|
|
||||||
@Parameter(description = "조회할 지표 목록 (쉼표로 구분)")
|
@Parameter(description = "조회할 지표 목록 (쉼표로 구분)")
|
||||||
@RequestParam(required = false)
|
@RequestParam(required = false)
|
||||||
String metrics
|
String metrics
|
||||||
@@ -74,7 +62,7 @@ public class TimelineAnalyticsController {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
TimelineAnalyticsResponse response = timelineAnalyticsService.getTimelineAnalytics(
|
TimelineAnalyticsResponse response = timelineAnalyticsService.getTimelineAnalytics(
|
||||||
eventId, interval, startDate, endDate, metricList
|
eventId, interval, metricList
|
||||||
);
|
);
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
|||||||
+6
-18
@@ -22,7 +22,7 @@ import java.time.LocalDateTime;
|
|||||||
@Tag(name = "User Analytics", description = "사용자 전체 이벤트 통합 성과 분석 API")
|
@Tag(name = "User Analytics", description = "사용자 전체 이벤트 통합 성과 분석 API")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/users")
|
@RequestMapping("/users")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserAnalyticsDashboardController {
|
public class UserAnalyticsDashboardController {
|
||||||
|
|
||||||
@@ -31,31 +31,19 @@ public class UserAnalyticsDashboardController {
|
|||||||
/**
|
/**
|
||||||
* 사용자 전체 성과 대시보드 조회
|
* 사용자 전체 성과 대시보드 조회
|
||||||
*
|
*
|
||||||
* @param userId 사용자 ID
|
* @param userId 사용자 ID
|
||||||
* @param startDate 조회 시작 날짜
|
* @param refresh 캐시 갱신 여부
|
||||||
* @param endDate 조회 종료 날짜
|
* @return 전체 통합 성과 대시보드 (userId 기반 전체 이벤트 조회)
|
||||||
* @param refresh 캐시 갱신 여부
|
|
||||||
* @return 전체 통합 성과 대시보드
|
|
||||||
*/
|
*/
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "사용자 전체 성과 대시보드 조회",
|
summary = "사용자 전체 성과 대시보드 조회",
|
||||||
description = "사용자의 모든 이벤트 성과를 통합하여 조회합니다."
|
description = "사용자의 모든 이벤트 성과를 통합하여 조회합니다. (userId 기반 전체 이벤트 조회)"
|
||||||
)
|
)
|
||||||
@GetMapping("/{userId}/analytics")
|
@GetMapping("/{userId}/analytics")
|
||||||
public ResponseEntity<ApiResponse<UserAnalyticsDashboardResponse>> getUserAnalytics(
|
public ResponseEntity<ApiResponse<UserAnalyticsDashboardResponse>> getUserAnalytics(
|
||||||
@Parameter(description = "사용자 ID", required = true)
|
@Parameter(description = "사용자 ID", required = true)
|
||||||
@PathVariable String userId,
|
@PathVariable String userId,
|
||||||
|
|
||||||
@Parameter(description = "조회 시작 날짜 (ISO 8601 format)")
|
|
||||||
@RequestParam(required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
|
||||||
LocalDateTime startDate,
|
|
||||||
|
|
||||||
@Parameter(description = "조회 종료 날짜 (ISO 8601 format)")
|
|
||||||
@RequestParam(required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
|
||||||
LocalDateTime endDate,
|
|
||||||
|
|
||||||
@Parameter(description = "캐시 갱신 여부")
|
@Parameter(description = "캐시 갱신 여부")
|
||||||
@RequestParam(required = false, defaultValue = "false")
|
@RequestParam(required = false, defaultValue = "false")
|
||||||
Boolean refresh
|
Boolean refresh
|
||||||
@@ -63,7 +51,7 @@ public class UserAnalyticsDashboardController {
|
|||||||
log.info("사용자 전체 성과 대시보드 조회 API 호출: userId={}, refresh={}", userId, refresh);
|
log.info("사용자 전체 성과 대시보드 조회 API 호출: userId={}, refresh={}", userId, refresh);
|
||||||
|
|
||||||
UserAnalyticsDashboardResponse response = userAnalyticsService.getUserDashboardData(
|
UserAnalyticsDashboardResponse response = userAnalyticsService.getUserDashboardData(
|
||||||
userId, startDate, endDate, refresh
|
userId, refresh
|
||||||
);
|
);
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
|||||||
+3
-21
@@ -22,7 +22,7 @@ import java.util.List;
|
|||||||
@Tag(name = "User Channels", description = "사용자 전체 이벤트 채널별 성과 분석 API")
|
@Tag(name = "User Channels", description = "사용자 전체 이벤트 채널별 성과 분석 API")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/users")
|
@RequestMapping("/users")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserChannelAnalyticsController {
|
public class UserChannelAnalyticsController {
|
||||||
|
|
||||||
@@ -30,17 +30,13 @@ public class UserChannelAnalyticsController {
|
|||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "사용자 전체 채널별 성과 분석",
|
summary = "사용자 전체 채널별 성과 분석",
|
||||||
description = "사용자의 모든 이벤트 채널 성과를 통합하여 분석합니다."
|
description = "사용자의 모든 이벤트 채널 성과를 통합하여 분석합니다. (전체 채널 무조건 표시)"
|
||||||
)
|
)
|
||||||
@GetMapping("/{userId}/analytics/channels")
|
@GetMapping("/{userId}/analytics/channels")
|
||||||
public ResponseEntity<ApiResponse<UserChannelAnalyticsResponse>> getUserChannelAnalytics(
|
public ResponseEntity<ApiResponse<UserChannelAnalyticsResponse>> getUserChannelAnalytics(
|
||||||
@Parameter(description = "사용자 ID", required = true)
|
@Parameter(description = "사용자 ID", required = true)
|
||||||
@PathVariable String userId,
|
@PathVariable String userId,
|
||||||
|
|
||||||
@Parameter(description = "조회할 채널 목록 (쉼표로 구분)")
|
|
||||||
@RequestParam(required = false)
|
|
||||||
String channels,
|
|
||||||
|
|
||||||
@Parameter(description = "정렬 기준")
|
@Parameter(description = "정렬 기준")
|
||||||
@RequestParam(required = false, defaultValue = "participants")
|
@RequestParam(required = false, defaultValue = "participants")
|
||||||
String sortBy,
|
String sortBy,
|
||||||
@@ -49,28 +45,14 @@ public class UserChannelAnalyticsController {
|
|||||||
@RequestParam(required = false, defaultValue = "desc")
|
@RequestParam(required = false, defaultValue = "desc")
|
||||||
String order,
|
String order,
|
||||||
|
|
||||||
@Parameter(description = "조회 시작 날짜")
|
|
||||||
@RequestParam(required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
|
||||||
LocalDateTime startDate,
|
|
||||||
|
|
||||||
@Parameter(description = "조회 종료 날짜")
|
|
||||||
@RequestParam(required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
|
||||||
LocalDateTime endDate,
|
|
||||||
|
|
||||||
@Parameter(description = "캐시 갱신 여부")
|
@Parameter(description = "캐시 갱신 여부")
|
||||||
@RequestParam(required = false, defaultValue = "false")
|
@RequestParam(required = false, defaultValue = "false")
|
||||||
Boolean refresh
|
Boolean refresh
|
||||||
) {
|
) {
|
||||||
log.info("사용자 채널 분석 API 호출: userId={}, sortBy={}", userId, sortBy);
|
log.info("사용자 채널 분석 API 호출: userId={}, sortBy={}", userId, sortBy);
|
||||||
|
|
||||||
List<String> channelList = channels != null && !channels.isBlank()
|
|
||||||
? Arrays.asList(channels.split(","))
|
|
||||||
: null;
|
|
||||||
|
|
||||||
UserChannelAnalyticsResponse response = userChannelAnalyticsService.getUserChannelAnalytics(
|
UserChannelAnalyticsResponse response = userChannelAnalyticsService.getUserChannelAnalytics(
|
||||||
userId, channelList, sortBy, order, startDate, endDate, refresh
|
userId, sortBy, order, refresh
|
||||||
);
|
);
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
|||||||
+3
-13
@@ -20,7 +20,7 @@ import java.time.LocalDateTime;
|
|||||||
@Tag(name = "User ROI", description = "사용자 전체 이벤트 ROI 분석 API")
|
@Tag(name = "User ROI", description = "사용자 전체 이벤트 ROI 분석 API")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/users")
|
@RequestMapping("/users")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserRoiAnalyticsController {
|
public class UserRoiAnalyticsController {
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ public class UserRoiAnalyticsController {
|
|||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "사용자 전체 ROI 상세 분석",
|
summary = "사용자 전체 ROI 상세 분석",
|
||||||
description = "사용자의 모든 이벤트 ROI를 통합하여 분석합니다."
|
description = "사용자의 모든 이벤트 ROI를 통합하여 분석합니다. (userId 기반 전체 이벤트 조회)"
|
||||||
)
|
)
|
||||||
@GetMapping("/{userId}/analytics/roi")
|
@GetMapping("/{userId}/analytics/roi")
|
||||||
public ResponseEntity<ApiResponse<UserRoiAnalyticsResponse>> getUserRoiAnalytics(
|
public ResponseEntity<ApiResponse<UserRoiAnalyticsResponse>> getUserRoiAnalytics(
|
||||||
@@ -39,16 +39,6 @@ public class UserRoiAnalyticsController {
|
|||||||
@RequestParam(required = false, defaultValue = "true")
|
@RequestParam(required = false, defaultValue = "true")
|
||||||
Boolean includeProjection,
|
Boolean includeProjection,
|
||||||
|
|
||||||
@Parameter(description = "조회 시작 날짜")
|
|
||||||
@RequestParam(required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
|
||||||
LocalDateTime startDate,
|
|
||||||
|
|
||||||
@Parameter(description = "조회 종료 날짜")
|
|
||||||
@RequestParam(required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
|
||||||
LocalDateTime endDate,
|
|
||||||
|
|
||||||
@Parameter(description = "캐시 갱신 여부")
|
@Parameter(description = "캐시 갱신 여부")
|
||||||
@RequestParam(required = false, defaultValue = "false")
|
@RequestParam(required = false, defaultValue = "false")
|
||||||
Boolean refresh
|
Boolean refresh
|
||||||
@@ -56,7 +46,7 @@ public class UserRoiAnalyticsController {
|
|||||||
log.info("사용자 ROI 분석 API 호출: userId={}, includeProjection={}", userId, includeProjection);
|
log.info("사용자 ROI 분석 API 호출: userId={}, includeProjection={}", userId, includeProjection);
|
||||||
|
|
||||||
UserRoiAnalyticsResponse response = userRoiAnalyticsService.getUserRoiAnalytics(
|
UserRoiAnalyticsResponse response = userRoiAnalyticsService.getUserRoiAnalytics(
|
||||||
userId, includeProjection, startDate, endDate, refresh
|
userId, includeProjection, refresh
|
||||||
);
|
);
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
|||||||
+3
-13
@@ -22,7 +22,7 @@ import java.util.List;
|
|||||||
@Tag(name = "User Timeline", description = "사용자 전체 이벤트 시간대별 분석 API")
|
@Tag(name = "User Timeline", description = "사용자 전체 이벤트 시간대별 분석 API")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/users")
|
@RequestMapping("/users")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserTimelineAnalyticsController {
|
public class UserTimelineAnalyticsController {
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ public class UserTimelineAnalyticsController {
|
|||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "사용자 전체 시간대별 참여 추이",
|
summary = "사용자 전체 시간대별 참여 추이",
|
||||||
description = "사용자의 모든 이벤트 시간대별 데이터를 통합하여 분석합니다."
|
description = "사용자의 모든 이벤트 시간대별 데이터를 통합하여 분석합니다. (userId 기반 전체 이벤트 조회)"
|
||||||
)
|
)
|
||||||
@GetMapping("/{userId}/analytics/timeline")
|
@GetMapping("/{userId}/analytics/timeline")
|
||||||
public ResponseEntity<ApiResponse<UserTimelineAnalyticsResponse>> getUserTimelineAnalytics(
|
public ResponseEntity<ApiResponse<UserTimelineAnalyticsResponse>> getUserTimelineAnalytics(
|
||||||
@@ -41,16 +41,6 @@ public class UserTimelineAnalyticsController {
|
|||||||
@RequestParam(required = false, defaultValue = "daily")
|
@RequestParam(required = false, defaultValue = "daily")
|
||||||
String interval,
|
String interval,
|
||||||
|
|
||||||
@Parameter(description = "조회 시작 날짜")
|
|
||||||
@RequestParam(required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
|
||||||
LocalDateTime startDate,
|
|
||||||
|
|
||||||
@Parameter(description = "조회 종료 날짜")
|
|
||||||
@RequestParam(required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
|
||||||
LocalDateTime endDate,
|
|
||||||
|
|
||||||
@Parameter(description = "조회할 지표 목록 (쉼표로 구분)")
|
@Parameter(description = "조회할 지표 목록 (쉼표로 구분)")
|
||||||
@RequestParam(required = false)
|
@RequestParam(required = false)
|
||||||
String metrics,
|
String metrics,
|
||||||
@@ -66,7 +56,7 @@ public class UserTimelineAnalyticsController {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
UserTimelineAnalyticsResponse response = userTimelineAnalyticsService.getUserTimelineAnalytics(
|
UserTimelineAnalyticsResponse response = userTimelineAnalyticsService.getUserTimelineAnalytics(
|
||||||
userId, interval, startDate, endDate, metricList, refresh
|
userId, interval, metricList, refresh
|
||||||
);
|
);
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user