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/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 0c539cf..f0a5018 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -16,6 +16,11 @@
"Bash(git commit:*)",
"Bash(git push)",
"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:*)",
diff --git a/.gitignore b/.gitignore
index 1a93c5a..9f987d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ yarn-error.log*
# IDE
.idea/
.vscode/
+.run/
*.swp
*.swo
*~
@@ -23,6 +24,21 @@ build/
.gradle/
logs/
+# Gradle
+.gradle/
+!gradle/wrapper/gradle-wrapper.jar
+
+# Logs
+logs/
+*.log
+
+# Gradle
+.gradle/
+gradle-app.setting
+!gradle-wrapper.jar
+!gradle-wrapper.properties
+.gradletasknamecache
+
# Environment
.env
.env.local
diff --git a/.run/ParticipationServiceApplication.run.xml b/.run/ParticipationServiceApplication.run.xml
index a323100..8102290 100644
--- a/.run/ParticipationServiceApplication.run.xml
+++ b/.run/ParticipationServiceApplication.run.xml
@@ -43,7 +43,7 @@
diff --git a/ai-service/build.gradle b/ai-service/build.gradle
index a39127e..ffa12b5 100644
--- a/ai-service/build.gradle
+++ b/ai-service/build.gradle
@@ -2,8 +2,8 @@ dependencies {
// Kafka Consumer
implementation 'org.springframework.kafka:spring-kafka'
- // Redis for result caching
- implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+ // Redis for result caching (already in root build.gradle)
+ // implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// OpenFeign for Claude/GPT API
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
@@ -14,4 +14,20 @@ dependencies {
// Jackson for JSON
implementation 'com.fasterxml.jackson.core:jackson-databind'
+
+ // JWT (for security)
+ implementation "io.jsonwebtoken:jjwt-api:${jjwtVersion}"
+ runtimeOnly "io.jsonwebtoken:jjwt-impl:${jjwtVersion}"
+ runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jjwtVersion}"
+
+ // Note: PostgreSQL dependency is in root build.gradle but AI Service doesn't use DB
+ // We still include it for consistency, but no JPA entities will be created
+}
+
+// Kafka Manual Test 실행 태스크
+task runKafkaManualTest(type: JavaExec) {
+ group = 'verification'
+ description = 'Run Kafka manual test'
+ classpath = sourceSets.test.runtimeClasspath
+ mainClass = 'com.kt.ai.test.manual.KafkaManualTest'
}
diff --git a/ai-service/src/main/java/com/kt/ai/AiServiceApplication.java b/ai-service/src/main/java/com/kt/ai/AiServiceApplication.java
new file mode 100644
index 0000000..be8b721
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/AiServiceApplication.java
@@ -0,0 +1,24 @@
+package com.kt.ai;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+
+/**
+ * AI Service Application
+ * - Kafka를 통한 비동기 AI 추천 처리
+ * - Claude API / GPT-4 API 연동
+ * - Redis 기반 결과 캐싱
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@EnableFeignClients
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
+public class AiServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AiServiceApplication.class, args);
+ }
+}
diff --git a/ai-service/src/main/java/com/kt/ai/circuitbreaker/CircuitBreakerManager.java b/ai-service/src/main/java/com/kt/ai/circuitbreaker/CircuitBreakerManager.java
new file mode 100644
index 0000000..870b4b1
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/circuitbreaker/CircuitBreakerManager.java
@@ -0,0 +1,87 @@
+package com.kt.ai.circuitbreaker;
+
+import com.kt.ai.exception.CircuitBreakerOpenException;
+import io.github.resilience4j.circuitbreaker.CircuitBreaker;
+import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.function.Supplier;
+
+/**
+ * Circuit Breaker Manager
+ * - Claude API / GPT-4 API 호출 시 Circuit Breaker 적용
+ * - Fallback 처리
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class CircuitBreakerManager {
+
+ private final CircuitBreakerRegistry circuitBreakerRegistry;
+
+ /**
+ * Circuit Breaker를 통한 API 호출
+ *
+ * @param circuitBreakerName Circuit Breaker 이름 (claudeApi, gpt4Api)
+ * @param supplier API 호출 로직
+ * @param fallback Fallback 로직
+ * @return API 호출 결과 또는 Fallback 결과
+ */
+ public T executeWithCircuitBreaker(
+ String circuitBreakerName,
+ Supplier supplier,
+ Supplier fallback
+ ) {
+ CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(circuitBreakerName);
+
+ try {
+ // Circuit Breaker 상태 확인
+ if (circuitBreaker.getState() == CircuitBreaker.State.OPEN) {
+ log.warn("Circuit Breaker is OPEN: {}", circuitBreakerName);
+ throw new CircuitBreakerOpenException(circuitBreakerName);
+ }
+
+ // Circuit Breaker를 통한 API 호출
+ return circuitBreaker.executeSupplier(() -> {
+ log.debug("Executing with Circuit Breaker: {}", circuitBreakerName);
+ return supplier.get();
+ });
+
+ } catch (CircuitBreakerOpenException e) {
+ // Circuit Breaker가 열린 경우 Fallback 실행
+ log.warn("Circuit Breaker OPEN, executing fallback: {}", circuitBreakerName);
+ if (fallback != null) {
+ return fallback.get();
+ }
+ throw e;
+
+ } catch (Exception e) {
+ // 기타 예외 발생 시 Fallback 실행
+ log.error("API call failed, executing fallback: {}", circuitBreakerName, e);
+ if (fallback != null) {
+ return fallback.get();
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Circuit Breaker를 통한 API 호출 (Fallback 없음)
+ */
+ public T executeWithCircuitBreaker(String circuitBreakerName, Supplier supplier) {
+ return executeWithCircuitBreaker(circuitBreakerName, supplier, null);
+ }
+
+ /**
+ * Circuit Breaker 상태 조회
+ */
+ public CircuitBreaker.State getCircuitBreakerState(String circuitBreakerName) {
+ CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(circuitBreakerName);
+ return circuitBreaker.getState();
+ }
+}
diff --git a/ai-service/src/main/java/com/kt/ai/circuitbreaker/fallback/AIServiceFallback.java b/ai-service/src/main/java/com/kt/ai/circuitbreaker/fallback/AIServiceFallback.java
new file mode 100644
index 0000000..d7860cf
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/circuitbreaker/fallback/AIServiceFallback.java
@@ -0,0 +1,130 @@
+package com.kt.ai.circuitbreaker.fallback;
+
+import com.kt.ai.model.dto.response.EventRecommendation;
+import com.kt.ai.model.dto.response.ExpectedMetrics;
+import com.kt.ai.model.dto.response.TrendAnalysis;
+import com.kt.ai.model.enums.EventMechanicsType;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * AI Service Fallback 처리
+ * - Circuit Breaker가 열린 경우 기본 데이터 반환
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@Slf4j
+@Component
+public class AIServiceFallback {
+
+ /**
+ * 기본 트렌드 분석 결과 반환
+ */
+ public TrendAnalysis getDefaultTrendAnalysis(String industry, String region) {
+ log.info("Fallback: 기본 트렌드 분석 결과 반환 - industry={}, region={}", industry, region);
+
+ List industryTrends = List.of(
+ TrendAnalysis.TrendKeyword.builder()
+ .keyword("고객 만족도 향상")
+ .relevance(0.8)
+ .description(industry + " 업종에서 고객 만족도가 중요한 트렌드입니다")
+ .build(),
+ TrendAnalysis.TrendKeyword.builder()
+ .keyword("디지털 마케팅")
+ .relevance(0.75)
+ .description("SNS 및 온라인 마케팅이 효과적입니다")
+ .build()
+ );
+
+ List regionalTrends = List.of(
+ TrendAnalysis.TrendKeyword.builder()
+ .keyword("지역 커뮤니티")
+ .relevance(0.7)
+ .description(region + " 지역 커뮤니티 참여가 효과적입니다")
+ .build()
+ );
+
+ List seasonalTrends = List.of(
+ TrendAnalysis.TrendKeyword.builder()
+ .keyword("시즌 이벤트")
+ .relevance(0.85)
+ .description("계절 특성을 반영한 이벤트가 효과적입니다")
+ .build()
+ );
+
+ return TrendAnalysis.builder()
+ .industryTrends(industryTrends)
+ .regionalTrends(regionalTrends)
+ .seasonalTrends(seasonalTrends)
+ .build();
+ }
+
+ /**
+ * 기본 이벤트 추천안 반환
+ */
+ public List getDefaultRecommendations(String objective, String industry) {
+ log.info("Fallback: 기본 이벤트 추천안 반환 - objective={}, industry={}", objective, industry);
+
+ List recommendations = new ArrayList<>();
+
+ // 옵션 1: 저비용 이벤트
+ recommendations.add(createDefaultRecommendation(1, "저비용 SNS 이벤트", objective, industry, 100000, 200000));
+
+ // 옵션 2: 중비용 이벤트
+ recommendations.add(createDefaultRecommendation(2, "중비용 방문 유도 이벤트", objective, industry, 300000, 500000));
+
+ // 옵션 3: 고비용 이벤트
+ recommendations.add(createDefaultRecommendation(3, "고비용 프리미엄 이벤트", objective, industry, 500000, 1000000));
+
+ return recommendations;
+ }
+
+ /**
+ * 기본 추천안 생성
+ */
+ private EventRecommendation createDefaultRecommendation(
+ int optionNumber,
+ String concept,
+ String objective,
+ String industry,
+ int minCost,
+ int maxCost
+ ) {
+ return EventRecommendation.builder()
+ .optionNumber(optionNumber)
+ .concept(concept)
+ .title(objective + " - " + concept)
+ .description("AI 서비스가 일시적으로 사용 불가능하여 기본 추천안을 제공합니다. " +
+ industry + " 업종에 적합한 " + concept + "입니다.")
+ .targetAudience("일반 고객")
+ .duration(EventRecommendation.Duration.builder()
+ .recommendedDays(14)
+ .recommendedPeriod("2주")
+ .build())
+ .mechanics(EventRecommendation.Mechanics.builder()
+ .type(EventMechanicsType.DISCOUNT)
+ .details("할인 쿠폰 제공 또는 경품 추첨")
+ .build())
+ .promotionChannels(List.of("Instagram", "네이버 블로그", "카카오톡 채널"))
+ .estimatedCost(EventRecommendation.EstimatedCost.builder()
+ .min(minCost)
+ .max(maxCost)
+ .breakdown(Map.of(
+ "경품비", minCost / 2,
+ "홍보비", minCost / 2
+ ))
+ .build())
+ .expectedMetrics(ExpectedMetrics.builder()
+ .newCustomers(ExpectedMetrics.Range.builder().min(30.0).max(50.0).build())
+ .revenueIncrease(ExpectedMetrics.Range.builder().min(10.0).max(20.0).build())
+ .roi(ExpectedMetrics.Range.builder().min(100.0).max(150.0).build())
+ .build())
+ .differentiator("AI 분석이 제한적으로 제공되는 기본 추천안입니다")
+ .build();
+ }
+}
diff --git a/ai-service/src/main/java/com/kt/ai/client/ClaudeApiClient.java b/ai-service/src/main/java/com/kt/ai/client/ClaudeApiClient.java
new file mode 100644
index 0000000..abc2137
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/client/ClaudeApiClient.java
@@ -0,0 +1,39 @@
+package com.kt.ai.client;
+
+import com.kt.ai.client.config.FeignClientConfig;
+import com.kt.ai.client.dto.ClaudeRequest;
+import com.kt.ai.client.dto.ClaudeResponse;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+
+/**
+ * Claude API Feign Client
+ * API Docs: https://docs.anthropic.com/claude/reference/messages_post
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@FeignClient(
+ name = "claudeApiClient",
+ url = "${ai.claude.api-url}",
+ configuration = FeignClientConfig.class
+)
+public interface ClaudeApiClient {
+
+ /**
+ * Claude Messages API 호출
+ *
+ * @param apiKey Claude API Key
+ * @param anthropicVersion API Version (2023-06-01)
+ * @param request Claude 요청
+ * @return Claude 응답
+ */
+ @PostMapping(consumes = "application/json", produces = "application/json")
+ ClaudeResponse sendMessage(
+ @RequestHeader("x-api-key") String apiKey,
+ @RequestHeader("anthropic-version") String anthropicVersion,
+ @RequestBody ClaudeRequest request
+ );
+}
diff --git a/ai-service/src/main/java/com/kt/ai/client/config/FeignClientConfig.java b/ai-service/src/main/java/com/kt/ai/client/config/FeignClientConfig.java
new file mode 100644
index 0000000..f68466c
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/client/config/FeignClientConfig.java
@@ -0,0 +1,57 @@
+package com.kt.ai.client.config;
+
+import feign.Logger;
+import feign.Request;
+import feign.Retryer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Feign Client 설정
+ * - Claude API / GPT-4 API 연동 설정
+ * - Timeout, Retry 설정
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@Configuration
+public class FeignClientConfig {
+
+ /**
+ * Feign Logger Level 설정
+ */
+ @Bean
+ public Logger.Level feignLoggerLevel() {
+ return Logger.Level.FULL;
+ }
+
+ /**
+ * Feign Request Options (Timeout 설정)
+ * - Connect Timeout: 10초
+ * - Read Timeout: 5분 (300초)
+ */
+ @Bean
+ public Request.Options requestOptions() {
+ return new Request.Options(
+ 10, TimeUnit.SECONDS, // connectTimeout
+ 300, TimeUnit.SECONDS, // readTimeout (5분)
+ true // followRedirects
+ );
+ }
+
+ /**
+ * Feign Retryer 설정
+ * - 최대 3회 재시도
+ * - Exponential Backoff: 1초, 5초, 10초
+ */
+ @Bean
+ public Retryer retryer() {
+ return new Retryer.Default(
+ 1000L, // period (1초)
+ 5000L, // maxPeriod (5초)
+ 3 // maxAttempts (3회)
+ );
+ }
+}
diff --git a/ai-service/src/main/java/com/kt/ai/client/dto/ClaudeRequest.java b/ai-service/src/main/java/com/kt/ai/client/dto/ClaudeRequest.java
new file mode 100644
index 0000000..6dd394b
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/client/dto/ClaudeRequest.java
@@ -0,0 +1,67 @@
+package com.kt.ai.client.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * Claude API 요청 DTO
+ * API Docs: https://docs.anthropic.com/claude/reference/messages_post
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ClaudeRequest {
+ /**
+ * 모델명 (예: claude-3-5-sonnet-20241022)
+ */
+ private String model;
+
+ /**
+ * 메시지 목록
+ */
+ private List messages;
+
+ /**
+ * 최대 토큰 수
+ */
+ @JsonProperty("max_tokens")
+ private Integer maxTokens;
+
+ /**
+ * Temperature (0.0 ~ 1.0)
+ */
+ private Double temperature;
+
+ /**
+ * System 프롬프트 (선택)
+ */
+ private String system;
+
+ /**
+ * 메시지
+ */
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class Message {
+ /**
+ * 역할 (user, assistant)
+ */
+ private String role;
+
+ /**
+ * 메시지 내용
+ */
+ private String content;
+ }
+}
diff --git a/ai-service/src/main/java/com/kt/ai/client/dto/ClaudeResponse.java b/ai-service/src/main/java/com/kt/ai/client/dto/ClaudeResponse.java
new file mode 100644
index 0000000..d587474
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/client/dto/ClaudeResponse.java
@@ -0,0 +1,108 @@
+package com.kt.ai.client.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * Claude API 응답 DTO
+ * API Docs: https://docs.anthropic.com/claude/reference/messages_post
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ClaudeResponse {
+ /**
+ * 응답 ID
+ */
+ private String id;
+
+ /**
+ * 타입 (message)
+ */
+ private String type;
+
+ /**
+ * 역할 (assistant)
+ */
+ private String role;
+
+ /**
+ * 콘텐츠 목록
+ */
+ private List content;
+
+ /**
+ * 모델명
+ */
+ private String model;
+
+ /**
+ * 중단 이유 (end_turn, max_tokens, stop_sequence)
+ */
+ @JsonProperty("stop_reason")
+ private String stopReason;
+
+ /**
+ * 사용량
+ */
+ private Usage usage;
+
+ /**
+ * 콘텐츠
+ */
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class Content {
+ /**
+ * 타입 (text)
+ */
+ private String type;
+
+ /**
+ * 텍스트 내용
+ */
+ private String text;
+ }
+
+ /**
+ * 토큰 사용량
+ */
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class Usage {
+ /**
+ * 입력 토큰 수
+ */
+ @JsonProperty("input_tokens")
+ private Integer inputTokens;
+
+ /**
+ * 출력 토큰 수
+ */
+ @JsonProperty("output_tokens")
+ private Integer outputTokens;
+ }
+
+ /**
+ * 텍스트 내용 추출
+ */
+ public String extractText() {
+ if (content != null && !content.isEmpty()) {
+ return content.get(0).getText();
+ }
+ return null;
+ }
+}
diff --git a/ai-service/src/main/java/com/kt/ai/config/CircuitBreakerConfig.java b/ai-service/src/main/java/com/kt/ai/config/CircuitBreakerConfig.java
new file mode 100644
index 0000000..c4e7b8d
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/config/CircuitBreakerConfig.java
@@ -0,0 +1,71 @@
+package com.kt.ai.config;
+
+import io.github.resilience4j.circuitbreaker.CircuitBreaker;
+import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig.SlidingWindowType;
+import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
+import io.github.resilience4j.timelimiter.TimeLimiterConfig;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+
+/**
+ * Circuit Breaker 설정
+ * - Claude API / GPT-4 API 장애 대응
+ * - Timeout: 5분 (300초)
+ * - Failure Threshold: 50%
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@Configuration
+public class CircuitBreakerConfig {
+
+ /**
+ * Circuit Breaker Registry 설정
+ */
+ @Bean
+ public CircuitBreakerRegistry circuitBreakerRegistry() {
+ io.github.resilience4j.circuitbreaker.CircuitBreakerConfig config =
+ io.github.resilience4j.circuitbreaker.CircuitBreakerConfig.custom()
+ .failureRateThreshold(50)
+ .slowCallRateThreshold(50)
+ .slowCallDurationThreshold(Duration.ofSeconds(60))
+ .permittedNumberOfCallsInHalfOpenState(3)
+ .maxWaitDurationInHalfOpenState(Duration.ZERO)
+ .slidingWindowType(SlidingWindowType.COUNT_BASED)
+ .slidingWindowSize(10)
+ .minimumNumberOfCalls(5)
+ .waitDurationInOpenState(Duration.ofSeconds(60))
+ .automaticTransitionFromOpenToHalfOpenEnabled(true)
+ .build();
+
+ return CircuitBreakerRegistry.of(config);
+ }
+
+ /**
+ * Claude API Circuit Breaker
+ */
+ @Bean
+ public CircuitBreaker claudeApiCircuitBreaker(CircuitBreakerRegistry registry) {
+ return registry.circuitBreaker("claudeApi");
+ }
+
+ /**
+ * GPT-4 API Circuit Breaker
+ */
+ @Bean
+ public CircuitBreaker gpt4ApiCircuitBreaker(CircuitBreakerRegistry registry) {
+ return registry.circuitBreaker("gpt4Api");
+ }
+
+ /**
+ * Time Limiter 설정 (5분)
+ */
+ @Bean
+ public TimeLimiterConfig timeLimiterConfig() {
+ return TimeLimiterConfig.custom()
+ .timeoutDuration(Duration.ofSeconds(300))
+ .build();
+ }
+}
diff --git a/ai-service/src/main/java/com/kt/ai/config/JacksonConfig.java b/ai-service/src/main/java/com/kt/ai/config/JacksonConfig.java
new file mode 100644
index 0000000..16de92f
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/config/JacksonConfig.java
@@ -0,0 +1,25 @@
+package com.kt.ai.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Jackson ObjectMapper 설정
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@Configuration
+public class JacksonConfig {
+
+ @Bean
+ public ObjectMapper objectMapper() {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.registerModule(new JavaTimeModule());
+ mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ return mapper;
+ }
+}
diff --git a/ai-service/src/main/java/com/kt/ai/config/KafkaConsumerConfig.java b/ai-service/src/main/java/com/kt/ai/config/KafkaConsumerConfig.java
new file mode 100644
index 0000000..23df4d9
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/config/KafkaConsumerConfig.java
@@ -0,0 +1,76 @@
+package com.kt.ai.config;
+
+import com.kt.ai.kafka.message.AIJobMessage;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.common.serialization.StringDeserializer;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.annotation.EnableKafka;
+import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
+import org.springframework.kafka.core.ConsumerFactory;
+import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
+import org.springframework.kafka.listener.ContainerProperties;
+import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer;
+import org.springframework.kafka.support.serializer.JsonDeserializer;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Kafka Consumer 설정
+ * - Topic: ai-event-generation-job
+ * - Consumer Group: ai-service-consumers
+ * - Manual ACK 모드
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@EnableKafka
+@Configuration
+public class KafkaConsumerConfig {
+
+ @Value("${spring.kafka.bootstrap-servers}")
+ private String bootstrapServers;
+
+ @Value("${spring.kafka.consumer.group-id}")
+ private String groupId;
+
+ /**
+ * Kafka Consumer 팩토리 설정
+ */
+ @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.AUTO_OFFSET_RESET_CONFIG, "earliest");
+ props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
+ props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 10);
+ props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 30000);
+
+ // Key Deserializer
+ props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+
+ // Value Deserializer with Error Handling
+ props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
+ props.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class.getName());
+ props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, AIJobMessage.class.getName());
+ props.put(JsonDeserializer.TRUSTED_PACKAGES, "*");
+
+ return new DefaultKafkaConsumerFactory<>(props);
+ }
+
+ /**
+ * Kafka Listener Container Factory 설정
+ * - Manual ACK 모드
+ */
+ @Bean
+ public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
+ ConcurrentKafkaListenerContainerFactory factory =
+ new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(consumerFactory());
+ factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
+ return factory;
+ }
+}
diff --git a/ai-service/src/main/java/com/kt/ai/config/RedisConfig.java b/ai-service/src/main/java/com/kt/ai/config/RedisConfig.java
new file mode 100644
index 0000000..1790966
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/config/RedisConfig.java
@@ -0,0 +1,120 @@
+package com.kt.ai.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import io.lettuce.core.ClientOptions;
+import io.lettuce.core.SocketOptions;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import java.time.Duration;
+
+/**
+ * Redis 설정
+ * - 작업 상태 및 추천 결과 캐싱
+ * - TTL: 추천 24시간, Job 상태 24시간, 트렌드 1시간
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@Configuration
+public class RedisConfig {
+
+ @Value("${spring.data.redis.host}")
+ private String redisHost;
+
+ @Value("${spring.data.redis.port}")
+ private int redisPort;
+
+ @Value("${spring.data.redis.password}")
+ private String redisPassword;
+
+ @Value("${spring.data.redis.database}")
+ private int redisDatabase;
+
+ @Value("${spring.data.redis.timeout:3000}")
+ private long redisTimeout;
+
+ /**
+ * Redis 연결 팩토리 설정
+ */
+ @Bean
+ public RedisConnectionFactory redisConnectionFactory() {
+ RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
+ config.setHostName(redisHost);
+ config.setPort(redisPort);
+ if (redisPassword != null && !redisPassword.isEmpty()) {
+ config.setPassword(redisPassword);
+ }
+ config.setDatabase(redisDatabase);
+
+ // Lettuce Client 설정: Timeout 및 Connection 옵션
+ SocketOptions socketOptions = SocketOptions.builder()
+ .connectTimeout(Duration.ofMillis(redisTimeout))
+ .keepAlive(true)
+ .build();
+
+ ClientOptions clientOptions = ClientOptions.builder()
+ .socketOptions(socketOptions)
+ .autoReconnect(true)
+ .build();
+
+ LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
+ .commandTimeout(Duration.ofMillis(redisTimeout))
+ .clientOptions(clientOptions)
+ .build();
+
+ // afterPropertiesSet() 제거: Spring이 자동으로 호출함
+ return new LettuceConnectionFactory(config, clientConfig);
+ }
+
+ /**
+ * ObjectMapper for Redis (Java 8 Date/Time 지원)
+ */
+ @Bean
+ public ObjectMapper redisObjectMapper() {
+ ObjectMapper mapper = new ObjectMapper();
+
+ // Java 8 Date/Time 모듈 등록
+ mapper.registerModule(new JavaTimeModule());
+
+ // Timestamp 대신 ISO-8601 형식으로 직렬화
+ mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+
+ return mapper;
+ }
+
+ /**
+ * RedisTemplate 설정
+ * - Key: String
+ * - Value: JSON (Jackson with Java 8 Date/Time support)
+ */
+ @Bean
+ public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(connectionFactory);
+
+ // Key Serializer: String
+ template.setKeySerializer(new StringRedisSerializer());
+ template.setHashKeySerializer(new StringRedisSerializer());
+
+ // Value Serializer: JSON with Java 8 Date/Time support
+ GenericJackson2JsonRedisSerializer serializer =
+ new GenericJackson2JsonRedisSerializer(redisObjectMapper());
+
+ template.setValueSerializer(serializer);
+ template.setHashValueSerializer(serializer);
+
+ template.afterPropertiesSet();
+ return template;
+ }
+}
diff --git a/ai-service/src/main/java/com/kt/ai/config/SecurityConfig.java b/ai-service/src/main/java/com/kt/ai/config/SecurityConfig.java
new file mode 100644
index 0000000..08e9b2e
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/config/SecurityConfig.java
@@ -0,0 +1,67 @@
+package com.kt.ai.config;
+
+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.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Spring Security 설정
+ * - Internal API만 제공 (Event Service에서만 호출)
+ * - JWT 인증 없음 (내부 통신)
+ * - CORS 설정
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+
+ /**
+ * Security Filter Chain 설정
+ * - 모든 요청 허용 (내부 API)
+ * - CSRF 비활성화
+ * - Stateless 세션
+ */
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/health", "/actuator/**", "/v3/api-docs/**", "/swagger-ui/**").permitAll()
+ .requestMatchers("/internal/**").permitAll() // Internal API
+ .anyRequest().permitAll()
+ );
+
+ return http.build();
+ }
+
+ /**
+ * CORS 설정
+ */
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+ configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080"));
+ configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
+ configuration.setAllowedHeaders(List.of("*"));
+ configuration.setAllowCredentials(true);
+ configuration.setMaxAge(3600L);
+
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", configuration);
+ return source;
+ }
+}
diff --git a/ai-service/src/main/java/com/kt/ai/config/SwaggerConfig.java b/ai-service/src/main/java/com/kt/ai/config/SwaggerConfig.java
new file mode 100644
index 0000000..4523c0d
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/config/SwaggerConfig.java
@@ -0,0 +1,64 @@
+package com.kt.ai.config;
+
+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.servers.Server;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+/**
+ * Swagger/OpenAPI 설정
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@Configuration
+public class SwaggerConfig {
+
+ @Bean
+ public OpenAPI openAPI() {
+ Server localServer = new Server();
+ localServer.setUrl("http://localhost:8083");
+ localServer.setDescription("Local Development Server");
+
+ Server devServer = new Server();
+ devServer.setUrl("https://dev-api.kt-event-marketing.com/ai/v1");
+ devServer.setDescription("Development Server");
+
+ Server prodServer = new Server();
+ prodServer.setUrl("https://api.kt-event-marketing.com/ai/v1");
+ prodServer.setDescription("Production Server");
+
+ Contact contact = new Contact();
+ contact.setName("Digital Garage Team");
+ contact.setEmail("support@kt-event-marketing.com");
+
+ Info info = new Info()
+ .title("AI Service API")
+ .version("1.0.0")
+ .description("""
+ KT AI 기반 소상공인 이벤트 자동 생성 서비스 - AI Service
+
+ ## 서비스 개요
+ - Kafka를 통한 비동기 AI 추천 처리
+ - Claude API / GPT-4 API 연동
+ - Redis 기반 결과 캐싱 (TTL 24시간)
+
+ ## 처리 흐름
+ 1. Event Service가 Kafka Topic에 Job 메시지 발행
+ 2. AI Service가 메시지 구독 및 처리
+ 3. 트렌드 분석 수행 (Claude/GPT-4 API)
+ 4. 3가지 이벤트 추천안 생성
+ 5. 결과를 Redis에 저장 (TTL 24시간)
+ 6. Job 상태를 Redis에 업데이트
+ """)
+ .contact(contact);
+
+ return new OpenAPI()
+ .info(info)
+ .servers(List.of(localServer, devServer, prodServer));
+ }
+}
diff --git a/ai-service/src/main/java/com/kt/ai/controller/HealthController.java b/ai-service/src/main/java/com/kt/ai/controller/HealthController.java
new file mode 100644
index 0000000..b54b890
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/controller/HealthController.java
@@ -0,0 +1,91 @@
+package com.kt.ai.controller;
+
+import com.kt.ai.model.dto.response.HealthCheckResponse;
+import com.kt.ai.model.enums.CircuitBreakerState;
+import com.kt.ai.model.enums.ServiceStatus;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDateTime;
+
+/**
+ * 헬스체크 Controller
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@Slf4j
+@Tag(name = "Health Check", description = "서비스 상태 확인")
+@RestController
+public class HealthController {
+
+ @Autowired(required = false)
+ private RedisTemplate redisTemplate;
+
+ /**
+ * 서비스 헬스체크
+ */
+ @Operation(summary = "서비스 헬스체크", description = "AI Service 상태 및 외부 연동 확인")
+ @GetMapping("/api/v1/ai-service/health")
+ public ResponseEntity healthCheck() {
+ // Redis 상태 확인
+ ServiceStatus redisStatus = checkRedis();
+
+ // 전체 서비스 상태 (Redis가 DOWN이면 DEGRADED, UNKNOWN이면 UP으로 처리)
+ ServiceStatus overallStatus;
+ if (redisStatus == ServiceStatus.DOWN) {
+ overallStatus = ServiceStatus.DEGRADED;
+ } else {
+ overallStatus = ServiceStatus.UP;
+ }
+
+ HealthCheckResponse.Services services = HealthCheckResponse.Services.builder()
+ .kafka(ServiceStatus.UP) // TODO: 실제 Kafka 상태 확인
+ .redis(redisStatus)
+ .claudeApi(ServiceStatus.UP) // TODO: 실제 Claude API 상태 확인
+ .gpt4Api(ServiceStatus.UP) // TODO: 실제 GPT-4 API 상태 확인 (선택)
+ .circuitBreaker(CircuitBreakerState.CLOSED) // TODO: 실제 Circuit Breaker 상태 확인
+ .build();
+
+ HealthCheckResponse response = HealthCheckResponse.builder()
+ .status(overallStatus)
+ .timestamp(LocalDateTime.now())
+ .services(services)
+ .build();
+
+ return ResponseEntity.ok(response);
+ }
+
+ /**
+ * Redis 연결 상태 확인
+ */
+ private ServiceStatus checkRedis() {
+ // RedisTemplate이 주입되지 않은 경우 (로컬 환경 등)
+ if (redisTemplate == null) {
+ log.warn("RedisTemplate이 주입되지 않았습니다. Redis 상태를 UNKNOWN으로 표시합니다.");
+ return ServiceStatus.UNKNOWN;
+ }
+
+ try {
+ log.debug("Redis 연결 테스트 시작...");
+ String pong = redisTemplate.getConnectionFactory().getConnection().ping();
+ log.info("✅ Redis 연결 성공! PING 응답: {}", pong);
+ return ServiceStatus.UP;
+ } catch (Exception e) {
+ log.error("❌ Redis 연결 실패", e);
+ log.error("상세 오류 정보:");
+ log.error(" - 오류 타입: {}", e.getClass().getName());
+ log.error(" - 오류 메시지: {}", e.getMessage());
+ if (e.getCause() != null) {
+ log.error(" - 원인: {}", e.getCause().getMessage());
+ }
+ return ServiceStatus.DOWN;
+ }
+ }
+}
diff --git a/ai-service/src/main/java/com/kt/ai/controller/InternalJobController.java b/ai-service/src/main/java/com/kt/ai/controller/InternalJobController.java
new file mode 100644
index 0000000..aba5cc0
--- /dev/null
+++ b/ai-service/src/main/java/com/kt/ai/controller/InternalJobController.java
@@ -0,0 +1,92 @@
+package com.kt.ai.controller;
+
+import com.kt.ai.model.dto.response.JobStatusResponse;
+import com.kt.ai.model.enums.JobStatus;
+import com.kt.ai.service.CacheService;
+import com.kt.ai.service.JobStatusService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Internal Job Controller
+ * Event Service에서 호출하는 내부 API
+ *
+ * @author AI Service Team
+ * @since 1.0.0
+ */
+@Slf4j
+@Tag(name = "Internal API", description = "내부 서비스 간 통신용 API")
+@RestController
+@RequestMapping("/api/v1/ai-service/internal/jobs")
+@RequiredArgsConstructor
+public class InternalJobController {
+
+ private final JobStatusService jobStatusService;
+ private final CacheService cacheService;
+
+ /**
+ * 작업 상태 조회
+ */
+ @Operation(summary = "작업 상태 조회", description = "Redis에 저장된 AI 추천 작업 상태 조회")
+ @GetMapping("/{jobId}/status")
+ public ResponseEntity getJobStatus(@PathVariable String jobId) {
+ log.info("Job 상태 조회 요청: jobId={}", jobId);
+ JobStatusResponse response = jobStatusService.getJobStatus(jobId);
+ return ResponseEntity.ok(response);
+ }
+
+ /**
+ * Redis 디버그: Job 상태 테스트 데이터 생성
+ */
+ @Operation(summary = "Job 테스트 데이터 생성 (디버그)", description = "Redis에 샘플 Job 상태 데이터 저장")
+ @GetMapping("/debug/create-test-job/{jobId}")
+ public ResponseEntity