diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index 5d56e9d..0000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/.claude/commands/deploy-actions-cicd-guide-back.md b/.claude/commands/deploy-actions-cicd-guide-back.md
new file mode 100644
index 0000000..0ec39e4
--- /dev/null
+++ b/.claude/commands/deploy-actions-cicd-guide-back.md
@@ -0,0 +1,14 @@
+---
+command: "/deploy-actions-cicd-guide-back"
+---
+
+@cicd
+'백엔드GitHubActions파이프라인작성가이드'에 따라 GitHub Actions를 이용한 CI/CD 가이드를 작성해 주세요.
+프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
+{안내메시지}
+'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
+[실행정보]
+- ACR_NAME: acrdigitalgarage01
+- RESOURCE_GROUP: rg-digitalgarage-01
+- AKS_CLUSTER: aks-digitalgarage-01
+- NAMESPACE: phonebill-dg0500
diff --git a/.claude/commands/deploy-actions-cicd-guide-front.md b/.claude/commands/deploy-actions-cicd-guide-front.md
new file mode 100644
index 0000000..0975422
--- /dev/null
+++ b/.claude/commands/deploy-actions-cicd-guide-front.md
@@ -0,0 +1,15 @@
+---
+command: "/deploy-actions-cicd-guide-front"
+---
+
+@cicd
+'프론트엔드GitHubActions파이프라인작성가이드'에 따라 GitHub Actions를 이용한 CI/CD 가이드를 작성해 주세요.
+프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
+{안내메시지}
+'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
+[실행정보]
+- SYSTEM_NAME: phonebill
+- ACR_NAME: acrdigitalgarage01
+- RESOURCE_GROUP: rg-digitalgarage-01
+- AKS_CLUSTER: aks-digitalgarage-01
+- NAMESPACE: phonebill-dg0500
diff --git a/.claude/commands/deploy-build-image-back.md b/.claude/commands/deploy-build-image-back.md
new file mode 100644
index 0000000..5305a1b
--- /dev/null
+++ b/.claude/commands/deploy-build-image-back.md
@@ -0,0 +1,6 @@
+---
+command: "/deploy-build-image-back"
+---
+
+@cicd
+'백엔드컨테이너이미지작성가이드'에 따라 컨테이너 이미지를 작성해 주세요.
diff --git a/.claude/commands/deploy-build-image-front.md b/.claude/commands/deploy-build-image-front.md
new file mode 100644
index 0000000..1cfe9d1
--- /dev/null
+++ b/.claude/commands/deploy-build-image-front.md
@@ -0,0 +1,6 @@
+---
+command: "/deploy-build-image-front"
+---
+
+@cicd
+'프론트엔드컨테이너이미지작성가이드'에 따라 컨테이너 이미지를 작성해 주세요.
diff --git a/.claude/commands/deploy-help.md b/.claude/commands/deploy-help.md
new file mode 100644
index 0000000..d6ec88f
--- /dev/null
+++ b/.claude/commands/deploy-help.md
@@ -0,0 +1,81 @@
+---
+command: "/deploy-help"
+---
+
+# 배포 작업 순서
+
+## 1단계: 컨테이너 이미지 작성
+### 백엔드
+```
+/deploy-build-image-back
+```
+- 백엔드컨테이너이미지작성가이드를 참고하여 컨테이너 이미지를 빌드합니다
+
+### 프론트엔드
+```
+/deploy-build-image-front
+```
+- 프론트엔드컨테이너이미지작성가이드를 참고하여 컨테이너 이미지를 빌드합니다
+
+## 2단계: 컨테이너 실행 가이드 작성
+### 백엔드
+```
+/deploy-run-container-guide-back
+```
+- 백엔드컨테이너실행방법가이드를 참고하여 컨테이너 실행 방법을 작성합니다
+- 실행정보(ACR명, VM정보)가 필요합니다
+
+### 프론트엔드
+```
+/deploy-run-container-guide-front
+```
+- 프론트엔드컨테이너실행방법가이드를 참고하여 컨테이너 실행 방법을 작성합니다
+- 실행정보(시스템명, ACR명, VM정보)가 필요합니다
+
+## 3단계: Kubernetes 배포 가이드 작성
+### 백엔드
+```
+/deploy-k8s-guide-back
+```
+- 백엔드배포가이드를 참고하여 쿠버네티스 배포 방법을 작성합니다
+- 실행정보(ACR명, k8s명, 네임스페이스, 리소스 설정)가 필요합니다
+
+### 프론트엔드
+```
+/deploy-k8s-guide-front
+```
+- 프론트엔드배포가이드를 참고하여 쿠버네티스 배포 방법을 작성합니다
+- 실행정보(시스템명, ACR명, k8s명, 네임스페이스, Gateway Host, 리소스 설정)가 필요합니다
+
+## 4단계: CI/CD 파이프라인 구성
+
+### Jenkins 사용 시
+#### 백엔드
+```
+/deploy-jenkins-cicd-guide-back
+```
+- 백엔드Jenkins파이프라인작성가이드를 참고하여 Jenkins CI/CD 파이프라인을 구성합니다
+
+#### 프론트엔드
+```
+/deploy-jenkins-cicd-guide-front
+```
+- 프론트엔드Jenkins파이프라인작성가이드를 참고하여 Jenkins CI/CD 파이프라인을 구성합니다
+
+### GitHub Actions 사용 시
+#### 백엔드
+```
+/deploy-actions-cicd-guide-back
+```
+- 백엔드GitHubActions파이프라인작성가이드를 참고하여 GitHub Actions CI/CD 파이프라인을 구성합니다
+
+#### 프론트엔드
+```
+/deploy-actions-cicd-guide-front
+```
+- 프론트엔드GitHubActions파이프라인작성가이드를 참고하여 GitHub Actions CI/CD 파이프라인을 구성합니다
+
+## 참고사항
+- 각 명령 실행 전 필요한 실행정보를 프롬프트에 포함해야 합니다
+- 실행정보가 없으면 안내 메시지가 표시되며 작업이 중단됩니다
+- CI/CD 도구는 Jenkins 또는 GitHub Actions 중 선택하여 사용합니다
diff --git a/.claude/commands/deploy-jenkins-cicd-guide-back.md b/.claude/commands/deploy-jenkins-cicd-guide-back.md
new file mode 100644
index 0000000..dbd3e8b
--- /dev/null
+++ b/.claude/commands/deploy-jenkins-cicd-guide-back.md
@@ -0,0 +1,14 @@
+---
+command: "/deploy-jenkins-cicd-guide-back"
+---
+
+@cicd
+'백엔드Jenkins파이프라인작성가이드'에 따라 Jenkins를 이용한 CI/CD 가이드를 작성해 주세요.
+프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
+{안내메시지}
+'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
+[실행정보]
+- ACR_NAME: acrdigitalgarage01
+- RESOURCE_GROUP: rg-digitalgarage-01
+- AKS_CLUSTER: aks-digitalgarage-01
+- NAMESPACE: phonebill-dg0500
diff --git a/.claude/commands/deploy-jenkins-cicd-guide-front.md b/.claude/commands/deploy-jenkins-cicd-guide-front.md
new file mode 100644
index 0000000..5df6fad
--- /dev/null
+++ b/.claude/commands/deploy-jenkins-cicd-guide-front.md
@@ -0,0 +1,15 @@
+---
+command: "/deploy-jenkins-cicd-guide-front"
+---
+
+@cicd
+'프론트엔드Jenkins파이프라인작성가이드'에 따라 Jenkins를 이용한 CI/CD 가이드를 작성해 주세요.
+프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
+{안내메시지}
+'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
+[실행정보]
+- SYSTEM_NAME: phonebill
+- ACR_NAME: acrdigitalgarage01
+- RESOURCE_GROUP: rg-digitalgarage-01
+- AKS_CLUSTER: aks-digitalgarage-01
+- NAMESPACE: phonebill-dg0500
diff --git a/.claude/commands/deploy-k8s-guide-back.md b/.claude/commands/deploy-k8s-guide-back.md
new file mode 100644
index 0000000..8fccb04
--- /dev/null
+++ b/.claude/commands/deploy-k8s-guide-back.md
@@ -0,0 +1,16 @@
+---
+command: "/deploy-k8s-guide-back"
+---
+
+@cicd
+'백엔드배포가이드'에 따라 백엔드 서비스 배포 방법을 작성해 주세요.
+프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
+{안내메시지}
+'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
+[실행정보]
+- ACR명: acrdigitalgarage01
+- k8s명: aks-digitalgarage-01
+- 네임스페이스: tripgen
+- 파드수: 2
+- 리소스(CPU): 256m/1024m
+- 리소스(메모리): 256Mi/1024Mi
diff --git a/.claude/commands/deploy-k8s-guide-front.md b/.claude/commands/deploy-k8s-guide-front.md
new file mode 100644
index 0000000..54a069d
--- /dev/null
+++ b/.claude/commands/deploy-k8s-guide-front.md
@@ -0,0 +1,18 @@
+---
+command: "/deploy-k8s-guide-front"
+---
+
+@cicd
+'프론트엔드배포가이드'에 따라 프론트엔드 서비스 배포 방법을 작성해 주세요.
+프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
+{안내메시지}
+'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
+[실행정보]
+- 시스템명: tripgen
+- ACR명: acrdigitalgarage01
+- k8s명: aks-digitalgarage-01
+- 네임스페이스: tripgen
+- 파드수: 2
+- 리소스(CPU): 256m/1024m
+- 리소스(메모리): 256Mi/1024Mi
+- Gateway Host: http://tripgen-api.20.214.196.128.nip.io
diff --git a/.claude/commands/deploy-run-container-guide-back.md b/.claude/commands/deploy-run-container-guide-back.md
new file mode 100644
index 0000000..c93388f
--- /dev/null
+++ b/.claude/commands/deploy-run-container-guide-back.md
@@ -0,0 +1,15 @@
+---
+command: "/deploy-run-container-guide-back"
+---
+
+@cicd
+'백엔드컨테이너실행방법가이드'에 따라 컨테이너 실행 가이드를 작성해 주세요.
+프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
+{안내메시지}
+'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
+[실행정보]
+- ACR명: acrdigitalgarage01
+- VM
+ - KEY파일: ~/home/bastion-dg0500
+ - USERID: azureuser
+ - IP: 4.230.5.6
diff --git a/.claude/commands/deploy-run-container-guide-front.md b/.claude/commands/deploy-run-container-guide-front.md
new file mode 100644
index 0000000..eb68f9a
--- /dev/null
+++ b/.claude/commands/deploy-run-container-guide-front.md
@@ -0,0 +1,16 @@
+---
+command: "/deploy-run-container-guide-front"
+---
+
+@cicd
+'프론트엔드컨테이너실행방법가이드'에 따라 컨테이너 실행 가이드를 작성해 주세요.
+프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
+{안내메시지}
+'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
+[실행정보]
+- 시스템명: tripgen
+- ACR명: acrdigitalgarage01
+- VM
+ - KEY파일: ~/home/bastion-dg0500
+ - USERID: azureuser
+ - IP: 4.230.5.6
diff --git a/.claude/commands/design-api.md b/.claude/commands/design-api.md
index 5375bf7..750eae3 100644
--- a/.claude/commands/design-api.md
+++ b/.claude/commands/design-api.md
@@ -1,3 +1,6 @@
+---
+command: "/design-api"
+---
@architecture
API를 설계해 주세요:
-- '공통설계원칙'과 'API설계가이드'를 준용하여 설계
+- '공통설계원칙'과 'API설계가이드'를 준용하여 설계
\ No newline at end of file
diff --git a/.claude/commands/design-class.md b/.claude/commands/design-class.md
index dc76da9..178bdb1 100644
--- a/.claude/commands/design-class.md
+++ b/.claude/commands/design-class.md
@@ -1,3 +1,6 @@
+---
+command: "/design-class"
+---
@architecture
'공통설계원칙'과 '클래스설계가이드'를 준용하여 클래스를 설계해 주세요.
프롬프트에 '[클래스설계 정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시합니다.
@@ -9,4 +12,4 @@
- User: Layered
- Trip: Clean
- Location: Layered
- - AI: Layered
+ - AI: Layered
\ No newline at end of file
diff --git a/.claude/commands/design-data.md b/.claude/commands/design-data.md
index 8d9fd77..b5ff1dd 100644
--- a/.claude/commands/design-data.md
+++ b/.claude/commands/design-data.md
@@ -1,3 +1,6 @@
+---
+command: "/design-data"
+---
@architecture
데이터 설계를 해주세요:
-- '공통설계원칙'과 '데이터설계가이드'를 준용하여 설계
+- '공통설계원칙'과 '데이터설계가이드'를 준용하여 설계
\ No newline at end of file
diff --git a/.claude/commands/design-fix-prototype.md b/.claude/commands/design-fix-prototype.md
index d1ddb8a..5cc1890 100644
--- a/.claude/commands/design-fix-prototype.md
+++ b/.claude/commands/design-fix-prototype.md
@@ -1,5 +1,8 @@
+---
+command: "/design-fix-prototype"
+---
@fix as @front
'[오류내용]'섹션에 제공된 오류를 해결해 주세요.
프롬프트에 '[오류내용]'섹션이 없으면 수행 중단하고 안내 메시지 표시
{안내메시지}
-'[오류내용]'섹션 하위에 오류 내용을 제공
+'[오류내용]'섹션 하위에 오류 내용을 제공
\ No newline at end of file
diff --git a/.claude/commands/design-front.md b/.claude/commands/design-front.md
index 67bc0a5..8dd99c9 100644
--- a/.claude/commands/design-front.md
+++ b/.claude/commands/design-front.md
@@ -1,3 +1,6 @@
+---
+command: "/design-front"
+---
@plan as @front
'프론트엔드설계가이드'를 준용하여 **프론트엔드설계서**를 작성해 주세요.
프롬프트에 '[백엔드시스템]'항목이 없으면 수행을 중단하고 안내 메시지를 표시합니다.
@@ -13,4 +16,4 @@
- ai service: http://localhost:8084/v3/api-docs
[요구사항]
- 각 화면에 Back 아이콘 버튼과 화면 타이틀 표시
-- 하단 네비게이션 바 아이콘화: 홈, 새여행, 주변장소검색, 여행보기
+- 하단 네비게이션 바 아이콘화: 홈, 새여행, 주변장소검색, 여행보기
\ No newline at end of file
diff --git a/.claude/commands/design-high-level.md b/.claude/commands/design-high-level.md
index d7028b1..0debc5e 100644
--- a/.claude/commands/design-high-level.md
+++ b/.claude/commands/design-high-level.md
@@ -1,6 +1,9 @@
+---
+command: "/design-high-level"
+---
@architecture
'HighLevel아키텍처정의가이드'를 준용하여 High Level 아키텍처 정의서를 작성해 주세요.
'CLOUD' 정보가 없으면 수행을 중단하고 안내메시지를 표시하세요.
{안내메시지}
아래 예와 같이 CLOUD 제공자를 Azure, AWS, Google과 같이 제공하세요.
-- CLOUD: Azure
+- CLOUD: Azure
\ No newline at end of file
diff --git a/.claude/commands/design-improve-prototype.md b/.claude/commands/design-improve-prototype.md
index 0d1b31b..22bc079 100644
--- a/.claude/commands/design-improve-prototype.md
+++ b/.claude/commands/design-improve-prototype.md
@@ -1,5 +1,8 @@
+---
+command: "/design-improve-prototype"
+---
@improve as @front
'[개선내용]'섹션에 있는 내용을 개선해 주세요.
프롬프트에 '[개선내용]'항목이 없으면 수행을 중단하고 안내 메시지 표시
{안내메시지}
-'[개선내용]'섹션 하위에 개선할 내용을 제공
+'[개선내용]'섹션 하위에 개선할 내용을 제공
\ No newline at end of file
diff --git a/.claude/commands/design-improve-userstory.md b/.claude/commands/design-improve-userstory.md
index a1055f2..73fd453 100644
--- a/.claude/commands/design-improve-userstory.md
+++ b/.claude/commands/design-improve-userstory.md
@@ -1,2 +1,5 @@
+---
+command: "/design-improve-userstory"
+---
@analyze as @front 프로토타입을 웹브라우저에서 분석한 후,
-@document as @scribe 수정된 프로토타입에 따라 유저스토리를 업데이트 해주십시오.
+@document as @scribe 수정된 프로토타입에 따라 유저스토리를 업데이트 해주십시오.
\ No newline at end of file
diff --git a/.claude/commands/design-logical.md b/.claude/commands/design-logical.md
index 28f15e9..3d50c8f 100644
--- a/.claude/commands/design-logical.md
+++ b/.claude/commands/design-logical.md
@@ -1,3 +1,6 @@
+---
+command: "/design-logical"
+---
@architecture
논리 아키텍처를 설계해 주세요:
-- '공통설계원칙'과 '논리아키텍처 설계 가이드'를 준용하여 설계
+- '공통설계원칙'과 '논리아키텍처 설계 가이드'를 준용하여 설계
\ No newline at end of file
diff --git a/.claude/commands/design-pattern.md b/.claude/commands/design-pattern.md
index 06ed88d..decb145 100644
--- a/.claude/commands/design-pattern.md
+++ b/.claude/commands/design-pattern.md
@@ -1,3 +1,6 @@
+---
+command: "/design-pattern"
+---
@design-pattern
클라우드 아키텍처 패턴 적용 방안을 작성해 주세요:
-- '클라우드아키텍처패턴선정가이드'를 준용하여 작성
+- '클라우드아키텍처패턴선정가이드'를 준용하여 작성
\ No newline at end of file
diff --git a/.claude/commands/design-physical.md b/.claude/commands/design-physical.md
index 2dc8a51..7df5bca 100644
--- a/.claude/commands/design-physical.md
+++ b/.claude/commands/design-physical.md
@@ -1,6 +1,9 @@
+---
+command: "/design-physical"
+---
@architecture
'물리아키텍처설계가이드'를 준용하여 물리아키텍처를 설계해 주세요.
'CLOUD' 정보가 없으면 수행을 중단하고 안내메시지를 표시하세요.
{안내메시지}
아래 예와 같이 CLOUD 제공자를 Azure, AWS, Google과 같이 제공하세요.
-- CLOUD: Azure
+- CLOUD: Azure
\ No newline at end of file
diff --git a/.claude/commands/design-prototype.md b/.claude/commands/design-prototype.md
index f43547f..dbd24a0 100644
--- a/.claude/commands/design-prototype.md
+++ b/.claude/commands/design-prototype.md
@@ -1,3 +1,6 @@
+---
+command: "/design-prototype"
+---
@prototype
프로토타입을 작성해 주세요:
-- '프로토타입작성가이드'를 준용하여 작성
+- '프로토타입작성가이드'를 준용하여 작성
\ No newline at end of file
diff --git a/.claude/commands/design-seq-inner.md b/.claude/commands/design-seq-inner.md
index 5583610..d2bc4ac 100644
--- a/.claude/commands/design-seq-inner.md
+++ b/.claude/commands/design-seq-inner.md
@@ -1,3 +1,6 @@
+---
+command: "/design-seq-inner"
+---
@architecture
내부 시퀀스 설계를 해 주세요:
-- '공통설계원칙'과 '내부시퀀스설계 가이드'를 준용하여 설계
+- '공통설계원칙'과 '내부시퀀스설계 가이드'를 준용하여 설계
\ No newline at end of file
diff --git a/.claude/commands/design-seq-outer.md b/.claude/commands/design-seq-outer.md
index 0546370..8e05435 100644
--- a/.claude/commands/design-seq-outer.md
+++ b/.claude/commands/design-seq-outer.md
@@ -1,3 +1,6 @@
+---
+command: "/design-seq-outer"
+---
@architecture
외부 시퀀스 설계를 해 주세요:
-- '공통설계원칙'과 '외부시퀀스설계가이드'를 준용하여 설계
+- '공통설계원칙'과 '외부시퀀스설계가이드'를 준용하여 설계
\ No newline at end of file
diff --git a/.claude/commands/design-test-prototype.md b/.claude/commands/design-test-prototype.md
index bd45346..350788a 100644
--- a/.claude/commands/design-test-prototype.md
+++ b/.claude/commands/design-test-prototype.md
@@ -1,2 +1,5 @@
+---
+command: "/design-test-prototype"
+---
@test-front
-프로토타입을 테스트 해 주세요.
+프로토타입을 테스트 해 주세요.
\ No newline at end of file
diff --git a/.claude/commands/design-uiux.md b/.claude/commands/design-uiux.md
index 2b1c387..d68d857 100644
--- a/.claude/commands/design-uiux.md
+++ b/.claude/commands/design-uiux.md
@@ -1,3 +1,6 @@
+---
+command: "/design-uiux"
+---
@uiux
UI/UX 설계를 해주세요:
-- 'UI/UX설계가이드'를 준용하여 작성
+- 'UI/UX설계가이드'를 준용하여 작성
\ No newline at end of file
diff --git a/.claude/commands/design-update-uiux.md b/.claude/commands/design-update-uiux.md
index 6994cd9..afd7cf9 100644
--- a/.claude/commands/design-update-uiux.md
+++ b/.claude/commands/design-update-uiux.md
@@ -1,2 +1,5 @@
+---
+command: "/design-update-uiux"
+---
@document @front
-현재 프로토타입과 유저스토리를 기준으로 UI/UX설계서와 스타일가이드를 수정해 주세요.
+현재 프로토타입과 유저스토리를 기준으로 UI/UX설계서와 스타일가이드를 수정해 주세요.
\ No newline at end of file
diff --git a/.claude/commands/develop-make-run-profile.md b/.claude/commands/develop-make-run-profile.md
index 65740e5..06b2768 100644
--- a/.claude/commands/develop-make-run-profile.md
+++ b/.claude/commands/develop-make-run-profile.md
@@ -1,5 +1,5 @@
@test-backend
-'서비스실행파일작성가이드'에 따라 테스트를 해 주세요.
+'서비스실행프로파일작성가이드'에 따라 테스트를 해 주세요.
프롬프트에 '[작성정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
DB나 Redis의 접근 정보는 지정할 필요 없습니다. 특별히 없으면 '[작성정보]'섹션에 '없음'이라고 하세요.
{안내메시지}
diff --git a/.claude/commands/think-help.md b/.claude/commands/think-help.md
index 49bc697..17ad05a 100644
--- a/.claude/commands/think-help.md
+++ b/.claude/commands/think-help.md
@@ -1,3 +1,6 @@
+---
+command: "/think-help"
+---
기획 작업 순서
1단계: 서비스 기획
diff --git a/.claude/commands/think-planning.md b/.claude/commands/think-planning.md
index c40eaec..beec938 100644
--- a/.claude/commands/think-planning.md
+++ b/.claude/commands/think-planning.md
@@ -1,3 +1,6 @@
+---
+command: "/think-planning"
+---
아래 내용을 터미널에 표시만 하고 수행을 하지는 않습니다.
```
아래 가이드를 참고하여 서비스 기획을 수행합니다.
diff --git a/.claude/commands/think-userstory.md b/.claude/commands/think-userstory.md
index abdcb97..a002c30 100644
--- a/.claude/commands/think-userstory.md
+++ b/.claude/commands/think-userstory.md
@@ -1,3 +1,7 @@
+---
+command: "/think-userstory"
+---
+```
@document
유저스토리를 작성하세요.
프롬프트에 '[요구사항]'섹션이 없으면 수행을 중단하고 안내 메시지를 표시합니다.
@@ -16,3 +20,5 @@ Case 2) 다른 방법으로 이벤트스토밍을 한 경우는 요구사항을
2. 유저스토리 작성
- '유저스토리작성방법'과 '유저스토리예제'를 참고하여 유저스토리를 작성
- 결과파일은 'design/userstory.md'에 생성
+
+```
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 8d1f14d..f0a5018 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -15,7 +15,40 @@
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git push)",
- "Bash(git pull:*)"
+ "Bash(git pull:*)",
+ "Bash(netstat:*)",
+ "Bash(findstr:*)",
+ "Bash(./gradlew analytics-service:compileJava:*)",
+ "Bash(python -m json.tool:*)",
+ "Bash(powershell:*)"
+ "Bash(./gradlew participation-service:compileJava:*)",
+ "Bash(find:*)",
+ "Bash(netstat:*)",
+ "Bash(findstr:*)",
+ "Bash(docker-compose up:*)",
+ "Bash(docker --version:*)",
+ "Bash(timeout 60 bash:*)",
+ "Bash(docker ps:*)",
+ "Bash(docker exec:*)",
+ "Bash(docker-compose down:*)",
+ "Bash(git rm:*)",
+ "Bash(git restore:*)",
+ "Bash(./gradlew participation-service:test:*)",
+ "Bash(timeout 30 bash:*)",
+ "Bash(helm list:*)",
+ "Bash(helm upgrade:*)",
+ "Bash(helm repo add:*)",
+ "Bash(helm repo update:*)",
+ "Bash(kubectl get:*)",
+ "Bash(python3:*)",
+ "Bash(timeout 120 bash -c 'while true; do sleep 5; kubectl get pods -n kt-event-marketing | grep kafka | grep -v Running && continue; echo \"\"\"\"All Kafka pods are Running!\"\"\"\"; break; done')",
+ "Bash(kubectl delete:*)",
+ "Bash(kubectl logs:*)",
+ "Bash(kubectl describe:*)",
+ "Bash(kubectl exec:*)",
+ "mcp__context7__resolve-library-id",
+ "mcp__context7__get-library-docs",
+ "Bash(python -m json.tool:*)"
],
"deny": [],
"ask": []
diff --git a/.gitignore b/.gitignore
index 2a41541..635b6bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,16 @@ Thumbs.db
dist/
build/
*.log
+.gradle/
+logs/
+
+# Gradle
+.gradle/
+!gradle/wrapper/gradle-wrapper.jar
+
+# Logs
+logs/
+*.log
# Environment
.env
@@ -30,3 +40,16 @@ build/
tmp/
temp/
*.tmp
+
+# Kubernetes Secrets (민감한 정보 포함)
+k8s/**/secret.yaml
+k8s/**/*-secret.yaml
+k8s/**/*-prod.yaml
+k8s/**/*-dev.yaml
+k8s/**/*-local.yaml
+
+# IntelliJ 실행 프로파일 (민감한 환경 변수 포함 가능)
+.run/*.run.xml
+
+# Gradle (로컬 환경 설정)
+gradle.properties
diff --git a/.gradle/8.10/checksums/checksums.lock b/.gradle/8.10/checksums/checksums.lock
deleted file mode 100644
index 837e5b9..0000000
Binary files a/.gradle/8.10/checksums/checksums.lock and /dev/null differ
diff --git a/.gradle/8.10/checksums/md5-checksums.bin b/.gradle/8.10/checksums/md5-checksums.bin
deleted file mode 100644
index 04c6d00..0000000
Binary files a/.gradle/8.10/checksums/md5-checksums.bin and /dev/null differ
diff --git a/.gradle/8.10/checksums/sha1-checksums.bin b/.gradle/8.10/checksums/sha1-checksums.bin
deleted file mode 100644
index 19a5410..0000000
Binary files a/.gradle/8.10/checksums/sha1-checksums.bin and /dev/null differ
diff --git a/.gradle/8.10/dependencies-accessors/gc.properties b/.gradle/8.10/dependencies-accessors/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/.gradle/8.10/executionHistory/executionHistory.bin b/.gradle/8.10/executionHistory/executionHistory.bin
deleted file mode 100644
index 2177cdd..0000000
Binary files a/.gradle/8.10/executionHistory/executionHistory.bin and /dev/null differ
diff --git a/.gradle/8.10/executionHistory/executionHistory.lock b/.gradle/8.10/executionHistory/executionHistory.lock
deleted file mode 100644
index 0ce4c96..0000000
Binary files a/.gradle/8.10/executionHistory/executionHistory.lock and /dev/null differ
diff --git a/.gradle/8.10/fileChanges/last-build.bin b/.gradle/8.10/fileChanges/last-build.bin
deleted file mode 100644
index f76dd23..0000000
Binary files a/.gradle/8.10/fileChanges/last-build.bin and /dev/null differ
diff --git a/.gradle/8.10/fileHashes/fileHashes.bin b/.gradle/8.10/fileHashes/fileHashes.bin
deleted file mode 100644
index 8088fbb..0000000
Binary files a/.gradle/8.10/fileHashes/fileHashes.bin and /dev/null differ
diff --git a/.gradle/8.10/fileHashes/fileHashes.lock b/.gradle/8.10/fileHashes/fileHashes.lock
deleted file mode 100644
index 340e0dd..0000000
Binary files a/.gradle/8.10/fileHashes/fileHashes.lock and /dev/null differ
diff --git a/.gradle/8.10/fileHashes/resourceHashesCache.bin b/.gradle/8.10/fileHashes/resourceHashesCache.bin
deleted file mode 100644
index 3d21896..0000000
Binary files a/.gradle/8.10/fileHashes/resourceHashesCache.bin and /dev/null differ
diff --git a/.gradle/8.10/gc.properties b/.gradle/8.10/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/.gradle/9.1.0/checksums/checksums.lock b/.gradle/9.1.0/checksums/checksums.lock
deleted file mode 100644
index 3d9ab52..0000000
Binary files a/.gradle/9.1.0/checksums/checksums.lock and /dev/null differ
diff --git a/.gradle/9.1.0/executionHistory/executionHistory.bin b/.gradle/9.1.0/executionHistory/executionHistory.bin
deleted file mode 100644
index c3b4cb1..0000000
Binary files a/.gradle/9.1.0/executionHistory/executionHistory.bin and /dev/null differ
diff --git a/.gradle/9.1.0/executionHistory/executionHistory.lock b/.gradle/9.1.0/executionHistory/executionHistory.lock
deleted file mode 100644
index 4cc7cd5..0000000
Binary files a/.gradle/9.1.0/executionHistory/executionHistory.lock and /dev/null differ
diff --git a/.gradle/9.1.0/fileChanges/last-build.bin b/.gradle/9.1.0/fileChanges/last-build.bin
deleted file mode 100644
index f76dd23..0000000
Binary files a/.gradle/9.1.0/fileChanges/last-build.bin and /dev/null differ
diff --git a/.gradle/9.1.0/fileHashes/fileHashes.bin b/.gradle/9.1.0/fileHashes/fileHashes.bin
deleted file mode 100644
index 5c96b1a..0000000
Binary files a/.gradle/9.1.0/fileHashes/fileHashes.bin and /dev/null differ
diff --git a/.gradle/9.1.0/fileHashes/fileHashes.lock b/.gradle/9.1.0/fileHashes/fileHashes.lock
deleted file mode 100644
index abbb4d0..0000000
Binary files a/.gradle/9.1.0/fileHashes/fileHashes.lock and /dev/null differ
diff --git a/.gradle/9.1.0/gc.properties b/.gradle/9.1.0/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock
deleted file mode 100644
index 0350ff2..0000000
Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ
diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties
deleted file mode 100644
index 80e1268..0000000
--- a/.gradle/buildOutputCleanup/cache.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-#Thu Oct 23 17:51:21 KST 2025
-gradle.version=8.10
diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin
deleted file mode 100644
index 4ed6f06..0000000
Binary files a/.gradle/buildOutputCleanup/outputFiles.bin and /dev/null differ
diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe
deleted file mode 100644
index ac4beb4..0000000
Binary files a/.gradle/file-system.probe and /dev/null differ
diff --git a/.gradle/vcs-1/gc.properties b/.gradle/vcs-1/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/.run/ParticipationServiceApplication.run.xml b/.run/ParticipationServiceApplication.run.xml
new file mode 100644
index 0000000..a323100
--- /dev/null
+++ b/.run/ParticipationServiceApplication.run.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ false
+
+
+
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 6b665aa..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "liveServer.settings.port": 5501
-}
diff --git a/analytics-service/.run/analytics-service.run.xml b/analytics-service/.run/analytics-service.run.xml
new file mode 100644
index 0000000..44dfb98
--- /dev/null
+++ b/analytics-service/.run/analytics-service.run.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ false
+
+
+
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/AnalyticsServiceApplication.java b/analytics-service/src/main/java/com/kt/event/analytics/AnalyticsServiceApplication.java
new file mode 100644
index 0000000..c109743
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/AnalyticsServiceApplication.java
@@ -0,0 +1,29 @@
+package com.kt.event.analytics;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.kafka.annotation.EnableKafka;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * Analytics Service 애플리케이션 메인 클래스
+ *
+ * 실시간 효과 측정 및 통합 대시보드를 제공하는 Analytics Service
+ */
+@SpringBootApplication(scanBasePackages = {"com.kt.event.analytics", "com.kt.event.common"})
+@EntityScan(basePackages = {"com.kt.event.analytics.entity", "com.kt.event.common.entity"})
+@EnableJpaRepositories(basePackages = "com.kt.event.analytics.repository")
+@EnableJpaAuditing
+@EnableFeignClients
+@EnableKafka
+@EnableScheduling
+public class AnalyticsServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AnalyticsServiceApplication.class, args);
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/batch/AnalyticsBatchScheduler.java b/analytics-service/src/main/java/com/kt/event/analytics/batch/AnalyticsBatchScheduler.java
new file mode 100644
index 0000000..82263fd
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/batch/AnalyticsBatchScheduler.java
@@ -0,0 +1,116 @@
+package com.kt.event.analytics.batch;
+
+import com.kt.event.analytics.entity.EventStats;
+import com.kt.event.analytics.repository.EventStatsRepository;
+import com.kt.event.analytics.service.AnalyticsService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * Analytics 배치 스케줄러
+ *
+ * 5분 단위로 Analytics 대시보드 데이터를 갱신하는 배치 작업
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AnalyticsBatchScheduler {
+
+ private final AnalyticsService analyticsService;
+ private final EventStatsRepository eventStatsRepository;
+ private final RedisTemplate redisTemplate;
+
+ /**
+ * 5분 단위 Analytics 데이터 갱신 배치
+ *
+ * - 각 이벤트마다 Redis 캐시 확인
+ * - 캐시 있음 → 건너뛰기 (1시간 유효)
+ * - 캐시 없음 → PostgreSQL + 외부 API → Redis 저장
+ */
+ @Scheduled(fixedRate = 300000) // 5분 = 300,000ms
+ public void refreshAnalyticsDashboard() {
+ log.info("===== Analytics 배치 시작: {} =====", LocalDateTime.now());
+
+ try {
+ // 1. 모든 활성 이벤트 조회
+ List activeEvents = eventStatsRepository.findAll();
+ log.info("활성 이벤트 수: {}", activeEvents.size());
+
+ // 2. 각 이벤트별로 캐시 확인 및 갱신
+ int successCount = 0;
+ int skipCount = 0;
+ int failCount = 0;
+
+ for (EventStats event : activeEvents) {
+ String cacheKey = "analytics:dashboard:" + event.getEventId();
+
+ try {
+ // 2-1. Redis 캐시 확인
+ if (redisTemplate.hasKey(cacheKey)) {
+ log.debug("✅ 캐시 유효, 건너뜀: eventId={}", event.getEventId());
+ skipCount++;
+ continue;
+ }
+
+ // 2-2. 캐시 없음 → 데이터 갱신
+ log.info("캐시 만료, 갱신 시작: eventId={}, title={}",
+ event.getEventId(), event.getEventTitle());
+
+ // refresh=true로 호출하여 캐시 갱신 및 외부 API 호출
+ analyticsService.getDashboardData(event.getEventId(), null, null, true);
+
+ successCount++;
+ log.info("✅ 배치 갱신 완료: eventId={}", event.getEventId());
+
+ } catch (Exception e) {
+ failCount++;
+ log.error("❌ 배치 갱신 실패: eventId={}, error={}",
+ event.getEventId(), e.getMessage(), e);
+ }
+ }
+
+ log.info("===== Analytics 배치 완료: 성공={}, 건너뜀={}, 실패={}, 종료시각={} =====",
+ successCount, skipCount, failCount, LocalDateTime.now());
+
+ } catch (Exception e) {
+ log.error("Analytics 배치 실행 중 오류 발생: {}", e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 초기 데이터 로딩 (애플리케이션 시작 후 30초 뒤 1회 실행)
+ *
+ * - 서버 시작 직후 캐시 워밍업
+ * - 첫 API 요청 시 응답 시간 단축
+ */
+ @Scheduled(initialDelay = 30000, fixedDelay = Long.MAX_VALUE)
+ public void initialDataLoad() {
+ log.info("===== 초기 데이터 로딩 시작: {} =====", LocalDateTime.now());
+
+ try {
+ List allEvents = eventStatsRepository.findAll();
+ log.info("초기 로딩 대상 이벤트 수: {}", allEvents.size());
+
+ for (EventStats event : allEvents) {
+ try {
+ analyticsService.getDashboardData(event.getEventId(), null, null, true);
+ log.debug("초기 데이터 로딩 완료: eventId={}", event.getEventId());
+ } catch (Exception e) {
+ log.warn("초기 데이터 로딩 실패: eventId={}, error={}",
+ event.getEventId(), e.getMessage());
+ }
+ }
+
+ log.info("===== 초기 데이터 로딩 완료: {} =====", LocalDateTime.now());
+
+ } catch (Exception e) {
+ log.error("초기 데이터 로딩 중 오류 발생: {}", e.getMessage(), e);
+ }
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/config/KafkaConsumerConfig.java b/analytics-service/src/main/java/com/kt/event/analytics/config/KafkaConsumerConfig.java
new file mode 100644
index 0000000..8ffefb7
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/config/KafkaConsumerConfig.java
@@ -0,0 +1,50 @@
+package com.kt.event.analytics.config;
+
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.common.serialization.StringDeserializer;
+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.config.ConcurrentKafkaListenerContainerFactory;
+import org.springframework.kafka.core.ConsumerFactory;
+import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Kafka Consumer 설정
+ */
+@Configuration
+@ConditionalOnProperty(name = "spring.kafka.enabled", havingValue = "true", matchIfMissing = true)
+public class KafkaConsumerConfig {
+
+ @Value("${spring.kafka.bootstrap-servers}")
+ private String bootstrapServers;
+
+ @Value("${spring.kafka.consumer.group-id:analytics-service}")
+ private String groupId;
+
+ @Bean
+ public ConsumerFactory consumerFactory() {
+ Map props = new HashMap<>();
+ props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
+ props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+ props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+ props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
+ props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
+ return new DefaultKafkaConsumerFactory<>(props);
+ }
+
+ @Bean
+ public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
+ ConcurrentKafkaListenerContainerFactory factory =
+ new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(consumerFactory());
+ // Kafka Consumer 자동 시작 활성화
+ factory.setAutoStartup(true);
+ return factory;
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/config/KafkaTopicConfig.java b/analytics-service/src/main/java/com/kt/event/analytics/config/KafkaTopicConfig.java
new file mode 100644
index 0000000..3c77521
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/config/KafkaTopicConfig.java
@@ -0,0 +1,53 @@
+package com.kt.event.analytics.config;
+
+import org.apache.kafka.clients.admin.NewTopic;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.config.TopicBuilder;
+
+/**
+ * Kafka 토픽 자동 생성 설정
+ *
+ * ⚠️ MVP 전용: 샘플 데이터용 토픽을 생성합니다.
+ * 실제 운영 토픽(event.created 등)과 구분하기 위해 "sample." 접두사 사용
+ *
+ * 서비스 시작 시 필요한 Kafka 토픽을 자동으로 생성합니다.
+ */
+@Configuration
+@ConditionalOnProperty(name = "spring.kafka.enabled", havingValue = "true", matchIfMissing = false)
+public class KafkaTopicConfig {
+
+ /**
+ * sample.event.created 토픽 (MVP 샘플 데이터용)
+ */
+ @Bean
+ public NewTopic eventCreatedTopic() {
+ return TopicBuilder.name("sample.event.created")
+ .partitions(3)
+ .replicas(1)
+ .build();
+ }
+
+ /**
+ * sample.participant.registered 토픽 (MVP 샘플 데이터용)
+ */
+ @Bean
+ public NewTopic participantRegisteredTopic() {
+ return TopicBuilder.name("sample.participant.registered")
+ .partitions(3)
+ .replicas(1)
+ .build();
+ }
+
+ /**
+ * sample.distribution.completed 토픽 (MVP 샘플 데이터용)
+ */
+ @Bean
+ public NewTopic distributionCompletedTopic() {
+ return TopicBuilder.name("sample.distribution.completed")
+ .partitions(3)
+ .replicas(1)
+ .build();
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/config/RedisConfig.java b/analytics-service/src/main/java/com/kt/event/analytics/config/RedisConfig.java
new file mode 100644
index 0000000..5c6eebb
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/config/RedisConfig.java
@@ -0,0 +1,35 @@
+package com.kt.event.analytics.config;
+
+import io.lettuce.core.ReadFrom;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * Redis 캐시 설정
+ */
+@Configuration
+public class RedisConfig {
+
+ @Bean
+ public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(connectionFactory);
+ template.setKeySerializer(new StringRedisSerializer());
+ template.setValueSerializer(new StringRedisSerializer());
+ template.setHashKeySerializer(new StringRedisSerializer());
+ template.setHashValueSerializer(new StringRedisSerializer());
+
+ // Read-only 오류 방지: 마스터 노드 우선 사용
+ if (connectionFactory instanceof LettuceConnectionFactory) {
+ LettuceConnectionFactory lettuceFactory = (LettuceConnectionFactory) connectionFactory;
+ lettuceFactory.setValidateConnection(true);
+ }
+
+ return template;
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/config/Resilience4jConfig.java b/analytics-service/src/main/java/com/kt/event/analytics/config/Resilience4jConfig.java
new file mode 100644
index 0000000..ab4f50e
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/config/Resilience4jConfig.java
@@ -0,0 +1,27 @@
+package com.kt.event.analytics.config;
+
+import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
+import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+
+/**
+ * Resilience4j Circuit Breaker 설정
+ */
+@Configuration
+public class Resilience4jConfig {
+
+ @Bean
+ public CircuitBreakerRegistry circuitBreakerRegistry() {
+ CircuitBreakerConfig config = CircuitBreakerConfig.custom()
+ .failureRateThreshold(50)
+ .waitDurationInOpenState(Duration.ofSeconds(30))
+ .slidingWindowSize(10)
+ .permittedNumberOfCallsInHalfOpenState(3)
+ .build();
+
+ return CircuitBreakerRegistry.of(config);
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/config/SampleDataLoader.java b/analytics-service/src/main/java/com/kt/event/analytics/config/SampleDataLoader.java
new file mode 100644
index 0000000..72d27f4
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/config/SampleDataLoader.java
@@ -0,0 +1,361 @@
+package com.kt.event.analytics.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.kt.event.analytics.messaging.event.DistributionCompletedEvent;
+import com.kt.event.analytics.messaging.event.EventCreatedEvent;
+import com.kt.event.analytics.messaging.event.ParticipantRegisteredEvent;
+import com.kt.event.analytics.repository.ChannelStatsRepository;
+import com.kt.event.analytics.repository.EventStatsRepository;
+import com.kt.event.analytics.repository.TimelineDataRepository;
+import jakarta.annotation.PreDestroy;
+import jakarta.persistence.EntityManager;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.UUID;
+
+/**
+ * 샘플 데이터 로더 (Kafka Producer 방식)
+ *
+ * ⚠️ MVP 전용: 다른 마이크로서비스(Event, Participant, Distribution)가
+ * 없는 환경에서 해당 서비스들의 역할을 시뮬레이션합니다.
+ *
+ * ⚠️ 실제 운영: Analytics Service는 순수 Consumer 역할만 수행해야 하며,
+ * 이 클래스는 비활성화되어야 합니다.
+ * → SAMPLE_DATA_ENABLED=false 설정
+ *
+ * - 서비스 시작 시: Kafka 이벤트 발행하여 샘플 데이터 자동 생성
+ * - 서비스 종료 시: PostgreSQL 전체 데이터 삭제
+ *
+ * 활성화 조건: spring.sample-data.enabled=true (기본값: true)
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(name = "spring.sample-data.enabled", havingValue = "true", matchIfMissing = true)
+@RequiredArgsConstructor
+public class SampleDataLoader implements ApplicationRunner {
+
+ private final KafkaTemplate kafkaTemplate;
+ private final ObjectMapper objectMapper;
+ private final EventStatsRepository eventStatsRepository;
+ private final ChannelStatsRepository channelStatsRepository;
+ private final TimelineDataRepository timelineDataRepository;
+ private final EntityManager entityManager;
+ private final RedisTemplate redisTemplate;
+
+ private final Random random = new Random();
+
+ // Kafka Topic Names (MVP용 샘플 토픽)
+ private static final String EVENT_CREATED_TOPIC = "sample.event.created";
+ private static final String PARTICIPANT_REGISTERED_TOPIC = "sample.participant.registered";
+ private static final String DISTRIBUTION_COMPLETED_TOPIC = "sample.distribution.completed";
+
+ @Override
+ @Transactional
+ public void run(ApplicationArguments args) {
+ log.info("========================================");
+ log.info("🚀 서비스 시작: Kafka 이벤트 발행하여 샘플 데이터 생성");
+ log.info("========================================");
+
+ // 항상 기존 데이터 삭제 후 새로 생성
+ long existingCount = eventStatsRepository.count();
+ if (existingCount > 0) {
+ log.info("기존 데이터 {} 건 삭제 중...", existingCount);
+ timelineDataRepository.deleteAll();
+ channelStatsRepository.deleteAll();
+ eventStatsRepository.deleteAll();
+
+ // 삭제 커밋 보장
+ entityManager.flush();
+ entityManager.clear();
+
+ log.info("✅ 기존 데이터 삭제 완료");
+ }
+
+ // Redis 멱등성 키 삭제 (새로운 이벤트 처리를 위해)
+ log.info("Redis 멱등성 키 삭제 중...");
+ redisTemplate.delete("processed_events");
+ redisTemplate.delete("distribution_completed");
+ redisTemplate.delete("processed_participants");
+ log.info("✅ Redis 멱등성 키 삭제 완료");
+
+ try {
+ // 1. EventCreated 이벤트 발행 (3개 이벤트)
+ publishEventCreatedEvents();
+ log.info("⏳ EventStats 생성 대기 중... (5초)");
+ Thread.sleep(5000); // EventCreatedConsumer가 EventStats 생성할 시간
+
+ // 2. DistributionCompleted 이벤트 발행 (각 이벤트당 4개 채널)
+ publishDistributionCompletedEvents();
+ log.info("⏳ ChannelStats 생성 대기 중... (3초)");
+ Thread.sleep(3000); // DistributionCompletedConsumer가 ChannelStats 생성할 시간
+
+ // 3. ParticipantRegistered 이벤트 발행 (각 이벤트당 다수 참여자)
+ publishParticipantRegisteredEvents();
+
+ log.info("========================================");
+ log.info("🎉 Kafka 이벤트 발행 완료! (Consumer가 처리 중...)");
+ log.info("========================================");
+ log.info("발행된 이벤트:");
+ log.info(" - EventCreated: 3건");
+ log.info(" - DistributionCompleted: 3건 (각 이벤트당 4개 채널 배열)");
+ log.info(" - ParticipantRegistered: 180건 (MVP 테스트용)");
+ log.info("========================================");
+
+ // Consumer 처리 대기 (5초)
+ log.info("⏳ 참여자 수 업데이트 대기 중... (5초)");
+ Thread.sleep(5000);
+
+ // 4. TimelineData 생성 (시간대별 데이터)
+ createTimelineData();
+ log.info("✅ TimelineData 생성 완료");
+
+ } catch (Exception e) {
+ log.error("샘플 데이터 적재 중 오류 발생", e);
+ }
+ }
+
+ /**
+ * 서비스 종료 시 전체 데이터 삭제
+ */
+ @PreDestroy
+ @Transactional
+ public void onShutdown() {
+ log.info("========================================");
+ log.info("🛑 서비스 종료: PostgreSQL 전체 데이터 삭제");
+ log.info("========================================");
+
+ try {
+ long timelineCount = timelineDataRepository.count();
+ long channelCount = channelStatsRepository.count();
+ long eventCount = eventStatsRepository.count();
+
+ log.info("삭제 대상: 이벤트={}, 채널={}, 타임라인={}",
+ eventCount, channelCount, timelineCount);
+
+ timelineDataRepository.deleteAll();
+ channelStatsRepository.deleteAll();
+ eventStatsRepository.deleteAll();
+
+ // 삭제 커밋 보장
+ entityManager.flush();
+ entityManager.clear();
+
+ log.info("✅ 모든 샘플 데이터 삭제 완료!");
+ log.info("========================================");
+
+ } catch (Exception e) {
+ log.error("샘플 데이터 삭제 중 오류 발생", e);
+ }
+ }
+
+ /**
+ * EventCreated 이벤트 발행
+ */
+ private void publishEventCreatedEvents() throws Exception {
+ // 이벤트 1: 신년맞이 할인 이벤트 (진행중, 높은 성과)
+ EventCreatedEvent event1 = EventCreatedEvent.builder()
+ .eventId("evt_2025012301")
+ .eventTitle("신년맞이 20% 할인 이벤트")
+ .storeId("store_001")
+ .totalInvestment(new BigDecimal("5000000"))
+ .status("ACTIVE")
+ .build();
+ publishEvent(EVENT_CREATED_TOPIC, event1);
+
+ // 이벤트 2: 설날 특가 이벤트 (진행중, 중간 성과)
+ EventCreatedEvent event2 = EventCreatedEvent.builder()
+ .eventId("evt_2025020101")
+ .eventTitle("설날 특가 선물세트 이벤트")
+ .storeId("store_001")
+ .totalInvestment(new BigDecimal("3500000"))
+ .status("ACTIVE")
+ .build();
+ publishEvent(EVENT_CREATED_TOPIC, event2);
+
+ // 이벤트 3: 겨울 신메뉴 런칭 이벤트 (종료, 저조한 성과)
+ EventCreatedEvent event3 = EventCreatedEvent.builder()
+ .eventId("evt_2025011501")
+ .eventTitle("겨울 신메뉴 런칭 이벤트")
+ .storeId("store_001")
+ .totalInvestment(new BigDecimal("2000000"))
+ .status("COMPLETED")
+ .build();
+ publishEvent(EVENT_CREATED_TOPIC, event3);
+
+ log.info("✅ EventCreated 이벤트 3건 발행 완료");
+ }
+
+ /**
+ * DistributionCompleted 이벤트 발행 (설계서 기준 - 이벤트당 1번 발행, 여러 채널 배열)
+ */
+ private void publishDistributionCompletedEvents() throws Exception {
+ String[] eventIds = {"evt_2025012301", "evt_2025020101", "evt_2025011501"};
+ int[][] expectedViews = {
+ {5000, 10000, 3000, 2000}, // 이벤트1: 우리동네TV, 지니TV, 링고비즈, SNS
+ {3500, 7000, 2000, 1500}, // 이벤트2
+ {1500, 3000, 1000, 500} // 이벤트3
+ };
+
+ for (int i = 0; i < eventIds.length; i++) {
+ String eventId = eventIds[i];
+
+ // 4개 채널을 배열로 구성
+ List channels = new ArrayList<>();
+
+ // 1. 우리동네TV (TV)
+ channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
+ .channel("우리동네TV")
+ .channelType("TV")
+ .status("SUCCESS")
+ .expectedViews(expectedViews[i][0])
+ .build());
+
+ // 2. 지니TV (TV)
+ channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
+ .channel("지니TV")
+ .channelType("TV")
+ .status("SUCCESS")
+ .expectedViews(expectedViews[i][1])
+ .build());
+
+ // 3. 링고비즈 (CALL)
+ channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
+ .channel("링고비즈")
+ .channelType("CALL")
+ .status("SUCCESS")
+ .expectedViews(expectedViews[i][2])
+ .build());
+
+ // 4. SNS (SNS)
+ channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
+ .channel("SNS")
+ .channelType("SNS")
+ .status("SUCCESS")
+ .expectedViews(expectedViews[i][3])
+ .build());
+
+ // 이벤트 발행 (채널 배열 포함)
+ DistributionCompletedEvent event = DistributionCompletedEvent.builder()
+ .eventId(eventId)
+ .distributedChannels(channels)
+ .completedAt(java.time.LocalDateTime.now())
+ .build();
+
+ publishEvent(DISTRIBUTION_COMPLETED_TOPIC, event);
+ }
+
+ log.info("✅ DistributionCompleted 이벤트 3건 발행 완료 (3 이벤트 × 4 채널 배열)");
+ }
+
+ /**
+ * ParticipantRegistered 이벤트 발행
+ */
+ private void publishParticipantRegisteredEvents() throws Exception {
+ String[] eventIds = {"evt_2025012301", "evt_2025020101", "evt_2025011501"};
+ int[] totalParticipants = {100, 50, 30}; // MVP 테스트용 샘플 데이터 (총 180명)
+ String[] channels = {"우리동네TV", "지니TV", "링고비즈", "SNS"};
+
+ int totalPublished = 0;
+
+ for (int i = 0; i < eventIds.length; i++) {
+ String eventId = eventIds[i];
+ int participants = totalParticipants[i];
+
+ // 각 이벤트에 대해 참여자 수만큼 ParticipantRegistered 이벤트 발행
+ for (int j = 0; j < participants; j++) {
+ String participantId = UUID.randomUUID().toString();
+ String channel = channels[j % channels.length]; // 채널 순환 배정
+
+ ParticipantRegisteredEvent event = ParticipantRegisteredEvent.builder()
+ .eventId(eventId)
+ .participantId(participantId)
+ .channel(channel)
+ .build();
+
+ publishEvent(PARTICIPANT_REGISTERED_TOPIC, event);
+ totalPublished++;
+ }
+ }
+
+ log.info("✅ ParticipantRegistered 이벤트 {}건 발행 완료", totalPublished);
+ }
+
+ /**
+ * TimelineData 생성 (시간대별 샘플 데이터)
+ *
+ * - 각 이벤트마다 30일 치 daily 데이터 생성
+ * - 참여자 수, 조회수, 참여행동, 전환수, 누적 참여자 수
+ */
+ private void createTimelineData() {
+ log.info("📊 TimelineData 생성 시작...");
+
+ String[] eventIds = {"evt_2025012301", "evt_2025020101", "evt_2025011501"};
+
+ // 각 이벤트별 기준 참여자 수 (이벤트 성과에 따라 다름)
+ int[] baseParticipants = {20, 12, 5}; // 이벤트1(높음), 이벤트2(중간), 이벤트3(낮음)
+
+ for (int eventIndex = 0; eventIndex < eventIds.length; eventIndex++) {
+ String eventId = eventIds[eventIndex];
+ int baseParticipant = baseParticipants[eventIndex];
+ int cumulativeParticipants = 0;
+
+ // 30일 치 데이터 생성 (2024-09-24부터)
+ java.time.LocalDateTime startDate = java.time.LocalDateTime.of(2024, 9, 24, 0, 0);
+
+ for (int day = 0; day < 30; day++) {
+ java.time.LocalDateTime timestamp = startDate.plusDays(day);
+
+ // 랜덤한 참여자 수 생성 (기준값 ± 50%)
+ int dailyParticipants = baseParticipant + random.nextInt(baseParticipant + 1);
+ cumulativeParticipants += dailyParticipants;
+
+ // 조회수는 참여자의 3~5배
+ int dailyViews = dailyParticipants * (3 + random.nextInt(3));
+
+ // 참여행동은 참여자의 1~2배
+ int dailyEngagement = dailyParticipants * (1 + random.nextInt(2));
+
+ // 전환수는 참여자의 50~80%
+ int dailyConversions = (int) (dailyParticipants * (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(dailyParticipants)
+ .views(dailyViews)
+ .engagement(dailyEngagement)
+ .conversions(dailyConversions)
+ .cumulativeParticipants(cumulativeParticipants)
+ .build();
+
+ timelineDataRepository.save(timelineData);
+ }
+
+ log.info("✅ TimelineData 생성 완료: eventId={}, 30일 데이터", eventId);
+ }
+
+ log.info("✅ 전체 TimelineData 생성 완료: 3개 이벤트 × 30일 = 90건");
+ }
+
+ /**
+ * Kafka 이벤트 발행 공통 메서드
+ */
+ private void publishEvent(String topic, Object event) throws Exception {
+ String jsonMessage = objectMapper.writeValueAsString(event);
+ kafkaTemplate.send(topic, jsonMessage);
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/config/SecurityConfig.java b/analytics-service/src/main/java/com/kt/event/analytics/config/SecurityConfig.java
new file mode 100644
index 0000000..b340f83
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/config/SecurityConfig.java
@@ -0,0 +1,79 @@
+package com.kt.event.analytics.config;
+
+import com.kt.event.common.security.JwtAuthenticationFilter;
+import com.kt.event.common.security.JwtTokenProvider;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+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.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+import java.util.Arrays;
+
+/**
+ * Spring Security 설정
+ * JWT 기반 인증 및 API 보안 설정
+ */
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+ private final JwtTokenProvider jwtTokenProvider;
+
+ @Value("${cors.allowed-origins:http://localhost:*}")
+ private String allowedOrigins;
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ return http
+ .csrf(AbstractHttpConfigurer::disable)
+ .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(auth -> auth
+ // Actuator endpoints
+ .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),
+ UsernamePasswordAuthenticationFilter.class)
+ .build();
+ }
+
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+
+ String[] origins = allowedOrigins.split(",");
+ configuration.setAllowedOriginPatterns(Arrays.asList(origins));
+
+ configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
+
+ configuration.setAllowedHeaders(Arrays.asList(
+ "Authorization", "Content-Type", "X-Requested-With", "Accept",
+ "Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"
+ ));
+
+ configuration.setAllowCredentials(true);
+ configuration.setMaxAge(3600L);
+
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", configuration);
+ return source;
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/config/SwaggerConfig.java b/analytics-service/src/main/java/com/kt/event/analytics/config/SwaggerConfig.java
new file mode 100644
index 0000000..c0660af
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/config/SwaggerConfig.java
@@ -0,0 +1,63 @@
+package com.kt.event.analytics.config;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import io.swagger.v3.oas.models.servers.Server;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Swagger/OpenAPI 설정
+ * Analytics Service API 문서화를 위한 설정
+ */
+@Configuration
+public class SwaggerConfig {
+
+ @Bean
+ public OpenAPI openAPI() {
+ return new OpenAPI()
+ .info(apiInfo())
+ .addServersItem(new Server()
+ .url("http://localhost:8086")
+ .description("Local Development"))
+ .addServersItem(new Server()
+ .url("{protocol}://{host}:{port}")
+ .description("Custom Server")
+ .variables(new io.swagger.v3.oas.models.servers.ServerVariables()
+ .addServerVariable("protocol", new io.swagger.v3.oas.models.servers.ServerVariable()
+ ._default("http")
+ .description("Protocol (http or https)")
+ .addEnumItem("http")
+ .addEnumItem("https"))
+ .addServerVariable("host", new io.swagger.v3.oas.models.servers.ServerVariable()
+ ._default("localhost")
+ .description("Server host"))
+ .addServerVariable("port", new io.swagger.v3.oas.models.servers.ServerVariable()
+ ._default("8086")
+ .description("Server port"))))
+ .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
+ .components(new Components()
+ .addSecuritySchemes("Bearer Authentication", createAPIKeyScheme()));
+ }
+
+ private Info apiInfo() {
+ return new Info()
+ .title("Analytics Service API")
+ .description("실시간 효과 측정 및 통합 대시보드를 제공하는 Analytics Service API")
+ .version("1.0.0")
+ .contact(new Contact()
+ .name("Digital Garage Team")
+ .email("support@kt-event-marketing.com"));
+ }
+
+ private SecurityScheme createAPIKeyScheme() {
+ return new SecurityScheme()
+ .type(SecurityScheme.Type.HTTP)
+ .bearerFormat("JWT")
+ .scheme("bearer");
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/controller/AnalyticsDashboardController.java b/analytics-service/src/main/java/com/kt/event/analytics/controller/AnalyticsDashboardController.java
new file mode 100644
index 0000000..2dc1d8a
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/controller/AnalyticsDashboardController.java
@@ -0,0 +1,71 @@
+package com.kt.event.analytics.controller;
+
+import com.kt.event.analytics.dto.response.AnalyticsDashboardResponse;
+import com.kt.event.analytics.service.AnalyticsService;
+import com.kt.event.common.dto.ApiResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * Analytics Dashboard Controller
+ *
+ * 이벤트 성과 대시보드 API
+ */
+@Tag(name = "Analytics", description = "이벤트 성과 분석 및 대시보드 API")
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/events")
+@RequiredArgsConstructor
+public class AnalyticsDashboardController {
+
+ private final AnalyticsService analyticsService;
+
+ /**
+ * 성과 대시보드 조회
+ *
+ * @param eventId 이벤트 ID
+ * @param startDate 조회 시작 날짜
+ * @param endDate 조회 종료 날짜
+ * @param refresh 캐시 갱신 여부
+ * @return 성과 대시보드
+ */
+ @Operation(
+ summary = "성과 대시보드 조회",
+ description = "이벤트의 전체 성과를 통합하여 조회합니다."
+ )
+ @GetMapping("/{eventId}/analytics")
+ public ResponseEntity> getEventAnalytics(
+ @Parameter(description = "이벤트 ID", required = true)
+ @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 호출)")
+ @RequestParam(required = false, defaultValue = "false")
+ Boolean refresh
+ ) {
+ log.info("성과 대시보드 조회 API 호출: eventId={}, refresh={}", eventId, refresh);
+
+ AnalyticsDashboardResponse response = analyticsService.getDashboardData(
+ eventId, startDate, endDate, refresh
+ );
+
+ return ResponseEntity.ok(ApiResponse.success(response));
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/controller/ChannelAnalyticsController.java b/analytics-service/src/main/java/com/kt/event/analytics/controller/ChannelAnalyticsController.java
new file mode 100644
index 0000000..ea78687
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/controller/ChannelAnalyticsController.java
@@ -0,0 +1,73 @@
+package com.kt.event.analytics.controller;
+
+import com.kt.event.analytics.dto.response.ChannelAnalyticsResponse;
+import com.kt.event.analytics.service.ChannelAnalyticsService;
+import com.kt.event.common.dto.ApiResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Channel Analytics Controller
+ *
+ * 채널별 성과 분석 API
+ */
+@Tag(name = "Channels", description = "채널별 성과 분석 API")
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/events")
+@RequiredArgsConstructor
+public class ChannelAnalyticsController {
+
+ private final ChannelAnalyticsService channelAnalyticsService;
+
+ /**
+ * 채널별 성과 분석
+ *
+ * @param eventId 이벤트 ID
+ * @param channels 조회할 채널 목록 (쉼표로 구분)
+ * @param sortBy 정렬 기준
+ * @param order 정렬 순서
+ * @return 채널별 성과 분석
+ */
+ @Operation(
+ summary = "채널별 성과 분석",
+ description = "각 배포 채널별 성과를 상세하게 분석합니다."
+ )
+ @GetMapping("/{eventId}/analytics/channels")
+ public ResponseEntity> getChannelAnalytics(
+ @Parameter(description = "이벤트 ID", required = true)
+ @PathVariable String eventId,
+
+ @Parameter(description = "조회할 채널 목록 (쉼표로 구분, 미지정 시 전체)")
+ @RequestParam(required = false)
+ String channels,
+
+ @Parameter(description = "정렬 기준 (views, participants, engagement_rate, conversion_rate, roi)")
+ @RequestParam(required = false, defaultValue = "roi")
+ String sortBy,
+
+ @Parameter(description = "정렬 순서 (asc, desc)")
+ @RequestParam(required = false, defaultValue = "desc")
+ String order
+ ) {
+ log.info("채널별 성과 분석 API 호출: eventId={}, sortBy={}", eventId, sortBy);
+
+ List channelList = channels != null && !channels.isBlank()
+ ? Arrays.asList(channels.split(","))
+ : null;
+
+ ChannelAnalyticsResponse response = channelAnalyticsService.getChannelAnalytics(
+ eventId, channelList, sortBy, order
+ );
+
+ return ResponseEntity.ok(ApiResponse.success(response));
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/controller/RoiAnalyticsController.java b/analytics-service/src/main/java/com/kt/event/analytics/controller/RoiAnalyticsController.java
new file mode 100644
index 0000000..29d6980
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/controller/RoiAnalyticsController.java
@@ -0,0 +1,54 @@
+package com.kt.event.analytics.controller;
+
+import com.kt.event.analytics.dto.response.RoiAnalyticsResponse;
+import com.kt.event.analytics.service.RoiAnalyticsService;
+import com.kt.event.common.dto.ApiResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * ROI Analytics Controller
+ *
+ * 투자 대비 수익률 분석 API
+ */
+@Tag(name = "ROI", description = "투자 대비 수익률 분석 API")
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/events")
+@RequiredArgsConstructor
+public class RoiAnalyticsController {
+
+ private final RoiAnalyticsService roiAnalyticsService;
+
+ /**
+ * 투자 대비 수익률 상세
+ *
+ * @param eventId 이벤트 ID
+ * @param includeProjection 예상 수익 포함 여부
+ * @return ROI 상세 분석
+ */
+ @Operation(
+ summary = "투자 대비 수익률 상세",
+ description = "이벤트의 투자 대비 수익률을 상세하게 분석합니다."
+ )
+ @GetMapping("/{eventId}/analytics/roi")
+ public ResponseEntity> getRoiAnalytics(
+ @Parameter(description = "이벤트 ID", required = true)
+ @PathVariable String eventId,
+
+ @Parameter(description = "예상 수익 포함 여부")
+ @RequestParam(required = false, defaultValue = "true")
+ Boolean includeProjection
+ ) {
+ log.info("ROI 상세 분석 API 호출: eventId={}, includeProjection={}", eventId, includeProjection);
+
+ RoiAnalyticsResponse response = roiAnalyticsService.getRoiAnalytics(eventId, includeProjection);
+
+ return ResponseEntity.ok(ApiResponse.success(response));
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/controller/TimelineAnalyticsController.java b/analytics-service/src/main/java/com/kt/event/analytics/controller/TimelineAnalyticsController.java
new file mode 100644
index 0000000..5fc882f
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/controller/TimelineAnalyticsController.java
@@ -0,0 +1,82 @@
+package com.kt.event.analytics.controller;
+
+import com.kt.event.analytics.dto.response.TimelineAnalyticsResponse;
+import com.kt.event.analytics.service.TimelineAnalyticsService;
+import com.kt.event.common.dto.ApiResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Timeline Analytics Controller
+ *
+ * 시간대별 분석 API
+ */
+@Tag(name = "Timeline", description = "시간대별 분석 API")
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/events")
+@RequiredArgsConstructor
+public class TimelineAnalyticsController {
+
+ private final TimelineAnalyticsService timelineAnalyticsService;
+
+ /**
+ * 시간대별 참여 추이
+ *
+ * @param eventId 이벤트 ID
+ * @param interval 시간 간격 단위
+ * @param startDate 조회 시작 날짜
+ * @param endDate 조회 종료 날짜
+ * @param metrics 조회할 지표 목록
+ * @return 시간대별 참여 추이
+ */
+ @Operation(
+ summary = "시간대별 참여 추이",
+ description = "이벤트 기간 동안의 시간대별 참여 추이를 분석합니다."
+ )
+ @GetMapping("/{eventId}/analytics/timeline")
+ public ResponseEntity> getTimelineAnalytics(
+ @Parameter(description = "이벤트 ID", required = true)
+ @PathVariable String eventId,
+
+ @Parameter(description = "시간 간격 단위 (hourly, daily, weekly)")
+ @RequestParam(required = false, defaultValue = "daily")
+ 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 = "조회할 지표 목록 (쉼표로 구분)")
+ @RequestParam(required = false)
+ String metrics
+ ) {
+ log.info("시간대별 참여 추이 API 호출: eventId={}, interval={}", eventId, interval);
+
+ List metricList = metrics != null && !metrics.isBlank()
+ ? Arrays.asList(metrics.split(","))
+ : null;
+
+ TimelineAnalyticsResponse response = timelineAnalyticsService.getTimelineAnalytics(
+ eventId, interval, startDate, endDate, metricList
+ );
+
+ return ResponseEntity.ok(ApiResponse.success(response));
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/dto/response/AnalyticsDashboardResponse.java b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/AnalyticsDashboardResponse.java
new file mode 100644
index 0000000..9fb9b3e
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/AnalyticsDashboardResponse.java
@@ -0,0 +1,59 @@
+package com.kt.event.analytics.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 이벤트 성과 대시보드 응답
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AnalyticsDashboardResponse {
+
+ /**
+ * 이벤트 ID
+ */
+ private String eventId;
+
+ /**
+ * 이벤트 제목
+ */
+ private String eventTitle;
+
+ /**
+ * 조회 기간 정보
+ */
+ private PeriodInfo period;
+
+ /**
+ * 성과 요약
+ */
+ private AnalyticsSummary summary;
+
+ /**
+ * 채널별 성과 요약
+ */
+ private List channelPerformance;
+
+ /**
+ * ROI 요약
+ */
+ private RoiSummary roi;
+
+ /**
+ * 마지막 업데이트 시간
+ */
+ private LocalDateTime lastUpdatedAt;
+
+ /**
+ * 데이터 출처 (real-time, cached, fallback)
+ */
+ private String dataSource;
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/dto/response/AnalyticsSummary.java b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/AnalyticsSummary.java
new file mode 100644
index 0000000..e4fb561
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/AnalyticsSummary.java
@@ -0,0 +1,51 @@
+package com.kt.event.analytics.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 성과 요약
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AnalyticsSummary {
+
+ /**
+ * 총 참여자 수
+ */
+ private Integer totalParticipants;
+
+ /**
+ * 총 조회수
+ */
+ private Integer totalViews;
+
+ /**
+ * 총 도달 수
+ */
+ private Integer totalReach;
+
+ /**
+ * 참여율 (%)
+ */
+ private Double engagementRate;
+
+ /**
+ * 전환율 (%)
+ */
+ private Double conversionRate;
+
+ /**
+ * 평균 참여 시간 (초)
+ */
+ private Integer averageEngagementTime;
+
+ /**
+ * SNS 반응 통계
+ */
+ private SocialInteractionStats socialInteractions;
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelAnalytics.java b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelAnalytics.java
new file mode 100644
index 0000000..51dccaa
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelAnalytics.java
@@ -0,0 +1,46 @@
+package com.kt.event.analytics.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 채널별 상세 분석
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChannelAnalytics {
+
+ /**
+ * 채널명
+ */
+ private String channelName;
+
+ /**
+ * 채널 유형
+ */
+ private String channelType;
+
+ /**
+ * 채널 지표
+ */
+ private ChannelMetrics metrics;
+
+ /**
+ * 성과 지표
+ */
+ private ChannelPerformance performance;
+
+ /**
+ * 비용 정보
+ */
+ private ChannelCosts costs;
+
+ /**
+ * 외부 API 연동 상태 (success, fallback, failed)
+ */
+ private String externalApiStatus;
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelAnalyticsResponse.java b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelAnalyticsResponse.java
new file mode 100644
index 0000000..2bd8f0c
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelAnalyticsResponse.java
@@ -0,0 +1,39 @@
+package com.kt.event.analytics.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 채널별 성과 분석 응답
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChannelAnalyticsResponse {
+
+ /**
+ * 이벤트 ID
+ */
+ private String eventId;
+
+ /**
+ * 채널별 상세 분석
+ */
+ private List channels;
+
+ /**
+ * 채널 간 비교 분석
+ */
+ private ChannelComparison comparison;
+
+ /**
+ * 마지막 업데이트 시간
+ */
+ private LocalDateTime lastUpdatedAt;
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelComparison.java b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelComparison.java
new file mode 100644
index 0000000..24d2584
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelComparison.java
@@ -0,0 +1,28 @@
+package com.kt.event.analytics.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+
+/**
+ * 채널 간 비교 분석
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChannelComparison {
+
+ /**
+ * 최고 성과 채널
+ */
+ private Map bestPerforming;
+
+ /**
+ * 전체 채널 평균 지표
+ */
+ private Map averageMetrics;
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelCosts.java b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelCosts.java
new file mode 100644
index 0000000..d74e647
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelCosts.java
@@ -0,0 +1,43 @@
+package com.kt.event.analytics.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+/**
+ * 채널별 비용
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChannelCosts {
+
+ /**
+ * 배포 비용 (원)
+ */
+ private BigDecimal distributionCost;
+
+ /**
+ * 조회당 비용 (CPV, 원)
+ */
+ private Double costPerView;
+
+ /**
+ * 클릭당 비용 (CPC, 원)
+ */
+ private Double costPerClick;
+
+ /**
+ * 고객 획득 비용 (CPA, 원)
+ */
+ private Double costPerAcquisition;
+
+ /**
+ * ROI (%)
+ */
+ private Double roi;
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelMetrics.java b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelMetrics.java
new file mode 100644
index 0000000..0029a71
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelMetrics.java
@@ -0,0 +1,51 @@
+package com.kt.event.analytics.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 채널 지표
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChannelMetrics {
+
+ /**
+ * 노출 수
+ */
+ private Integer impressions;
+
+ /**
+ * 조회수
+ */
+ private Integer views;
+
+ /**
+ * 클릭 수
+ */
+ private Integer clicks;
+
+ /**
+ * 참여자 수
+ */
+ private Integer participants;
+
+ /**
+ * 전환 수
+ */
+ private Integer conversions;
+
+ /**
+ * SNS 반응 통계
+ */
+ private SocialInteractionStats socialInteractions;
+
+ /**
+ * 링고비즈 통화 통계
+ */
+ private VoiceCallStats voiceCallStats;
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelPerformance.java b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelPerformance.java
new file mode 100644
index 0000000..0e4db39
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelPerformance.java
@@ -0,0 +1,41 @@
+package com.kt.event.analytics.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 채널 성과 지표
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChannelPerformance {
+
+ /**
+ * 클릭률 (CTR, %)
+ */
+ private Double clickThroughRate;
+
+ /**
+ * 참여율 (%)
+ */
+ private Double engagementRate;
+
+ /**
+ * 전환율 (%)
+ */
+ private Double conversionRate;
+
+ /**
+ * 평균 참여 시간 (초)
+ */
+ private Integer averageEngagementTime;
+
+ /**
+ * 이탈율 (%)
+ */
+ private Double bounceRate;
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelSummary.java b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelSummary.java
new file mode 100644
index 0000000..49e99da
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/ChannelSummary.java
@@ -0,0 +1,46 @@
+package com.kt.event.analytics.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 채널별 성과 요약
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChannelSummary {
+
+ /**
+ * 채널명
+ */
+ private String channelName;
+
+ /**
+ * 조회수
+ */
+ private Integer views;
+
+ /**
+ * 참여자 수
+ */
+ private Integer participants;
+
+ /**
+ * 참여율 (%)
+ */
+ private Double engagementRate;
+
+ /**
+ * 전환율 (%)
+ */
+ private Double conversionRate;
+
+ /**
+ * ROI (%)
+ */
+ private Double roi;
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/dto/response/CostEfficiency.java b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/CostEfficiency.java
new file mode 100644
index 0000000..7c3919b
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/CostEfficiency.java
@@ -0,0 +1,36 @@
+package com.kt.event.analytics.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 비용 효율성
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CostEfficiency {
+
+ /**
+ * 참여자당 비용 (원)
+ */
+ private Double costPerParticipant;
+
+ /**
+ * 전환당 비용 (원)
+ */
+ private Double costPerConversion;
+
+ /**
+ * 조회당 비용 (원)
+ */
+ private Double costPerView;
+
+ /**
+ * 참여자당 수익 (원)
+ */
+ private Double revenuePerParticipant;
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/dto/response/InvestmentDetails.java b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/InvestmentDetails.java
new file mode 100644
index 0000000..abff813
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/InvestmentDetails.java
@@ -0,0 +1,45 @@
+package com.kt.event.analytics.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 투자 비용 상세
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InvestmentDetails {
+
+ /**
+ * 콘텐츠 제작비 (원)
+ */
+ private BigDecimal contentCreation;
+
+ /**
+ * 배포 비용 (원)
+ */
+ private BigDecimal distribution;
+
+ /**
+ * 운영 비용 (원)
+ */
+ private BigDecimal operation;
+
+ /**
+ * 총 투자 비용 (원)
+ */
+ private BigDecimal total;
+
+ /**
+ * 채널별 비용 상세
+ */
+ private List