Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c768fff11e | |||
| f07002ac33 | |||
| 2ca453f89e | |||
| e2179daaf7 | |||
| de32a70f29 | |||
| 435ba1a86c | |||
| 429f737066 | |||
| d56ff7684b | |||
| c152faff54 | |||
| ee664a6134 | |||
| 50043add5d | |||
| d89ee4edf7 | |||
| e0fc4286c7 | |||
| 060921e756 | |||
| b198c46d06 | |||
| 003b3843cc | |||
| 55e546e0b3 | |||
| e70f121db5 | |||
| 6465719b2c |
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
command: "/deploy-build-image-back"
|
||||
---
|
||||
|
||||
@cicd
|
||||
'백엔드컨테이너이미지작성가이드'에 따라 컨테이너 이미지를 작성해 주세요.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
command: "/deploy-build-image-front"
|
||||
---
|
||||
|
||||
@cicd
|
||||
'프론트엔드컨테이너이미지작성가이드'에 따라 컨테이너 이미지를 작성해 주세요.
|
||||
@@ -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 중 선택하여 사용합니다
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
command: "/deploy-k8s-guide-back"
|
||||
---
|
||||
|
||||
@cicd
|
||||
'백엔드배포가이드'에 따라 백엔드 서비스 배포 방법을 작성해 주세요.
|
||||
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
||||
{안내메시지}
|
||||
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
||||
[실행정보]
|
||||
- ACR명: acrdigitalgarage01
|
||||
- k8s명: aks-digitalgarage-01
|
||||
- 네임스페이스: tripgen
|
||||
- 파드수: 2
|
||||
- 리소스(CPU): 256m/1024m
|
||||
- 리소스(메모리): 256Mi/1024Mi
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,3 +1,6 @@
|
||||
---
|
||||
command: "/design-api"
|
||||
---
|
||||
@architecture
|
||||
API를 설계해 주세요:
|
||||
- '공통설계원칙'과 'API설계가이드'를 준용하여 설계
|
||||
- '공통설계원칙'과 'API설계가이드'를 준용하여 설계
|
||||
@@ -1,3 +1,6 @@
|
||||
---
|
||||
command: "/design-class"
|
||||
---
|
||||
@architecture
|
||||
'공통설계원칙'과 '클래스설계가이드'를 준용하여 클래스를 설계해 주세요.
|
||||
프롬프트에 '[클래스설계 정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시합니다.
|
||||
@@ -9,4 +12,4 @@
|
||||
- User: Layered
|
||||
- Trip: Clean
|
||||
- Location: Layered
|
||||
- AI: Layered
|
||||
- AI: Layered
|
||||
@@ -1,3 +1,6 @@
|
||||
---
|
||||
command: "/design-data"
|
||||
---
|
||||
@architecture
|
||||
데이터 설계를 해주세요:
|
||||
- '공통설계원칙'과 '데이터설계가이드'를 준용하여 설계
|
||||
- '공통설계원칙'과 '데이터설계가이드'를 준용하여 설계
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
command: "/design-fix-prototype"
|
||||
---
|
||||
@fix as @front
|
||||
'[오류내용]'섹션에 제공된 오류를 해결해 주세요.
|
||||
프롬프트에 '[오류내용]'섹션이 없으면 수행 중단하고 안내 메시지 표시
|
||||
{안내메시지}
|
||||
'[오류내용]'섹션 하위에 오류 내용을 제공
|
||||
'[오류내용]'섹션 하위에 오류 내용을 제공
|
||||
@@ -1,3 +1,6 @@
|
||||
---
|
||||
command: "/design-front"
|
||||
---
|
||||
@plan as @front
|
||||
'프론트엔드설계가이드'를 준용하여 **프론트엔드설계서**를 작성해 주세요.
|
||||
프롬프트에 '[백엔드시스템]'항목이 없으면 수행을 중단하고 안내 메시지를 표시합니다.
|
||||
@@ -13,4 +16,4 @@
|
||||
- ai service: http://localhost:8084/v3/api-docs
|
||||
[요구사항]
|
||||
- 각 화면에 Back 아이콘 버튼과 화면 타이틀 표시
|
||||
- 하단 네비게이션 바 아이콘화: 홈, 새여행, 주변장소검색, 여행보기
|
||||
- 하단 네비게이션 바 아이콘화: 홈, 새여행, 주변장소검색, 여행보기
|
||||
@@ -1,6 +1,9 @@
|
||||
---
|
||||
command: "/design-high-level"
|
||||
---
|
||||
@architecture
|
||||
'HighLevel아키텍처정의가이드'를 준용하여 High Level 아키텍처 정의서를 작성해 주세요.
|
||||
'CLOUD' 정보가 없으면 수행을 중단하고 안내메시지를 표시하세요.
|
||||
{안내메시지}
|
||||
아래 예와 같이 CLOUD 제공자를 Azure, AWS, Google과 같이 제공하세요.
|
||||
- CLOUD: Azure
|
||||
- CLOUD: Azure
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
command: "/design-improve-prototype"
|
||||
---
|
||||
@improve as @front
|
||||
'[개선내용]'섹션에 있는 내용을 개선해 주세요.
|
||||
프롬프트에 '[개선내용]'항목이 없으면 수행을 중단하고 안내 메시지 표시
|
||||
{안내메시지}
|
||||
'[개선내용]'섹션 하위에 개선할 내용을 제공
|
||||
'[개선내용]'섹션 하위에 개선할 내용을 제공
|
||||
@@ -1,2 +1,5 @@
|
||||
---
|
||||
command: "/design-improve-userstory"
|
||||
---
|
||||
@analyze as @front 프로토타입을 웹브라우저에서 분석한 후,
|
||||
@document as @scribe 수정된 프로토타입에 따라 유저스토리를 업데이트 해주십시오.
|
||||
@document as @scribe 수정된 프로토타입에 따라 유저스토리를 업데이트 해주십시오.
|
||||
@@ -1,3 +1,6 @@
|
||||
---
|
||||
command: "/design-logical"
|
||||
---
|
||||
@architecture
|
||||
논리 아키텍처를 설계해 주세요:
|
||||
- '공통설계원칙'과 '논리아키텍처 설계 가이드'를 준용하여 설계
|
||||
- '공통설계원칙'과 '논리아키텍처 설계 가이드'를 준용하여 설계
|
||||
@@ -1,3 +1,6 @@
|
||||
---
|
||||
command: "/design-pattern"
|
||||
---
|
||||
@design-pattern
|
||||
클라우드 아키텍처 패턴 적용 방안을 작성해 주세요:
|
||||
- '클라우드아키텍처패턴선정가이드'를 준용하여 작성
|
||||
- '클라우드아키텍처패턴선정가이드'를 준용하여 작성
|
||||
@@ -1,6 +1,9 @@
|
||||
---
|
||||
command: "/design-physical"
|
||||
---
|
||||
@architecture
|
||||
'물리아키텍처설계가이드'를 준용하여 물리아키텍처를 설계해 주세요.
|
||||
'CLOUD' 정보가 없으면 수행을 중단하고 안내메시지를 표시하세요.
|
||||
{안내메시지}
|
||||
아래 예와 같이 CLOUD 제공자를 Azure, AWS, Google과 같이 제공하세요.
|
||||
- CLOUD: Azure
|
||||
- CLOUD: Azure
|
||||
@@ -1,3 +1,6 @@
|
||||
---
|
||||
command: "/design-prototype"
|
||||
---
|
||||
@prototype
|
||||
프로토타입을 작성해 주세요:
|
||||
- '프로토타입작성가이드'를 준용하여 작성
|
||||
- '프로토타입작성가이드'를 준용하여 작성
|
||||
@@ -1,3 +1,6 @@
|
||||
---
|
||||
command: "/design-seq-inner"
|
||||
---
|
||||
@architecture
|
||||
내부 시퀀스 설계를 해 주세요:
|
||||
- '공통설계원칙'과 '내부시퀀스설계 가이드'를 준용하여 설계
|
||||
- '공통설계원칙'과 '내부시퀀스설계 가이드'를 준용하여 설계
|
||||
@@ -1,3 +1,6 @@
|
||||
---
|
||||
command: "/design-seq-outer"
|
||||
---
|
||||
@architecture
|
||||
외부 시퀀스 설계를 해 주세요:
|
||||
- '공통설계원칙'과 '외부시퀀스설계가이드'를 준용하여 설계
|
||||
- '공통설계원칙'과 '외부시퀀스설계가이드'를 준용하여 설계
|
||||
@@ -1,2 +1,5 @@
|
||||
---
|
||||
command: "/design-test-prototype"
|
||||
---
|
||||
@test-front
|
||||
프로토타입을 테스트 해 주세요.
|
||||
프로토타입을 테스트 해 주세요.
|
||||
@@ -1,3 +1,6 @@
|
||||
---
|
||||
command: "/design-uiux"
|
||||
---
|
||||
@uiux
|
||||
UI/UX 설계를 해주세요:
|
||||
- 'UI/UX설계가이드'를 준용하여 작성
|
||||
- 'UI/UX설계가이드'를 준용하여 작성
|
||||
@@ -1,2 +1,5 @@
|
||||
---
|
||||
command: "/design-update-uiux"
|
||||
---
|
||||
@document @front
|
||||
현재 프로토타입과 유저스토리를 기준으로 UI/UX설계서와 스타일가이드를 수정해 주세요.
|
||||
현재 프로토타입과 유저스토리를 기준으로 UI/UX설계서와 스타일가이드를 수정해 주세요.
|
||||
@@ -1,3 +1,6 @@
|
||||
---
|
||||
command: "/think-help"
|
||||
---
|
||||
기획 작업 순서
|
||||
|
||||
1단계: 서비스 기획
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
---
|
||||
command: "/think-planning"
|
||||
---
|
||||
아래 내용을 터미널에 표시만 하고 수행을 하지는 않습니다.
|
||||
```
|
||||
아래 가이드를 참고하여 서비스 기획을 수행합니다.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
command: "/think-userstory"
|
||||
---
|
||||
```
|
||||
@document
|
||||
유저스토리를 작성하세요.
|
||||
프롬프트에 '[요구사항]'섹션이 없으면 수행을 중단하고 안내 메시지를 표시합니다.
|
||||
@@ -16,3 +20,5 @@ Case 2) 다른 방법으로 이벤트스토밍을 한 경우는 요구사항을
|
||||
2. 유저스토리 작성
|
||||
- '유저스토리작성방법'과 '유저스토리예제'를 참고하여 유저스토리를 작성
|
||||
- 결과파일은 'design/userstory.md'에 생성
|
||||
|
||||
```
|
||||
|
||||
@@ -61,3 +61,5 @@ k8s/**/*-local.yaml
|
||||
|
||||
# Gradle (로컬 환경 설정)
|
||||
gradle.properties
|
||||
*.hprof
|
||||
test-data.json
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="EventServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" folderName="Event Service">
|
||||
<option name="ACTIVE_PROFILES" />
|
||||
<option name="ENABLE_LAUNCH_OPTIMIZATION" value="true" />
|
||||
<envs>
|
||||
<env name="DB_HOST" value="20.249.177.232" />
|
||||
<env name="DB_PORT" value="5432" />
|
||||
<env name="DB_NAME" value="eventdb" />
|
||||
<env name="DB_USERNAME" value="eventuser" />
|
||||
<env name="DB_PASSWORD" value="Hi5Jessica!" />
|
||||
<env name="REDIS_HOST" value="localhost" />
|
||||
<env name="REDIS_PORT" value="6379" />
|
||||
<env name="REDIS_PASSWORD" value="" />
|
||||
<env name="KAFKA_BOOTSTRAP_SERVERS" value="localhost:9092" />
|
||||
<env name="SERVER_PORT" value="8081" />
|
||||
<env name="DDL_AUTO" value="update" />
|
||||
<env name="LOG_LEVEL" value="DEBUG" />
|
||||
<env name="SQL_LOG_LEVEL" value="DEBUG" />
|
||||
<env name="DISTRIBUTION_SERVICE_URL" value="http://localhost:8084" />
|
||||
</envs>
|
||||
<module name="kt-event-marketing.event-service.main" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.event.eventservice.EventServiceApplication" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1,89 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="analytics-service" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="env">
|
||||
<map>
|
||||
<!-- Database Settings -->
|
||||
<entry key="DB_KIND" value="postgresql" />
|
||||
<entry key="DB_HOST" value="4.230.49.9" />
|
||||
<entry key="DB_PORT" value="5432" />
|
||||
<entry key="DB_NAME" value="analyticdb" />
|
||||
<entry key="DB_USERNAME" value="eventuser" />
|
||||
<entry key="DB_PASSWORD" value="Hi5Jessica!" />
|
||||
|
||||
<!-- Redis Settings -->
|
||||
<entry key="REDIS_HOST" value="20.214.210.71" />
|
||||
<entry key="REDIS_PORT" value="6379" />
|
||||
<entry key="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||
<entry key="REDIS_DATABASE" value="5" />
|
||||
|
||||
<!-- Kafka Settings -->
|
||||
<entry key="KAFKA_ENABLED" value="true" />
|
||||
<entry key="KAFKA_BOOTSTRAP_SERVERS" value="4.230.50.63:9092" />
|
||||
<entry key="KAFKA_CONSUMER_GROUP_ID" value="analytics-service" />
|
||||
|
||||
<!-- Sample Data Settings (MVP Only) -->
|
||||
<!-- ⚠️ 실제 운영 환경에서는 false로 설정 (다른 서비스들이 이벤트 발행) -->
|
||||
<entry key="SAMPLE_DATA_ENABLED" value="true" />
|
||||
|
||||
<!-- JPA Settings -->
|
||||
<entry key="SHOW_SQL" value="true" />
|
||||
<entry key="DDL_AUTO" value="update" />
|
||||
|
||||
<!-- Server Settings -->
|
||||
<entry key="SERVER_PORT" value="8086" />
|
||||
|
||||
<!-- JWT Settings -->
|
||||
<entry key="JWT_SECRET" value="dev-jwt-secret-key-for-development-only-analytics-service-2024" />
|
||||
<entry key="JWT_ACCESS_TOKEN_VALIDITY" value="1800" />
|
||||
<entry key="JWT_REFRESH_TOKEN_VALIDITY" value="86400" />
|
||||
|
||||
<!-- CORS Settings -->
|
||||
<entry key="CORS_ALLOWED_ORIGINS" value="http://localhost:*" />
|
||||
|
||||
<!-- Logging Settings -->
|
||||
<entry key="LOG_LEVEL_APP" value="DEBUG" />
|
||||
<entry key="LOG_LEVEL_WEB" value="INFO" />
|
||||
<entry key="LOG_LEVEL_SQL" value="DEBUG" />
|
||||
<entry key="LOG_LEVEL_SQL_TYPE" value="TRACE" />
|
||||
<entry key="LOG_FILE" value="logs/analytics-service.log" />
|
||||
|
||||
<!-- Batch Settings -->
|
||||
<entry key="BATCH_ENABLED" value="true" />
|
||||
<entry key="BATCH_REFRESH_INTERVAL" value="300000" />
|
||||
<entry key="BATCH_INITIAL_DELAY" value="30000" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="analytics-service:bootRun" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
|
||||
<extension name="net.ashald.envfile">
|
||||
<option name="IS_ENABLED" value="false" />
|
||||
<option name="IS_SUBST" value="false" />
|
||||
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
|
||||
<option name="IS_IGNORE_MISSING_FILES" value="false" />
|
||||
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
|
||||
<ENTRIES>
|
||||
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
|
||||
</ENTRIES>
|
||||
</extension>
|
||||
</EXTENSION>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -286,6 +286,11 @@ public class SampleDataLoader implements ApplicationRunner {
|
||||
|
||||
publishEvent(PARTICIPANT_REGISTERED_TOPIC, event);
|
||||
totalPublished++;
|
||||
|
||||
// 동시성 충돌 방지: 10개마다 100ms 대기
|
||||
if ((j + 1) % 10 == 0) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -11,6 +11,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -37,7 +38,10 @@ public class DistributionCompletedConsumer {
|
||||
|
||||
/**
|
||||
* DistributionCompleted 이벤트 처리 (설계서 기준 - 여러 채널 배열)
|
||||
*
|
||||
* @Transactional 필수: DB 저장 작업을 위해 트랜잭션 컨텍스트 필요
|
||||
*/
|
||||
@Transactional
|
||||
@KafkaListener(topics = "sample.distribution.completed", groupId = "${spring.kafka.consumer.group-id}")
|
||||
public void handleDistributionCompleted(String message) {
|
||||
try {
|
||||
@@ -128,8 +132,8 @@ public class DistributionCompletedConsumer {
|
||||
.mapToInt(ChannelStats::getImpressions)
|
||||
.sum();
|
||||
|
||||
// EventStats 업데이트
|
||||
eventStatsRepository.findByEventId(eventId)
|
||||
// EventStats 업데이트 - 비관적 락 적용
|
||||
eventStatsRepository.findByEventIdWithLock(eventId)
|
||||
.ifPresentOrElse(
|
||||
eventStats -> {
|
||||
eventStats.setTotalViews(totalViews);
|
||||
|
||||
+4
@@ -10,6 +10,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -34,7 +35,10 @@ public class EventCreatedConsumer {
|
||||
|
||||
/**
|
||||
* EventCreated 이벤트 처리 (MVP용 샘플 토픽)
|
||||
*
|
||||
* @Transactional 필수: DB 저장 작업을 위해 트랜잭션 컨텍스트 필요
|
||||
*/
|
||||
@Transactional
|
||||
@KafkaListener(topics = "sample.event.created", groupId = "${spring.kafka.consumer.group-id}")
|
||||
public void handleEventCreated(String message) {
|
||||
try {
|
||||
|
||||
+6
-2
@@ -10,6 +10,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -34,7 +35,10 @@ public class ParticipantRegisteredConsumer {
|
||||
|
||||
/**
|
||||
* ParticipantRegistered 이벤트 처리 (MVP용 샘플 토픽)
|
||||
*
|
||||
* @Transactional 필수: 비관적 락 사용을 위해 트랜잭션 컨텍스트 필요
|
||||
*/
|
||||
@Transactional
|
||||
@KafkaListener(topics = "sample.participant.registered", groupId = "${spring.kafka.consumer.group-id}")
|
||||
public void handleParticipantRegistered(String message) {
|
||||
try {
|
||||
@@ -51,8 +55,8 @@ public class ParticipantRegisteredConsumer {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 이벤트 통계 업데이트 (참여자 수 +1)
|
||||
eventStatsRepository.findByEventId(eventId)
|
||||
// 2. 이벤트 통계 업데이트 (참여자 수 +1) - 비관적 락 적용
|
||||
eventStatsRepository.findByEventIdWithLock(eventId)
|
||||
.ifPresentOrElse(
|
||||
eventStats -> {
|
||||
eventStats.incrementParticipants();
|
||||
|
||||
+18
@@ -1,7 +1,11 @@
|
||||
package com.kt.event.analytics.repository;
|
||||
|
||||
import com.kt.event.analytics.entity.EventStats;
|
||||
import jakarta.persistence.LockModeType;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Lock;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
@@ -20,6 +24,20 @@ public interface EventStatsRepository extends JpaRepository<EventStats, Long> {
|
||||
*/
|
||||
Optional<EventStats> findByEventId(String eventId);
|
||||
|
||||
/**
|
||||
* 이벤트 ID로 통계 조회 (비관적 락 적용)
|
||||
*
|
||||
* 동시성 충돌 방지를 위해 PESSIMISTIC_WRITE 락 사용
|
||||
* - 읽는 순간부터 락을 걸어 다른 트랜잭션 차단
|
||||
* - ParticipantRegistered 이벤트 처리 시 사용
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @return 이벤트 통계
|
||||
*/
|
||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||
@Query("SELECT e FROM EventStats e WHERE e.eventId = :eventId")
|
||||
Optional<EventStats> findByEventIdWithLock(@Param("eventId") String eventId);
|
||||
|
||||
/**
|
||||
* 매장 ID와 이벤트 ID로 통계 조회
|
||||
*
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# 백엔드 컨테이너이미지 작성가이드
|
||||
|
||||
[요청사항]
|
||||
- 백엔드 각 서비스를의 컨테이너 이미지 생성
|
||||
- 실제 빌드 수행 및 검증까지 완료
|
||||
- '[결과파일]'에 수행한 명령어를 포함하여 컨테이너 이미지 작성 과정 생성
|
||||
|
||||
[작업순서]
|
||||
- 서비스명 확인
|
||||
서비스명은 settings.gradle에서 확인
|
||||
|
||||
예시) include 'common'하위의 4개가 서비스명임.
|
||||
```
|
||||
rootProject.name = 'tripgen'
|
||||
|
||||
include 'common'
|
||||
include 'user-service'
|
||||
include 'location-service'
|
||||
include 'ai-service'
|
||||
include 'trip-service'
|
||||
```
|
||||
|
||||
- 실행Jar 파일 설정
|
||||
실행Jar 파일명을 서비스명과 일치하도록 build.gradle에 설정 합니다.
|
||||
```
|
||||
bootJar {
|
||||
archiveFileName = '{서비스명}.jar'
|
||||
}
|
||||
```
|
||||
|
||||
- Dockerfile 생성
|
||||
아래 내용으로 deployment/container/Dockerfile-backend 생성
|
||||
```
|
||||
# Build stage
|
||||
FROM openjdk:23-oraclelinux8 AS builder
|
||||
ARG BUILD_LIB_DIR
|
||||
ARG ARTIFACTORY_FILE
|
||||
COPY ${BUILD_LIB_DIR}/${ARTIFACTORY_FILE} app.jar
|
||||
|
||||
# Run stage
|
||||
FROM openjdk:23-slim
|
||||
ENV USERNAME=k8s
|
||||
ENV ARTIFACTORY_HOME=/home/${USERNAME}
|
||||
ENV JAVA_OPTS=""
|
||||
|
||||
# Add a non-root user
|
||||
RUN adduser --system --group ${USERNAME} && \
|
||||
mkdir -p ${ARTIFACTORY_HOME} && \
|
||||
chown ${USERNAME}:${USERNAME} ${ARTIFACTORY_HOME}
|
||||
|
||||
WORKDIR ${ARTIFACTORY_HOME}
|
||||
COPY --from=builder app.jar app.jar
|
||||
RUN chown ${USERNAME}:${USERNAME} app.jar
|
||||
|
||||
USER ${USERNAME}
|
||||
|
||||
ENTRYPOINT [ "sh", "-c" ]
|
||||
CMD ["java ${JAVA_OPTS} -jar app.jar"]
|
||||
```
|
||||
|
||||
- 컨테이너 이미지 생성
|
||||
아래 명령으로 각 서비스 빌드. shell 파일을 생성하지 말고 command로 수행.
|
||||
서브에이젼트를 생성하여 병렬로 수행.
|
||||
```
|
||||
DOCKER_FILE=deployment/container/Dockerfile-backend
|
||||
service={서비스명}
|
||||
|
||||
docker build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_LIB_DIR="${서비스명}/build/libs" \
|
||||
--build-arg ARTIFACTORY_FILE="${서비스명}.jar" \
|
||||
-f ${DOCKER_FILE} \
|
||||
-t ${서비스명}:latest .
|
||||
```
|
||||
- 생성된 이미지 확인
|
||||
아래 명령으로 모든 서비스의 이미지가 빌드되었는지 확인
|
||||
```
|
||||
docker images | grep {서비스명}
|
||||
```
|
||||
|
||||
[결과파일]
|
||||
deployment/container/build-image.md
|
||||
@@ -0,0 +1,220 @@
|
||||
# 설계 프롬프트
|
||||
아래 순서대로 설계합니다.
|
||||
|
||||
## UI/UX 설계
|
||||
command: "/design-uiux"
|
||||
prompt:
|
||||
```
|
||||
@uiux
|
||||
UI/UX 설계를 해주세요:
|
||||
- 'UI/UX설계가이드'를 준용하여 작성
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 프로토타입 작성
|
||||
command: "/design-prototype"
|
||||
prompt:
|
||||
**1.작성**
|
||||
```
|
||||
@prototype
|
||||
프로토타입을 작성해 주세요:
|
||||
- '프로토타입작성가이드'를 준용하여 작성
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**2.검증**
|
||||
command: "/design-test-prototype"
|
||||
prompt:
|
||||
```
|
||||
@test-front
|
||||
프로토타입을 테스트 해 주세요.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**3.오류수정**
|
||||
command: "/design-fix-prototype"
|
||||
prompt:
|
||||
```
|
||||
@fix as @front
|
||||
'[오류내용]'섹션에 제공된 오류를 해결해 주세요.
|
||||
프롬프트에 '[오류내용]'섹션이 없으면 수행 중단하고 안내 메시지 표시
|
||||
{안내메시지}
|
||||
'[오류내용]'섹션 하위에 오류 내용을 제공
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**4.개선**
|
||||
command: "/design-improve-prototype"
|
||||
prompt:
|
||||
```
|
||||
@improve as @front
|
||||
'[개선내용]'섹션에 있는 내용을 개선해 주세요.
|
||||
프롬프트에 '[개선내용]'항목이 없으면 수행을 중단하고 안내 메시지 표시
|
||||
{안내메시지}
|
||||
'[개선내용]'섹션 하위에 개선할 내용을 제공
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**5.유저스토리 품질 높이기**
|
||||
command: "/design-improve-userstory"
|
||||
prompt:
|
||||
```
|
||||
@analyze as @front 프로토타입을 웹브라우저에서 분석한 후,
|
||||
@document as @scribe 수정된 프로토타입에 따라 유저스토리를 업데이트 해주십시오.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**6.설계서 다시 업데이트**
|
||||
command: "/design-update-uiux"
|
||||
prompt:
|
||||
```
|
||||
@document @front
|
||||
현재 프로토타입과 유저스토리를 기준으로 UI/UX설계서와 스타일가이드를 수정해 주세요.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 클라우드 아키텍처 패턴 선정
|
||||
command: "/design-pattern"
|
||||
prompt:
|
||||
```
|
||||
@design-pattern
|
||||
클라우드 아키텍처 패턴 적용 방안을 작성해 주세요:
|
||||
- '클라우드아키텍처패턴선정가이드'를 준용하여 작성
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 논리아키텍처 설계
|
||||
command: "/design-logical"
|
||||
prompt:
|
||||
```
|
||||
@architecture
|
||||
논리 아키텍처를 설계해 주세요:
|
||||
- '공통설계원칙'과 '논리아키텍처 설계 가이드'를 준용하여 설계
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 외부 시퀀스 설계
|
||||
command: "/design-seq-outer"
|
||||
prompt:
|
||||
```
|
||||
@architecture
|
||||
외부 시퀀스 설계를 해 주세요:
|
||||
- '공통설계원칙'과 '외부시퀀스설계가이드'를 준용하여 설계
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 내부 시퀀스 설계
|
||||
command: "/design-seq-inner"
|
||||
prompt:
|
||||
```
|
||||
@architecture
|
||||
내부 시퀀스 설계를 해 주세요:
|
||||
- '공통설계원칙'과 '내부시퀀스설계 가이드'를 준용하여 설계
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 설계
|
||||
command: "/design-api"
|
||||
prompt:
|
||||
```
|
||||
@architecture
|
||||
API를 설계해 주세요:
|
||||
- '공통설계원칙'과 'API설계가이드'를 준용하여 설계
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 클래스 설계
|
||||
command: "/design-class"
|
||||
prompt:
|
||||
```
|
||||
@architecture
|
||||
'공통설계원칙'과 '클래스설계가이드'를 준용하여 클래스를 설계해 주세요.
|
||||
프롬프트에 '[클래스설계 정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시합니다.
|
||||
{안내메시지}
|
||||
'[클래스설계 정보]' 섹션에 아래 예와 같은 정보를 제공해 주십시오.
|
||||
[클래스설계 정보]
|
||||
- 패키지 그룹: com.unicorn.tripgen
|
||||
- 설계 아키텍처 패턴
|
||||
- User: Layered
|
||||
- Trip: Clean
|
||||
- Location: Layered
|
||||
- AI: Layered
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 데이터 설계
|
||||
command: "/design-data"
|
||||
prompt:
|
||||
```
|
||||
@architecture
|
||||
데이터 설계를 해주세요:
|
||||
- '공통설계원칙'과 '데이터설계가이드'를 준용하여 설계
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## High Level 아키텍처 정의서 작성
|
||||
command: "/design-high-level"
|
||||
prompt:
|
||||
```
|
||||
@architecture
|
||||
'HighLevel아키텍처정의가이드'를 준용하여 High Level 아키텍처 정의서를 작성해 주세요.
|
||||
'CLOUD' 정보가 없으면 수행을 중단하고 안내메시지를 표시하세요.
|
||||
{안내메시지}
|
||||
아래 예와 같이 CLOUD 제공자를 Azure, AWS, Google과 같이 제공하세요.
|
||||
- CLOUD: Azure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 물리 아키텍처 설계
|
||||
command: "/design-physical"
|
||||
prompt:
|
||||
```
|
||||
@architecture
|
||||
'물리아키텍처설계가이드'를 준용하여 물리아키텍처를 설계해 주세요.
|
||||
'CLOUD' 정보가 없으면 수행을 중단하고 안내메시지를 표시하세요.
|
||||
{안내메시지}
|
||||
아래 예와 같이 CLOUD 제공자를 Azure, AWS, Google과 같이 제공하세요.
|
||||
- CLOUD: Azure
|
||||
```
|
||||
|
||||
## 프론트엔드 설계
|
||||
command: "/design-front"
|
||||
prompt:
|
||||
```
|
||||
@plan as @front
|
||||
'프론트엔드설계가이드'를 준용하여 **프론트엔드설계서**를 작성해 주세요.
|
||||
프롬프트에 '[백엔드시스템]'항목이 없으면 수행을 중단하고 안내 메시지를 표시합니다.
|
||||
{안내메시지}
|
||||
'[백엔드시스템]' 섹션에 아래 예와 같은 정보를 제공해 주십시오.
|
||||
[백엔드시스템]
|
||||
- 시스템: tripgen
|
||||
- 마이크로서비스: user-service, location-service, trip-service, ai-service
|
||||
- API문서
|
||||
- user service: http://localhost:8081/v3/api-docs
|
||||
- location service: http://localhost:8082/v3/api-docs
|
||||
- trip service: http://localhost:8083/v3/api-docs
|
||||
- ai service: http://localhost:8084/v3/api-docs
|
||||
[요구사항]
|
||||
- 각 화면에 Back 아이콘 버튼과 화면 타이틀 표시
|
||||
- 하단 네비게이션 바 아이콘화: 홈, 새여행, 주변장소검색, 여행보기
|
||||
```
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
# 개발 프롬프트
|
||||
|
||||
## 데이터베이스 설치계획서 작성 요청
|
||||
command: "/develop-db-guide"
|
||||
prompt:
|
||||
```
|
||||
@backing-service
|
||||
"데이터베이스설치계획서가이드"에 따라 데이터베이스 설치계획서를 작성해 주십시오.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 데이터베이스 설치 수행 요청
|
||||
command: "/develop-db-install"
|
||||
prompt:
|
||||
```
|
||||
@backing-service
|
||||
[요구사항]
|
||||
'데이터베이스설치가이드'에 따라 설치해 주세요.
|
||||
'[설치정보]'섹션이 없으면 수행을 중단하고 안내 메시지를 표시하세요.
|
||||
{안내메시지}
|
||||
'[설치정보]'섹션 하위에 아래 예와 같이 설치에 필요한 정보를 추가해 주세요.
|
||||
- 설치대상환경: 개발환경
|
||||
- AKS Resource Group: rg-digitalgarage-01
|
||||
- AKS Name: aks-digitalgarage-01
|
||||
- Namespace: tripgen-dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 데이터베이스 설치 제거 요청 (필요시)
|
||||
command: "/develop-db-remove"
|
||||
prompt:
|
||||
```
|
||||
@backing-service
|
||||
[요구사항]
|
||||
- "데이터베이스설치결과서"를 보고 관련된 모든 리소스를 삭제
|
||||
- "캐시설치결과서"를 보고 관련된 모든 리소스를 삭제
|
||||
- 현재 OS에 맞게 수행
|
||||
- 서브 에이젼트를 병렬로 수행하여 삭제
|
||||
- 결과파일은 생성할 필요 없고 화면에만 결과 표시
|
||||
[참고자료]
|
||||
- 데이터베이스설치결과서
|
||||
- 캐시설치결과서
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message Queue 설치 계획서 작성 요청
|
||||
command: "/develop-mq-guide"
|
||||
prompt:
|
||||
```
|
||||
@backing-service
|
||||
"MQ설치게획서가이드"에 따라 Message Queue 설치계획서를 작성해 주세요.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message Queue 설치 수행 요청(필요시)
|
||||
command: "/develop-mq-install"
|
||||
prompt:
|
||||
```
|
||||
@backing-service
|
||||
[요구사항]
|
||||
'MQ설치가이드'에 따라 설치해 주세요.
|
||||
'[설치정보]'섹션이 없으면 수행을 중단하고 안내 메시지를 표시하세요.
|
||||
{안내메시지}
|
||||
'[설치정보]'섹션 하위에 아래 예와 같이 설치에 필요한 정보를 추가해 주세요.
|
||||
- 설치대상환경: 개발환경
|
||||
- Resource Group: rg-digitalgarage-01
|
||||
- Namespace: tripgen-dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message Queue 설치 제거 요청
|
||||
command: "/develop-mq-remove"
|
||||
prompt:
|
||||
```
|
||||
@backing-service
|
||||
[요구사항]
|
||||
- "MQ설치결과서"를 보고 관련된 모든 리소스를 삭제
|
||||
- 현재 OS에 맞게 수행
|
||||
- 서브 에이젼트를 병렬로 수행하여 삭제
|
||||
- 결과파일은 생성할 필요 없고 화면에만 결과 표시
|
||||
[참고자료]
|
||||
- MQ설치결과서
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 백엔드 개발 요청
|
||||
command: "/develop-dev-backend"
|
||||
prompt:
|
||||
```
|
||||
@dev-backend
|
||||
"백엔드개발가이드"에 따라 개발해 주세요.
|
||||
프롬프트에 '[개발정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
||||
{안내메시지}
|
||||
[개발정보]
|
||||
- 개발 아키텍처패턴
|
||||
- auth: Layered
|
||||
- bill-inquiry: Clean
|
||||
- product-change: Layered
|
||||
- kos-mock: Layered
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 백엔드 오류 해결 요청
|
||||
command: "/develop-fix-backend"
|
||||
prompt:
|
||||
```
|
||||
@fix as @back
|
||||
개발된 각 서비스와 common 모듈을 컴파일하고 에러를 해결해 주세요.
|
||||
- common 모듈 우선 수행
|
||||
- 각 서비스별로 서브 에이젠트를 병렬로 수행
|
||||
- 컴파일이 모두 성공할때까지 계속 수행
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 서비스 실행파일 작성 요청
|
||||
command: "/develop-make-run-profile"
|
||||
prompt:
|
||||
```
|
||||
@test-backend
|
||||
'서비스실행파일작성가이드'에 따라 테스트를 해 주세요.
|
||||
프롬프트에 '[작성정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
||||
DB나 Redis의 접근 정보는 지정할 필요 없습니다. 특별히 없으면 '[작성정보]'섹션에 '없음'이라고 하세요.
|
||||
{안내메시지}
|
||||
[작성정보]
|
||||
- API Key
|
||||
- Claude: sk-ant-ap...
|
||||
- OpenAI: sk-proj-An4Q...
|
||||
- Open Weather Map: 1aa5b...
|
||||
- Kakao API Key: 5cdc24....
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 백엔드 테스트 요청
|
||||
command: "/develop-test-backend"
|
||||
prompt:
|
||||
```
|
||||
@test-backend
|
||||
'백엔드테스트가이드'에 따라 테스트를 해 주세요.
|
||||
프롬프트에 '[테스트정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
||||
테스트 대상 서비스를 지정안하면 모든 서비스를 테스트 합니다.
|
||||
{안내메시지}
|
||||
'[테스트정보]'섹션 하위에 아래 예와 같이 테스트에 필요한 정보를 제시해 주세요.
|
||||
테스트 대상 서비스를 콤마로 구분하여 입력할 수 있으며 전체를 테스트 할 때는 '전체'라고 입력하세요.
|
||||
- 서비스: user-service
|
||||
- API Key
|
||||
- Claude: sk-ant-ap...
|
||||
- OpenAI: sk-proj-An4Q...
|
||||
- Open Weather Map: 1aa5b...
|
||||
- Kakao API Key: 5cdc24....
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 프론트엔드 개발 요청
|
||||
command: "/develop-dev-front"
|
||||
prompt:
|
||||
```
|
||||
@dev-front
|
||||
"프론트엔드개발가이드"에 따라 개발해 주세요.
|
||||
프롬프트에 '[개발정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
||||
{안내메시지}
|
||||
'[개발정보]'섹션 하위에 아래 예와 같이 개발에 필요한 정보를 제시해 주세요.
|
||||
[개발정보]
|
||||
- 개발프레임워크: Typescript + React 18
|
||||
- UI프레임워크: MUI v5
|
||||
- 상태관리: Redux Toolkit
|
||||
- 라우팅: React Router v6
|
||||
- API통신: Axios
|
||||
- 스타일링: MUI + styled-components
|
||||
- 빌드도구: Vite
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
# 서비스 기획 프롬프트
|
||||
|
||||
## 서비스 기획
|
||||
command: "/think-planning"
|
||||
prompt:
|
||||
아래 내용을 터미널에 표시만 하고 수행을 하지는 않습니다.
|
||||
```
|
||||
아래 가이드를 참고하여 서비스 기획을 수행합니다.
|
||||
|
||||
https://github.com/cna-bootcamp/aiguide/blob/main/AI%ED%99%9C%EC%9A%A9%20%EC%84%9C%EB%B9%84%EC%8A%A4%20%EA%B8%B0%ED%9A%8D%20%EA%B0%80%EC%9D%B4%EB%93%9C.md
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 유저스토리 작성
|
||||
command: "/think-userstory"
|
||||
prompt:
|
||||
|
||||
```
|
||||
@document
|
||||
유저스토리를 작성하세요.
|
||||
프롬프트에 '[요구사항]'섹션이 없으면 수행을 중단하고 안내 메시지를 표시합니다.
|
||||
{안내메시지}
|
||||
'[요구사항]' 섹션에 아래 예와 같은 정보를 제공해 주십시오.
|
||||
[요구사항]
|
||||
Case 1) 이벤트스토밍을 피그마로 수행한 경우는 피그마 채널ID를 제공
|
||||
예) 피그마 채널ID 'abcde'에 접속하여 분석
|
||||
Case 2) 다른 방법으로 이벤트스토밍을 한 경우는 요구사항을 정리한 파일 경로를 제공
|
||||
예) 요구사항문서 'design/requirement.md'를 읽어 분석
|
||||
|
||||
프롬프트에 '[요구사항]'섹션이 있으면 아래와 같이 수행합니다.
|
||||
1. 요구사항 분석
|
||||
- 피그마 채널ID가 제공된 경우 figma MCP를 이용하여 해당 채널에 접속하여 분석
|
||||
- 요구사항문서 경로가 제공된 경우 해당 문서를 읽어 요구사항을 분석
|
||||
2. 유저스토리 작성
|
||||
- '유저스토리작성방법'과 '유저스토리예제'를 참고하여 유저스토리를 작성
|
||||
- 결과파일은 'design/userstory.md'에 생성
|
||||
|
||||
```
|
||||
|
||||
@@ -32,4 +32,7 @@ dependencies {
|
||||
// Jackson for JSON
|
||||
api 'com.fasterxml.jackson.core:jackson-databind'
|
||||
api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
|
||||
|
||||
// Swagger/OpenAPI
|
||||
api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
|
||||
}
|
||||
|
||||
@@ -171,7 +171,11 @@ public class GlobalExceptionHandler {
|
||||
*/
|
||||
@ExceptionHandler(DataIntegrityViolationException.class)
|
||||
public ResponseEntity<ErrorResponse> handleDataIntegrityViolationException(DataIntegrityViolationException ex) {
|
||||
log.warn("Data integrity violation: {}", ex.getMessage());
|
||||
log.error("=== DataIntegrityViolationException 발생 ===");
|
||||
log.error("Exception type: {}", ex.getClass().getSimpleName());
|
||||
log.error("Exception message: {}", ex.getMessage());
|
||||
log.error("Root cause: {}", ex.getRootCause() != null ? ex.getRootCause().getMessage() : "null");
|
||||
log.error("Stack trace: ", ex);
|
||||
|
||||
String message = "데이터 중복 또는 무결성 제약 위반이 발생했습니다";
|
||||
String details = ex.getMessage();
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
# 백엔드 컨테이너 실행 가이드
|
||||
|
||||
백엔드 서비스를 Azure VM에서 Docker 컨테이너로 실행하는 가이드를 제공합니다.
|
||||
|
||||
## 📋 목차
|
||||
|
||||
1. [사전 준비](#사전-준비)
|
||||
2. [컨테이너 이미지 확인](#컨테이너-이미지-확인)
|
||||
3. [컨테이너 실행](#컨테이너-실행)
|
||||
4. [컨테이너 관리](#컨테이너-관리)
|
||||
5. [문제 해결](#문제-해결)
|
||||
|
||||
---
|
||||
|
||||
## 사전 준비
|
||||
|
||||
### 1. VM 접속 정보
|
||||
```yaml
|
||||
ACR: acrdigitalgarage01
|
||||
VM:
|
||||
KEY파일: ~/home/bastion-dg0505
|
||||
사용자: azureuser
|
||||
IP: 20.196.65.160
|
||||
```
|
||||
|
||||
### 2. VM 접속
|
||||
```bash
|
||||
# SSH 접속
|
||||
ssh -i ~/home/bastion-dg0505 azureuser@20.196.65.160
|
||||
```
|
||||
|
||||
### 3. Docker 및 ACR 로그인 확인
|
||||
```bash
|
||||
# Docker 실행 확인
|
||||
docker --version
|
||||
|
||||
# ACR 로그인 (필요시)
|
||||
az acr login --name acrdigitalgarage01
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 컨테이너 이미지 확인
|
||||
|
||||
### 1. ACR에서 이미지 목록 조회
|
||||
```bash
|
||||
# 이미지 목록 확인
|
||||
az acr repository list --name acrdigitalgarage01 --output table
|
||||
|
||||
# 특정 이미지의 태그 확인
|
||||
az acr repository show-tags --name acrdigitalgarage01 \
|
||||
--repository {service-name} --output table
|
||||
```
|
||||
|
||||
### 2. 실행할 이미지 Pull
|
||||
```bash
|
||||
# 이미지 다운로드
|
||||
docker pull acrdigitalgarage01.azurecr.io/{service-name}:{tag}
|
||||
|
||||
# 예: participation-service
|
||||
docker pull acrdigitalgarage01.azurecr.io/participation-service:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 컨테이너 실행
|
||||
|
||||
### 1. 환경 변수 준비
|
||||
|
||||
각 서비스별 환경 변수를 확인하고 준비합니다.
|
||||
|
||||
```bash
|
||||
# .env 파일 생성 (예시)
|
||||
cat > ~/event-marketing.env << EOF
|
||||
# Database
|
||||
DB_HOST=your-db-host
|
||||
DB_PORT=5432
|
||||
DB_NAME=event_marketing
|
||||
DB_USERNAME=your-username
|
||||
DB_PASSWORD=your-password
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=your-redis-host
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Kafka
|
||||
KAFKA_BOOTSTRAP_SERVERS=your-kafka:9092
|
||||
|
||||
# Application
|
||||
SERVER_PORT=8080
|
||||
SPRING_PROFILES_ACTIVE=prod
|
||||
EOF
|
||||
```
|
||||
|
||||
### 2. 네트워크 생성 (선택사항)
|
||||
|
||||
여러 컨테이너를 함께 실행할 경우 네트워크를 생성합니다.
|
||||
|
||||
```bash
|
||||
# Docker 네트워크 생성
|
||||
docker network create event-marketing-network
|
||||
```
|
||||
|
||||
### 3. 컨테이너 실행
|
||||
|
||||
#### 기본 실행
|
||||
```bash
|
||||
docker run -d \
|
||||
--name {service-name} \
|
||||
--env-file ~/event-marketing.env \
|
||||
-p 8080:8080 \
|
||||
acrdigitalgarage01.azurecr.io/{service-name}:latest
|
||||
```
|
||||
|
||||
#### 네트워크 포함 실행
|
||||
```bash
|
||||
docker run -d \
|
||||
--name {service-name} \
|
||||
--network event-marketing-network \
|
||||
--env-file ~/event-marketing.env \
|
||||
-p 8080:8080 \
|
||||
acrdigitalgarage01.azurecr.io/{service-name}:latest
|
||||
```
|
||||
|
||||
#### 볼륨 마운트 포함 실행
|
||||
```bash
|
||||
docker run -d \
|
||||
--name {service-name} \
|
||||
--network event-marketing-network \
|
||||
--env-file ~/event-marketing.env \
|
||||
-p 8080:8080 \
|
||||
-v ~/logs/{service-name}:/app/logs \
|
||||
acrdigitalgarage01.azurecr.io/{service-name}:latest
|
||||
```
|
||||
|
||||
### 4. 여러 서비스 실행 (docker-compose 사용)
|
||||
|
||||
`docker-compose.yml` 파일 생성:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
participation-service:
|
||||
image: acrdigitalgarage01.azurecr.io/participation-service:latest
|
||||
container_name: participation-service
|
||||
env_file:
|
||||
- ./event-marketing.env
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- event-marketing-network
|
||||
volumes:
|
||||
- ./logs/participation:/app/logs
|
||||
restart: unless-stopped
|
||||
|
||||
# 다른 서비스 추가...
|
||||
|
||||
networks:
|
||||
event-marketing-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
logs:
|
||||
```
|
||||
|
||||
실행:
|
||||
```bash
|
||||
# docker-compose로 모든 서비스 시작
|
||||
docker-compose up -d
|
||||
|
||||
# 특정 서비스만 시작
|
||||
docker-compose up -d participation-service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 컨테이너 관리
|
||||
|
||||
### 1. 컨테이너 상태 확인
|
||||
```bash
|
||||
# 실행 중인 컨테이너 확인
|
||||
docker ps
|
||||
|
||||
# 모든 컨테이너 확인 (중지된 것 포함)
|
||||
docker ps -a
|
||||
|
||||
# 특정 컨테이너 상세 정보
|
||||
docker inspect {container-name}
|
||||
```
|
||||
|
||||
### 2. 로그 확인
|
||||
```bash
|
||||
# 실시간 로그 확인
|
||||
docker logs -f {container-name}
|
||||
|
||||
# 최근 100줄 로그 확인
|
||||
docker logs --tail 100 {container-name}
|
||||
|
||||
# 타임스탬프 포함 로그 확인
|
||||
docker logs -t {container-name}
|
||||
```
|
||||
|
||||
### 3. 컨테이너 중지/시작/재시작
|
||||
```bash
|
||||
# 중지
|
||||
docker stop {container-name}
|
||||
|
||||
# 시작
|
||||
docker start {container-name}
|
||||
|
||||
# 재시작
|
||||
docker restart {container-name}
|
||||
|
||||
# 강제 중지
|
||||
docker kill {container-name}
|
||||
```
|
||||
|
||||
### 4. 컨테이너 삭제
|
||||
```bash
|
||||
# 중지된 컨테이너 삭제
|
||||
docker rm {container-name}
|
||||
|
||||
# 실행 중인 컨테이너 강제 삭제
|
||||
docker rm -f {container-name}
|
||||
|
||||
# 중지된 모든 컨테이너 삭제
|
||||
docker container prune
|
||||
```
|
||||
|
||||
### 5. 컨테이너 내부 접속
|
||||
```bash
|
||||
# bash 쉘로 접속
|
||||
docker exec -it {container-name} bash
|
||||
|
||||
# 특정 명령 실행
|
||||
docker exec {container-name} ls -la /app
|
||||
```
|
||||
|
||||
### 6. 리소스 사용량 확인
|
||||
```bash
|
||||
# 실시간 리소스 사용량
|
||||
docker stats
|
||||
|
||||
# 특정 컨테이너의 리소스 사용량
|
||||
docker stats {container-name}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 문제 해결
|
||||
|
||||
### 1. 컨테이너가 시작되지 않는 경우
|
||||
|
||||
```bash
|
||||
# 로그 확인
|
||||
docker logs {container-name}
|
||||
|
||||
# 컨테이너 상태 확인
|
||||
docker inspect {container-name}
|
||||
|
||||
# 환경 변수 확인
|
||||
docker exec {container-name} env
|
||||
```
|
||||
|
||||
### 2. 포트 충돌
|
||||
|
||||
```bash
|
||||
# 포트 사용 확인
|
||||
netstat -tuln | grep {port}
|
||||
|
||||
# 다른 포트로 매핑
|
||||
docker run -d -p 8081:8080 ...
|
||||
```
|
||||
|
||||
### 3. 네트워크 연결 문제
|
||||
|
||||
```bash
|
||||
# 네트워크 목록 확인
|
||||
docker network ls
|
||||
|
||||
# 네트워크 상세 정보
|
||||
docker network inspect {network-name}
|
||||
|
||||
# 컨테이너를 네트워크에 연결
|
||||
docker network connect {network-name} {container-name}
|
||||
```
|
||||
|
||||
### 4. 이미지 Pull 실패
|
||||
|
||||
```bash
|
||||
# ACR 로그인 재시도
|
||||
az acr login --name acrdigitalgarage01
|
||||
|
||||
# 수동으로 Pull
|
||||
docker pull acrdigitalgarage01.azurecr.io/{service-name}:{tag}
|
||||
```
|
||||
|
||||
### 5. 디스크 공간 부족
|
||||
|
||||
```bash
|
||||
# 사용하지 않는 이미지 삭제
|
||||
docker image prune -a
|
||||
|
||||
# 사용하지 않는 볼륨 삭제
|
||||
docker volume prune
|
||||
|
||||
# 전체 정리 (주의!)
|
||||
docker system prune -a
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 헬스체크 및 모니터링
|
||||
|
||||
### 1. 헬스체크 엔드포인트 확인
|
||||
```bash
|
||||
# Spring Boot Actuator health endpoint
|
||||
curl http://localhost:8080/actuator/health
|
||||
|
||||
# 상세 헬스 정보
|
||||
curl http://localhost:8080/actuator/health/readiness
|
||||
curl http://localhost:8080/actuator/health/liveness
|
||||
```
|
||||
|
||||
### 2. 메트릭 확인
|
||||
```bash
|
||||
# 메트릭 엔드포인트
|
||||
curl http://localhost:8080/actuator/metrics
|
||||
|
||||
# 특정 메트릭 확인
|
||||
curl http://localhost:8080/actuator/metrics/jvm.memory.used
|
||||
```
|
||||
|
||||
### 3. 로그 모니터링 스크립트
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# monitor-logs.sh
|
||||
|
||||
SERVICE_NAME=$1
|
||||
if [ -z "$SERVICE_NAME" ]; then
|
||||
echo "Usage: ./monitor-logs.sh {service-name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 에러 로그 모니터링
|
||||
docker logs -f $SERVICE_NAME 2>&1 | grep -i error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 자동화 스크립트
|
||||
|
||||
### 1. 서비스 재배포 스크립트
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# redeploy.sh
|
||||
|
||||
SERVICE_NAME=$1
|
||||
IMAGE_TAG=${2:-latest}
|
||||
|
||||
if [ -z "$SERVICE_NAME" ]; then
|
||||
echo "Usage: ./redeploy.sh {service-name} [tag]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Pulling latest image..."
|
||||
docker pull acrdigitalgarage01.azurecr.io/$SERVICE_NAME:$IMAGE_TAG
|
||||
|
||||
echo "🛑 Stopping old container..."
|
||||
docker stop $SERVICE_NAME
|
||||
docker rm $SERVICE_NAME
|
||||
|
||||
echo "🚀 Starting new container..."
|
||||
docker run -d \
|
||||
--name $SERVICE_NAME \
|
||||
--env-file ~/event-marketing.env \
|
||||
-p 8080:8080 \
|
||||
acrdigitalgarage01.azurecr.io/$SERVICE_NAME:$IMAGE_TAG
|
||||
|
||||
echo "✅ Deployment complete!"
|
||||
docker logs -f $SERVICE_NAME
|
||||
```
|
||||
|
||||
### 2. 헬스체크 스크립트
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# healthcheck.sh
|
||||
|
||||
SERVICE_NAME=$1
|
||||
MAX_RETRIES=30
|
||||
RETRY_INTERVAL=2
|
||||
|
||||
if [ -z "$SERVICE_NAME" ]; then
|
||||
echo "Usage: ./healthcheck.sh {service-name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "⏳ Waiting for $SERVICE_NAME to be healthy..."
|
||||
|
||||
for i in $(seq 1 $MAX_RETRIES); do
|
||||
if curl -f http://localhost:8080/actuator/health > /dev/null 2>&1; then
|
||||
echo "✅ $SERVICE_NAME is healthy!"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $i/$MAX_RETRIES failed. Retrying in ${RETRY_INTERVAL}s..."
|
||||
sleep $RETRY_INTERVAL
|
||||
done
|
||||
|
||||
echo "❌ $SERVICE_NAME failed to become healthy"
|
||||
exit 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 보안 고려사항
|
||||
|
||||
### 1. 환경 변수 보호
|
||||
```bash
|
||||
# .env 파일 권한 설정
|
||||
chmod 600 ~/event-marketing.env
|
||||
|
||||
# 민감 정보는 Azure Key Vault 사용 권장
|
||||
```
|
||||
|
||||
### 2. 컨테이너 보안
|
||||
```bash
|
||||
# 읽기 전용 파일시스템으로 실행
|
||||
docker run -d --read-only ...
|
||||
|
||||
# 리소스 제한
|
||||
docker run -d \
|
||||
--memory="512m" \
|
||||
--cpus="0.5" \
|
||||
...
|
||||
```
|
||||
|
||||
### 3. 네트워크 보안
|
||||
```bash
|
||||
# 필요한 포트만 노출
|
||||
# 내부 통신은 Docker 네트워크 사용
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 서비스별 실행 예시
|
||||
|
||||
### Participation Service
|
||||
```bash
|
||||
docker run -d \
|
||||
--name participation-service \
|
||||
--network event-marketing-network \
|
||||
--env-file ~/event-marketing.env \
|
||||
-e SERVER_PORT=8080 \
|
||||
-e SPRING_PROFILES_ACTIVE=prod \
|
||||
-p 8080:8080 \
|
||||
-v ~/logs/participation:/app/logs \
|
||||
acrdigitalgarage01.azurecr.io/participation-service:latest
|
||||
```
|
||||
|
||||
### Event Service
|
||||
```bash
|
||||
docker run -d \
|
||||
--name event-service \
|
||||
--network event-marketing-network \
|
||||
--env-file ~/event-marketing.env \
|
||||
-e SERVER_PORT=8081 \
|
||||
-e SPRING_PROFILES_ACTIVE=prod \
|
||||
-p 8081:8081 \
|
||||
-v ~/logs/event:/app/logs \
|
||||
acrdigitalgarage01.azurecr.io/event-service:latest
|
||||
```
|
||||
|
||||
### User Service
|
||||
```bash
|
||||
docker run -d \
|
||||
--name user-service \
|
||||
--network event-marketing-network \
|
||||
--env-file ~/event-marketing.env \
|
||||
-e SERVER_PORT=8082 \
|
||||
-e SPRING_PROFILES_ACTIVE=prod \
|
||||
-p 8082:8082 \
|
||||
-v ~/logs/user:/app/logs \
|
||||
acrdigitalgarage01.azurecr.io/user-service:latest
|
||||
```
|
||||
|
||||
### Analytics Service
|
||||
```bash
|
||||
docker run -d \
|
||||
--name analytics-service \
|
||||
--network event-marketing-network \
|
||||
--env-file ~/event-marketing.env \
|
||||
-e SERVER_PORT=8083 \
|
||||
-e SPRING_PROFILES_ACTIVE=prod \
|
||||
-p 8083:8083 \
|
||||
-v ~/logs/analytics:/app/logs \
|
||||
acrdigitalgarage01.azurecr.io/analytics-service:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
이 가이드를 통해 백엔드 서비스를 안전하고 효율적으로 컨테이너로 실행할 수 있습니다. 추가 질문이나 문제가 있으면 언제든지 문의해 주세요! 🚀
|
||||
+223
-128
@@ -2,7 +2,8 @@
|
||||
|
||||
## 문서 정보
|
||||
- **작성일**: 2025-10-24
|
||||
- **버전**: 1.0
|
||||
- **최종 수정일**: 2025-10-28
|
||||
- **버전**: 2.0
|
||||
- **작성자**: Event Service Team
|
||||
- **관련 문서**:
|
||||
- [API 설계서](../../design/backend/api/API-설계서.md)
|
||||
@@ -14,16 +15,18 @@
|
||||
|
||||
### 구현 현황
|
||||
- **설계된 API**: 14개
|
||||
- **구현된 API**: 7개 (50.0%)
|
||||
- **미구현 API**: 7개 (50.0%)
|
||||
- **구현된 API**: 14개 (100%) ✅
|
||||
- **미구현 API**: 0개 (0%)
|
||||
|
||||
### 구현률 세부
|
||||
| 카테고리 | 설계 | 구현 | 미구현 | 구현률 |
|
||||
|---------|------|------|--------|--------|
|
||||
| Dashboard & Event List | 2 | 2 | 0 | 100% |
|
||||
| Event Creation Flow | 8 | 1 | 7 | 12.5% |
|
||||
| Event Management | 3 | 3 | 0 | 100% |
|
||||
| Job Status | 1 | 1 | 0 | 100% |
|
||||
| Dashboard & Event List | 2 | 2 | 0 | 100% ✅ |
|
||||
| Event Creation Flow | 8 | 8 | 0 | 100% ✅ |
|
||||
| Event Management | 3 | 3 | 0 | 100% ✅ |
|
||||
| Job Status | 1 | 1 | 0 | 100% ✅ |
|
||||
|
||||
**🎉 모든 API 구현 완료!** Event Service의 설계된 14개 API가 모두 구현되었습니다.
|
||||
|
||||
---
|
||||
|
||||
@@ -33,56 +36,53 @@
|
||||
|
||||
| 설계서 API | Controller | 메서드 | 경로 | 구현 여부 | 비고 |
|
||||
|-----------|-----------|--------|------|----------|------|
|
||||
| 이벤트 목록 조회 | EventController | GET | /api/events | ✅ 구현 | EventController:84 |
|
||||
| 이벤트 상세 조회 | EventController | GET | /api/events/{eventId} | ✅ 구현 | EventController:130 |
|
||||
| 이벤트 목록 조회 | EventController | GET | /api/v1/events | ✅ 구현 | EventController:87 |
|
||||
| 이벤트 상세 조회 | EventController | GET | /api/v1/events/{eventId} | ✅ 구현 | EventController:133 |
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Event Creation Flow (구현률 12.5%)
|
||||
### 2.2 Event Creation Flow (구현률 100% ✅)
|
||||
|
||||
#### Step 1: 이벤트 목적 선택
|
||||
| 설계서 API | Controller | 메서드 | 경로 | 구현 여부 | 비고 |
|
||||
|-----------|-----------|--------|------|----------|------|
|
||||
| 이벤트 목적 선택 | EventController | POST | /api/events/objectives | ✅ 구현 | EventController:52 |
|
||||
| 이벤트 목적 선택 | EventController | POST | /api/v1/events/objectives | ✅ 구현 | EventController:51 |
|
||||
|
||||
#### Step 2: AI 추천 (미구현)
|
||||
| 설계서 API | Controller | 메서드 | 경로 | 구현 여부 | 미구현 이유 |
|
||||
|-----------|-----------|--------|------|----------|-----------|
|
||||
| AI 추천 요청 | - | POST | /api/events/{eventId}/ai-recommendations | ❌ 미구현 | AI Service 연동 필요 |
|
||||
| AI 추천 선택 | - | PUT | /api/events/{eventId}/recommendations | ❌ 미구현 | AI Service 연동 필요 |
|
||||
#### Step 2: AI 추천 (구현률 100% ✅)
|
||||
| 설계서 API | Controller | 메서드 | 경로 | 구현 여부 | 비고 |
|
||||
|-----------|-----------|--------|------|----------|------|
|
||||
| AI 추천 요청 | EventController | POST | /api/v1/events/{eventId}/ai-recommendations | ✅ 구현 | EventController:272 |
|
||||
| AI 추천 선택 | EventController | PUT | /api/v1/events/{eventId}/recommendations | ✅ 구현 | EventController:300 |
|
||||
|
||||
**미구현 상세 이유**:
|
||||
- Kafka Topic `ai-event-generation-job` 발행 로직 필요
|
||||
- AI Service와의 연동이 선행되어야 함
|
||||
- Redis에서 AI 추천 결과를 읽어오는 로직 필요
|
||||
- 현재 단계에서는 이벤트 생명주기 관리에 집중
|
||||
**구현 내용**:
|
||||
- **AI 추천 요청**: Kafka Topic `ai-event-generation-job`에 메시지 발행, Job ID 반환
|
||||
- **AI 추천 선택**: 사용자가 AI 추천 중 하나를 선택하고 커스터마이징하여 이벤트에 적용
|
||||
|
||||
#### Step 3: 이미지 생성 (미구현)
|
||||
| 설계서 API | Controller | 메서드 | 경로 | 구현 여부 | 미구현 이유 |
|
||||
|-----------|-----------|--------|------|----------|-----------|
|
||||
| 이미지 생성 요청 | - | POST | /api/events/{eventId}/images | ❌ 미구현 | Content Service 연동 필요 |
|
||||
| 이미지 선택 | - | PUT | /api/events/{eventId}/images/{imageId}/select | ❌ 미구현 | Content Service 연동 필요 |
|
||||
| 이미지 편집 | - | PUT | /api/events/{eventId}/images/{imageId}/edit | ❌ 미구현 | Content Service 연동 필요 |
|
||||
#### Step 3: 이미지 생성 (구현률 100% ✅)
|
||||
| 설계서 API | Controller | 메서드 | 경로 | 구현 여부 | 비고 |
|
||||
|-----------|-----------|--------|------|----------|------|
|
||||
| 이미지 생성 요청 | EventController | POST | /api/v1/events/{eventId}/images | ✅ 구현 | EventController:214 |
|
||||
| 이미지 선택 | EventController | PUT | /api/v1/events/{eventId}/images/{imageId}/select | ✅ 구현 | EventController:243 |
|
||||
| 이미지 편집 | EventController | PUT | /api/v1/events/{eventId}/images/{imageId}/edit | ✅ 구현 | EventController:328 |
|
||||
|
||||
**미구현 상세 이유**:
|
||||
- Kafka Topic `image-generation-job` 발행 로직 필요
|
||||
- Content Service와의 연동이 선행되어야 함
|
||||
- Redis에서 생성된 이미지 URL을 읽어오는 로직 필요
|
||||
- 이미지 편집은 Content Service의 이미지 재생성 API와 연동 필요
|
||||
**구현 내용**:
|
||||
- **이미지 생성 요청**: Kafka Topic `image-generation-job`에 메시지 발행, Job ID 반환
|
||||
- **이미지 선택**: 사용자가 생성된 이미지 중 하나를 선택하여 이벤트에 연결
|
||||
- **이미지 편집**: 선택된 이미지를 편집하고 Content Service를 통해 재생성
|
||||
|
||||
#### Step 4: 배포 채널 선택 (미구현)
|
||||
| 설계서 API | Controller | 메서드 | 경로 | 구현 여부 | 미구현 이유 |
|
||||
|-----------|-----------|--------|------|----------|-----------|
|
||||
| 배포 채널 선택 | - | PUT | /api/events/{eventId}/channels | ❌ 미구현 | Distribution Service 연동 필요 |
|
||||
#### Step 4: 배포 채널 선택 (구현률 100% ✅)
|
||||
| 설계서 API | Controller | 메서드 | 경로 | 구현 여부 | 비고 |
|
||||
|-----------|-----------|--------|------|----------|------|
|
||||
| 배포 채널 선택 | EventController | PUT | /api/v1/events/{eventId}/channels | ✅ 구현 | EventController:357 |
|
||||
|
||||
**미구현 상세 이유**:
|
||||
- Distribution Service의 채널 목록 검증 로직 필요
|
||||
- Event 엔티티의 channels 필드 업데이트 로직은 구현 가능하나, 채널별 검증은 Distribution Service 개발 후 추가 예정
|
||||
**구현 내용**:
|
||||
- 이벤트를 배포할 채널(SMS, KakaoTalk, App Push 등)을 선택
|
||||
- Distribution Service와의 연동은 추후 추가 예정
|
||||
|
||||
#### Step 5: 최종 승인 및 배포
|
||||
| 설계서 API | Controller | 메서드 | 경로 | 구현 여부 | 비고 |
|
||||
|-----------|-----------|--------|------|----------|------|
|
||||
| 최종 승인 및 배포 | EventController | POST | /api/events/{eventId}/publish | ✅ 구현 | EventController:172 |
|
||||
| 최종 승인 및 배포 | EventController | POST | /api/v1/events/{eventId}/publish | ✅ 구현 | EventController:175 |
|
||||
|
||||
**구현 내용**:
|
||||
- 이벤트 상태를 DRAFT → PUBLISHED로 변경
|
||||
@@ -91,19 +91,18 @@
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Event Management (구현률 100%)
|
||||
### 2.3 Event Management (구현률 100% ✅)
|
||||
|
||||
| 설계서 API | Controller | 메서드 | 경로 | 구현 여부 | 비고 |
|
||||
|-----------|-----------|--------|------|----------|------|
|
||||
| 이벤트 수정 | - | PUT | /api/events/{eventId} | ❌ 미구현 | 이유는 아래 참조 |
|
||||
| 이벤트 삭제 | EventController | DELETE | /api/events/{eventId} | ✅ 구현 | EventController:151 |
|
||||
| 이벤트 조기 종료 | EventController | POST | /api/events/{eventId}/end | ✅ 구현 | EventController:193 |
|
||||
| 이벤트 수정 | EventController | PUT | /api/v1/events/{eventId} | ✅ 구현 | EventController:384 |
|
||||
| 이벤트 삭제 | EventController | DELETE | /api/v1/events/{eventId} | ✅ 구현 | EventController:150 |
|
||||
| 이벤트 조기 종료 | EventController | POST | /api/v1/events/{eventId}/end | ✅ 구현 | EventController:192 |
|
||||
|
||||
**이벤트 수정 API 미구현 이유**:
|
||||
- 이벤트 수정은 여러 단계의 데이터를 수정하는 복잡한 로직
|
||||
- AI 추천 재선택, 이미지 재생성 등 다른 서비스와의 연동이 필요
|
||||
- 우선순위: 신규 이벤트 생성 플로우 완성 후 구현 예정
|
||||
- 현재는 DRAFT 상태에서만 삭제 가능하므로 수정 대신 삭제 후 재생성 가능
|
||||
**구현 내용**:
|
||||
- **이벤트 수정**: 기존 이벤트의 정보를 수정합니다. DRAFT 상태만 수정 가능
|
||||
- **이벤트 삭제**: DRAFT 상태의 이벤트만 삭제 가능
|
||||
- **이벤트 조기 종료**: PUBLISHED 상태의 이벤트를 ENDED 상태로 변경
|
||||
|
||||
---
|
||||
|
||||
@@ -111,15 +110,15 @@
|
||||
|
||||
| 설계서 API | Controller | 메서드 | 경로 | 구현 여부 | 비고 |
|
||||
|-----------|-----------|--------|------|----------|------|
|
||||
| Job 상태 폴링 | JobController | GET | /api/jobs/{jobId} | ✅ 구현 | JobController:42 |
|
||||
| Job 상태 폴링 | JobController | GET | /api/v1/jobs/{jobId} | ✅ 구현 | JobController:42 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 구현된 API 상세
|
||||
|
||||
### 3.1 EventController (6개 API)
|
||||
### 3.1 EventController (13개 API)
|
||||
|
||||
#### 1. POST /api/events/objectives
|
||||
#### 1. POST /api/v1/events/objectives
|
||||
- **설명**: 이벤트 생성의 첫 단계로 목적을 선택
|
||||
- **유저스토리**: UFR-EVENT-020
|
||||
- **요청**: SelectObjectiveRequest (objective)
|
||||
@@ -129,7 +128,7 @@
|
||||
- 초기 상태는 DRAFT
|
||||
- EventService.createEvent() 호출
|
||||
|
||||
#### 2. GET /api/events
|
||||
#### 2. GET /api/v1/events
|
||||
- **설명**: 사용자의 이벤트 목록 조회 (페이징, 필터링, 정렬)
|
||||
- **유저스토리**: UFR-EVENT-010, UFR-EVENT-070
|
||||
- **요청 파라미터**:
|
||||
@@ -143,7 +142,7 @@
|
||||
- Repository에서 필터링 및 페이징 처리
|
||||
- EventService.getEvents() 호출
|
||||
|
||||
#### 3. GET /api/events/{eventId}
|
||||
#### 3. GET /api/v1/events/{eventId}
|
||||
- **설명**: 특정 이벤트의 상세 정보 조회
|
||||
- **유저스토리**: UFR-EVENT-060
|
||||
- **요청**: eventId (UUID)
|
||||
@@ -153,7 +152,7 @@
|
||||
- 사용자 소유 이벤트만 조회 가능 (보안)
|
||||
- EventService.getEvent() 호출
|
||||
|
||||
#### 4. DELETE /api/events/{eventId}
|
||||
#### 4. DELETE /api/v1/events/{eventId}
|
||||
- **설명**: 이벤트 삭제 (DRAFT 상태만 가능)
|
||||
- **유저스토리**: UFR-EVENT-070
|
||||
- **요청**: eventId (UUID)
|
||||
@@ -163,7 +162,7 @@
|
||||
- 다른 상태(PUBLISHED, ENDED)는 삭제 불가
|
||||
- EventService.deleteEvent() 호출
|
||||
|
||||
#### 5. POST /api/events/{eventId}/publish
|
||||
#### 5. POST /api/v1/events/{eventId}/publish
|
||||
- **설명**: 이벤트 배포 (DRAFT → PUBLISHED)
|
||||
- **유저스토리**: UFR-EVENT-050
|
||||
- **요청**: eventId (UUID)
|
||||
@@ -173,7 +172,7 @@
|
||||
- Distribution Service 호출은 추후 추가 예정
|
||||
- EventService.publishEvent() 호출
|
||||
|
||||
#### 6. POST /api/events/{eventId}/end
|
||||
#### 6. POST /api/v1/events/{eventId}/end
|
||||
- **설명**: 이벤트 조기 종료 (PUBLISHED → ENDED)
|
||||
- **유저스토리**: UFR-EVENT-060
|
||||
- **요청**: eventId (UUID)
|
||||
@@ -183,11 +182,81 @@
|
||||
- PUBLISHED 상태만 종료 가능
|
||||
- EventService.endEvent() 호출
|
||||
|
||||
#### 7. POST /api/v1/events/{eventId}/images
|
||||
- **설명**: AI를 통해 이벤트 이미지를 생성 요청
|
||||
- **유저스토리**: UFR-CONT-010
|
||||
- **요청**: ImageGenerationRequest (prompt, style, count)
|
||||
- **응답**: ImageGenerationResponse (jobId)
|
||||
- **비즈니스 로직**:
|
||||
- Kafka Topic `image-generation-job`에 메시지 발행
|
||||
- 비동기 작업을 위한 Job 엔티티 생성 및 반환
|
||||
- EventService.requestImageGeneration() 호출
|
||||
|
||||
#### 8. PUT /api/v1/events/{eventId}/images/{imageId}/select
|
||||
- **설명**: 생성된 이미지 중 하나를 선택
|
||||
- **유저스토리**: UFR-CONT-020
|
||||
- **요청**: SelectImageRequest (imageId)
|
||||
- **응답**: ApiResponse<Void>
|
||||
- **비즈니스 로직**:
|
||||
- 선택한 이미지를 이벤트에 연결
|
||||
- 이미지 URL을 Event 엔티티에 저장
|
||||
- EventService.selectImage() 호출
|
||||
|
||||
#### 9. POST /api/v1/events/{eventId}/ai-recommendations
|
||||
- **설명**: AI 서비스에 이벤트 추천 생성을 요청
|
||||
- **유저스토리**: UFR-EVENT-030
|
||||
- **요청**: AiRecommendationRequest (이벤트 컨텍스트 정보)
|
||||
- **응답**: JobAcceptedResponse (jobId)
|
||||
- **비즈니스 로직**:
|
||||
- Kafka Topic `ai-event-generation-job`에 메시지 발행
|
||||
- 비동기 작업을 위한 Job 엔티티 생성 및 반환
|
||||
- EventService.requestAiRecommendations() 호출
|
||||
|
||||
#### 10. PUT /api/v1/events/{eventId}/recommendations
|
||||
- **설명**: AI가 생성한 추천 중 하나를 선택하고 커스터마이징
|
||||
- **유저스토리**: UFR-EVENT-030
|
||||
- **요청**: SelectRecommendationRequest (recommendationId, customizations)
|
||||
- **응답**: ApiResponse<Void>
|
||||
- **비즈니스 로직**:
|
||||
- 선택한 AI 추천을 이벤트에 적용
|
||||
- 사용자 커스터마이징 반영
|
||||
- EventService.selectRecommendation() 호출
|
||||
|
||||
#### 11. PUT /api/v1/events/{eventId}/images/{imageId}/edit
|
||||
- **설명**: 선택된 이미지를 편집
|
||||
- **유저스토리**: UFR-CONT-030
|
||||
- **요청**: ImageEditRequest (editInstructions)
|
||||
- **응답**: ImageEditResponse (editedImageUrl, jobId)
|
||||
- **비즈니스 로직**:
|
||||
- Content Service와 연동하여 이미지 편집 요청
|
||||
- 편집된 이미지를 다시 생성하고 CDN에 업로드
|
||||
- EventService.editImage() 호출
|
||||
|
||||
#### 12. PUT /api/v1/events/{eventId}/channels
|
||||
- **설명**: 이벤트를 배포할 채널을 선택
|
||||
- **유저스토리**: UFR-EVENT-040
|
||||
- **요청**: SelectChannelsRequest (channels: List<String>)
|
||||
- **응답**: ApiResponse<Void>
|
||||
- **비즈니스 로직**:
|
||||
- 배포 채널(SMS, KakaoTalk, App Push 등) 선택
|
||||
- Event 엔티티의 channels 필드 업데이트
|
||||
- EventService.selectChannels() 호출
|
||||
|
||||
#### 13. PUT /api/v1/events/{eventId}
|
||||
- **설명**: 기존 이벤트의 정보를 수정
|
||||
- **유저스토리**: UFR-EVENT-080
|
||||
- **요청**: UpdateEventRequest (이벤트 수정 정보)
|
||||
- **응답**: EventDetailResponse (수정된 이벤트 정보)
|
||||
- **비즈니스 로직**:
|
||||
- DRAFT 상태의 이벤트만 수정 가능
|
||||
- 이벤트 기본 정보, AI 추천, 이미지, 채널 등 수정
|
||||
- EventService.updateEvent() 호출
|
||||
|
||||
---
|
||||
|
||||
### 3.2 JobController (1개 API)
|
||||
|
||||
#### 1. GET /api/jobs/{jobId}
|
||||
#### 1. GET /api/v1/jobs/{jobId}
|
||||
- **설명**: 비동기 작업의 상태를 조회 (폴링 방식)
|
||||
- **유저스토리**: UFR-EVENT-030, UFR-CONT-010
|
||||
- **요청**: jobId (UUID)
|
||||
@@ -199,94 +268,120 @@
|
||||
|
||||
---
|
||||
|
||||
## 4. 미구현 API 개발 계획
|
||||
|
||||
### 4.1 우선순위 1 (AI Service 연동)
|
||||
- **POST /api/events/{eventId}/ai-recommendations** - AI 추천 요청
|
||||
- **PUT /api/events/{eventId}/recommendations** - AI 추천 선택
|
||||
|
||||
**개발 선행 조건**:
|
||||
1. AI Service 개발 완료
|
||||
2. Kafka Topic `ai-event-generation-job` 설정
|
||||
3. Redis 캐시 연동 구현
|
||||
|
||||
---
|
||||
|
||||
### 4.2 우선순위 2 (Content Service 연동)
|
||||
- **POST /api/events/{eventId}/images** - 이미지 생성 요청
|
||||
- **PUT /api/events/{eventId}/images/{imageId}/select** - 이미지 선택
|
||||
- **PUT /api/events/{eventId}/images/{imageId}/edit** - 이미지 편집
|
||||
|
||||
**개발 선행 조건**:
|
||||
1. Content Service 개발 완료
|
||||
2. Kafka Topic `image-generation-job` 설정
|
||||
3. Redis 캐시 연동 구현
|
||||
4. CDN (Azure Blob Storage) 연동
|
||||
|
||||
---
|
||||
|
||||
### 4.3 우선순위 3 (Distribution Service 연동)
|
||||
- **PUT /api/events/{eventId}/channels** - 배포 채널 선택
|
||||
|
||||
**개발 선행 조건**:
|
||||
1. Distribution Service 개발 완료
|
||||
2. 채널별 검증 로직 구현
|
||||
3. POST /api/events/{eventId}/publish API에 Distribution Service 동기 호출 추가
|
||||
|
||||
---
|
||||
|
||||
### 4.4 우선순위 4 (이벤트 수정)
|
||||
- **PUT /api/events/{eventId}** - 이벤트 수정
|
||||
|
||||
**개발 선행 조건**:
|
||||
1. 우선순위 1~3 API 모두 구현 완료
|
||||
2. 이벤트 수정 범위 정의 (이름/설명/날짜만 수정 vs 전체 재생성)
|
||||
3. 각 단계별 수정 로직 설계
|
||||
|
||||
---
|
||||
|
||||
## 5. 추가 구현된 API (설계서에 없음)
|
||||
## 4. 추가 구현된 API (설계서에 없음)
|
||||
|
||||
현재 추가 구현된 API는 없습니다. 모든 구현은 설계서를 기준으로 진행되었습니다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 다음 단계
|
||||
## 5. 다음 단계
|
||||
|
||||
### 6.1 즉시 가능한 작업
|
||||
### 5.1 즉시 가능한 작업
|
||||
1. **서버 시작 테스트**:
|
||||
- PostgreSQL 연결 확인
|
||||
- Kafka 연결 확인
|
||||
- Redis 연결 확인
|
||||
- Swagger UI 접근 테스트 (http://localhost:8081/swagger-ui.html)
|
||||
|
||||
2. **구현된 API 테스트**:
|
||||
- POST /api/events/objectives
|
||||
- GET /api/events
|
||||
- GET /api/events/{eventId}
|
||||
- DELETE /api/events/{eventId}
|
||||
- POST /api/events/{eventId}/publish
|
||||
- POST /api/events/{eventId}/end
|
||||
- GET /api/jobs/{jobId}
|
||||
2. **구현된 전체 API 테스트** (14개):
|
||||
- POST /api/v1/events/objectives (이벤트 목적 선택)
|
||||
- GET /api/v1/events (이벤트 목록 조회)
|
||||
- GET /api/v1/events/{eventId} (이벤트 상세 조회)
|
||||
- DELETE /api/v1/events/{eventId} (이벤트 삭제)
|
||||
- PUT /api/v1/events/{eventId} (이벤트 수정)
|
||||
- POST /api/v1/events/{eventId}/ai-recommendations (AI 추천 요청)
|
||||
- PUT /api/v1/events/{eventId}/recommendations (AI 추천 선택)
|
||||
- POST /api/v1/events/{eventId}/images (이미지 생성 요청)
|
||||
- PUT /api/v1/events/{eventId}/images/{imageId}/select (이미지 선택)
|
||||
- PUT /api/v1/events/{eventId}/images/{imageId}/edit (이미지 편집)
|
||||
- PUT /api/v1/events/{eventId}/channels (배포 채널 선택)
|
||||
- POST /api/v1/events/{eventId}/publish (이벤트 배포)
|
||||
- POST /api/v1/events/{eventId}/end (이벤트 종료)
|
||||
- GET /api/v1/jobs/{jobId} (Job 상태 조회)
|
||||
|
||||
### 6.2 후속 개발 필요
|
||||
1. AI Service 개발 완료 → AI 추천 API 구현
|
||||
2. Content Service 개발 완료 → 이미지 관련 API 구현
|
||||
3. Distribution Service 개발 완료 → 배포 채널 선택 API 구현
|
||||
4. 전체 서비스 연동 → 이벤트 수정 API 구현
|
||||
### 5.2 서비스 간 연동 완성 필요
|
||||
1. **AI Service 연동**:
|
||||
- Kafka Consumer에서 `ai-event-generation-job` 처리
|
||||
- Redis를 통한 AI 추천 결과 캐싱
|
||||
- AI 추천 API 완전 통합 테스트
|
||||
|
||||
2. **Content Service 연동**:
|
||||
- 이미지 생성/편집 API 통합
|
||||
- CDN 업로드 로직 연동
|
||||
- 이미지 편집 API 완전 통합 테스트
|
||||
|
||||
3. **Distribution Service 연동**:
|
||||
- 배포 채널 검증 로직 추가
|
||||
- 이벤트 배포 시 Distribution Service 동기 호출
|
||||
- 채널별 배포 상태 추적
|
||||
|
||||
### 5.3 통합 테스트 시나리오
|
||||
전체 이벤트 생성 플로우를 End-to-End로 테스트:
|
||||
1. 이벤트 목적 선택
|
||||
2. AI 추천 요청 및 선택
|
||||
3. 이미지 생성 및 선택/편집
|
||||
4. 배포 채널 선택
|
||||
5. 최종 배포 및 모니터링
|
||||
|
||||
---
|
||||
|
||||
## 부록
|
||||
|
||||
### A. 개발 우선순위 결정 근거
|
||||
### A. 개발 완료 요약
|
||||
|
||||
**현재 구현 범위 선정 이유**:
|
||||
1. **핵심 생명주기 먼저**: 이벤트 생성, 조회, 삭제, 상태 변경
|
||||
2. **서비스 독립성**: 다른 서비스 없이도 Event Service 단독 테스트 가능
|
||||
3. **점진적 통합**: 각 서비스 개발 완료 시점에 순차적 통합
|
||||
4. **리스크 최소화**: 복잡한 서비스 간 연동은 각 서비스 안정화 후 진행
|
||||
**Event Service API 개발 현황**:
|
||||
- ✅ **전체 API 구현 완료**: 설계된 14개 API 모두 구현
|
||||
- ✅ **핵심 생명주기 관리**: 이벤트 생성, 조회, 수정, 삭제, 상태 변경
|
||||
- ✅ **AI 추천 플로우**: AI 추천 요청 및 선택 API 완성
|
||||
- ✅ **이미지 관리**: 생성, 선택, 편집 API 완성
|
||||
- ✅ **배포 관리**: 채널 선택 및 배포 API 완성
|
||||
- ✅ **비동기 작업 추적**: Job 상태 조회 API 완성
|
||||
|
||||
**다음 단계**:
|
||||
- AI Service, Content Service, Distribution Service와의 완전한 통합 테스트
|
||||
- End-to-End 시나리오 기반 통합 검증
|
||||
- 성능 최적화 및 에러 핸들링 강화
|
||||
|
||||
---
|
||||
|
||||
**문서 버전**: 1.0
|
||||
**최종 수정일**: 2025-10-24
|
||||
**문서 버전**: 2.0
|
||||
**최종 수정일**: 2025-10-28
|
||||
**작성자**: Event Service Team
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
### v2.0 (2025-10-28) - 🎉 전체 API 구현 완료
|
||||
- **구현 현황 업데이트**: 9개 → 14개 API (100% 구현 완료!)
|
||||
- **신규 구현 API 추가 (5개)**:
|
||||
1. POST /api/v1/events/{eventId}/ai-recommendations - AI 추천 요청
|
||||
2. PUT /api/v1/events/{eventId}/recommendations - AI 추천 선택
|
||||
3. PUT /api/v1/events/{eventId}/images/{imageId}/edit - 이미지 편집
|
||||
4. PUT /api/v1/events/{eventId}/channels - 배포 채널 선택
|
||||
5. PUT /api/v1/events/{eventId} - 이벤트 수정
|
||||
- **구현률 100% 달성**:
|
||||
- Event Creation Flow: 37.5% → 100%
|
||||
- Event Management: 66.7% → 100%
|
||||
- 모든 카테고리 100% 완성
|
||||
- **문서 구조 개선**:
|
||||
- 미구현 API 계획 섹션 제거
|
||||
- 서비스 간 연동 완성 가이드 추가
|
||||
- 통합 테스트 시나리오 추가
|
||||
- **라인 번호 업데이트**: 모든 Controller 메서드의 정확한 라인 번호 반영
|
||||
|
||||
### v1.1 (2025-10-27)
|
||||
- **구현 현황 업데이트**: 7개 → 9개 API (64.3% 구현)
|
||||
- **신규 구현 API 추가**:
|
||||
- POST /api/v1/events/{eventId}/images - 이미지 생성 요청
|
||||
- PUT /api/v1/events/{eventId}/images/{imageId}/select - 이미지 선택
|
||||
- **API 경로 수정**: /api/events → /api/v1/events (버전 명시)
|
||||
- **구현률 재계산**:
|
||||
- Event Creation Flow: 12.5% → 37.5%
|
||||
- Event Management: 100% → 66.7% (이벤트 수정 미구현 반영)
|
||||
- **미구현 API 계획 업데이트**: Content Service 연동 우선순위 조정
|
||||
|
||||
### v1.0 (2025-10-24)
|
||||
- 초기 문서 작성
|
||||
- 설계된 14개 API 목록 정리
|
||||
- 초기 구현 상태 기록 (7개 API)
|
||||
|
||||
+345
-323
@@ -1,389 +1,411 @@
|
||||
# Content Service 백엔드 테스트 결과서
|
||||
# Event Service 백엔드 API 테스트 결과
|
||||
|
||||
## 1. 테스트 개요
|
||||
## 테스트 개요
|
||||
|
||||
### 1.1 테스트 정보
|
||||
- **테스트 일시**: 2025-10-23
|
||||
- **테스트 환경**: Local 개발 환경
|
||||
- **서비스명**: Content Service
|
||||
- **서비스 포트**: 8084
|
||||
- **프로파일**: local (H2 in-memory database)
|
||||
- **테스트 대상**: REST API 7개 엔드포인트
|
||||
**테스트 일시**: 2025-10-28
|
||||
**서비스**: Event Service
|
||||
**베이스 URL**: http://localhost:8080
|
||||
**인증 방식**: 없음 (개발 환경)
|
||||
|
||||
### 1.2 테스트 목적
|
||||
- Content Service의 모든 REST API 엔드포인트 정상 동작 검증
|
||||
- Mock 서비스 (MockGenerateImagesService, MockRedisGateway) 정상 동작 확인
|
||||
- Local 환경에서 외부 인프라 의존성 없이 독립 실행 가능 여부 검증
|
||||
## 테스트 환경 설정
|
||||
|
||||
## 2. 테스트 환경 구성
|
||||
### 1. 환경 변수 검증 결과
|
||||
|
||||
### 2.1 데이터베이스
|
||||
- **DB 타입**: H2 In-Memory Database
|
||||
- **연결 URL**: jdbc:h2:mem:contentdb
|
||||
- **스키마 생성**: 자동 (ddl-auto: create-drop)
|
||||
- **생성된 테이블**:
|
||||
- contents (콘텐츠 정보)
|
||||
- generated_images (생성된 이미지 정보)
|
||||
- jobs (작업 상태 추적)
|
||||
**application.yml 설정**:
|
||||
- ✅ 모든 환경 변수가 플레이스홀더 형식으로 정의됨
|
||||
- ✅ 기본값 설정 확인: `${변수명:기본값}` 형식 사용
|
||||
|
||||
### 2.2 Mock 서비스
|
||||
- **MockRedisGateway**: Redis 캐시 기능 Mock 구현
|
||||
- **MockGenerateImagesService**: AI 이미지 생성 비동기 처리 Mock 구현
|
||||
- 1초 지연 후 4개 이미지 자동 생성 (FANCY/SIMPLE x INSTAGRAM/KAKAO)
|
||||
**event-service.run.xml 실행 프로파일**:
|
||||
- ✅ 모든 필수 환경 변수 정의됨
|
||||
- ✅ application.yml과 일치하는 변수명 사용
|
||||
|
||||
### 2.3 서버 시작 로그
|
||||
```
|
||||
Started ContentApplication in 2.856 seconds (process running for 3.212)
|
||||
Hibernate: create table contents (...)
|
||||
Hibernate: create table generated_images (...)
|
||||
Hibernate: create table jobs (...)
|
||||
```
|
||||
**환경 변수 매핑 확인**:
|
||||
| 환경 변수 | application.yml | run.xml | 일치 여부 |
|
||||
|----------|----------------|---------|----------|
|
||||
| SERVER_PORT | ✅ ${SERVER_PORT:8080} | ✅ 8080 | ✅ |
|
||||
| DB_HOST | ✅ ${DB_HOST:localhost} | ✅ 20.249.177.232 | ✅ |
|
||||
| DB_PORT | ✅ ${DB_PORT:5432} | ✅ 5432 | ✅ |
|
||||
| DB_NAME | ✅ ${DB_NAME:eventdb} | ✅ eventdb | ✅ |
|
||||
| DB_USERNAME | ✅ ${DB_USERNAME:eventuser} | ✅ eventuser | ✅ |
|
||||
| DB_PASSWORD | ✅ ${DB_PASSWORD:eventpass} | ✅ Hi5Jessica! | ✅ |
|
||||
| REDIS_HOST | ✅ ${REDIS_HOST:localhost} | ✅ 20.214.210.71 | ✅ |
|
||||
| REDIS_PORT | ✅ ${REDIS_PORT:6379} | ✅ 6379 | ✅ |
|
||||
| REDIS_PASSWORD | ✅ ${REDIS_PASSWORD:} | ✅ Hi5Jessica! | ✅ |
|
||||
| KAFKA_BOOTSTRAP_SERVERS | ✅ ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092} | ✅ 20.249.182.13:9095,4.217.131.59:9095 | ✅ |
|
||||
| JWT_SECRET | ✅ ${JWT_SECRET:default...} | ✅ kt-event-marketing-secret... | ✅ |
|
||||
| LOG_LEVEL | ✅ ${LOG_LEVEL:INFO} | ✅ DEBUG | ✅ |
|
||||
|
||||
## 3. API 테스트 결과
|
||||
**결론**: ✅ 설정 일치 확인 완료
|
||||
|
||||
### 3.1 POST /content/images/generate - 이미지 생성 요청
|
||||
|
||||
**목적**: AI 이미지 생성 작업 시작
|
||||
### 2. 서비스 Health Check
|
||||
|
||||
**요청**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8084/content/images/generate \
|
||||
curl http://localhost:8080/actuator/health
|
||||
```
|
||||
|
||||
**응답**:
|
||||
```json
|
||||
{
|
||||
"status": "UP",
|
||||
"components": {
|
||||
"db": {
|
||||
"status": "UP",
|
||||
"details": {
|
||||
"database": "PostgreSQL",
|
||||
"validationQuery": "isValid()"
|
||||
}
|
||||
},
|
||||
"diskSpace": {
|
||||
"status": "UP",
|
||||
"details": {
|
||||
"total": 511724277760,
|
||||
"free": 268097769472,
|
||||
"threshold": 10485760,
|
||||
"path": "C:\\Users\\KTDS\\home\\workspace\\kt-event-marketing\\.",
|
||||
"exists": true
|
||||
}
|
||||
},
|
||||
"livenessState": {
|
||||
"status": "UP"
|
||||
},
|
||||
"ping": {
|
||||
"status": "UP"
|
||||
},
|
||||
"readinessState": {
|
||||
"status": "UP"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**결과**: ✅ **서비스 정상 (UP)**
|
||||
- PostgreSQL: UP
|
||||
- Disk Space: UP
|
||||
- Liveness: UP
|
||||
- Readiness: UP
|
||||
|
||||
---
|
||||
|
||||
## API 테스트 결과
|
||||
|
||||
### 1. Redis 연결 테스트
|
||||
|
||||
**엔드포인트**: `GET /api/v1/redis-test/ping`
|
||||
|
||||
**요청**:
|
||||
```bash
|
||||
curl http://localhost:8080/api/v1/redis-test/ping
|
||||
```
|
||||
|
||||
**응답**:
|
||||
```
|
||||
Redis OK - pong:1730104879446
|
||||
```
|
||||
|
||||
**결과**: ✅ **성공**
|
||||
**비고**: Redis 연결 및 데이터 저장/조회 정상 동작
|
||||
|
||||
---
|
||||
|
||||
### 2. 이벤트 생성 API (목적 선택)
|
||||
|
||||
**엔드포인트**: `POST /api/v1/events/objectives`
|
||||
|
||||
**요청**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/events/objectives \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"objective":"customer_retention"}'
|
||||
```
|
||||
|
||||
**응답**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"eventId": "9caa45e8-668e-4e84-a4d4-98c841e6f727",
|
||||
"status": "DRAFT",
|
||||
"objective": "customer_retention",
|
||||
"createdAt": "2025-10-28T14:54:40.1796612"
|
||||
},
|
||||
"timestamp": "2025-10-28T14:54:40.1906609"
|
||||
}
|
||||
```
|
||||
|
||||
**결과**: ✅ **성공**
|
||||
**생성된 이벤트 ID**: 9caa45e8-668e-4e84-a4d4-98c841e6f727
|
||||
|
||||
---
|
||||
|
||||
### 3. AI 추천 요청 API
|
||||
|
||||
**엔드포인트**: `POST /api/v1/events/{eventId}/ai-recommendations`
|
||||
|
||||
**요청**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/events/9caa45e8-668e-4e84-a4d4-98c841e6f727/ai-recommendations \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"eventDraftId": 1,
|
||||
"styles": ["FANCY", "SIMPLE"],
|
||||
"platforms": ["INSTAGRAM", "KAKAO"]
|
||||
"storeInfo": {
|
||||
"storeId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"storeName": "Woojin BBQ",
|
||||
"category": "Restaurant",
|
||||
"description": "Korean BBQ restaurant in Seoul"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**응답**:
|
||||
- **HTTP 상태**: 202 Accepted
|
||||
- **응답 본문**:
|
||||
```json
|
||||
{
|
||||
"id": "job-mock-7ada8bd3",
|
||||
"eventDraftId": 1,
|
||||
"jobType": "image-generation",
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"resultMessage": null,
|
||||
"errorMessage": null,
|
||||
"createdAt": "2025-10-23T21:52:57.511438",
|
||||
"updatedAt": "2025-10-23T21:52:57.511438"
|
||||
}
|
||||
```
|
||||
|
||||
**검증 결과**: ✅ PASS
|
||||
- Job이 정상적으로 생성되어 PENDING 상태로 반환됨
|
||||
- 비동기 처리를 위한 Job ID 발급 확인
|
||||
|
||||
---
|
||||
|
||||
### 3.2 GET /content/images/jobs/{jobId} - 작업 상태 조회
|
||||
|
||||
**목적**: 이미지 생성 작업의 진행 상태 확인
|
||||
|
||||
**요청**:
|
||||
```bash
|
||||
curl http://localhost:8084/content/images/jobs/job-mock-7ada8bd3
|
||||
```
|
||||
|
||||
**응답** (1초 후):
|
||||
- **HTTP 상태**: 200 OK
|
||||
- **응답 본문**:
|
||||
```json
|
||||
{
|
||||
"id": "job-mock-7ada8bd3",
|
||||
"eventDraftId": 1,
|
||||
"jobType": "image-generation",
|
||||
"status": "COMPLETED",
|
||||
"progress": 100,
|
||||
"resultMessage": "4개의 이미지가 성공적으로 생성되었습니다.",
|
||||
"errorMessage": null,
|
||||
"createdAt": "2025-10-23T21:52:57.511438",
|
||||
"updatedAt": "2025-10-23T21:52:58.571923"
|
||||
}
|
||||
```
|
||||
|
||||
**검증 결과**: ✅ PASS
|
||||
- Job 상태가 PENDING → COMPLETED로 정상 전환
|
||||
- progress가 0 → 100으로 업데이트
|
||||
- resultMessage에 생성 결과 포함
|
||||
|
||||
---
|
||||
|
||||
### 3.3 GET /content/events/{eventDraftId} - 이벤트 콘텐츠 조회
|
||||
|
||||
**목적**: 특정 이벤트의 전체 콘텐츠 정보 조회 (이미지 포함)
|
||||
|
||||
**요청**:
|
||||
```bash
|
||||
curl http://localhost:8084/content/events/1
|
||||
```
|
||||
|
||||
**응답**:
|
||||
- **HTTP 상태**: 200 OK
|
||||
- **응답 본문**:
|
||||
```json
|
||||
{
|
||||
"eventDraftId": 1,
|
||||
"eventTitle": "Mock 이벤트 제목 1",
|
||||
"eventDescription": "Mock 이벤트 설명입니다. 테스트를 위한 Mock 데이터입니다.",
|
||||
"images": [
|
||||
{
|
||||
"id": 1,
|
||||
"style": "FANCY",
|
||||
"platform": "INSTAGRAM",
|
||||
"cdnUrl": "https://mock-cdn.azure.com/images/1/fancy_instagram_7ada8bd3.png",
|
||||
"prompt": "Mock prompt for FANCY style on INSTAGRAM platform",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"style": "FANCY",
|
||||
"platform": "KAKAO",
|
||||
"cdnUrl": "https://mock-cdn.azure.com/images/1/fancy_kakao_3e2eaacf.png",
|
||||
"prompt": "Mock prompt for FANCY style on KAKAO platform",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"style": "SIMPLE",
|
||||
"platform": "INSTAGRAM",
|
||||
"cdnUrl": "https://mock-cdn.azure.com/images/1/simple_instagram_56d91422.png",
|
||||
"prompt": "Mock prompt for SIMPLE style on INSTAGRAM platform",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"style": "SIMPLE",
|
||||
"platform": "KAKAO",
|
||||
"cdnUrl": "https://mock-cdn.azure.com/images/1/simple_kakao_7c9a666a.png",
|
||||
"prompt": "Mock prompt for SIMPLE style on KAKAO platform",
|
||||
"selected": false
|
||||
}
|
||||
],
|
||||
"createdAt": "2025-10-23T21:52:57.52133",
|
||||
"updatedAt": "2025-10-23T21:52:57.52133"
|
||||
}
|
||||
```
|
||||
|
||||
**검증 결과**: ✅ PASS
|
||||
- 콘텐츠 정보와 생성된 이미지 목록이 모두 조회됨
|
||||
- 4개 이미지 (FANCY/SIMPLE x INSTAGRAM/KAKAO) 생성 확인
|
||||
- 첫 번째 이미지(FANCY+INSTAGRAM)가 selected:true로 설정됨
|
||||
|
||||
---
|
||||
|
||||
### 3.4 GET /content/events/{eventDraftId}/images - 이미지 목록 조회
|
||||
|
||||
**목적**: 특정 이벤트의 이미지 목록만 조회
|
||||
|
||||
**요청**:
|
||||
```bash
|
||||
curl http://localhost:8084/content/events/1/images
|
||||
```
|
||||
|
||||
**응답**:
|
||||
- **HTTP 상태**: 200 OK
|
||||
- **응답 본문**: 4개의 이미지 객체 배열
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"eventDraftId": 1,
|
||||
"style": "FANCY",
|
||||
"platform": "INSTAGRAM",
|
||||
"cdnUrl": "https://mock-cdn.azure.com/images/1/fancy_instagram_7ada8bd3.png",
|
||||
"prompt": "Mock prompt for FANCY style on INSTAGRAM platform",
|
||||
"selected": true,
|
||||
"createdAt": "2025-10-23T21:52:57.524759",
|
||||
"updatedAt": "2025-10-23T21:52:57.524759"
|
||||
"success": true,
|
||||
"data": {
|
||||
"jobId": "3e3e8214-131a-4a1f-93ce-bf8b7702cb81",
|
||||
"status": "PENDING",
|
||||
"message": "AI 추천 생성 요청이 접수되었습니다. /jobs/3e3e8214-131a-4a1f-93ce-bf8b7702cb81로 상태를 확인하세요."
|
||||
},
|
||||
// ... 나머지 3개 이미지
|
||||
]
|
||||
```
|
||||
|
||||
**검증 결과**: ✅ PASS
|
||||
- 이벤트에 속한 모든 이미지가 정상 조회됨
|
||||
- createdAt, updatedAt 타임스탬프 포함
|
||||
|
||||
---
|
||||
|
||||
### 3.5 GET /content/images/{imageId} - 개별 이미지 상세 조회
|
||||
|
||||
**목적**: 특정 이미지의 상세 정보 조회
|
||||
|
||||
**요청**:
|
||||
```bash
|
||||
curl http://localhost:8084/content/images/1
|
||||
```
|
||||
|
||||
**응답**:
|
||||
- **HTTP 상태**: 200 OK
|
||||
- **응답 본문**:
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"eventDraftId": 1,
|
||||
"style": "FANCY",
|
||||
"platform": "INSTAGRAM",
|
||||
"cdnUrl": "https://mock-cdn.azure.com/images/1/fancy_instagram_7ada8bd3.png",
|
||||
"prompt": "Mock prompt for FANCY style on INSTAGRAM platform",
|
||||
"selected": true,
|
||||
"createdAt": "2025-10-23T21:52:57.524759",
|
||||
"updatedAt": "2025-10-23T21:52:57.524759"
|
||||
"timestamp": "2025-10-28T14:55:23.4982302"
|
||||
}
|
||||
```
|
||||
|
||||
**검증 결과**: ✅ PASS
|
||||
- 개별 이미지 정보가 정상적으로 조회됨
|
||||
- 모든 필드가 올바르게 반환됨
|
||||
**결과**: ✅ **성공**
|
||||
**생성된 Job ID**: 3e3e8214-131a-4a1f-93ce-bf8b7702cb81
|
||||
**비고**: Kafka 메시지 발행 성공 (비동기 처리)
|
||||
|
||||
---
|
||||
|
||||
### 3.6 POST /content/images/{imageId}/regenerate - 이미지 재생성
|
||||
### 4. Job 상태 조회 API
|
||||
|
||||
**목적**: 특정 이미지를 다시 생성하는 작업 시작
|
||||
**엔드포인트**: `GET /api/v1/jobs/{jobId}`
|
||||
|
||||
**요청**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8084/content/images/1/regenerate \
|
||||
-H "Content-Type: application/json"
|
||||
curl http://localhost:8080/api/v1/jobs/3e3e8214-131a-4a1f-93ce-bf8b7702cb81
|
||||
```
|
||||
|
||||
**응답**:
|
||||
- **HTTP 상태**: 200 OK
|
||||
- **응답 본문**:
|
||||
```json
|
||||
{
|
||||
"id": "job-regen-df2bb3a3",
|
||||
"eventDraftId": 999,
|
||||
"jobType": "image-regeneration",
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"resultMessage": null,
|
||||
"errorMessage": null,
|
||||
"createdAt": "2025-10-23T21:55:40.490627",
|
||||
"updatedAt": "2025-10-23T21:55:40.490627"
|
||||
"success": true,
|
||||
"data": {
|
||||
"jobId": "3e3e8214-131a-4a1f-93ce-bf8b7702cb81",
|
||||
"jobType": "AI_RECOMMENDATION",
|
||||
"status": "PENDING",
|
||||
"eventId": "9caa45e8-668e-4e84-a4d4-98c841e6f727",
|
||||
"createdAt": "2025-10-28T14:55:23.4982302",
|
||||
"updatedAt": "2025-10-28T14:55:23.4982302",
|
||||
"completedAt": null,
|
||||
"errorMessage": null
|
||||
},
|
||||
"timestamp": "2025-10-28T14:55:47.9869931"
|
||||
}
|
||||
```
|
||||
|
||||
**검증 결과**: ✅ PASS
|
||||
- 재생성 Job이 정상적으로 생성됨
|
||||
- jobType이 "image-regeneration"으로 설정됨
|
||||
- PENDING 상태로 시작
|
||||
**결과**: ✅ **성공**
|
||||
**비고**: Job 상태 추적 정상 동작
|
||||
|
||||
---
|
||||
|
||||
### 3.7 DELETE /content/images/{imageId} - 이미지 삭제
|
||||
### 5. 이벤트 상세 조회 API
|
||||
|
||||
**목적**: 특정 이미지 삭제
|
||||
**엔드포인트**: `GET /api/v1/events/{eventId}`
|
||||
|
||||
**요청**:
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8084/content/images/4
|
||||
curl http://localhost:8080/api/v1/events/9caa45e8-668e-4e84-a4d4-98c841e6f727
|
||||
```
|
||||
|
||||
**응답**:
|
||||
- **HTTP 상태**: 204 No Content
|
||||
- **응답 본문**: 없음 (정상)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"eventId": "9caa45e8-668e-4e84-a4d4-98c841e6f727",
|
||||
"userId": null,
|
||||
"storeId": null,
|
||||
"eventName": null,
|
||||
"description": null,
|
||||
"objective": "customer_retention",
|
||||
"startDate": null,
|
||||
"endDate": null,
|
||||
"status": "DRAFT",
|
||||
"selectedImageId": null,
|
||||
"selectedImageUrl": null,
|
||||
"generatedImages": [],
|
||||
"aiRecommendations": [],
|
||||
"channels": [],
|
||||
"createdAt": "2025-10-28T14:54:40.179661",
|
||||
"updatedAt": "2025-10-28T14:54:40.179661"
|
||||
},
|
||||
"timestamp": "2025-10-28T14:56:08.6623502"
|
||||
}
|
||||
```
|
||||
|
||||
**검증 결과**: ✅ PASS
|
||||
- 삭제 요청이 정상적으로 처리됨
|
||||
- HTTP 204 상태로 응답
|
||||
|
||||
**참고**: H2 in-memory 데이터베이스 특성상 물리적 삭제가 즉시 반영되지 않을 수 있음
|
||||
**결과**: ✅ **성공**
|
||||
|
||||
---
|
||||
|
||||
## 4. 종합 테스트 결과
|
||||
### 6. 이벤트 목록 조회 API
|
||||
|
||||
### 4.1 테스트 요약
|
||||
| API | Method | Endpoint | 상태 | 비고 |
|
||||
|-----|--------|----------|------|------|
|
||||
| 이미지 생성 | POST | /content/images/generate | ✅ PASS | Job 생성 확인 |
|
||||
| 작업 조회 | GET | /content/images/jobs/{jobId} | ✅ PASS | 상태 전환 확인 |
|
||||
| 콘텐츠 조회 | GET | /content/events/{eventDraftId} | ✅ PASS | 이미지 포함 조회 |
|
||||
| 이미지 목록 | GET | /content/events/{eventDraftId}/images | ✅ PASS | 4개 이미지 확인 |
|
||||
| 이미지 상세 | GET | /content/images/{imageId} | ✅ PASS | 단일 이미지 조회 |
|
||||
| 이미지 재생성 | POST | /content/images/{imageId}/regenerate | ✅ PASS | 재생성 Job 확인 |
|
||||
| 이미지 삭제 | DELETE | /content/images/{imageId} | ✅ PASS | 204 응답 확인 |
|
||||
**엔드포인트**: `GET /api/v1/events`
|
||||
|
||||
### 4.2 전체 결과
|
||||
- **총 테스트 케이스**: 7개
|
||||
- **성공**: 7개
|
||||
- **실패**: 0개
|
||||
- **성공률**: 100%
|
||||
|
||||
## 5. 검증된 기능
|
||||
|
||||
### 5.1 비즈니스 로직
|
||||
✅ 이미지 생성 요청 → Job 생성 → 비동기 처리 → 완료 확인 흐름 정상 동작
|
||||
✅ Mock 서비스를 통한 4개 조합(2 스타일 x 2 플랫폼) 이미지 자동 생성
|
||||
✅ 첫 번째 이미지 자동 선택(selected:true) 로직 정상 동작
|
||||
✅ Content와 GeneratedImage 엔티티 연관 관계 정상 동작
|
||||
|
||||
### 5.2 기술 구현
|
||||
✅ Clean Architecture (Hexagonal Architecture) 구조 정상 동작
|
||||
✅ @Profile 기반 환경별 Bean 선택 정상 동작 (Mock vs Production)
|
||||
✅ H2 In-Memory 데이터베이스 자동 스키마 생성 및 데이터 저장
|
||||
✅ @Async 비동기 처리 정상 동작
|
||||
✅ Spring Data JPA 엔티티 관계 및 쿼리 정상 동작
|
||||
✅ REST API 표준 HTTP 상태 코드 사용 (200, 202, 204)
|
||||
|
||||
### 5.3 Mock 서비스
|
||||
✅ MockGenerateImagesService: 1초 지연 후 이미지 생성 시뮬레이션
|
||||
✅ MockRedisGateway: Redis 캐시 기능 Mock 구현
|
||||
✅ Local 프로파일에서 외부 의존성 없이 독립 실행
|
||||
|
||||
## 6. 확인된 이슈 및 개선사항
|
||||
|
||||
### 6.1 경고 메시지 (Non-Critical)
|
||||
**요청**:
|
||||
```bash
|
||||
curl "http://localhost:8080/api/v1/events?page=0&size=10"
|
||||
```
|
||||
WARN: Index "IDX_EVENT_DRAFT_ID" already exists
|
||||
|
||||
**응답**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"content": [
|
||||
{
|
||||
"eventId": "9caa45e8-668e-4e84-a4d4-98c841e6f727",
|
||||
"userId": null,
|
||||
"storeId": null,
|
||||
"eventName": null,
|
||||
"description": null,
|
||||
"objective": "customer_retention",
|
||||
"startDate": null,
|
||||
"endDate": null,
|
||||
"status": "DRAFT",
|
||||
"selectedImageId": null,
|
||||
"selectedImageUrl": null,
|
||||
"generatedImages": [],
|
||||
"aiRecommendations": [],
|
||||
"channels": [],
|
||||
"createdAt": "2025-10-28T14:54:40.179661",
|
||||
"updatedAt": "2025-10-28T14:54:40.179661"
|
||||
}
|
||||
],
|
||||
"page": 0,
|
||||
"size": 10,
|
||||
"totalElements": 1,
|
||||
"totalPages": 1,
|
||||
"first": true,
|
||||
"last": true
|
||||
},
|
||||
"timestamp": "2025-10-28T14:56:33.9042874"
|
||||
}
|
||||
```
|
||||
- **원인**: generated_images와 jobs 테이블에 동일한 이름의 인덱스 사용
|
||||
- **영향**: H2에서만 발생하는 경고, 기능에 영향 없음
|
||||
- **개선 방안**: 각 테이블별로 고유한 인덱스 이름 사용 권장
|
||||
- `idx_generated_images_event_draft_id`
|
||||
- `idx_jobs_event_draft_id`
|
||||
|
||||
### 6.2 Redis 구현 현황
|
||||
✅ **Production용 구현 완료**:
|
||||
- RedisConfig.java - RedisTemplate 설정
|
||||
- RedisGateway.java - Redis 읽기/쓰기 구현
|
||||
**결과**: ✅ **성공**
|
||||
**비고**: 페이지네이션 정상 동작
|
||||
|
||||
✅ **Local/Test용 Mock 구현**:
|
||||
- MockRedisGateway - 캐시 기능 Mock
|
||||
---
|
||||
|
||||
## 7. 다음 단계
|
||||
## 통합 기능 검증
|
||||
|
||||
### 7.1 추가 테스트 필요 사항
|
||||
- [ ] 에러 케이스 테스트
|
||||
- 존재하지 않는 eventDraftId 조회
|
||||
- 존재하지 않는 imageId 조회
|
||||
- 잘못된 요청 파라미터 (validation 테스트)
|
||||
- [ ] 동시성 테스트
|
||||
- 동일 이벤트에 대한 동시 이미지 생성 요청
|
||||
- [ ] 성능 테스트
|
||||
- 대량 이미지 생성 시 성능 측정
|
||||
### 1. PostgreSQL 연동
|
||||
- ✅ **연결**: 정상 (20.249.177.232:5432)
|
||||
- ✅ **데이터베이스**: eventdb
|
||||
- ✅ **CRUD 작업**: 정상 동작
|
||||
- ✅ **JPA/Hibernate**: 정상 동작
|
||||
|
||||
### 7.2 통합 테스트
|
||||
- [ ] PostgreSQL 연동 테스트 (Production 프로파일)
|
||||
- [ ] Redis 실제 연동 테스트
|
||||
- [ ] Kafka 메시지 발행/구독 테스트
|
||||
- [ ] 타 서비스(event-service 등)와의 통합 테스트
|
||||
### 2. Redis 연동
|
||||
- ✅ **연결**: 정상 (20.214.210.71:6379)
|
||||
- ✅ **데이터 저장/조회**: 정상 동작
|
||||
- ✅ **Lettuce 클라이언트**: 정상 동작
|
||||
|
||||
## 8. 결론
|
||||
### 3. Kafka 연동
|
||||
- ✅ **Producer**: 정상 동작 (메시지 발행 성공)
|
||||
- ⚠️ **Consumer**: 역직렬화 오류 로그 발생 (기능 동작은 정상)
|
||||
- ✅ **ErrorHandlingDeserializer**: 적용됨
|
||||
|
||||
Content Service의 모든 핵심 REST API가 정상적으로 동작하며, Local 환경에서 Mock 서비스를 통해 독립적으로 실행 및 테스트 가능함을 확인했습니다.
|
||||
---
|
||||
|
||||
### 주요 성과
|
||||
1. ✅ 7개 API 엔드포인트 100% 정상 동작
|
||||
2. ✅ Clean Architecture 구조 정상 동작
|
||||
3. ✅ Profile 기반 환경 분리 정상 동작
|
||||
4. ✅ 비동기 이미지 생성 흐름 정상 동작
|
||||
5. ✅ Redis Gateway Production/Mock 구현 완료
|
||||
## 발견된 이슈 및 개선사항
|
||||
|
||||
Content Service는 Local 환경에서 완전히 검증되었으며, Production 환경 배포를 위한 준비가 완료되었습니다.
|
||||
### 1. Kafka Consumer 역직렬화 오류 (경미)
|
||||
|
||||
**현상**:
|
||||
```
|
||||
No type information in headers and no default type provided
|
||||
```
|
||||
|
||||
**원인**:
|
||||
- 토픽에 이전 테스트 메시지가 남아있음
|
||||
- ErrorHandlingDeserializer가 오류를 처리하지만 로그에 기록됨
|
||||
|
||||
**영향**:
|
||||
- 서비스 기능에는 영향 없음
|
||||
- 오류 메시지 스킵 후 정상 동작
|
||||
|
||||
**해결 방안**:
|
||||
- ✅ ErrorHandlingDeserializer 이미 적용됨
|
||||
- ⚠️ 운영 환경에서는 토픽 초기화 또는 consumer group 재설정 권장
|
||||
|
||||
### 2. UTF-8 인코딩 이슈 (환경 제약)
|
||||
|
||||
**현상**:
|
||||
```bash
|
||||
curl -d '{"storeName":"우진네 고깃집"}'
|
||||
# → "Invalid UTF-8 start byte 0xbf" 오류
|
||||
```
|
||||
|
||||
**원인**:
|
||||
- MINGW64 bash 터미널의 인코딩 제약
|
||||
|
||||
**해결 방법**:
|
||||
- ✅ 영문 텍스트로 테스트 진행 (기능 검증 완료)
|
||||
- 💡 **권장**: 한글 데이터 테스트 시 Postman 사용 또는 JSON 파일로 저장 후 `curl -d @file.json` 방식 사용
|
||||
|
||||
---
|
||||
|
||||
## 테스트 요약
|
||||
|
||||
### 성공한 테스트 (8/8)
|
||||
|
||||
| # | API | 엔드포인트 | 결과 |
|
||||
|---|-----|-----------|------|
|
||||
| 1 | Health Check | GET /actuator/health | ✅ |
|
||||
| 2 | Redis 테스트 | GET /api/v1/redis-test/ping | ✅ |
|
||||
| 3 | 이벤트 생성 | POST /api/v1/events/objectives | ✅ |
|
||||
| 4 | AI 추천 요청 | POST /api/v1/events/{id}/ai-recommendations | ✅ |
|
||||
| 5 | Job 상태 조회 | GET /api/v1/jobs/{jobId} | ✅ |
|
||||
| 6 | 이벤트 조회 | GET /api/v1/events/{id} | ✅ |
|
||||
| 7 | 이벤트 목록 | GET /api/v1/events | ✅ |
|
||||
| 8 | 설정 일치 검증 | application.yml ↔ run.xml | ✅ |
|
||||
|
||||
**성공률**: 100% (8/8)
|
||||
|
||||
### 테스트되지 않은 API
|
||||
|
||||
다음 API는 Content Service 또는 Distribution Service가 필요하여 테스트 미진행:
|
||||
- POST /api/v1/events/{eventId}/images - 이미지 생성 요청
|
||||
- PUT /api/v1/events/{eventId}/images/{imageId}/select - 이미지 선택
|
||||
- PUT /api/v1/events/{eventId}/recommendations - AI 추천 선택
|
||||
- PUT /api/v1/events/{eventId} - 이벤트 수정
|
||||
- POST /api/v1/events/{eventId}/publish - 이벤트 배포
|
||||
- PUT /api/v1/events/{eventId}/channels - 배포 채널 선택
|
||||
|
||||
---
|
||||
|
||||
## 결론
|
||||
|
||||
**전체 평가**: ✅ **매우 양호**
|
||||
|
||||
Event Service는 독립적으로 실행 가능한 모든 핵심 기능이 정상 동작합니다.
|
||||
|
||||
**검증 완료 항목**:
|
||||
- ✅ PostgreSQL 연동 및 데이터 영속성
|
||||
- ✅ Redis 캐싱 기능
|
||||
- ✅ Kafka Producer (메시지 발행)
|
||||
- ✅ REST API CRUD 작업
|
||||
- ✅ 비동기 Job 처리 패턴
|
||||
- ✅ 환경 변수 설정 일관성
|
||||
|
||||
**남은 과제**:
|
||||
1. Content Service 연동 후 이미지 생성/선택 기능 테스트
|
||||
2. Distribution Service 연동 후 이벤트 배포 기능 테스트
|
||||
3. AI Service 실제 연동 후 추천 생성 완료 테스트
|
||||
4. Kafka Consumer 토픽 초기화 또는 설정 개선
|
||||
|
||||
**다음 단계 권장사항**:
|
||||
1. Content Service 개발 및 통합 테스트
|
||||
2. Distribution Service 개발 및 통합 테스트
|
||||
3. 전체 서비스 통합 시나리오 테스트
|
||||
4. 성능 테스트 및 부하 테스트
|
||||
5. 운영 환경 배포 준비 (Kafka 토픽 설정, 로그 레벨 조정)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7.2-alpine
|
||||
container_name: kt-event-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
command: redis-server --appendonly yes
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- kt-event-network
|
||||
|
||||
zookeeper:
|
||||
image: confluentinc/cp-zookeeper:7.5.0
|
||||
container_name: kt-event-zookeeper
|
||||
environment:
|
||||
ZOOKEEPER_CLIENT_PORT: 2181
|
||||
ZOOKEEPER_TICK_TIME: 2000
|
||||
ports:
|
||||
- "2181:2181"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- kt-event-network
|
||||
|
||||
kafka:
|
||||
image: confluentinc/cp-kafka:7.5.0
|
||||
container_name: kt-event-kafka
|
||||
depends_on:
|
||||
- zookeeper
|
||||
ports:
|
||||
- "9092:9092"
|
||||
environment:
|
||||
KAFKA_BROKER_ID: 1
|
||||
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
||||
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
|
||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT
|
||||
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
|
||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- kt-event-network
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
kt-event-network:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,71 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="event-service" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="env">
|
||||
<map>
|
||||
<!-- Server Configuration -->
|
||||
<entry key="SERVER_PORT" value="8080" />
|
||||
|
||||
<!-- Database Configuration -->
|
||||
<entry key="DB_HOST" value="20.249.177.232" />
|
||||
<entry key="DB_PORT" value="5432" />
|
||||
<entry key="DB_NAME" value="eventdb" />
|
||||
<entry key="DB_USERNAME" value="eventuser" />
|
||||
<entry key="DB_PASSWORD" value="Hi5Jessica!" />
|
||||
|
||||
<!-- JPA Configuration -->
|
||||
<entry key="DDL_AUTO" value="update" />
|
||||
|
||||
<!-- Redis Configuration -->
|
||||
<entry key="REDIS_HOST" value="20.214.210.71" />
|
||||
<entry key="REDIS_PORT" value="6379" />
|
||||
<entry key="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||
|
||||
<!-- Kafka Configuration -->
|
||||
<entry key="KAFKA_BOOTSTRAP_SERVERS" value="20.249.182.13:9095,4.217.131.59:9095" />
|
||||
|
||||
<!-- Service URLs -->
|
||||
<entry key="CONTENT_SERVICE_URL" value="http://localhost:8082" />
|
||||
<entry key="DISTRIBUTION_SERVICE_URL" value="http://localhost:8084" />
|
||||
|
||||
<!-- JWT Configuration -->
|
||||
<entry key="JWT_SECRET" value="kt-event-marketing-secret-key-for-development-only-please-change-in-production" />
|
||||
|
||||
<!-- Logging Configuration -->
|
||||
<entry key="LOG_LEVEL" value="DEBUG" />
|
||||
<entry key="SQL_LOG_LEVEL" value="DEBUG" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="event-service:bootRun" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="-Xms512m -Xmx2048m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Dspring.jmx.enabled=false -Dspring.devtools.restart.enabled=false" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
|
||||
<extension name="net.ashald.envfile">
|
||||
<option name="IS_ENABLED" value="false" />
|
||||
<option name="IS_SUBST" value="false" />
|
||||
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
|
||||
<option name="IS_IGNORE_MISSING_FILES" value="false" />
|
||||
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
|
||||
<ENTRIES>
|
||||
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
|
||||
</ENTRIES>
|
||||
</extension>
|
||||
</EXTENSION>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -24,7 +24,11 @@ import org.springframework.kafka.annotation.EnableKafka;
|
||||
"com.kt.event.eventservice",
|
||||
"com.kt.event.common"
|
||||
},
|
||||
exclude = {UserDetailsServiceAutoConfiguration.class}
|
||||
exclude = {
|
||||
UserDetailsServiceAutoConfiguration.class,
|
||||
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.class,
|
||||
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration.class
|
||||
}
|
||||
)
|
||||
@EnableJpaAuditing
|
||||
@EnableKafka
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.kt.event.eventservice.application.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* AI 추천 요청 DTO
|
||||
*
|
||||
* AI 서비스에 이벤트 추천 생성을 요청합니다.
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "AI 추천 요청")
|
||||
public class AiRecommendationRequest {
|
||||
|
||||
@NotNull(message = "매장 정보는 필수입니다.")
|
||||
@Valid
|
||||
@Schema(description = "매장 정보", required = true)
|
||||
private StoreInfo storeInfo;
|
||||
|
||||
/**
|
||||
* 매장 정보
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "매장 정보")
|
||||
public static class StoreInfo {
|
||||
|
||||
@NotNull(message = "매장 ID는 필수입니다.")
|
||||
@Schema(description = "매장 ID", required = true, example = "550e8400-e29b-41d4-a716-446655440002")
|
||||
private UUID storeId;
|
||||
|
||||
@NotNull(message = "매장명은 필수입니다.")
|
||||
@Schema(description = "매장명", required = true, example = "우진네 고깃집")
|
||||
private String storeName;
|
||||
|
||||
@NotNull(message = "업종은 필수입니다.")
|
||||
@Schema(description = "업종", required = true, example = "음식점")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "매장 설명", example = "신선한 한우를 제공하는 고깃집")
|
||||
private String description;
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.kt.event.eventservice.application.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 이미지 편집 요청 DTO
|
||||
*
|
||||
* 선택된 이미지를 편집합니다.
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "이미지 편집 요청")
|
||||
public class ImageEditRequest {
|
||||
|
||||
@NotNull(message = "편집 유형은 필수입니다.")
|
||||
@Schema(description = "편집 유형", required = true, example = "TEXT_OVERLAY",
|
||||
allowableValues = {"TEXT_OVERLAY", "COLOR_ADJUST", "CROP", "FILTER"})
|
||||
private EditType editType;
|
||||
|
||||
@NotNull(message = "편집 파라미터는 필수입니다.")
|
||||
@Schema(description = "편집 파라미터 (편집 유형에 따라 다름)", required = true,
|
||||
example = "{\"text\": \"20% 할인\", \"fontSize\": 48, \"color\": \"#FF0000\", \"position\": \"center\"}")
|
||||
private Map<String, Object> parameters;
|
||||
|
||||
/**
|
||||
* 편집 유형
|
||||
*/
|
||||
public enum EditType {
|
||||
TEXT_OVERLAY, // 텍스트 오버레이
|
||||
COLOR_ADJUST, // 색상 조정
|
||||
CROP, // 자르기
|
||||
FILTER // 필터 적용
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.kt.event.eventservice.application.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 이미지 생성 요청 DTO
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ImageGenerationRequest {
|
||||
|
||||
@NotEmpty(message = "이미지 스타일은 최소 1개 이상 선택해야 합니다.")
|
||||
private List<String> styles;
|
||||
|
||||
@NotEmpty(message = "플랫폼은 최소 1개 이상 선택해야 합니다.")
|
||||
private List<String> platforms;
|
||||
|
||||
@Min(value = 1, message = "이미지 개수는 최소 1개 이상이어야 합니다.")
|
||||
@Max(value = 9, message = "이미지 개수는 최대 9개까지 가능합니다.")
|
||||
@Builder.Default
|
||||
private int imageCount = 3;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.kt.event.eventservice.application.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 배포 채널 선택 요청 DTO
|
||||
*
|
||||
* 이벤트를 배포할 채널을 선택합니다.
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "배포 채널 선택 요청")
|
||||
public class SelectChannelsRequest {
|
||||
|
||||
@NotEmpty(message = "배포 채널을 최소 1개 이상 선택해야 합니다.")
|
||||
@Schema(description = "배포 채널 목록", required = true,
|
||||
example = "[\"WEBSITE\", \"KAKAO\", \"INSTAGRAM\"]")
|
||||
private List<String> channels;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.kt.event.eventservice.application.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 이미지 선택 요청 DTO
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class SelectImageRequest {
|
||||
|
||||
@NotNull(message = "이미지 ID는 필수입니다.")
|
||||
private UUID imageId;
|
||||
|
||||
private String imageUrl;
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.kt.event.eventservice.application.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* AI 추천 선택 요청 DTO
|
||||
*
|
||||
* AI가 생성한 추천 중 하나를 선택하고 커스터마이징합니다.
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "AI 추천 선택 요청")
|
||||
public class SelectRecommendationRequest {
|
||||
|
||||
@NotNull(message = "추천 ID는 필수입니다.")
|
||||
@Schema(description = "선택한 추천 ID", required = true, example = "550e8400-e29b-41d4-a716-446655440007")
|
||||
private UUID recommendationId;
|
||||
|
||||
@Valid
|
||||
@Schema(description = "커스터마이징 항목")
|
||||
private Customizations customizations;
|
||||
|
||||
/**
|
||||
* 커스터마이징 항목
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "커스터마이징 항목")
|
||||
public static class Customizations {
|
||||
|
||||
@Schema(description = "수정된 이벤트명", example = "봄맞이 특별 할인 이벤트")
|
||||
private String eventName;
|
||||
|
||||
@Schema(description = "수정된 설명", example = "봄을 맞이하여 전 메뉴 20% 할인")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "수정된 시작일", example = "2025-03-01")
|
||||
private LocalDate startDate;
|
||||
|
||||
@Schema(description = "수정된 종료일", example = "2025-03-31")
|
||||
private LocalDate endDate;
|
||||
|
||||
@Schema(description = "수정된 할인율", example = "20")
|
||||
private Integer discountRate;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.kt.event.eventservice.application.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 이벤트 수정 요청 DTO
|
||||
*
|
||||
* 기존 이벤트의 정보를 수정합니다.
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "이벤트 수정 요청")
|
||||
public class UpdateEventRequest {
|
||||
|
||||
@Schema(description = "이벤트명", example = "봄맞이 특별 할인 이벤트")
|
||||
private String eventName;
|
||||
|
||||
@Schema(description = "이벤트 설명", example = "봄을 맞이하여 전 메뉴 20% 할인")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "시작일", example = "2025-03-01")
|
||||
private LocalDate startDate;
|
||||
|
||||
@Schema(description = "종료일", example = "2025-03-31")
|
||||
private LocalDate endDate;
|
||||
|
||||
@Schema(description = "할인율", example = "20")
|
||||
private Integer discountRate;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.kt.event.eventservice.application.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 이미지 편집 응답 DTO
|
||||
*
|
||||
* 편집된 이미지 정보를 반환합니다.
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "이미지 편집 응답")
|
||||
public class ImageEditResponse {
|
||||
|
||||
@Schema(description = "편집된 이미지 ID", example = "550e8400-e29b-41d4-a716-446655440008")
|
||||
private UUID imageId;
|
||||
|
||||
@Schema(description = "편집된 이미지 URL", example = "https://cdn.kt-event.com/images/event-img-001-edited.jpg")
|
||||
private String imageUrl;
|
||||
|
||||
@Schema(description = "편집일시", example = "2025-02-16T15:20:00")
|
||||
private LocalDateTime editedAt;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.kt.event.eventservice.application.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 이미지 생성 응답 DTO
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ImageGenerationResponse {
|
||||
|
||||
private UUID jobId;
|
||||
private String status;
|
||||
private String message;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.kt.event.eventservice.application.dto.response;
|
||||
|
||||
import com.kt.event.eventservice.domain.enums.JobStatus;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Job 접수 응답 DTO
|
||||
*
|
||||
* 비동기 작업이 접수되었음을 알리는 응답입니다.
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "Job 접수 응답")
|
||||
public class JobAcceptedResponse {
|
||||
|
||||
@Schema(description = "생성된 Job ID", example = "550e8400-e29b-41d4-a716-446655440005")
|
||||
private UUID jobId;
|
||||
|
||||
@Schema(description = "Job 상태 (초기 상태는 PENDING)", example = "PENDING")
|
||||
private JobStatus status;
|
||||
|
||||
@Schema(description = "안내 메시지", example = "AI 추천 생성 요청이 접수되었습니다. /jobs/{jobId}로 상태를 확인하세요.")
|
||||
private String message;
|
||||
}
|
||||
+317
-3
@@ -2,12 +2,17 @@ package com.kt.event.eventservice.application.service;
|
||||
|
||||
import com.kt.event.common.exception.BusinessException;
|
||||
import com.kt.event.common.exception.ErrorCode;
|
||||
import com.kt.event.eventservice.application.dto.request.SelectObjectiveRequest;
|
||||
import com.kt.event.eventservice.application.dto.response.EventCreatedResponse;
|
||||
import com.kt.event.eventservice.application.dto.response.EventDetailResponse;
|
||||
import com.kt.event.eventservice.application.dto.request.*;
|
||||
import com.kt.event.eventservice.application.dto.response.*;
|
||||
import com.kt.event.eventservice.domain.enums.JobType;
|
||||
import com.kt.event.eventservice.domain.entity.*;
|
||||
import com.kt.event.eventservice.domain.enums.EventStatus;
|
||||
import com.kt.event.eventservice.domain.repository.EventRepository;
|
||||
import com.kt.event.eventservice.domain.repository.JobRepository;
|
||||
import com.kt.event.eventservice.infrastructure.client.ContentServiceClient;
|
||||
import com.kt.event.eventservice.infrastructure.client.dto.ContentImageGenerationRequest;
|
||||
import com.kt.event.eventservice.infrastructure.client.dto.ContentJobResponse;
|
||||
import com.kt.event.eventservice.infrastructure.kafka.AIJobKafkaProducer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.hibernate.Hibernate;
|
||||
@@ -35,6 +40,9 @@ import java.util.stream.Collectors;
|
||||
public class EventService {
|
||||
|
||||
private final EventRepository eventRepository;
|
||||
private final JobRepository jobRepository;
|
||||
private final ContentServiceClient contentServiceClient;
|
||||
private final AIJobKafkaProducer aiJobKafkaProducer;
|
||||
|
||||
/**
|
||||
* 이벤트 생성 (Step 1: 목적 선택)
|
||||
@@ -186,6 +194,312 @@ public class EventService {
|
||||
log.info("이벤트 종료 완료 - eventId: {}", eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 생성 요청
|
||||
*
|
||||
* @param userId 사용자 ID (UUID)
|
||||
* @param eventId 이벤트 ID
|
||||
* @param request 이미지 생성 요청
|
||||
* @return 이미지 생성 응답 (Job ID 포함)
|
||||
*/
|
||||
@Transactional
|
||||
public ImageGenerationResponse requestImageGeneration(UUID userId, UUID eventId, ImageGenerationRequest request) {
|
||||
log.info("이미지 생성 요청 - userId: {}, eventId: {}", userId, eventId);
|
||||
|
||||
// 이벤트 조회 및 권한 확인
|
||||
Event event = eventRepository.findByEventIdAndUserId(eventId, userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
||||
|
||||
// DRAFT 상태 확인
|
||||
if (!event.isModifiable()) {
|
||||
throw new BusinessException(ErrorCode.EVENT_002);
|
||||
}
|
||||
|
||||
// Content Service 요청 DTO 생성
|
||||
ContentImageGenerationRequest contentRequest = ContentImageGenerationRequest.builder()
|
||||
.eventDraftId(event.getEventId().getMostSignificantBits())
|
||||
.eventTitle(event.getEventName() != null ? event.getEventName() : "")
|
||||
.eventDescription(event.getDescription() != null ? event.getDescription() : "")
|
||||
.styles(request.getStyles())
|
||||
.platforms(request.getPlatforms())
|
||||
.build();
|
||||
|
||||
// Content Service 호출
|
||||
ContentJobResponse jobResponse = contentServiceClient.generateImages(contentRequest);
|
||||
|
||||
log.info("Content Service 이미지 생성 요청 완료 - jobId: {}", jobResponse.getId());
|
||||
|
||||
// 응답 생성
|
||||
return ImageGenerationResponse.builder()
|
||||
.jobId(UUID.fromString(jobResponse.getId()))
|
||||
.status(jobResponse.getStatus())
|
||||
.message("이미지 생성 요청이 접수되었습니다.")
|
||||
.createdAt(jobResponse.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 선택
|
||||
*
|
||||
* @param userId 사용자 ID (UUID)
|
||||
* @param eventId 이벤트 ID
|
||||
* @param imageId 이미지 ID
|
||||
* @param request 이미지 선택 요청
|
||||
*/
|
||||
@Transactional
|
||||
public void selectImage(UUID userId, UUID eventId, UUID imageId, SelectImageRequest request) {
|
||||
log.info("이미지 선택 - userId: {}, eventId: {}, imageId: {}", userId, eventId, imageId);
|
||||
|
||||
// 이벤트 조회 및 권한 확인
|
||||
Event event = eventRepository.findByEventIdAndUserId(eventId, userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
||||
|
||||
// DRAFT 상태 확인
|
||||
if (!event.isModifiable()) {
|
||||
throw new BusinessException(ErrorCode.EVENT_002);
|
||||
}
|
||||
|
||||
// 이미지 선택
|
||||
event.selectImage(request.getImageId(), request.getImageUrl());
|
||||
|
||||
eventRepository.save(event);
|
||||
|
||||
log.info("이미지 선택 완료 - eventId: {}, imageId: {}", eventId, imageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 추천 요청
|
||||
*
|
||||
* @param userId 사용자 ID (UUID)
|
||||
* @param eventId 이벤트 ID
|
||||
* @param request AI 추천 요청
|
||||
* @return Job 접수 응답
|
||||
*/
|
||||
@Transactional
|
||||
public JobAcceptedResponse requestAiRecommendations(UUID userId, UUID eventId, AiRecommendationRequest request) {
|
||||
log.info("AI 추천 요청 - userId: {}, eventId: {}", userId, eventId);
|
||||
|
||||
// 이벤트 조회 및 권한 확인
|
||||
Event event = eventRepository.findByEventIdAndUserId(eventId, userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
||||
|
||||
// DRAFT 상태 확인
|
||||
if (!event.isModifiable()) {
|
||||
throw new BusinessException(ErrorCode.EVENT_002);
|
||||
}
|
||||
|
||||
// Job 엔티티 생성
|
||||
Job job = Job.builder()
|
||||
.eventId(eventId)
|
||||
.jobType(JobType.AI_RECOMMENDATION)
|
||||
.build();
|
||||
|
||||
job = jobRepository.save(job);
|
||||
|
||||
// Kafka 메시지 발행
|
||||
aiJobKafkaProducer.publishAIGenerationJob(
|
||||
job.getJobId().toString(),
|
||||
userId.getMostSignificantBits(), // Long으로 변환
|
||||
eventId.toString(),
|
||||
request.getStoreInfo().getStoreName(),
|
||||
request.getStoreInfo().getCategory(),
|
||||
request.getStoreInfo().getDescription(),
|
||||
event.getObjective()
|
||||
);
|
||||
|
||||
log.info("AI 추천 요청 완료 - jobId: {}", job.getJobId());
|
||||
|
||||
return JobAcceptedResponse.builder()
|
||||
.jobId(job.getJobId())
|
||||
.status(job.getStatus())
|
||||
.message("AI 추천 생성 요청이 접수되었습니다. /jobs/" + job.getJobId() + "로 상태를 확인하세요.")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 추천 선택
|
||||
*
|
||||
* @param userId 사용자 ID (UUID)
|
||||
* @param eventId 이벤트 ID
|
||||
* @param request AI 추천 선택 요청
|
||||
*/
|
||||
@Transactional
|
||||
public void selectRecommendation(UUID userId, UUID eventId, SelectRecommendationRequest request) {
|
||||
log.info("AI 추천 선택 - userId: {}, eventId: {}, recommendationId: {}",
|
||||
userId, eventId, request.getRecommendationId());
|
||||
|
||||
// 이벤트 조회 및 권한 확인
|
||||
Event event = eventRepository.findByEventIdAndUserId(eventId, userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
||||
|
||||
// DRAFT 상태 확인
|
||||
if (!event.isModifiable()) {
|
||||
throw new BusinessException(ErrorCode.EVENT_002);
|
||||
}
|
||||
|
||||
// Lazy 컬렉션 초기화
|
||||
Hibernate.initialize(event.getAiRecommendations());
|
||||
|
||||
// AI 추천 조회
|
||||
AiRecommendation selectedRecommendation = event.getAiRecommendations().stream()
|
||||
.filter(rec -> rec.getRecommendationId().equals(request.getRecommendationId()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_003));
|
||||
|
||||
// 모든 추천 선택 해제
|
||||
event.getAiRecommendations().forEach(rec -> rec.setSelected(false));
|
||||
|
||||
// 선택한 추천만 선택 처리
|
||||
selectedRecommendation.setSelected(true);
|
||||
|
||||
// 커스터마이징이 있으면 적용
|
||||
if (request.getCustomizations() != null) {
|
||||
SelectRecommendationRequest.Customizations custom = request.getCustomizations();
|
||||
|
||||
if (custom.getEventName() != null) {
|
||||
event.updateEventName(custom.getEventName());
|
||||
} else {
|
||||
event.updateEventName(selectedRecommendation.getEventName());
|
||||
}
|
||||
|
||||
if (custom.getDescription() != null) {
|
||||
event.updateDescription(custom.getDescription());
|
||||
} else {
|
||||
event.updateDescription(selectedRecommendation.getDescription());
|
||||
}
|
||||
|
||||
if (custom.getStartDate() != null && custom.getEndDate() != null) {
|
||||
event.updateEventPeriod(custom.getStartDate(), custom.getEndDate());
|
||||
}
|
||||
} else {
|
||||
// 커스터마이징이 없으면 AI 추천 그대로 적용
|
||||
event.updateEventName(selectedRecommendation.getEventName());
|
||||
event.updateDescription(selectedRecommendation.getDescription());
|
||||
}
|
||||
|
||||
eventRepository.save(event);
|
||||
|
||||
log.info("AI 추천 선택 완료 - eventId: {}, recommendationId: {}", eventId, request.getRecommendationId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 편집
|
||||
*
|
||||
* @param userId 사용자 ID (UUID)
|
||||
* @param eventId 이벤트 ID
|
||||
* @param imageId 이미지 ID
|
||||
* @param request 이미지 편집 요청
|
||||
* @return 이미지 편집 응답
|
||||
*/
|
||||
@Transactional
|
||||
public ImageEditResponse editImage(UUID userId, UUID eventId, UUID imageId, ImageEditRequest request) {
|
||||
log.info("이미지 편집 - userId: {}, eventId: {}, imageId: {}", userId, eventId, imageId);
|
||||
|
||||
// 이벤트 조회 및 권한 확인
|
||||
Event event = eventRepository.findByEventIdAndUserId(eventId, userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
||||
|
||||
// DRAFT 상태 확인
|
||||
if (!event.isModifiable()) {
|
||||
throw new BusinessException(ErrorCode.EVENT_002);
|
||||
}
|
||||
|
||||
// 이미지가 선택된 이미지인지 확인
|
||||
if (!imageId.equals(event.getSelectedImageId())) {
|
||||
throw new BusinessException(ErrorCode.EVENT_003);
|
||||
}
|
||||
|
||||
// TODO: Content Service에 이미지 편집 요청
|
||||
// 현재는 Content Service 연동이 없으므로 Mock 응답 반환
|
||||
// 실제로는 ContentServiceClient를 통해 편집 요청을 보내야 함
|
||||
|
||||
log.info("이미지 편집 완료 - eventId: {}, imageId: {}", eventId, imageId);
|
||||
|
||||
// Mock 응답 (실제로는 Content Service의 응답을 반환해야 함)
|
||||
return ImageEditResponse.builder()
|
||||
.imageId(imageId)
|
||||
.imageUrl(event.getSelectedImageUrl()) // 편집된 URL은 Content Service에서 받아와야 함
|
||||
.editedAt(java.time.LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 배포 채널 선택
|
||||
*
|
||||
* @param userId 사용자 ID (UUID)
|
||||
* @param eventId 이벤트 ID
|
||||
* @param request 배포 채널 선택 요청
|
||||
*/
|
||||
@Transactional
|
||||
public void selectChannels(UUID userId, UUID eventId, SelectChannelsRequest request) {
|
||||
log.info("배포 채널 선택 - userId: {}, eventId: {}, channels: {}",
|
||||
userId, eventId, request.getChannels());
|
||||
|
||||
// 이벤트 조회 및 권한 확인
|
||||
Event event = eventRepository.findByEventIdAndUserId(eventId, userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
||||
|
||||
// DRAFT 상태 확인
|
||||
if (!event.isModifiable()) {
|
||||
throw new BusinessException(ErrorCode.EVENT_002);
|
||||
}
|
||||
|
||||
// 배포 채널 설정
|
||||
event.updateChannels(request.getChannels());
|
||||
|
||||
eventRepository.save(event);
|
||||
|
||||
log.info("배포 채널 선택 완료 - eventId: {}, channels: {}", eventId, request.getChannels());
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 수정
|
||||
*
|
||||
* @param userId 사용자 ID (UUID)
|
||||
* @param eventId 이벤트 ID
|
||||
* @param request 이벤트 수정 요청
|
||||
* @return 이벤트 상세 응답
|
||||
*/
|
||||
@Transactional
|
||||
public EventDetailResponse updateEvent(UUID userId, UUID eventId, UpdateEventRequest request) {
|
||||
log.info("이벤트 수정 - userId: {}, eventId: {}", userId, eventId);
|
||||
|
||||
// 이벤트 조회 및 권한 확인
|
||||
Event event = eventRepository.findByEventIdAndUserId(eventId, userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
||||
|
||||
// DRAFT 상태 확인
|
||||
if (!event.isModifiable()) {
|
||||
throw new BusinessException(ErrorCode.EVENT_002);
|
||||
}
|
||||
|
||||
// 이벤트명 수정
|
||||
if (request.getEventName() != null && !request.getEventName().trim().isEmpty()) {
|
||||
event.updateEventName(request.getEventName());
|
||||
}
|
||||
|
||||
// 설명 수정
|
||||
if (request.getDescription() != null && !request.getDescription().trim().isEmpty()) {
|
||||
event.updateDescription(request.getDescription());
|
||||
}
|
||||
|
||||
// 이벤트 기간 수정
|
||||
if (request.getStartDate() != null && request.getEndDate() != null) {
|
||||
event.updateEventPeriod(request.getStartDate(), request.getEndDate());
|
||||
}
|
||||
|
||||
event = eventRepository.save(event);
|
||||
|
||||
// Lazy 컬렉션 초기화
|
||||
Hibernate.initialize(event.getChannels());
|
||||
Hibernate.initialize(event.getGeneratedImages());
|
||||
Hibernate.initialize(event.getAiRecommendations());
|
||||
|
||||
log.info("이벤트 수정 완료 - eventId: {}", eventId);
|
||||
|
||||
return mapToDetailResponse(event);
|
||||
}
|
||||
|
||||
// ==== Private Helper Methods ==== //
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.springframework.kafka.annotation.EnableKafka;
|
||||
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
||||
import org.springframework.kafka.core.*;
|
||||
import org.springframework.kafka.listener.ContainerProperties;
|
||||
import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer;
|
||||
import org.springframework.kafka.support.serializer.JsonDeserializer;
|
||||
import org.springframework.kafka.support.serializer.JsonSerializer;
|
||||
|
||||
@@ -68,6 +69,7 @@ public class KafkaConfig {
|
||||
|
||||
/**
|
||||
* Kafka Consumer 설정
|
||||
* ErrorHandlingDeserializer를 사용하여 역직렬화 오류를 처리합니다.
|
||||
*
|
||||
* @return ConsumerFactory 인스턴스
|
||||
*/
|
||||
@@ -76,10 +78,20 @@ public class KafkaConfig {
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
|
||||
config.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroupId);
|
||||
config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
|
||||
config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
|
||||
|
||||
// ErrorHandlingDeserializer로 래핑하여 역직렬화 오류 처리
|
||||
config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
|
||||
config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
|
||||
|
||||
// 실제 Deserializer 설정
|
||||
config.put(ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS, StringDeserializer.class);
|
||||
config.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class);
|
||||
|
||||
// JsonDeserializer 설정
|
||||
config.put(JsonDeserializer.TRUSTED_PACKAGES, "*");
|
||||
config.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false);
|
||||
config.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "java.util.HashMap");
|
||||
|
||||
config.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
|
||||
config.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
|
||||
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.kt.event.eventservice.infrastructure.client;
|
||||
|
||||
import com.kt.event.eventservice.infrastructure.client.dto.ContentImageGenerationRequest;
|
||||
import com.kt.event.eventservice.infrastructure.client.dto.ContentJobResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
/**
|
||||
* Content Service Feign Client
|
||||
*
|
||||
* Content Service의 이미지 생성 API를 호출합니다.
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@FeignClient(
|
||||
name = "content-service",
|
||||
url = "${feign.content-service.url:http://localhost:8082}"
|
||||
)
|
||||
public interface ContentServiceClient {
|
||||
|
||||
/**
|
||||
* 이미지 생성 요청
|
||||
*
|
||||
* @param request 이미지 생성 요청 정보
|
||||
* @return Job 정보
|
||||
*/
|
||||
@PostMapping("/api/v1/content/images/generate")
|
||||
ContentJobResponse generateImages(@RequestBody ContentImageGenerationRequest request);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.kt.event.eventservice.infrastructure.client.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Content Service 이미지 생성 요청 DTO
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ContentImageGenerationRequest {
|
||||
|
||||
private Long eventDraftId;
|
||||
private String eventTitle;
|
||||
private String eventDescription;
|
||||
private List<String> styles;
|
||||
private List<String> platforms;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.kt.event.eventservice.infrastructure.client.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Content Service Job 응답 DTO
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ContentJobResponse {
|
||||
|
||||
private String id;
|
||||
private Long eventDraftId;
|
||||
private String jobType;
|
||||
private String status;
|
||||
private int progress;
|
||||
private String resultMessage;
|
||||
private String errorMessage;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package com.kt.event.eventservice.infrastructure.config;
|
||||
|
||||
import io.lettuce.core.ClientOptions;
|
||||
import io.lettuce.core.SocketOptions;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.core.StringRedisTemplate;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Redis 설정
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Value("${spring.data.redis.host:localhost}")
|
||||
private String redisHost;
|
||||
|
||||
@Value("${spring.data.redis.port:6379}")
|
||||
private int redisPort;
|
||||
|
||||
@Value("${spring.data.redis.password:}")
|
||||
private String redisPassword;
|
||||
|
||||
@Bean
|
||||
@org.springframework.context.annotation.Primary
|
||||
public RedisConnectionFactory redisConnectionFactory() {
|
||||
System.out.println("========================================");
|
||||
System.out.println("REDIS CONFIG: Configuring Redis connection");
|
||||
System.out.println("REDIS CONFIG: host=" + redisHost + ", port=" + redisPort);
|
||||
System.out.println("========================================");
|
||||
|
||||
log.info("Configuring Redis connection - host: {}, port: {}", redisHost, redisPort);
|
||||
|
||||
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
|
||||
redisConfig.setHostName(redisHost);
|
||||
redisConfig.setPort(redisPort);
|
||||
|
||||
if (redisPassword != null && !redisPassword.isEmpty()) {
|
||||
redisConfig.setPassword(redisPassword);
|
||||
}
|
||||
|
||||
// Lettuce Client 설정
|
||||
SocketOptions socketOptions = SocketOptions.builder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
|
||||
ClientOptions clientOptions = ClientOptions.builder()
|
||||
.socketOptions(socketOptions)
|
||||
.build();
|
||||
|
||||
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
|
||||
.commandTimeout(Duration.ofSeconds(10))
|
||||
.clientOptions(clientOptions)
|
||||
.build();
|
||||
|
||||
LettuceConnectionFactory factory = new LettuceConnectionFactory(redisConfig, clientConfig);
|
||||
|
||||
log.info("Redis connection factory created successfully");
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
return template;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
return new StringRedisTemplate(connectionFactory);
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.kt.event.eventservice.infrastructure.kafka;
|
||||
|
||||
import com.kt.event.eventservice.application.dto.kafka.AIEventGenerationJobMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.kafka.support.SendResult;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* AI 이벤트 생성 작업 메시지 발행 Producer
|
||||
*
|
||||
* ai-event-generation-job 토픽에 AI 추천 생성 작업 메시지를 발행합니다.
|
||||
*
|
||||
* @author Event Service Team
|
||||
* @version 1.0.0
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AIJobKafkaProducer {
|
||||
|
||||
private final KafkaTemplate<String, Object> kafkaTemplate;
|
||||
|
||||
@Value("${app.kafka.topics.ai-event-generation-job:ai-event-generation-job}")
|
||||
private String aiEventGenerationJobTopic;
|
||||
|
||||
/**
|
||||
* AI 이벤트 생성 작업 메시지 발행
|
||||
*
|
||||
* @param jobId 작업 ID
|
||||
* @param userId 사용자 ID
|
||||
* @param eventId 이벤트 ID
|
||||
* @param storeName 매장명
|
||||
* @param storeCategory 매장 업종
|
||||
* @param storeDescription 매장 설명
|
||||
* @param objective 이벤트 목적
|
||||
*/
|
||||
public void publishAIGenerationJob(
|
||||
String jobId,
|
||||
Long userId,
|
||||
String eventId,
|
||||
String storeName,
|
||||
String storeCategory,
|
||||
String storeDescription,
|
||||
String objective) {
|
||||
|
||||
AIEventGenerationJobMessage message = AIEventGenerationJobMessage.builder()
|
||||
.jobId(jobId)
|
||||
.userId(userId)
|
||||
.status("PENDING")
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
publishMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 이벤트 생성 작업 메시지 발행
|
||||
*
|
||||
* @param message AIEventGenerationJobMessage 객체
|
||||
*/
|
||||
public void publishMessage(AIEventGenerationJobMessage message) {
|
||||
try {
|
||||
CompletableFuture<SendResult<String, Object>> future =
|
||||
kafkaTemplate.send(aiEventGenerationJobTopic, message.getJobId(), message);
|
||||
|
||||
future.whenComplete((result, ex) -> {
|
||||
if (ex == null) {
|
||||
log.info("AI 작업 메시지 발행 성공 - Topic: {}, JobId: {}, Offset: {}",
|
||||
aiEventGenerationJobTopic,
|
||||
message.getJobId(),
|
||||
result.getRecordMetadata().offset());
|
||||
} else {
|
||||
log.error("AI 작업 메시지 발행 실패 - Topic: {}, JobId: {}, Error: {}",
|
||||
aiEventGenerationJobTopic,
|
||||
message.getJobId(),
|
||||
ex.getMessage(), ex);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("AI 작업 메시지 발행 중 예외 발생 - JobId: {}, Error: {}",
|
||||
message.getJobId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+199
-3
@@ -3,9 +3,8 @@ package com.kt.event.eventservice.presentation.controller;
|
||||
import com.kt.event.common.dto.ApiResponse;
|
||||
import com.kt.event.common.dto.PageResponse;
|
||||
import com.kt.event.common.security.UserPrincipal;
|
||||
import com.kt.event.eventservice.application.dto.request.SelectObjectiveRequest;
|
||||
import com.kt.event.eventservice.application.dto.response.EventCreatedResponse;
|
||||
import com.kt.event.eventservice.application.dto.response.EventDetailResponse;
|
||||
import com.kt.event.eventservice.application.dto.request.*;
|
||||
import com.kt.event.eventservice.application.dto.response.*;
|
||||
import com.kt.event.eventservice.application.service.EventService;
|
||||
import com.kt.event.eventservice.domain.enums.EventStatus;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -203,4 +202,201 @@ public class EventController {
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 생성 요청
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param request 이미지 생성 요청
|
||||
* @param userPrincipal 인증된 사용자 정보
|
||||
* @return 이미지 생성 응답 (Job ID 포함)
|
||||
*/
|
||||
@PostMapping("/{eventId}/images")
|
||||
@Operation(summary = "이미지 생성 요청", description = "AI를 통해 이벤트 이미지를 생성합니다.")
|
||||
public ResponseEntity<ApiResponse<ImageGenerationResponse>> requestImageGeneration(
|
||||
@PathVariable UUID eventId,
|
||||
@Valid @RequestBody ImageGenerationRequest request,
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal) {
|
||||
|
||||
log.info("이미지 생성 요청 API 호출 - userId: {}, eventId: {}",
|
||||
userPrincipal.getUserId(), eventId);
|
||||
|
||||
ImageGenerationResponse response = eventService.requestImageGeneration(
|
||||
userPrincipal.getUserId(),
|
||||
eventId,
|
||||
request
|
||||
);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.ACCEPTED)
|
||||
.body(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 선택
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param imageId 이미지 ID
|
||||
* @param request 이미지 선택 요청
|
||||
* @param userPrincipal 인증된 사용자 정보
|
||||
* @return 성공 응답
|
||||
*/
|
||||
@PutMapping("/{eventId}/images/{imageId}/select")
|
||||
@Operation(summary = "이미지 선택", description = "생성된 이미지 중 하나를 선택합니다.")
|
||||
public ResponseEntity<ApiResponse<Void>> selectImage(
|
||||
@PathVariable UUID eventId,
|
||||
@PathVariable UUID imageId,
|
||||
@Valid @RequestBody SelectImageRequest request,
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal) {
|
||||
|
||||
log.info("이미지 선택 API 호출 - userId: {}, eventId: {}, imageId: {}",
|
||||
userPrincipal.getUserId(), eventId, imageId);
|
||||
|
||||
eventService.selectImage(
|
||||
userPrincipal.getUserId(),
|
||||
eventId,
|
||||
imageId,
|
||||
request
|
||||
);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 추천 요청 (Step 2)
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param request AI 추천 요청
|
||||
* @param userPrincipal 인증된 사용자 정보
|
||||
* @return AI 추천 요청 응답 (Job ID 포함)
|
||||
*/
|
||||
@PostMapping("/{eventId}/ai-recommendations")
|
||||
@Operation(summary = "AI 추천 요청", description = "AI 서비스에 이벤트 추천 생성을 요청합니다.")
|
||||
public ResponseEntity<ApiResponse<JobAcceptedResponse>> requestAiRecommendations(
|
||||
@PathVariable UUID eventId,
|
||||
@Valid @RequestBody AiRecommendationRequest request,
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal) {
|
||||
|
||||
log.info("AI 추천 요청 API 호출 - userId: {}, eventId: {}",
|
||||
userPrincipal.getUserId(), eventId);
|
||||
|
||||
JobAcceptedResponse response = eventService.requestAiRecommendations(
|
||||
userPrincipal.getUserId(),
|
||||
eventId,
|
||||
request
|
||||
);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.ACCEPTED)
|
||||
.body(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 추천 선택 (Step 2-2)
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param request AI 추천 선택 요청
|
||||
* @param userPrincipal 인증된 사용자 정보
|
||||
* @return 성공 응답
|
||||
*/
|
||||
@PutMapping("/{eventId}/recommendations")
|
||||
@Operation(summary = "AI 추천 선택", description = "AI가 생성한 추천 중 하나를 선택하고 커스터마이징합니다.")
|
||||
public ResponseEntity<ApiResponse<Void>> selectRecommendation(
|
||||
@PathVariable UUID eventId,
|
||||
@Valid @RequestBody SelectRecommendationRequest request,
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal) {
|
||||
|
||||
log.info("AI 추천 선택 API 호출 - userId: {}, eventId: {}, recommendationId: {}",
|
||||
userPrincipal.getUserId(), eventId, request.getRecommendationId());
|
||||
|
||||
eventService.selectRecommendation(
|
||||
userPrincipal.getUserId(),
|
||||
eventId,
|
||||
request
|
||||
);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 편집 (Step 3-3)
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param imageId 이미지 ID
|
||||
* @param request 이미지 편집 요청
|
||||
* @param userPrincipal 인증된 사용자 정보
|
||||
* @return 이미지 편집 응답
|
||||
*/
|
||||
@PutMapping("/{eventId}/images/{imageId}/edit")
|
||||
@Operation(summary = "이미지 편집", description = "선택된 이미지를 편집합니다.")
|
||||
public ResponseEntity<ApiResponse<ImageEditResponse>> editImage(
|
||||
@PathVariable UUID eventId,
|
||||
@PathVariable UUID imageId,
|
||||
@Valid @RequestBody ImageEditRequest request,
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal) {
|
||||
|
||||
log.info("이미지 편집 API 호출 - userId: {}, eventId: {}, imageId: {}",
|
||||
userPrincipal.getUserId(), eventId, imageId);
|
||||
|
||||
ImageEditResponse response = eventService.editImage(
|
||||
userPrincipal.getUserId(),
|
||||
eventId,
|
||||
imageId,
|
||||
request
|
||||
);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
/**
|
||||
* 배포 채널 선택 (Step 4)
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param request 배포 채널 선택 요청
|
||||
* @param userPrincipal 인증된 사용자 정보
|
||||
* @return 성공 응답
|
||||
*/
|
||||
@PutMapping("/{eventId}/channels")
|
||||
@Operation(summary = "배포 채널 선택", description = "이벤트를 배포할 채널을 선택합니다.")
|
||||
public ResponseEntity<ApiResponse<Void>> selectChannels(
|
||||
@PathVariable UUID eventId,
|
||||
@Valid @RequestBody SelectChannelsRequest request,
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal) {
|
||||
|
||||
log.info("배포 채널 선택 API 호출 - userId: {}, eventId: {}, channels: {}",
|
||||
userPrincipal.getUserId(), eventId, request.getChannels());
|
||||
|
||||
eventService.selectChannels(
|
||||
userPrincipal.getUserId(),
|
||||
eventId,
|
||||
request
|
||||
);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 수정
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param request 이벤트 수정 요청
|
||||
* @param userPrincipal 인증된 사용자 정보
|
||||
* @return 성공 응답
|
||||
*/
|
||||
@PutMapping("/{eventId}")
|
||||
@Operation(summary = "이벤트 수정", description = "기존 이벤트의 정보를 수정합니다. DRAFT 상태만 수정 가능합니다.")
|
||||
public ResponseEntity<ApiResponse<EventDetailResponse>> updateEvent(
|
||||
@PathVariable UUID eventId,
|
||||
@Valid @RequestBody UpdateEventRequest request,
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal) {
|
||||
|
||||
log.info("이벤트 수정 API 호출 - userId: {}, eventId: {}",
|
||||
userPrincipal.getUserId(), eventId);
|
||||
|
||||
EventDetailResponse response = eventService.updateEvent(
|
||||
userPrincipal.getUserId(),
|
||||
eventId,
|
||||
request
|
||||
);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.kt.event.eventservice.presentation.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Redis 연결 테스트 컨트롤러
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/redis-test")
|
||||
@RequiredArgsConstructor
|
||||
public class RedisTestController {
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
@GetMapping("/ping")
|
||||
public String ping() {
|
||||
try {
|
||||
String key = "test:ping";
|
||||
String value = "pong:" + System.currentTimeMillis();
|
||||
|
||||
log.info("Redis test - setting key: {}, value: {}", key, value);
|
||||
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(60));
|
||||
|
||||
String result = redisTemplate.opsForValue().get(key);
|
||||
log.info("Redis test - retrieved value: {}", result);
|
||||
|
||||
return "Redis OK - " + result;
|
||||
} catch (Exception e) {
|
||||
log.error("Redis connection failed", e);
|
||||
return "Redis FAILED - " + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ spring:
|
||||
password: ${DB_PASSWORD:eventpass}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 10
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 5
|
||||
minimum-idle: 2
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
@@ -22,9 +22,9 @@ spring:
|
||||
ddl-auto: ${DDL_AUTO:update}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
format_sql: false
|
||||
show_sql: false
|
||||
use_sql_comments: true
|
||||
use_sql_comments: false
|
||||
jdbc:
|
||||
batch_size: 20
|
||||
time_zone: Asia/Seoul
|
||||
@@ -36,11 +36,15 @@ spring:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
timeout: 60000ms
|
||||
connect-timeout: 60000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 10
|
||||
max-idle: 5
|
||||
min-idle: 2
|
||||
max-active: 5
|
||||
max-idle: 3
|
||||
min-idle: 1
|
||||
max-wait: -1ms
|
||||
shutdown-timeout: 200ms
|
||||
|
||||
# Kafka Configuration
|
||||
kafka:
|
||||
@@ -75,26 +79,39 @@ management:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
base-path: /actuator
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
show-components: always
|
||||
health:
|
||||
redis:
|
||||
enabled: false
|
||||
livenessState:
|
||||
enabled: true
|
||||
db:
|
||||
readinessState:
|
||||
enabled: true
|
||||
|
||||
# Logging Configuration
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.kt.event: ${LOG_LEVEL:DEBUG}
|
||||
org.springframework: INFO
|
||||
org.hibernate.SQL: ${SQL_LOG_LEVEL:DEBUG}
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
||||
com.kt.event: ${LOG_LEVEL:INFO}
|
||||
org.springframework: WARN
|
||||
org.springframework.data.redis: WARN
|
||||
io.lettuce.core: WARN
|
||||
org.hibernate.SQL: ${SQL_LOG_LEVEL:WARN}
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: WARN
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: ${LOG_FILE:logs/event-service.log}
|
||||
logback:
|
||||
rollingpolicy:
|
||||
max-file-size: 10MB
|
||||
max-history: 7
|
||||
total-size-cap: 100MB
|
||||
|
||||
# Springdoc OpenAPI Configuration
|
||||
springdoc:
|
||||
@@ -115,6 +132,10 @@ feign:
|
||||
readTimeout: 10000
|
||||
loggerLevel: basic
|
||||
|
||||
# Content Service Client
|
||||
content-service:
|
||||
url: ${CONTENT_SERVICE_URL:http://localhost:8082}
|
||||
|
||||
# Distribution Service Client
|
||||
distribution-service:
|
||||
url: ${DISTRIBUTION_SERVICE_URL:http://localhost:8084}
|
||||
@@ -140,3 +161,8 @@ app:
|
||||
timeout:
|
||||
ai-generation: 300000 # 5분 (밀리초 단위)
|
||||
image-generation: 300000 # 5분 (밀리초 단위)
|
||||
|
||||
# JWT Configuration
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:default-jwt-secret-key-for-development-minimum-32-bytes-required}
|
||||
expiration: 86400000 # 24시간 (밀리초 단위)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
JWT 테스트 토큰 생성 스크립트
|
||||
Event Service API 테스트용
|
||||
"""
|
||||
|
||||
import jwt
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
# JWT Secret (run-event-service.ps1과 동일)
|
||||
JWT_SECRET = "kt-event-marketing-jwt-secret-key-for-development-only-minimum-256-bits-required"
|
||||
|
||||
# 유효기간을 매우 길게 설정 (테스트용)
|
||||
EXPIRATION_DAYS = 365
|
||||
|
||||
# 테스트 사용자 정보
|
||||
USER_ID = str(uuid.uuid4())
|
||||
STORE_ID = str(uuid.uuid4())
|
||||
EMAIL = "test@example.com"
|
||||
NAME = "Test User"
|
||||
ROLES = ["ROLE_USER"]
|
||||
|
||||
def generate_access_token():
|
||||
"""Access Token 생성"""
|
||||
now = datetime.datetime.utcnow()
|
||||
expiry = now + datetime.timedelta(days=EXPIRATION_DAYS)
|
||||
|
||||
payload = {
|
||||
'sub': USER_ID,
|
||||
'storeId': STORE_ID,
|
||||
'email': EMAIL,
|
||||
'name': NAME,
|
||||
'roles': ROLES,
|
||||
'type': 'access',
|
||||
'iat': now,
|
||||
'exp': expiry
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, JWT_SECRET, algorithm='HS256')
|
||||
return token
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 80)
|
||||
print("JWT 테스트 토큰 생성")
|
||||
print("=" * 80)
|
||||
print()
|
||||
print(f"User ID: {USER_ID}")
|
||||
print(f"Store ID: {STORE_ID}")
|
||||
print(f"Email: {EMAIL}")
|
||||
print(f"Name: {NAME}")
|
||||
print(f"Roles: {ROLES}")
|
||||
print()
|
||||
print("=" * 80)
|
||||
print("Access Token:")
|
||||
print("=" * 80)
|
||||
|
||||
token = generate_access_token()
|
||||
print(token)
|
||||
print()
|
||||
print("=" * 80)
|
||||
print("사용 방법:")
|
||||
print("=" * 80)
|
||||
print("curl -H \"Authorization: Bearer <token>\" http://localhost:8081/api/v1/events")
|
||||
print()
|
||||
@@ -0,0 +1,14 @@
|
||||
-- participation-service channel 컬럼 추가 스크립트
|
||||
-- 실행 방법: psql -h 4.230.72.147 -U eventuser -d participationdb -f add-channel-column.sql
|
||||
|
||||
-- channel 컬럼 추가
|
||||
ALTER TABLE participants
|
||||
ADD COLUMN IF NOT EXISTS channel VARCHAR(20);
|
||||
|
||||
-- 기존 데이터에 기본값 설정
|
||||
UPDATE participants
|
||||
SET channel = 'SNS'
|
||||
WHERE channel IS NULL;
|
||||
|
||||
-- 커밋
|
||||
COMMIT;
|
||||
+22
-5
@@ -44,14 +44,31 @@ public class ParticipationService {
|
||||
public ParticipationResponse participate(String eventId, ParticipationRequest request) {
|
||||
log.info("이벤트 참여 시작 - eventId: {}, phoneNumber: {}", eventId, request.getPhoneNumber());
|
||||
|
||||
// 중복 참여 체크
|
||||
if (participantRepository.existsByEventIdAndPhoneNumber(eventId, request.getPhoneNumber())) {
|
||||
// 중복 참여 체크 - 상세 디버깅
|
||||
log.info("중복 참여 체크 시작 - eventId: '{}', phoneNumber: '{}'", eventId, request.getPhoneNumber());
|
||||
|
||||
boolean isDuplicate = participantRepository.existsByEventIdAndPhoneNumber(eventId, request.getPhoneNumber());
|
||||
log.info("중복 참여 체크 결과 - isDuplicate: {}", isDuplicate);
|
||||
|
||||
if (isDuplicate) {
|
||||
log.warn("중복 참여 감지! eventId: '{}', phoneNumber: '{}'", eventId, request.getPhoneNumber());
|
||||
throw new DuplicateParticipationException();
|
||||
}
|
||||
|
||||
// 참여자 ID 생성
|
||||
Long maxId = participantRepository.findMaxIdByEventId(eventId).orElse(0L);
|
||||
String participantId = Participant.generateParticipantId(eventId, maxId + 1);
|
||||
log.info("중복 참여 체크 통과 - 참여 진행");
|
||||
|
||||
// 참여자 ID 생성 - 날짜별 최대 순번 기반
|
||||
String dateTime;
|
||||
if (eventId != null && eventId.length() >= 16 && eventId.startsWith("evt_")) {
|
||||
dateTime = eventId.substring(4, 12); // "20250124"
|
||||
} else {
|
||||
dateTime = java.time.LocalDate.now().format(
|
||||
java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
}
|
||||
|
||||
String datePrefix = "prt_" + dateTime + "_";
|
||||
Integer maxSequence = participantRepository.findMaxSequenceByDatePrefix(datePrefix);
|
||||
String participantId = String.format("prt_%s_%03d", dateTime, maxSequence + 1);
|
||||
|
||||
// 참여자 저장
|
||||
Participant participant = Participant.builder()
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ import lombok.*;
|
||||
indexes = {
|
||||
@Index(name = "idx_participant_event_id", columnList = "event_id"),
|
||||
@Index(name = "idx_participant_event_phone", columnList = "event_id, phone_number")
|
||||
|
||||
},
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_event_phone", columnNames = {"event_id", "phone_number"})
|
||||
|
||||
+12
@@ -106,4 +106,16 @@ public interface ParticipantRepository extends JpaRepository<Participant, Long>
|
||||
* @return 참여자 Optional
|
||||
*/
|
||||
Optional<Participant> findByEventIdAndParticipantId(String eventId, String participantId);
|
||||
|
||||
/**
|
||||
* 특정 날짜 패턴의 참여자 ID 중 최대 순번 조회
|
||||
*
|
||||
* @param datePrefix 날짜 접두사 (예: "prt_20251028_")
|
||||
* @return 최대 순번
|
||||
*/
|
||||
@Query(value = "SELECT COALESCE(MAX(CAST(SUBSTRING(participant_id FROM LENGTH(?1) + 1) AS INTEGER)), 0) " +
|
||||
"FROM participants " +
|
||||
"WHERE participant_id LIKE CONCAT(?1, '%')",
|
||||
nativeQuery = true)
|
||||
Integer findMaxSequenceByDatePrefix(@Param("datePrefix") String datePrefix);
|
||||
}
|
||||
|
||||
+2
@@ -24,6 +24,8 @@ public class SecurityConfig {
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Actuator endpoints
|
||||
.requestMatchers("/actuator/**").permitAll()
|
||||
.anyRequest().permitAll()
|
||||
);
|
||||
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package com.kt.event.participation.presentation.controller;
|
||||
|
||||
import com.kt.event.participation.domain.participant.Participant;
|
||||
import com.kt.event.participation.domain.participant.ParticipantRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 디버깅용 컨트롤러
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/debug")
|
||||
@RequiredArgsConstructor
|
||||
public class DebugController {
|
||||
|
||||
private final ParticipantRepository participantRepository;
|
||||
|
||||
/**
|
||||
* 중복 참여 체크 테스트
|
||||
*/
|
||||
@GetMapping("/exists/{eventId}/{phoneNumber}")
|
||||
public String testExists(@PathVariable String eventId, @PathVariable String phoneNumber) {
|
||||
try {
|
||||
log.info("디버그: 중복 체크 시작 - eventId: {}, phoneNumber: {}", eventId, phoneNumber);
|
||||
|
||||
boolean exists = participantRepository.existsByEventIdAndPhoneNumber(eventId, phoneNumber);
|
||||
|
||||
log.info("디버그: 중복 체크 결과 - exists: {}", exists);
|
||||
|
||||
long totalCount = participantRepository.count();
|
||||
long eventCount = participantRepository.countByEventId(eventId);
|
||||
|
||||
return String.format(
|
||||
"eventId: %s, phoneNumber: %s, exists: %s, totalCount: %d, eventCount: %d",
|
||||
eventId, phoneNumber, exists, totalCount, eventCount
|
||||
);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("디버그: 예외 발생", e);
|
||||
return "ERROR: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 참여자 데이터 조회
|
||||
*/
|
||||
@GetMapping("/participants")
|
||||
public String getAllParticipants() {
|
||||
try {
|
||||
List<Participant> participants = participantRepository.findAll();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Total participants: ").append(participants.size()).append("\n\n");
|
||||
|
||||
for (Participant p : participants) {
|
||||
sb.append(String.format("ID: %s, EventID: %s, Phone: %s, Name: %s\n",
|
||||
p.getParticipantId(), p.getEventId(), p.getPhoneNumber(), p.getName()));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("디버그: 참여자 조회 예외 발생", e);
|
||||
return "ERROR: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 전화번호의 참여 이력 조회
|
||||
*/
|
||||
@GetMapping("/phone/{phoneNumber}")
|
||||
public String getByPhoneNumber(@PathVariable String phoneNumber) {
|
||||
try {
|
||||
List<Participant> participants = participantRepository.findAll();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Participants with phone: ").append(phoneNumber).append("\n\n");
|
||||
|
||||
int count = 0;
|
||||
for (Participant p : participants) {
|
||||
if (phoneNumber.equals(p.getPhoneNumber())) {
|
||||
sb.append(String.format("ID: %s, EventID: %s, Name: %s\n",
|
||||
p.getParticipantId(), p.getEventId(), p.getName()));
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
sb.append("No participants found with this phone number.");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("디버그: 전화번호별 조회 예외 발생", e);
|
||||
return "ERROR: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -41,12 +41,21 @@ public class ParticipationController {
|
||||
@PathVariable String eventId,
|
||||
@Valid @RequestBody ParticipationRequest request) {
|
||||
|
||||
log.info("이벤트 참여 요청 - eventId: {}", eventId);
|
||||
ParticipationResponse response = participationService.participate(eventId, request);
|
||||
log.info("컨트롤러: 이벤트 참여 요청 시작 - eventId: '{}', phoneNumber: '{}'", eventId, request.getPhoneNumber());
|
||||
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.CREATED)
|
||||
.body(ApiResponse.success(response));
|
||||
try {
|
||||
log.info("컨트롤러: 서비스 호출 전");
|
||||
ParticipationResponse response = participationService.participate(eventId, request);
|
||||
log.info("컨트롤러: 서비스 호출 완료 - participantId: {}", response.getParticipantId());
|
||||
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.CREATED)
|
||||
.body(ApiResponse.success(response));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("컨트롤러: 예외 발생 - type: {}, message: {}", e.getClass().getSimpleName(), e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,7 @@ spring:
|
||||
# JPA 설정
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${DDL_AUTO:validate}
|
||||
ddl-auto: ${DDL_AUTO:update}
|
||||
show-sql: ${SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
@@ -73,3 +73,19 @@ logging:
|
||||
max-file-size: 10MB
|
||||
max-history: 7
|
||||
total-size-cap: 100MB
|
||||
# Actuator
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
base-path: /actuator
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
show-components: always
|
||||
health:
|
||||
livenessState:
|
||||
enabled: true
|
||||
readinessState:
|
||||
enabled: true
|
||||
@@ -0,0 +1,22 @@
|
||||
# Event Service 실행 스크립트
|
||||
|
||||
$env:SERVER_PORT="8081"
|
||||
$env:DB_HOST="20.249.177.232"
|
||||
$env:DB_PORT="5432"
|
||||
$env:DB_NAME="eventdb"
|
||||
$env:DB_USERNAME="eventuser"
|
||||
$env:DB_PASSWORD="Hi5Jessica!"
|
||||
$env:REDIS_HOST="localhost"
|
||||
$env:REDIS_PORT="6379"
|
||||
$env:REDIS_PASSWORD=""
|
||||
$env:KAFKA_BOOTSTRAP_SERVERS="20.249.182.13:9095,4.217.131.59:9095"
|
||||
$env:DDL_AUTO="update"
|
||||
$env:LOG_LEVEL="DEBUG"
|
||||
$env:SQL_LOG_LEVEL="DEBUG"
|
||||
$env:CONTENT_SERVICE_URL="http://localhost:8082"
|
||||
$env:DISTRIBUTION_SERVICE_URL="http://localhost:8084"
|
||||
$env:JWT_SECRET="kt-event-marketing-jwt-secret-key-for-development-only-minimum-256-bits-required"
|
||||
|
||||
Write-Host "Starting Event Service on port 8081..." -ForegroundColor Green
|
||||
Write-Host "Logs will be saved to logs/event-service.log" -ForegroundColor Yellow
|
||||
./gradlew event-service:bootRun 2>&1 | Tee-Object -FilePath logs/event-service.log
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "기존전화번호테스트",
|
||||
"phoneNumber": "010-2044-4103",
|
||||
"email": "test@example.com",
|
||||
"channel": "SNS",
|
||||
"storeVisited": false,
|
||||
"agreeMarketing": true,
|
||||
"agreePrivacy": true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "새로운테스트",
|
||||
"phoneNumber": "010-8888-8888",
|
||||
"email": "newtest@example.com",
|
||||
"channel": "SNS",
|
||||
"storeVisited": false,
|
||||
"agreeMarketing": true,
|
||||
"agreePrivacy": true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "새로운테스트",
|
||||
"phoneNumber": "010-9999-9999",
|
||||
"email": "newtest@example.com",
|
||||
"channel": "SNS",
|
||||
"storeVisited": false,
|
||||
"agreeMarketing": true,
|
||||
"agreePrivacy": true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "테스트",
|
||||
"phoneNumber": "010-2044-4103",
|
||||
"email": "test@example.com",
|
||||
"channel": "SNS",
|
||||
"storeVisited": false,
|
||||
"agreeMarketing": true,
|
||||
"agreePrivacy": true
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
C:\Users\KTDS\home\workspace\kt-event-marketing\generate-test-token.py:26: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
|
||||
now = datetime.datetime.utcnow()
|
||||
================================================================================
|
||||
JWT 테스트 토큰 생성
|
||||
================================================================================
|
||||
|
||||
User ID: 6db043d0-b303-4577-b9dd-6d366cc59fa0
|
||||
Store ID: 34000028-01fd-4ed1-975c-35f7c88b6547
|
||||
Email: test@example.com
|
||||
Name: Test User
|
||||
Roles: ['ROLE_USER']
|
||||
|
||||
================================================================================
|
||||
Access Token:
|
||||
================================================================================
|
||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2ZGIwNDNkMC1iMzAzLTQ1NzctYjlkZC02ZDM2NmNjNTlmYTAiLCJzdG9yZUlkIjoiMzQwMDAwMjgtMDFmZC00ZWQxLTk3NWMtMzVmN2M4OGI2NTQ3IiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmFtZSI6IlRlc3QgVXNlciIsInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNzYxNTQ5MjkxLCJleHAiOjE3OTMwODUyOTF9.PfQ_NhXRjdfsmQn0NcAKgxcje2XaIL-TlQk_f_DVU38
|
||||
|
||||
================================================================================
|
||||
사용 방법:
|
||||
================================================================================
|
||||
curl -H "Authorization: Bearer <token>" http://localhost:8081/api/v1/events
|
||||
|
||||
Reference in New Issue
Block a user