diff --git a/smarketing-ai/GITOPS_TEST.md b/smarketing-ai/GITOPS_TEST.md
new file mode 100644
index 0000000..852b85b
--- /dev/null
+++ b/smarketing-ai/GITOPS_TEST.md
@@ -0,0 +1 @@
+# GitOps Test Thu Jun 19 07:13:03 UTC 2025
diff --git a/smarketing-ai/app.py b/smarketing-ai/app.py
index d3c91da..218c44d 100644
--- a/smarketing-ai/app.py
+++ b/smarketing-ai/app.py
@@ -98,7 +98,7 @@ def create_app():
app.logger.error(traceback.format_exc())
return jsonify({'error': f'SNS 콘텐츠 생성 중 오류가 발생했습니다: {str(e)}'}), 500
- @app.route('/api/ai/poster', methods=['GET'])
+ @app.route('/api/ai/poster', methods=['POST'])
def generate_poster_content():
"""
홍보 포스터 생성 API
@@ -114,7 +114,7 @@ def create_app():
return jsonify({'error': '요청 데이터가 없습니다.'}), 400
# 필수 필드 검증
- required_fields = ['title', 'category', 'contentType', 'images']
+ required_fields = ['title', 'category', 'images']
for field in required_fields:
if field not in data:
return jsonify({'error': f'필수 필드가 누락되었습니다: {field}'}), 400
@@ -140,19 +140,14 @@ def create_app():
poster_request = PosterContentGetRequest(
title=data.get('title'),
category=data.get('category'),
- contentType=data.get('contentType'),
images=data.get('images', []),
- photoStyle=data.get('photoStyle'),
requirement=data.get('requirement'),
- toneAndManner=data.get('toneAndManner'),
- emotionIntensity=data.get('emotionIntensity'),
menuName=data.get('menuName'),
- eventName=data.get('eventName'),
startDate=start_date,
endDate=end_date
)
- # 포스터 생성 (V3 사용)
+ # 포스터 생성
result = poster_service.generate_poster(poster_request)
if result['success']:
diff --git a/smarketing-ai/deployment/Jenkinsfile b/smarketing-ai/deployment/Jenkinsfile
index e55f855..8e07c93 100644
--- a/smarketing-ai/deployment/Jenkinsfile
+++ b/smarketing-ai/deployment/Jenkinsfile
@@ -1,3 +1,5 @@
+// smarketing-backend/smarketing-ai/deployment/Jenkinsfile
+
def PIPELINE_ID = "${env.BUILD_NUMBER}"
def getImageTag() {
@@ -11,166 +13,184 @@ podTemplate(
serviceAccount: 'jenkins',
containers: [
containerTemplate(name: 'podman', image: "mgoltzsche/podman", ttyEnabled: true, command: 'cat', privileged: true),
- containerTemplate(name: 'azure-cli', image: 'hiondal/azure-kubectl:latest', command: 'cat', ttyEnabled: true),
- containerTemplate(name: 'envsubst', image: "hiondal/envsubst", command: 'sleep', args: '1h')
+ containerTemplate(name: 'git', image: 'alpine/git:latest', command: 'cat', ttyEnabled: true)
],
volumes: [
- emptyDirVolume(mountPath: '/run/podman', memory: false),
- emptyDirVolume(mountPath: '/root/.azure', memory: false)
+ emptyDirVolume(mountPath: '/run/podman', memory: false)
]
) {
node(PIPELINE_ID) {
def props
def imageTag = getImageTag()
- def manifest = "deploy.yaml"
- def namespace
+
+ // Manifest Repository 설정
+ def MANIFEST_REPO = 'https://github.com/won-ktds/smarketing-manifest.git'
+ def MANIFEST_CREDENTIAL_ID = 'github-credentials-smarketing'
- stage("Get Source") {
- checkout scm
- props = readProperties file: "smarketing-ai/deployment/deploy_env_vars"
- namespace = "${props.namespace}"
+ try {
+ stage("Get Source") {
+ checkout scm
+
+ // smarketing-ai 하위에 있는 설정 파일 읽기
+ props = readProperties file: "smarketing-ai/deployment/deploy_env_vars"
- echo "Registry: ${props.registry}"
- echo "Image Org: ${props.image_org}"
- echo "Team ID: ${props.teamid}"
- }
-
- stage("Check Changes") {
- script {
- def changes = sh(
- script: "git diff --name-only HEAD~1 HEAD",
- returnStdout: true
- ).trim()
-
- echo "Changed files: ${changes}"
-
- if (!changes.contains("smarketing-ai/")) {
- echo "No changes in smarketing-ai, skipping build"
- currentBuild.result = 'SUCCESS'
- error("Stopping pipeline - no changes detected")
- }
-
- echo "Changes detected in smarketing-ai, proceeding with build"
+ echo "=== Build Information ==="
+ echo "Service: smarketing-ai"
+ echo "Image Tag: ${imageTag}"
+ echo "Registry: ${props.registry}"
+ echo "Image Org: ${props.image_org}"
+ echo "Team ID: ${props.teamid}"
}
- }
- stage("Setup AKS") {
- container('azure-cli') {
- withCredentials([azureServicePrincipal('azure-credentials')]) {
- sh """
- az login --service-principal -u \$AZURE_CLIENT_ID -p \$AZURE_CLIENT_SECRET -t \$AZURE_TENANT_ID
- az aks get-credentials --resource-group rg-digitalgarage-02 --name aks-digitalgarage-02 --overwrite-existing
- kubectl create namespace ${namespace} --dry-run=client -o yaml | kubectl apply -f -
+ stage("Check Changes") {
+ script {
+ def changes = sh(
+ script: "git diff --name-only HEAD~1 HEAD",
+ returnStdout: true
+ ).trim()
+
+ echo "Changed files: ${changes}"
+
+ if (!changes.contains("smarketing-ai/")) {
+ echo "No changes in smarketing-ai, skipping build"
+ currentBuild.result = 'SUCCESS'
+ error("Stopping pipeline - no changes detected")
+ }
+
+ echo "Changes detected in smarketing-ai, proceeding with build"
+ }
+ }
+
+ stage('Build & Push Docker Image') {
+ container('podman') {
+ sh 'podman system service -t 0 unix:///run/podman/podman.sock & sleep 2'
+
+ withCredentials([usernamePassword(
+ credentialsId: 'acr-credentials',
+ usernameVariable: 'ACR_USERNAME',
+ passwordVariable: 'ACR_PASSWORD'
+ )]) {
+ sh """
+ echo "=========================================="
+ echo "Building smarketing-ai Python Flask application"
+ echo "Image Tag: ${imageTag}"
+ echo "=========================================="
+
+ # ACR 로그인
+ echo \$ACR_PASSWORD | podman login ${props.registry} --username \$ACR_USERNAME --password-stdin
+
+ # Docker 이미지 빌드
+ podman build \\
+ -f smarketing-ai/deployment/Dockerfile \\
+ -t ${props.registry}/${props.image_org}/smarketing-ai:${imageTag} .
+
+ # 이미지 푸시
+ podman push ${props.registry}/${props.image_org}/smarketing-ai:${imageTag}
+
+ echo "Successfully built and pushed: ${props.registry}/${props.image_org}/smarketing-ai:${imageTag}"
+ """
+ }
+ }
+ }
+
+ stage('Update Manifest Repository') {
+ container('git') {
+ script {
+ // Manifest Repository Clone
+ withCredentials([usernamePassword(
+ credentialsId: MANIFEST_CREDENTIAL_ID,
+ usernameVariable: 'GIT_USERNAME',
+ passwordVariable: 'GIT_PASSWORD'
+ )]) {
+ sh """
+ echo "=== Git 설정 ==="
+ git config --global user.name "Jenkins CI"
+ git config --global user.email "jenkins@company.com"
+
+ echo "=== Manifest Repository Clone ==="
+ rm -rf manifest-repo
+ git clone https://\$GIT_USERNAME:\$GIT_PASSWORD@github.com/won-ktds/smarketing-manifest.git manifest-repo
+ cd manifest-repo
+ """
+
+ def fullImageName = "${props.registry}/${props.image_org}/smarketing-ai:${imageTag}"
+ def deploymentFile = "smarketing-ai/deployments/smarketing-ai/smarketing-ai-deployment.yaml"
+
+ sh """
+ cd manifest-repo
+
+ echo "=== smarketing-ai 이미지 태그 업데이트 ==="
+ if [ -f "${deploymentFile}" ]; then
+ # 이미지 태그 업데이트 (sed 사용)
+ sed -i 's|image: ${props.registry}/${props.image_org}/smarketing-ai:.*|image: ${fullImageName}|g' "${deploymentFile}"
+ echo "Updated ${deploymentFile} with new image: ${fullImageName}"
+
+ # 변경사항 확인
+ echo "=== 변경된 내용 확인 ==="
+ grep "image: ${props.registry}/${props.image_org}/smarketing-ai" "${deploymentFile}" || echo "이미지 태그 업데이트 확인 실패"
+ else
+ echo "Warning: ${deploymentFile} not found"
+ fi
+ """
+
+ sh """
+ cd manifest-repo
+
+ echo "=== Git 변경사항 확인 ==="
+ git status
+ git diff
+
+ # 변경사항이 있으면 커밋 및 푸시
+ if [ -n "\$(git status --porcelain)" ]; then
+ git add .
+ git commit -m "Update smarketing-ai to ${imageTag} - Build ${env.BUILD_NUMBER}"
+ git push origin main
+ echo "✅ Successfully updated manifest repository"
+ else
+ echo "ℹ️ No changes to commit"
+ fi
+ """
+ }
+ }
+ }
+ }
+
+ stage('Trigger ArgoCD Sync') {
+ script {
+ echo """
+🎯 smarketing-ai CI Pipeline 완료!
+
+📦 빌드된 이미지:
+- ${props.registry}/${props.image_org}/smarketing-ai:${imageTag}
+
+🔄 ArgoCD 동작:
+- ArgoCD가 manifest repository 변경사항을 자동으로 감지합니다
+- smarketing-ai Application이 새로운 이미지로 동기화됩니다
+- ArgoCD UI에서 배포 상태를 모니터링하세요
+
+🌐 ArgoCD UI: [ArgoCD 접속 URL]
+📁 Manifest Repo: ${MANIFEST_REPO}
"""
}
}
- }
- stage('Build & Push Docker Image') {
+ // 성공 시 처리
+ echo """
+✅ smarketing-ai CI Pipeline 성공!
+🏷️ 새로운 이미지 태그: ${imageTag}
+🔄 ArgoCD가 자동으로 배포를 시작합니다
+ """
+
+ } catch (Exception e) {
+ // 실패 시 처리
+ echo "❌ smarketing-ai CI Pipeline 실패: ${e.getMessage()}"
+ throw e
+ } finally {
+ // 정리 작업 (항상 실행)
container('podman') {
- sh 'podman system service -t 0 unix:///run/podman/podman.sock & sleep 2'
-
- withCredentials([usernamePassword(
- credentialsId: 'acr-credentials',
- usernameVariable: 'ACR_USERNAME',
- passwordVariable: 'ACR_PASSWORD'
- )]) {
- sh """
- echo "=========================================="
- echo "Building smarketing-ai Python Flask application"
- echo "Image Tag: ${imageTag}"
- echo "=========================================="
-
- # ACR 로그인
- echo \$ACR_PASSWORD | podman login ${props.registry} --username \$ACR_USERNAME --password-stdin
-
- # Docker 이미지 빌드
- podman build \
- -f smarketing-ai/deployment/Dockerfile \
- -t ${props.registry}/${props.image_org}/smarketing-ai:${imageTag} .
-
- # 이미지 푸시
- podman push ${props.registry}/${props.image_org}/smarketing-ai:${imageTag}
-
- echo "Successfully built and pushed: ${props.registry}/${props.image_org}/smarketing-ai:${imageTag}"
- """
- }
- }
- }
-
- stage('Generate & Apply Manifest') {
- container('envsubst') {
- withCredentials([
- string(credentialsId: 'SECRET_KEY', variable: 'SECRET_KEY'),
- string(credentialsId: 'CLAUDE_API_KEY', variable: 'CLAUDE_API_KEY'),
- string(credentialsId: 'OPENAI_API_KEY', variable: 'OPENAI_API_KEY'),
- string(credentialsId: 'AZURE_STORAGE_ACCOUNT_NAME', variable: 'AZURE_STORAGE_ACCOUNT_NAME'),
- string(credentialsId: 'AZURE_STORAGE_ACCOUNT_KEY', variable: 'AZURE_STORAGE_ACCOUNT_KEY')
- ]) {
- sh """
- export namespace=${namespace}
- export replicas=${props.replicas}
- export resources_requests_cpu=${props.resources_requests_cpu}
- export resources_requests_memory=${props.resources_requests_memory}
- export resources_limits_cpu=${props.resources_limits_cpu}
- export resources_limits_memory=${props.resources_limits_memory}
- export upload_folder=${props.upload_folder}
- export max_content_length=${props.max_content_length}
- export allowed_extensions=${props.allowed_extensions}
- export server_host=${props.server_host}
- export server_port=${props.server_port}
- export azure_storage_container_name=${props.azure_storage_container_name}
-
- # 이미지 경로 환경변수 설정
- export smarketing_image_path=${props.registry}/${props.image_org}/smarketing-ai:${imageTag}
-
- # Sensitive 환경변수 설정 (Jenkins Credentials에서)
- export secret_key=\$SECRET_KEY
- export claude_api_key=\$CLAUDE_API_KEY
- export openai_api_key=\$OPENAI_API_KEY
- export azure_storage_account_name=\$AZURE_STORAGE_ACCOUNT_NAME
- export azure_storage_account_key=\$AZURE_STORAGE_ACCOUNT_KEY
-
- # manifest 생성
- envsubst < smarketing-ai/deployment/${manifest}.template > smarketing-ai/deployment/${manifest}
- echo "Generated manifest file:"
- cat smarketing-ai/deployment/${manifest}
- """
- }
- }
-
- container('azure-cli') {
- sh """
- kubectl apply -f smarketing-ai/deployment/${manifest}
-
- echo "Waiting for smarketing deployment to be ready..."
- kubectl -n ${namespace} wait --for=condition=available deployment/smarketing --timeout=300s
-
- echo "=========================================="
- echo "Getting LoadBalancer External IP..."
-
- # External IP 확인 (최대 5분 대기)
- for i in {1..30}; do
- EXTERNAL_IP=\$(kubectl -n ${namespace} get service smarketing-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
- if [ "\$EXTERNAL_IP" != "" ] && [ "\$EXTERNAL_IP" != "null" ]; then
- echo "External IP assigned: \$EXTERNAL_IP"
- break
- fi
- echo "Waiting for External IP... (attempt \$i/30)"
- sleep 10
- done
-
- # 서비스 상태 확인
- kubectl -n ${namespace} get pods -l app=smarketing
- kubectl -n ${namespace} get service smarketing-service
-
- echo "=========================================="
- echo "Deployment Complete!"
- echo "Service URL: http://\$EXTERNAL_IP:${props.server_port}"
- echo "Health Check: http://\$EXTERNAL_IP:${props.server_port}/health"
- echo "=========================================="
- """
+ sh 'podman system prune -f || true'
}
+ sh 'rm -rf manifest-repo || true'
}
}
-}
\ No newline at end of file
+}
diff --git a/smarketing-ai/deployment/Jenkinsfile_backup b/smarketing-ai/deployment/Jenkinsfile_backup
new file mode 100644
index 0000000..e55f855
--- /dev/null
+++ b/smarketing-ai/deployment/Jenkinsfile_backup
@@ -0,0 +1,176 @@
+def PIPELINE_ID = "${env.BUILD_NUMBER}"
+
+def getImageTag() {
+ def dateFormat = new java.text.SimpleDateFormat('yyyyMMddHHmmss')
+ def currentDate = new Date()
+ return dateFormat.format(currentDate)
+}
+
+podTemplate(
+ label: "${PIPELINE_ID}",
+ serviceAccount: 'jenkins',
+ containers: [
+ containerTemplate(name: 'podman', image: "mgoltzsche/podman", ttyEnabled: true, command: 'cat', privileged: true),
+ containerTemplate(name: 'azure-cli', image: 'hiondal/azure-kubectl:latest', command: 'cat', ttyEnabled: true),
+ containerTemplate(name: 'envsubst', image: "hiondal/envsubst", command: 'sleep', args: '1h')
+ ],
+ volumes: [
+ emptyDirVolume(mountPath: '/run/podman', memory: false),
+ emptyDirVolume(mountPath: '/root/.azure', memory: false)
+ ]
+) {
+ node(PIPELINE_ID) {
+ def props
+ def imageTag = getImageTag()
+ def manifest = "deploy.yaml"
+ def namespace
+
+ stage("Get Source") {
+ checkout scm
+ props = readProperties file: "smarketing-ai/deployment/deploy_env_vars"
+ namespace = "${props.namespace}"
+
+ echo "Registry: ${props.registry}"
+ echo "Image Org: ${props.image_org}"
+ echo "Team ID: ${props.teamid}"
+ }
+
+ stage("Check Changes") {
+ script {
+ def changes = sh(
+ script: "git diff --name-only HEAD~1 HEAD",
+ returnStdout: true
+ ).trim()
+
+ echo "Changed files: ${changes}"
+
+ if (!changes.contains("smarketing-ai/")) {
+ echo "No changes in smarketing-ai, skipping build"
+ currentBuild.result = 'SUCCESS'
+ error("Stopping pipeline - no changes detected")
+ }
+
+ echo "Changes detected in smarketing-ai, proceeding with build"
+ }
+ }
+
+ stage("Setup AKS") {
+ container('azure-cli') {
+ withCredentials([azureServicePrincipal('azure-credentials')]) {
+ sh """
+ az login --service-principal -u \$AZURE_CLIENT_ID -p \$AZURE_CLIENT_SECRET -t \$AZURE_TENANT_ID
+ az aks get-credentials --resource-group rg-digitalgarage-02 --name aks-digitalgarage-02 --overwrite-existing
+ kubectl create namespace ${namespace} --dry-run=client -o yaml | kubectl apply -f -
+ """
+ }
+ }
+ }
+
+ stage('Build & Push Docker Image') {
+ container('podman') {
+ sh 'podman system service -t 0 unix:///run/podman/podman.sock & sleep 2'
+
+ withCredentials([usernamePassword(
+ credentialsId: 'acr-credentials',
+ usernameVariable: 'ACR_USERNAME',
+ passwordVariable: 'ACR_PASSWORD'
+ )]) {
+ sh """
+ echo "=========================================="
+ echo "Building smarketing-ai Python Flask application"
+ echo "Image Tag: ${imageTag}"
+ echo "=========================================="
+
+ # ACR 로그인
+ echo \$ACR_PASSWORD | podman login ${props.registry} --username \$ACR_USERNAME --password-stdin
+
+ # Docker 이미지 빌드
+ podman build \
+ -f smarketing-ai/deployment/Dockerfile \
+ -t ${props.registry}/${props.image_org}/smarketing-ai:${imageTag} .
+
+ # 이미지 푸시
+ podman push ${props.registry}/${props.image_org}/smarketing-ai:${imageTag}
+
+ echo "Successfully built and pushed: ${props.registry}/${props.image_org}/smarketing-ai:${imageTag}"
+ """
+ }
+ }
+ }
+
+ stage('Generate & Apply Manifest') {
+ container('envsubst') {
+ withCredentials([
+ string(credentialsId: 'SECRET_KEY', variable: 'SECRET_KEY'),
+ string(credentialsId: 'CLAUDE_API_KEY', variable: 'CLAUDE_API_KEY'),
+ string(credentialsId: 'OPENAI_API_KEY', variable: 'OPENAI_API_KEY'),
+ string(credentialsId: 'AZURE_STORAGE_ACCOUNT_NAME', variable: 'AZURE_STORAGE_ACCOUNT_NAME'),
+ string(credentialsId: 'AZURE_STORAGE_ACCOUNT_KEY', variable: 'AZURE_STORAGE_ACCOUNT_KEY')
+ ]) {
+ sh """
+ export namespace=${namespace}
+ export replicas=${props.replicas}
+ export resources_requests_cpu=${props.resources_requests_cpu}
+ export resources_requests_memory=${props.resources_requests_memory}
+ export resources_limits_cpu=${props.resources_limits_cpu}
+ export resources_limits_memory=${props.resources_limits_memory}
+ export upload_folder=${props.upload_folder}
+ export max_content_length=${props.max_content_length}
+ export allowed_extensions=${props.allowed_extensions}
+ export server_host=${props.server_host}
+ export server_port=${props.server_port}
+ export azure_storage_container_name=${props.azure_storage_container_name}
+
+ # 이미지 경로 환경변수 설정
+ export smarketing_image_path=${props.registry}/${props.image_org}/smarketing-ai:${imageTag}
+
+ # Sensitive 환경변수 설정 (Jenkins Credentials에서)
+ export secret_key=\$SECRET_KEY
+ export claude_api_key=\$CLAUDE_API_KEY
+ export openai_api_key=\$OPENAI_API_KEY
+ export azure_storage_account_name=\$AZURE_STORAGE_ACCOUNT_NAME
+ export azure_storage_account_key=\$AZURE_STORAGE_ACCOUNT_KEY
+
+ # manifest 생성
+ envsubst < smarketing-ai/deployment/${manifest}.template > smarketing-ai/deployment/${manifest}
+ echo "Generated manifest file:"
+ cat smarketing-ai/deployment/${manifest}
+ """
+ }
+ }
+
+ container('azure-cli') {
+ sh """
+ kubectl apply -f smarketing-ai/deployment/${manifest}
+
+ echo "Waiting for smarketing deployment to be ready..."
+ kubectl -n ${namespace} wait --for=condition=available deployment/smarketing --timeout=300s
+
+ echo "=========================================="
+ echo "Getting LoadBalancer External IP..."
+
+ # External IP 확인 (최대 5분 대기)
+ for i in {1..30}; do
+ EXTERNAL_IP=\$(kubectl -n ${namespace} get service smarketing-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
+ if [ "\$EXTERNAL_IP" != "" ] && [ "\$EXTERNAL_IP" != "null" ]; then
+ echo "External IP assigned: \$EXTERNAL_IP"
+ break
+ fi
+ echo "Waiting for External IP... (attempt \$i/30)"
+ sleep 10
+ done
+
+ # 서비스 상태 확인
+ kubectl -n ${namespace} get pods -l app=smarketing
+ kubectl -n ${namespace} get service smarketing-service
+
+ echo "=========================================="
+ echo "Deployment Complete!"
+ echo "Service URL: http://\$EXTERNAL_IP:${props.server_port}"
+ echo "Health Check: http://\$EXTERNAL_IP:${props.server_port}/health"
+ echo "=========================================="
+ """
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/smarketing-ai/deployment/deploy.yaml.template b/smarketing-ai/deployment/deploy.yaml.template_backup
similarity index 100%
rename from smarketing-ai/deployment/deploy.yaml.template
rename to smarketing-ai/deployment/deploy.yaml.template_backup
diff --git a/smarketing-ai/deployment/deploy_env_vars b/smarketing-ai/deployment/deploy_env_vars
index 6f33b33..435a1af 100644
--- a/smarketing-ai/deployment/deploy_env_vars
+++ b/smarketing-ai/deployment/deploy_env_vars
@@ -24,4 +24,4 @@ server_host=0.0.0.0
server_port=5001
# Azure Storage Settings (non-sensitive)
-azure_storage_container_name=ai-content
\ No newline at end of file
+azure_storage_container_name=ai-content
diff --git a/smarketing-ai/models/request_models.py b/smarketing-ai/models/request_models.py
index 3f6952d..346e24e 100644
--- a/smarketing-ai/models/request_models.py
+++ b/smarketing-ai/models/request_models.py
@@ -33,16 +33,23 @@ class PosterContentGetRequest:
"""홍보 포스터 생성 요청 모델"""
title: str
category: str
- contentType: str
images: List[str] # 이미지 URL 리스트
- photoStyle: Optional[str] = None
requirement: Optional[str] = None
- toneAndManner: Optional[str] = None
- emotionIntensity: Optional[str] = None
menuName: Optional[str] = None
- eventName: Optional[str] = None
startDate: Optional[date] = None # LocalDate -> date
endDate: Optional[date] = None # LocalDate -> date
+ store_name: Optional[str] = None
+ business_type: Optional[str] = None
+ location: Optional[str] = None
+
+ class Config:
+ schema_extra = {
+ "example": {
+ "store_name": "더블샷 카페",
+ "business_type": "카페",
+ "location": "서울시 강남구 역삼동",
+ }
+ }
# 기존 모델들은 유지
diff --git a/smarketing-ai/models/예시.md b/smarketing-ai/models/예시.md
new file mode 100644
index 0000000..e69de29
diff --git a/smarketing-ai/services/poster_service.py b/smarketing-ai/services/poster_service.py
index c90119c..9dc5f97 100644
--- a/smarketing-ai/services/poster_service.py
+++ b/smarketing-ai/services/poster_service.py
@@ -73,8 +73,8 @@ class PosterService:
# 메인 이미지 분석
main_image_analysis = self._analyze_main_image(main_image_url)
- # 포스터 생성 프롬프트 생성 (예시 링크 10개 포함)
- prompt = self._create_poster_prompt_v3(request, main_image_analysis)
+ # 포스터 생성 프롬프트 생성
+ prompt = self._create_poster_prompt(request, main_image_analysis)
# OpenAI로 이미지 생성
image_url = self.ai_client.generate_image_with_openai(prompt, "1024x1536")
@@ -92,7 +92,7 @@ class PosterService:
def _analyze_main_image(self, image_url: str) -> Dict[str, Any]:
"""
- 메인 메뉴 이미지 분석
+ 메인 메뉴 이미지 분석 시작
"""
temp_files = []
try:
@@ -101,7 +101,7 @@ class PosterService:
if temp_path:
temp_files.append(temp_path)
- # 이미지 분석
+ # 이미지 분석 시작
image_info = self.image_processor.get_image_info(temp_path)
image_description = self.ai_client.analyze_image(temp_path)
colors = self.image_processor.analyze_colors(temp_path, 5)
@@ -125,13 +125,27 @@ class PosterService:
'error': str(e)
}
- def _create_poster_prompt_v3(self, request: PosterContentGetRequest,
- main_analysis: Dict[str, Any]) -> str:
+ def _create_poster_prompt(self, request: PosterContentGetRequest,
+ main_analysis: Dict[str, Any]) -> str:
+ """
+ 카테고리에 따른 포스터 생성 프롬프트 분기
+ """
+ if request.category == "음식":
+ return self._create_food_poster_prompt(request, main_analysis)
+ elif request.category == "이벤트":
+ return self._create_event_poster_prompt(request, main_analysis)
+ else:
+ # 기본값으로 음식 프롬프트 사용
+ return self._create_food_poster_prompt(request, main_analysis)
+
+ def _create_food_poster_prompt(self, request: PosterContentGetRequest,
+ main_analysis: Dict[str, Any]) -> str:
"""
포스터 생성을 위한 AI 프롬프트 생성 (한글, 글자 완전 제외, 메인 이미지 기반 + 예시 링크 7개 포함)
"""
# 메인 이미지 정보 활용
+ main_image = main_analysis.get('url')
main_description = main_analysis.get('description', '맛있는 음식')
main_colors = main_analysis.get('dominant_colors', [])
image_info = main_analysis.get('info', {})
@@ -150,21 +164,16 @@ class PosterService:
example_links = "\n".join([f"- {link}" for link in self.example_images])
prompt = f"""
- ## 카페 홍보 포스터 디자인 요청
+ ## {main_image}를 활용한 홍보 포스터 디자인 요청
### 📋 기본 정보
- 카테고리: {request.category}
- 콘텐츠 타입: {request.contentType}
+ 메뉴 이미지 : {main_image}
메뉴명: {request.menuName or '없음'}
메뉴 정보: {main_description}
- ### 📅 이벤트 기간
- 시작일: {request.startDate or '지금'}
- 종료일: {request.endDate or '한정 기간'}
- 이벤트 시작일과 종료일은 필수로 포스터에 명시해주세요.
-
### 🎨 디자인 요구사항
메인 이미지 처리
+ - {main_image}는 변경 없이 그대로 사용해주세요.
- 기존 메인 이미지는 변경하지 않고 그대로 유지
- 포스터 전체 크기의 1/3 이하로 배치
- 이미지와 조화로운 작은 장식 이미지 추가
@@ -196,7 +205,78 @@ class PosterService:
톤앤매너: 맛있어 보이는 색상, 방문 유도하는 비주얼
### 🎯 최종 목표
- 고객들이 "이 카페에 가보고 싶다!"라고 생각하게 만드는 시각적으로 매력적인 홍보 포스터 제작
+ 고객들이 이 음식을 먹고싶다 생각하게 만드는 시각적으로 매력적인 홍보 포스터 제작
"""
return prompt
+
+
+ def _create_event_poster_prompt(self, request: PosterContentGetRequest,
+ main_analysis: Dict[str, Any]) -> str:
+ """
+ 이벤트 카테고리 포스터 생성을 위한 AI 프롬프트 생성
+ """
+ # 메인 이미지 정보 활용
+ main_image = main_analysis.get('url')
+ main_description = main_analysis.get('description', '특별 이벤트')
+ main_colors = main_analysis.get('dominant_colors', [])
+ image_info = main_analysis.get('info', {})
+
+ # 이미지 크기 및 비율 정보
+ aspect_ratio = image_info.get('aspect_ratio', 1.0) if image_info else 1.0
+ image_orientation = "가로형" if aspect_ratio > 1.2 else "세로형" if aspect_ratio < 0.8 else "정사각형"
+
+ # 색상 정보를 텍스트로 변환
+ color_description = ""
+ if main_colors:
+ color_rgb = main_colors[:3] # 상위 3개 색상
+ color_description = f"주요 색상 RGB 값: {color_rgb}를 기반으로 한 임팩트 있는 색감"
+
+ # 예시 이미지 링크들을 문자열로 변환
+ example_links = "\n".join([f"- {link}" for link in self.example_images])
+
+ prompt = f"""
+ ## {main_image}를 활용한 이벤트 홍보 포스터 디자인 요청
+ 메인 이미지에서 최대한 배경만 변경하는 식으로 활용하기
+
+
+ ### 📋 기본 정보
+ 이벤트 이미지 : {main_image}
+ 메뉴명: {request.menuName or '없음'}
+ 이벤트명: {request.requirement or '특별 이벤트'} {request.requirement}가 이벤트와 관련이 있다면 활용
+ 이벤트 정보: {main_description}
+
+ ### 📅 이벤트 기간 (중요!)
+ 시작일: {request.startDate or '지금'}
+ 종료일: {request.endDate or '한정 기간'}
+ ⚠️ 이벤트 기간은 가장 눈에 띄게 크고 강조하여 표시해주세요!
+
+ ### 🎨 이벤트 특화 디자인 요구사항
+ 메인 이미지 처리
+ - {main_image}는 변경 없이 그대로 사용해주세요.
+ - AI가 그린 것 같지 않은, 현실적인 사진으로 사용
+ - 이벤트의 핵심 내용을 시각적으로 강조
+ - 포스터 전체 크기의 30% 영역에 배치 (텍스트 공간 확보)
+ - 이벤트 분위기에 맞는 역동적인 배치
+ - 크기: {image_orientation}
+
+ 텍스트 요소 (이벤트 전용)
+ - 이벤트명을 가장 크고 임팩트 있게 표시
+ - 둥글둥글한 폰트 활용
+ - 이벤트 기간을 박스나 강조 영역으로 처리
+ - 메뉴명과 이벤트명만 기입해주고 나머지 텍스트는 기입하지 않음.
+ - 한글로 작성 필수이고, 절대 한글이 깨지지 않게 만들어주세요. 만약 깨질 것 같다면 그냥 빼주세요.
+
+ 이벤트 포스터만의 특별 요소
+ - 이미지에 맞는 임팩트 및 패턴 강조
+ - 할인이나 혜택을 강조하는 스티커 효과
+
+ 색상 가이드
+ - {color_description}과 비슷한 톤으로 생성
+
+
+ ### 🎯 최종 목표
+ 고객들이 "지금 당장 가서 이 이벤트에 참여해야겠다!"라고 생각하게 만드는 강렬하고 화려한 이벤트 홍보 포스터 제작
+ """
+
+ return prompt
diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py
index 16fd8ba..3e37bb6 100644
--- a/smarketing-ai/services/sns_content_service.py
+++ b/smarketing-ai/services/sns_content_service.py
@@ -1380,7 +1380,7 @@ class SnsContentService:
'call_to_action': ['방문', '예약', '문의', '공감', '이웃추가'],
'image_placement_strategy': [
'매장 외관 → 인테리어 → 메뉴판 → 음식 → 분위기',
- '텍스트 2-3문장마다 이미지 배치',
+ '텍스트 2-3문장마다 입력받은 이미지 배치',
'이미지 설명은 간결하고 매력적으로',
'마지막에 대표 이미지로 마무리'
]
@@ -1560,6 +1560,9 @@ class SnsContentService:
if not images:
return None
+ # 🔥 핵심 수정: 실제 이미지 개수 계산
+ actual_image_count = len(request.images) if request.images else 0
+
# 이미지 타입별 분류
categorized_images = {
'매장외관': [],
@@ -1603,36 +1606,69 @@ class SnsContentService:
}
],
'image_sequence': [],
- 'usage_guide': []
+ 'usage_guide': [],
+ 'actual_image_count': actual_image_count # 🔥 실제 이미지 수 추가
}
- # 각 섹션에 적절한 이미지 배정
- # 인트로: 매장외관 또는 대표 음식
- if categorized_images['매장외관']:
- placement_plan['structure'][0]['recommended_images'].extend(categorized_images['매장외관'][:1])
- elif categorized_images['음식']:
- placement_plan['structure'][0]['recommended_images'].extend(categorized_images['음식'][:1])
+ # 🔥 핵심: 실제 이미지 수에 따라 배치 전략 조정
+ if actual_image_count == 1:
+ # 이미지 1개: 가장 대표적인 위치에 배치
+ if categorized_images['음식']:
+ placement_plan['structure'][2]['recommended_images'].extend(categorized_images['음식'][:1])
+ elif categorized_images['매장외관']:
+ placement_plan['structure'][0]['recommended_images'].extend(categorized_images['매장외관'][:1])
+ else:
+ placement_plan['structure'][0]['recommended_images'].extend(images[:1])
- # 매장 정보: 외관 + 인테리어
- placement_plan['structure'][1]['recommended_images'].extend(categorized_images['매장외관'])
- placement_plan['structure'][1]['recommended_images'].extend(categorized_images['인테리어'])
+ elif actual_image_count == 2:
+ # 이미지 2개: 인트로와 메뉴 소개에 각각 배치
+ if categorized_images['매장외관'] and categorized_images['음식']:
+ placement_plan['structure'][0]['recommended_images'].extend(categorized_images['매장외관'][:1])
+ placement_plan['structure'][2]['recommended_images'].extend(categorized_images['음식'][:1])
+ else:
+ placement_plan['structure'][0]['recommended_images'].extend(images[:1])
+ placement_plan['structure'][2]['recommended_images'].extend(images[1:2])
- # 메뉴 소개: 메뉴판 + 음식
- placement_plan['structure'][2]['recommended_images'].extend(categorized_images['메뉴판'])
- placement_plan['structure'][2]['recommended_images'].extend(categorized_images['음식'])
+ elif actual_image_count == 3:
+ # 이미지 3개: 인트로, 매장 정보, 메뉴 소개에 각각 배치
+ placement_plan['structure'][0]['recommended_images'].extend(images[:1])
+ placement_plan['structure'][1]['recommended_images'].extend(images[1:2])
+ placement_plan['structure'][2]['recommended_images'].extend(images[2:3])
- # 총평: 남은 음식 사진 또는 기타
- remaining_food = [img for img in categorized_images['음식']
- if img not in placement_plan['structure'][2]['recommended_images']]
- placement_plan['structure'][3]['recommended_images'].extend(remaining_food[:1])
- placement_plan['structure'][3]['recommended_images'].extend(categorized_images['기타'][:1])
+ else:
+ # 이미지 4개 이상: 기존 로직 유지하되 실제 이미지 수로 제한
+ remaining_images = images[:]
- # 전체 이미지 순서 생성
+ # 인트로: 매장외관 또는 대표 음식
+ if categorized_images['매장외관'] and remaining_images:
+ img = categorized_images['매장외관'][0]
+ placement_plan['structure'][0]['recommended_images'].append(img)
+ if img in remaining_images:
+ remaining_images.remove(img)
+ elif categorized_images['음식'] and remaining_images:
+ img = categorized_images['음식'][0]
+ placement_plan['structure'][0]['recommended_images'].append(img)
+ if img in remaining_images:
+ remaining_images.remove(img)
+
+ # 나머지 이미지를 순서대로 배치
+ section_index = 1
+ for img in remaining_images:
+ if section_index < len(placement_plan['structure']):
+ placement_plan['structure'][section_index]['recommended_images'].append(img)
+ section_index += 1
+ else:
+ break
+
+ # 전체 이미지 순서 생성 (실제 사용될 이미지만)
for section in placement_plan['structure']:
for img in section['recommended_images']:
if img not in placement_plan['image_sequence']:
placement_plan['image_sequence'].append(img)
+ # 🔥 핵심 수정: 실제 이미지 수만큼만 유지
+ placement_plan['image_sequence'] = placement_plan['image_sequence'][:actual_image_count]
+
# 사용 가이드 생성
placement_plan['usage_guide'] = [
"📸 이미지 배치 가이드라인:",
@@ -1674,6 +1710,15 @@ class SnsContentService:
"""
category_hashtags = self.category_keywords.get(request.category, {}).get('인스타그램', [])
+ # 🔥 핵심 추가: 실제 이미지 개수 계산
+ actual_image_count = len(request.images) if request.images else 0
+
+ # 🔥 핵심 추가: 이미지 태그 사용법에 개수 제한 명시
+ image_tag_usage = f"""**이미지 태그 사용법 (반드시 준수):**
+ - 총 {actual_image_count}개의 이미지만 사용 가능
+ - [IMAGE_{actual_image_count}]까지만 사용
+ - {actual_image_count}개를 초과하는 [IMAGE_X] 태그는 절대 사용 금지"""
+
prompt = f"""
당신은 인스타그램 마케팅 전문가입니다. 소상공인 음식점을 위한 매력적인 인스타그램 게시물을 작성해주세요.
**🍸 가게 정보:**
@@ -1688,6 +1733,8 @@ class SnsContentService:
- 이벤트: {request.eventName or '특별 이벤트'}
- 독자층: {request.target}
+{image_tag_usage}
+
**📱 인스타그램 특화 요구사항:**
- 글 구조: {platform_spec['content_structure']}
- 최대 길이: {platform_spec['max_length']}자
@@ -1709,9 +1756,20 @@ class SnsContentService:
1. 첫 문장은 반드시 관심을 끄는 후킹 문장으로 시작
2. 이모티콘을 적절히 활용하여 시각적 재미 추가
3. 스토리텔링을 통해 감정적 연결 유도
-4. 명확한 행동 유도 문구 포함 (팔로우, 댓글, 저장, 방문 등)
-5. 줄바꿈을 활용하여 가독성 향상
-6. 해시태그는 본문과 자연스럽게 연결되도록 배치
+4. 각 섹션마다 적절한 위치에 [IMAGE_X] 태그로 이미지 배치 위치 표시
+5. 명확한 행동 유도 문구 포함 (팔로우, 댓글, 저장, 방문 등)
+6. 줄바꿈을 활용하여 가독성 향상
+7. 해시태그는 본문과 자연스럽게 연결되도록 배치
+
+**⚠️ 중요한 제약사항:**
+- 반드시 제공된 {actual_image_count}개의 이미지 개수를 초과하지 마세요
+- [IMAGE_{actual_image_count}]까지만 사용하세요
+- 더 많은 이미지 태그를 사용하면 오류가 발생합니다
+
+**이미지 태그 사용법:**
+- [IMAGE_1]: 첫 번째 이미지 배치 위치
+- [IMAGE_2]: 두 번째 이미지 배치 위치
+- 각 이미지 태그 다음 줄에 이미지 설명 문구 작성
**필수 요구사항:**
{request.requirement} or '고객의 관심을 끌고 방문을 유도하는 매력적인 게시물'
@@ -1729,6 +1787,9 @@ class SnsContentService:
category_keywords = self.category_keywords.get(request.category, {}).get('네이버 블로그', [])
seo_keywords = platform_spec['seo_keywords']
+ # 🔥 핵심: 실제 이미지 개수 계산
+ actual_image_count = len(request.images) if request.images else 0
+
# 이미지 배치 정보 추가
image_placement_info = ""
if image_placement_plan:
@@ -1777,14 +1838,13 @@ class SnsContentService:
1. 검색자의 궁금증을 해결하는 정보 중심 작성
2. 구체적인 가격, 위치, 운영시간 등 실용 정보 포함
3. 개인적인 경험과 솔직한 후기 작성
-4. 각 섹션마다 적절한 위치에 [IMAGE_X] 태그로 이미지 배치 위치 표시
-5. 이미지마다 간단한 설명 문구 추가
-6. 지역 정보와 접근성 정보 포함
+4. 이미지마다 간단한 설명 문구 추가
+5. 지역 정보와 접근성 정보 포함
-**이미지 태그 사용법:**
-- [IMAGE_1]: 첫 번째 이미지 배치 위치
-- [IMAGE_2]: 두 번째 이미지 배치 위치
-- 각 이미지 태그 다음 줄에 이미지 설명 문구 작성
+**⚠️ 중요한 제약사항:**
+- 반드시 제공된 {actual_image_count}개의 이미지 개수를 초과하지 마세요
+- [IMAGE_{actual_image_count}]까지만 사용하세요
+- {actual_image_count}개를 초과하는 [IMAGE_X] 태그는 절대 사용 금지
**필수 요구사항:**
{request.requirement} or '유용한 정보를 제공하여 방문을 유도하는 신뢰성 있는 후기'
@@ -1792,6 +1852,7 @@ class SnsContentService:
네이버 검색에서 상위 노출되고, 실제로 도움이 되는 정보를 제공하는 블로그 포스트를 작성해주세요.
필수 요구사항을 반드시 참고하여 작성해주세요.
이미지 배치 위치를 [IMAGE_X] 태그로 명확히 표시해주세요.
+
"""
return prompt
@@ -1811,6 +1872,14 @@ class SnsContentService:
"""
import re
+ # 🔥 핵심 추가: 실제 이미지 개수 계산
+ actual_image_count = len(request.images) if request.images else 0
+
+ # 🔥 핵심 추가: [IMAGE_X] 패턴 찾기 및 초과 태그 제거
+ image_tags = re.findall(r'\[IMAGE_(\d+)\]', content)
+ found_tag_numbers = [int(tag) for tag in image_tags]
+ removed_tags = []
+
# 해시태그 개수 조정
hashtags = re.findall(r'#[\w가-힣]+', content)
if len(hashtags) > 15:
@@ -1867,6 +1936,14 @@ class SnsContentService:
# 이미지를 콘텐츠 맨 앞에 추가
content = images_html_content + content
+ # 🔥 핵심 수정: 인스타그램 본문에서 [IMAGE_X] 태그 모두 제거
+ import re
+ content = re.sub(r'\[IMAGE_\d+\]', '', content)
+
+ # 🔥 추가: 태그 제거 후 남은 빈 줄 정리
+ content = re.sub(r'\n\s*\n\s*\n', '\n\n', content) # 3개 이상의 연속 줄바꿈을 2개로
+ content = re.sub(r'
\s*
\s*
', '
', content) # 3개 이상의 연속
을 2개로
+
# 2. 네이버 블로그인 경우 이미지 태그를 실제 이미지로 변환
elif request.platform == '네이버 블로그' and image_placement_plan:
content = self._replace_image_tags_with_html(content, image_placement_plan, request.images)
diff --git a/smarketing-ai/uploads/temp/temp_1de3d733-f3bb-44cf-8b4d-3d1e7c31cc42.jpg b/smarketing-ai/uploads/temp/temp_1de3d733-f3bb-44cf-8b4d-3d1e7c31cc42.jpg
new file mode 100644
index 0000000..628c52a
Binary files /dev/null and b/smarketing-ai/uploads/temp/temp_1de3d733-f3bb-44cf-8b4d-3d1e7c31cc42.jpg differ
diff --git a/smarketing-ai/uploads/temp/temp_272b6c7e-161f-4977-b91a-185f85b0a6cf.jpg b/smarketing-ai/uploads/temp/temp_272b6c7e-161f-4977-b91a-185f85b0a6cf.jpg
new file mode 100644
index 0000000..628c52a
Binary files /dev/null and b/smarketing-ai/uploads/temp/temp_272b6c7e-161f-4977-b91a-185f85b0a6cf.jpg differ
diff --git a/smarketing-ai/uploads/temp/temp_3c2f489c-9a96-48e2-b180-ff13caaee373.jpg b/smarketing-ai/uploads/temp/temp_3c2f489c-9a96-48e2-b180-ff13caaee373.jpg
new file mode 100644
index 0000000..628c52a
Binary files /dev/null and b/smarketing-ai/uploads/temp/temp_3c2f489c-9a96-48e2-b180-ff13caaee373.jpg differ
diff --git a/smarketing-ai/uploads/temp/temp_4815be20-a36b-41f2-bcc8-50bf82f23bd1.jpg b/smarketing-ai/uploads/temp/temp_4815be20-a36b-41f2-bcc8-50bf82f23bd1.jpg
new file mode 100644
index 0000000..628c52a
Binary files /dev/null and b/smarketing-ai/uploads/temp/temp_4815be20-a36b-41f2-bcc8-50bf82f23bd1.jpg differ
diff --git a/smarketing-ai/uploads/temp/temp_5a50b9d5-7ae8-4410-bf76-3f114020ff87.jpg b/smarketing-ai/uploads/temp/temp_5a50b9d5-7ae8-4410-bf76-3f114020ff87.jpg
new file mode 100644
index 0000000..628c52a
Binary files /dev/null and b/smarketing-ai/uploads/temp/temp_5a50b9d5-7ae8-4410-bf76-3f114020ff87.jpg differ
diff --git a/smarketing-ai/uploads/temp/temp_60ad2f5c-8dd3-46b4-bd4b-5cc54b28a2fa.jpg b/smarketing-ai/uploads/temp/temp_60ad2f5c-8dd3-46b4-bd4b-5cc54b28a2fa.jpg
new file mode 100644
index 0000000..628c52a
Binary files /dev/null and b/smarketing-ai/uploads/temp/temp_60ad2f5c-8dd3-46b4-bd4b-5cc54b28a2fa.jpg differ
diff --git a/smarketing-ai/uploads/temp/temp_893f4988-2fe7-4c34-a0f5-bf3f8c1f35fa.jpg b/smarketing-ai/uploads/temp/temp_893f4988-2fe7-4c34-a0f5-bf3f8c1f35fa.jpg
new file mode 100644
index 0000000..628c52a
Binary files /dev/null and b/smarketing-ai/uploads/temp/temp_893f4988-2fe7-4c34-a0f5-bf3f8c1f35fa.jpg differ
diff --git a/smarketing-ai/uploads/temp/temp_997c1ddc-b000-46ac-9393-c077c9ee3d41.jpg b/smarketing-ai/uploads/temp/temp_997c1ddc-b000-46ac-9393-c077c9ee3d41.jpg
new file mode 100644
index 0000000..628c52a
Binary files /dev/null and b/smarketing-ai/uploads/temp/temp_997c1ddc-b000-46ac-9393-c077c9ee3d41.jpg differ
diff --git a/smarketing-ai/uploads/temp/temp_9fd7f543-96bf-4c0d-a9eb-7092e733a4ba.jpg b/smarketing-ai/uploads/temp/temp_9fd7f543-96bf-4c0d-a9eb-7092e733a4ba.jpg
new file mode 100644
index 0000000..628c52a
Binary files /dev/null and b/smarketing-ai/uploads/temp/temp_9fd7f543-96bf-4c0d-a9eb-7092e733a4ba.jpg differ
diff --git a/smarketing-ai/uploads/temp/temp_d126c548-36c7-4a51-8962-01f1d9b70480.jpg b/smarketing-ai/uploads/temp/temp_d126c548-36c7-4a51-8962-01f1d9b70480.jpg
new file mode 100644
index 0000000..628c52a
Binary files /dev/null and b/smarketing-ai/uploads/temp/temp_d126c548-36c7-4a51-8962-01f1d9b70480.jpg differ
diff --git a/smarketing-ai/uploads/temp/temp_e42a6e2a-cac7-4400-aa90-11c1651ca5fb.jpg b/smarketing-ai/uploads/temp/temp_e42a6e2a-cac7-4400-aa90-11c1651ca5fb.jpg
new file mode 100644
index 0000000..628c52a
Binary files /dev/null and b/smarketing-ai/uploads/temp/temp_e42a6e2a-cac7-4400-aa90-11c1651ca5fb.jpg differ
diff --git a/smarketing-ai/uploads/temp/temp_e63e2a7d-bf42-4c2c-9bcb-609578b6f8b7.jpg b/smarketing-ai/uploads/temp/temp_e63e2a7d-bf42-4c2c-9bcb-609578b6f8b7.jpg
new file mode 100644
index 0000000..628c52a
Binary files /dev/null and b/smarketing-ai/uploads/temp/temp_e63e2a7d-bf42-4c2c-9bcb-609578b6f8b7.jpg differ
diff --git a/smarketing-java/GITOPS_TEST.md b/smarketing-java/GITOPS_TEST.md
new file mode 100644
index 0000000..9ac9e6c
--- /dev/null
+++ b/smarketing-java/GITOPS_TEST.md
@@ -0,0 +1 @@
+# GitOps Test Thu Jun 19 05:36:03 UTC 2025
diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/SecurityConfig.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/SecurityConfig.java
new file mode 100644
index 0000000..08a3949
--- /dev/null
+++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/SecurityConfig.java
@@ -0,0 +1,88 @@
+package com.won.smarketing.recommend.config;
+
+import com.won.smarketing.common.security.JwtAuthenticationFilter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+import java.util.Arrays;
+
+/**
+ * Spring Security 설정 클래스
+ * JWT 기반 인증 및 CORS 설정
+ */
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig
+{
+
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
+ @Value("${allowed-origins}")
+ private String allowedOrigins;
+ /**
+ * Spring Security 필터 체인 설정
+ *
+ * @param http HttpSecurity 객체
+ * @return SecurityFilterChain
+ * @throws Exception 예외
+ */
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/api/auth/**", "/api/member/register", "/api/member/check-duplicate/**",
+ "/api/member/validate-password", "/swagger-ui/**", "/v3/api-docs/**",
+ "/swagger-resources/**", "/webjars/**", "/actuator/**", "/health/**", "/error"
+ ).permitAll()
+ .anyRequest().authenticated()
+ )
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
+
+ return http.build();
+ }
+
+ /**
+ * 패스워드 인코더 빈 등록
+ *
+ * @return BCryptPasswordEncoder
+ */
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ /**
+ * CORS 설정
+ *
+ * @return CorsConfigurationSource
+ */
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+ configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));
+ configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
+ configuration.setAllowedHeaders(Arrays.asList("*"));
+ configuration.setAllowCredentials(true);
+
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", configuration);
+ return source;
+ }
+}
diff --git a/smarketing-java/ai-recommend/src/main/resources/application.yml b/smarketing-java/ai-recommend/src/main/resources/application.yml
index d392c82..3b84c68 100644
--- a/smarketing-java/ai-recommend/src/main/resources/application.yml
+++ b/smarketing-java/ai-recommend/src/main/resources/application.yml
@@ -26,10 +26,10 @@ spring:
external:
store-service:
- base-url: ${STORE_SERVICE_URL:http://localhost:8082}
+ base-url: ${STORE_SERVICE_URL:http://smarketing.20.249.184.228.nip.io}
timeout: ${STORE_SERVICE_TIMEOUT:5000}
python-ai-service:
- base-url: ${PYTHON_AI_SERVICE_URL:http://localhost:5001}
+ base-url: ${PYTHON_AI_SERVICE_URL:http://20.249.113.247:5001}
api-key: ${PYTHON_AI_API_KEY:dummy-key}
timeout: ${PYTHON_AI_TIMEOUT:30000}
@@ -70,4 +70,6 @@ info:
app:
name: ${APP_NAME:smarketing-recommend}
version: "1.0.0-MVP"
- description: "AI 마케팅 서비스 MVP - recommend"
\ No newline at end of file
+ description: "AI 마케팅 서비스 MVP - recommend"
+
+allowed-origins: ${ALLOWED_ORIGINS:http://localhost:3000}
\ No newline at end of file
diff --git a/smarketing-java/build.gradle b/smarketing-java/build.gradle
index e917ca4..01426b7 100644
--- a/smarketing-java/build.gradle
+++ b/smarketing-java/build.gradle
@@ -53,6 +53,15 @@ subprojects {
implementation 'com.azure:azure-messaging-eventhubs-checkpointstore-blob:1.19.0'
implementation 'com.azure:azure-identity:1.11.4'
+ // Azure Blob Storage 의존성 추가
+ implementation 'com.azure:azure-storage-blob:12.25.0'
+ implementation 'com.azure:azure-identity:1.11.1'
+
+ implementation 'com.fasterxml.jackson.core:jackson-core'
+ implementation 'com.fasterxml.jackson.core:jackson-databind'
+ implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
+ implementation 'org.springframework.boot:spring-boot-starter-webflux'
+
}
tasks.named('test') {
diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile
index ce96650..821ed02 100644
--- a/smarketing-java/deployment/Jenkinsfile
+++ b/smarketing-java/deployment/Jenkinsfile
@@ -1,3 +1,5 @@
+// smarketing-backend/smarketing-java/deployment/Jenkinsfile
+
def PIPELINE_ID = "${env.BUILD_NUMBER}"
def getImageTag() {
@@ -12,230 +14,233 @@ podTemplate(
containers: [
containerTemplate(name: 'gradle', image: 'gradle:jdk17', ttyEnabled: true, command: 'cat'),
containerTemplate(name: 'docker', image: 'docker:20.10.16-dind', ttyEnabled: true, privileged: true),
- containerTemplate(name: 'azure-cli', image: 'hiondal/azure-kubectl:latest', command: 'cat', ttyEnabled: true),
- containerTemplate(name: 'envsubst', image: "hiondal/envsubst", command: 'sleep', args: '1h')
+ containerTemplate(name: 'git', image: 'alpine/git:latest', command: 'cat', ttyEnabled: true)
],
volumes: [
emptyDirVolume(mountPath: '/home/gradle/.gradle', memory: false),
- emptyDirVolume(mountPath: '/root/.azure', memory: false),
emptyDirVolume(mountPath: '/var/run', memory: false)
]
) {
node(PIPELINE_ID) {
def props
def imageTag = getImageTag()
- def manifest = "deploy.yaml"
- def namespace
def services = ['member', 'store', 'marketing-content', 'ai-recommend']
+
+ // Manifest Repository 설정
+ def MANIFEST_REPO = 'https://github.com/won-ktds/smarketing-manifest.git'
+ def MANIFEST_CREDENTIAL_ID = 'github-credentials-smarketing'
- stage("Get Source") {
- checkout scm
-
- // smarketing-java 하위에 있는 설정 파일 읽기
- props = readProperties file: "smarketing-java/deployment/deploy_env_vars"
- namespace = "${props.namespace}"
+ try {
+ stage("Get Source") {
+ checkout scm
+
+ // smarketing-java 하위에 있는 설정 파일 읽기
+ props = readProperties file: "smarketing-java/deployment/deploy_env_vars"
- echo "=== Build Information ==="
- echo "Services: ${services}"
- echo "Namespace: ${namespace}"
- echo "Image Tag: ${imageTag}"
- }
-
- stage("Check Changes") {
- script {
- def changes = sh(
- script: "git diff --name-only HEAD~1 HEAD",
- returnStdout: true
- ).trim()
-
- if (!changes.contains("smarketing-java/")) {
- echo "No changes in smarketing-java, skipping build"
- currentBuild.result = 'SUCCESS'
- error("Stopping pipeline - no changes detected")
- }
-
- echo "Changes detected in smarketing-java, proceeding with build"
+ echo "=== Build Information ==="
+ echo "Services: ${services}"
+ echo "Image Tag: ${imageTag}"
+ echo "Registry: ${props.registry}"
+ echo "Image Org: ${props.image_org}"
}
- }
- stage("Setup AKS") {
- container('azure-cli') {
- withCredentials([azureServicePrincipal('azure-credentials')]) {
+ stage("Check Changes") {
+ script {
+ def changes = sh(
+ script: "git diff --name-only HEAD~1 HEAD",
+ returnStdout: true
+ ).trim()
+
+ if (!changes.contains("smarketing-java/")) {
+ echo "No changes in smarketing-java, skipping build"
+ currentBuild.result = 'SUCCESS'
+ error("Stopping pipeline - no changes detected")
+ }
+
+ echo "Changes detected in smarketing-java, proceeding with build"
+ }
+ }
+
+ stage('Build Applications') {
+ container('gradle') {
sh """
- echo "=== Azure 로그인 ==="
- az login --service-principal -u \$AZURE_CLIENT_ID -p \$AZURE_CLIENT_SECRET -t \$AZURE_TENANT_ID
- az account set --subscription 2513dd36-7978-48e3-9a7c-b221d4874f66
+ echo "=== smarketing-java 디렉토리로 이동 ==="
+ cd smarketing-java
- echo "=== AKS 인증정보 가져오기 (rg-digitalgarage-02) ==="
- az aks get-credentials --resource-group rg-digitalgarage-02 --name aks-digitalgarage-02 --overwrite-existing
+ echo "=== gradlew 권한 설정 ==="
+ chmod +x gradlew
- echo "=== 네임스페이스 생성 ==="
- kubectl create namespace ${namespace} --dry-run=client -o yaml | kubectl apply -f -
+ echo "=== 전체 서비스 빌드 ==="
+ ./gradlew :member:clean :member:build -x test
+ ./gradlew :store:clean :store:build -x test
+ ./gradlew :marketing-content:clean :marketing-content:build -x test
+ ./gradlew :ai-recommend:clean :ai-recommend:build -x test
- echo "=== Image Pull Secret 생성 ==="
- kubectl create secret docker-registry acr-secret \\
- --docker-server=${props.registry} \\
- --docker-username=acrdigitalgarage02 \\
- --docker-password=\$(az acr credential show --name acrdigitalgarage02 --query passwords[0].value -o tsv) \\
- --namespace=${namespace} \\
- --dry-run=client -o yaml | kubectl apply -f -
-
- echo "=== 클러스터 상태 확인 ==="
- kubectl get nodes
- kubectl get ns ${namespace}
-
- echo "=== 현재 연결된 클러스터 확인 ==="
- kubectl config current-context
+ echo "=== 빌드 결과 확인 ==="
+ find . -name "*.jar" -path "*/build/libs/*" | grep -v 'plain.jar'
"""
}
}
- }
- stage('Build Applications') {
- container('gradle') {
- sh """
- echo "=== smarketing-java 디렉토리로 이동 ==="
- cd smarketing-java
-
- echo "=== gradlew 권한 설정 ==="
- chmod +x gradlew
-
- echo "=== 전체 서비스 빌드 ==="
- ./gradlew :member:clean :member:build -x test
- ./gradlew :store:clean :store:build -x test
- ./gradlew :marketing-content:clean :marketing-content:build -x test
- ./gradlew :ai-recommend:clean :ai-recommend:build -x test
-
- echo "=== 빌드 결과 확인 ==="
- find . -name "*.jar" -path "*/build/libs/*" | grep -v 'plain.jar'
- """
- }
- }
-
- stage('Build & Push Images') {
- container('docker') {
- sh """
- echo "=== Docker 데몬 시작 대기 ==="
- timeout 30 sh -c 'until docker info; do sleep 1; done'
- """
-
- // ACR Credential을 Jenkins에서 직접 사용
- withCredentials([usernamePassword(
- credentialsId: 'acr-credentials',
- usernameVariable: 'ACR_USERNAME',
- passwordVariable: 'ACR_PASSWORD'
- )]) {
+ stage('Build & Push Images') {
+ container('docker') {
sh """
- echo "=== Docker로 ACR 로그인 ==="
- echo "\$ACR_PASSWORD" | docker login ${props.registry} --username \$ACR_USERNAME --password-stdin
+ echo "=== Docker 데몬 시작 대기 ==="
+ timeout 30 sh -c 'until docker info; do sleep 1; done'
"""
- services.each { service ->
- script {
- def buildDir = "smarketing-java/${service}"
- def fullImageName = "${props.registry}/${props.image_org}/${service}:${imageTag}"
+ // ACR Credential을 Jenkins에서 직접 사용
+ withCredentials([usernamePassword(
+ credentialsId: 'acr-credentials',
+ usernameVariable: 'ACR_USERNAME',
+ passwordVariable: 'ACR_PASSWORD'
+ )]) {
+ sh """
+ echo "=== Docker로 ACR 로그인 ==="
+ echo "\$ACR_PASSWORD" | docker login ${props.registry} --username \$ACR_USERNAME --password-stdin
+ """
- echo "Building image for ${service}: ${fullImageName}"
+ services.each { service ->
+ script {
+ def buildDir = "smarketing-java/${service}"
+ def fullImageName = "${props.registry}/${props.image_org}/${service}:${imageTag}"
+
+ echo "Building image for ${service}: ${fullImageName}"
+
+ // 실제 JAR 파일명 동적 탐지
+ def actualJarFile = sh(
+ script: """
+ cd ${buildDir}/build/libs
+ ls *.jar | grep -v 'plain.jar' | head -1
+ """,
+ returnStdout: true
+ ).trim()
+
+ if (!actualJarFile) {
+ error "${service} JAR 파일을 찾을 수 없습니다"
+ }
+
+ echo "발견된 JAR 파일: ${actualJarFile}"
+
+ sh """
+ echo "=== ${service} 이미지 빌드 ==="
+ docker build \\
+ --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \\
+ --build-arg ARTIFACTORY_FILE="${actualJarFile}" \\
+ -f smarketing-java/deployment/container/Dockerfile \\
+ -t ${fullImageName} .
+
+ echo "=== ${service} 이미지 푸시 ==="
+ docker push ${fullImageName}
+
+ echo "Successfully built and pushed: ${fullImageName}"
+ """
+ }
+ }
+ }
+ }
+ }
+
+ stage('Update Manifest Repository') {
+ container('git') {
+ script {
+ // Manifest Repository Clone
+ withCredentials([usernamePassword(
+ credentialsId: MANIFEST_CREDENTIAL_ID,
+ usernameVariable: 'GIT_USERNAME',
+ passwordVariable: 'GIT_PASSWORD'
+ )]) {
+ sh """
+ echo "=== Git 설정 ==="
+ git config --global user.name "Jenkins CI"
+ git config --global user.email "jenkins@company.com"
+
+ echo "=== Manifest Repository Clone ==="
+ rm -rf manifest-repo
+ git clone https://\$GIT_USERNAME:\$GIT_PASSWORD@github.com/won-ktds/smarketing-manifest.git manifest-repo
+ cd manifest-repo
+ """
- // 실제 JAR 파일명 동적 탐지
- def actualJarFile = sh(
- script: """
- cd ${buildDir}/build/libs
- ls *.jar | grep -v 'plain.jar' | head -1
- """,
- returnStdout: true
- ).trim()
-
- if (!actualJarFile) {
- error "${service} JAR 파일을 찾을 수 없습니다"
+ services.each { service ->
+ def fullImageName = "${props.registry}/${props.image_org}/${service}:${imageTag}"
+ def deploymentFile = "smarketing/deployments/${service}/${service}-deployment.yaml"
+
+ sh """
+ cd manifest-repo
+
+ echo "=== ${service} 이미지 태그 업데이트 ==="
+ if [ -f "${deploymentFile}" ]; then
+ # 이미지 태그 업데이트 (sed 사용)
+ sed -i 's|image: ${props.registry}/${props.image_org}/${service}:.*|image: ${fullImageName}|g' "${deploymentFile}"
+ echo "Updated ${deploymentFile} with new image: ${fullImageName}"
+
+ # 변경사항 확인
+ echo "=== 변경된 내용 확인 ==="
+ grep "image: ${props.registry}/${props.image_org}/${service}" "${deploymentFile}" || echo "이미지 태그 업데이트 확인 실패"
+ else
+ echo "Warning: ${deploymentFile} not found"
+ fi
+ """
}
- echo "발견된 JAR 파일: ${actualJarFile}"
-
sh """
- echo "=== ${service} 이미지 빌드 ==="
- docker build \\
- --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \\
- --build-arg ARTIFACTORY_FILE="${actualJarFile}" \\
- -f smarketing-java/deployment/container/Dockerfile \\
- -t ${fullImageName} .
-
- echo "=== ${service} 이미지 푸시 ==="
- docker push ${fullImageName}
+ cd manifest-repo
- echo "Successfully built and pushed: ${fullImageName}"
+ echo "=== Git 변경사항 확인 ==="
+ git status
+ git diff
+
+ # 변경사항이 있으면 커밋 및 푸시
+ if [ -n "\$(git status --porcelain)" ]; then
+ git add .
+ git commit -m "Update SMarketing services to ${imageTag} - Build ${env.BUILD_NUMBER}"
+ git push origin main
+ echo "✅ Successfully updated manifest repository"
+ else
+ echo "ℹ️ No changes to commit"
+ fi
"""
}
}
}
}
- }
- stage('Generate & Apply Manifest') {
- container('envsubst') {
- sh """
- echo "=== 환경변수 설정 ==="
- export namespace=${namespace}
- export allowed_origins=${props.allowed_origins}
- export jwt_secret_key=${props.jwt_secret_key}
- export postgres_user=${props.postgres_user}
- export postgres_password=${props.postgres_password}
- export replicas=${props.replicas}
- # 리소스 요구사항 조정 (작게)
- export resources_requests_cpu=100m
- export resources_requests_memory=128Mi
- export resources_limits_cpu=500m
- export resources_limits_memory=512Mi
+ stage('Trigger ArgoCD Sync') {
+ script {
+ echo """
+🎯 CI Pipeline 완료!
- # 이미지 경로 환경변수 설정
- export member_image_path=${props.registry}/${props.image_org}/member:${imageTag}
- export store_image_path=${props.registry}/${props.image_org}/store:${imageTag}
- export marketing_content_image_path=${props.registry}/${props.image_org}/marketing-content:${imageTag}
- export ai_recommend_image_path=${props.registry}/${props.image_org}/ai-recommend:${imageTag}
+📦 빌드된 이미지들:
+${services.collect { "- ${props.registry}/${props.image_org}/${it}:${imageTag}" }.join('\n')}
- echo "=== Manifest 생성 ==="
- envsubst < smarketing-java/deployment/${manifest}.template > smarketing-java/deployment/${manifest}
-
- echo "=== Generated Manifest File ==="
- cat smarketing-java/deployment/${manifest}
- echo "==============================="
- """
+🔄 ArgoCD 동작:
+- ArgoCD가 manifest repository 변경사항을 자동으로 감지합니다
+- 각 서비스별 Application이 새로운 이미지로 동기화됩니다
+- ArgoCD UI에서 배포 상태를 모니터링하세요
+
+🌐 ArgoCD UI: [ArgoCD 접속 URL]
+📁 Manifest Repo: ${MANIFEST_REPO}
+ """
+ }
}
- container('azure-cli') {
- sh """
- echo "=== 현재 연결된 클러스터 재확인 ==="
- kubectl config current-context
- kubectl cluster-info | head -3
-
- echo "=== PostgreSQL 서비스 확인 ==="
- kubectl get svc -n ${namespace} | grep postgresql || echo "PostgreSQL 서비스가 없습니다. 먼저 설치해주세요."
-
- echo "=== Manifest 적용 ==="
- kubectl apply -f smarketing-java/deployment/${manifest}
+ // 성공 시 처리
+ echo """
+✅ CI Pipeline 성공!
+🏷️ 새로운 이미지 태그: ${imageTag}
+🔄 ArgoCD가 자동으로 배포를 시작합니다
+ """
- echo "=== 배포 상태 확인 (60초 대기) ==="
- kubectl -n ${namespace} get deployments
- kubectl -n ${namespace} get pods
-
- echo "=== 각 서비스 배포 대기 (60초 timeout) ==="
- timeout 60 kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=60s || echo "member deployment 대기 타임아웃"
- timeout 60 kubectl -n ${namespace} wait --for=condition=available deployment/store --timeout=60s || echo "store deployment 대기 타임아웃"
- timeout 60 kubectl -n ${namespace} wait --for=condition=available deployment/marketing-content --timeout=60s || echo "marketing-content deployment 대기 타임아웃"
- timeout 60 kubectl -n ${namespace} wait --for=condition=available deployment/ai-recommend --timeout=60s || echo "ai-recommend deployment 대기 타임아웃"
-
- echo "=== 최종 상태 ==="
- kubectl -n ${namespace} get all
-
- echo "=== 실패한 Pod 상세 정보 ==="
- for pod in \$(kubectl -n ${namespace} get pods --field-selector=status.phase!=Running -o name 2>/dev/null || true); do
- if [ ! -z "\$pod" ]; then
- echo "=== 실패한 Pod: \$pod ==="
- kubectl -n ${namespace} describe \$pod | tail -20
- fi
- done
- """
+ } catch (Exception e) {
+ // 실패 시 처리
+ echo "❌ CI Pipeline 실패: ${e.getMessage()}"
+ throw e
+ } finally {
+ // 정리 작업 (항상 실행)
+ container('docker') {
+ sh 'docker system prune -f || true'
}
+ sh 'rm -rf manifest-repo || true'
}
}
}
diff --git a/smarketing-java/deployment/Jenkinsfile.backup b/smarketing-java/deployment/Jenkinsfile.backup
new file mode 100644
index 0000000..a84cf56
--- /dev/null
+++ b/smarketing-java/deployment/Jenkinsfile.backup
@@ -0,0 +1,301 @@
+def PIPELINE_ID = "${env.BUILD_NUMBER}"
+
+def getImageTag() {
+ def dateFormat = new java.text.SimpleDateFormat('yyyyMMddHHmmss')
+ def currentDate = new Date()
+ return dateFormat.format(currentDate)
+}
+
+podTemplate(
+ label: "${PIPELINE_ID}",
+ serviceAccount: 'jenkins',
+ containers: [
+ containerTemplate(name: 'gradle', image: 'gradle:jdk17', ttyEnabled: true, command: 'cat'),
+ containerTemplate(name: 'docker', image: 'docker:20.10.16-dind', ttyEnabled: true, privileged: true),
+ containerTemplate(name: 'azure-cli', image: 'hiondal/azure-kubectl:latest', command: 'cat', ttyEnabled: true),
+ containerTemplate(name: 'envsubst', image: "hiondal/envsubst", command: 'sleep', args: '1h')
+ ],
+ volumes: [
+ emptyDirVolume(mountPath: '/home/gradle/.gradle', memory: false),
+ emptyDirVolume(mountPath: '/root/.azure', memory: false),
+ emptyDirVolume(mountPath: '/var/run', memory: false)
+ ]
+) {
+ node(PIPELINE_ID) {
+ def props
+ def imageTag = getImageTag()
+ def manifest = "deploy.yaml"
+ def namespace
+ def services = ['member', 'store', 'marketing-content', 'ai-recommend']
+
+ stage("Get Source") {
+ checkout scm
+
+ // smarketing-java 하위에 있는 설정 파일 읽기
+ props = readProperties file: "smarketing-java/deployment/deploy_env_vars"
+ namespace = "${props.namespace}"
+
+ echo "=== Build Information ==="
+ echo "Services: ${services}"
+ echo "Namespace: ${namespace}"
+ echo "Image Tag: ${imageTag}"
+ echo "Registry: ${props.registry}"
+ echo "Image Org: ${props.image_org}"
+ }
+
+ stage("Check Changes") {
+ script {
+ def changes = sh(
+ script: "git diff --name-only HEAD~1 HEAD",
+ returnStdout: true
+ ).trim()
+
+ if (!changes.contains("smarketing-java/")) {
+ echo "No changes in smarketing-java, skipping build"
+ currentBuild.result = 'SUCCESS'
+ error("Stopping pipeline - no changes detected")
+ }
+
+ echo "Changes detected in smarketing-java, proceeding with build"
+ }
+ }
+
+ stage("Setup AKS") {
+ container('azure-cli') {
+ withCredentials([azureServicePrincipal('azure-credentials')]) {
+ sh """
+ echo "=== Azure 로그인 ==="
+ az login --service-principal -u \$AZURE_CLIENT_ID -p \$AZURE_CLIENT_SECRET -t \$AZURE_TENANT_ID
+ az account set --subscription 2513dd36-7978-48e3-9a7c-b221d4874f66
+
+ echo "=== AKS 인증정보 가져오기 (rg-digitalgarage-02) ==="
+ az aks get-credentials --resource-group rg-digitalgarage-02 --name aks-digitalgarage-02 --overwrite-existing
+
+ echo "=== 네임스페이스 생성 ==="
+ kubectl create namespace ${namespace} --dry-run=client -o yaml | kubectl apply -f -
+
+ echo "=== Image Pull Secret 생성 ==="
+ kubectl create secret docker-registry acr-secret \\
+ --docker-server=${props.registry} \\
+ --docker-username=acrdigitalgarage02 \\
+ --docker-password=\$(az acr credential show --name acrdigitalgarage02 --query passwords[0].value -o tsv) \\
+ --namespace=${namespace} \\
+ --dry-run=client -o yaml | kubectl apply -f -
+
+ echo "=== 클러스터 상태 확인 ==="
+ kubectl get nodes
+ kubectl get ns ${namespace}
+
+ echo "=== 현재 연결된 클러스터 확인 ==="
+ kubectl config current-context
+ """
+ }
+ }
+ }
+
+ stage('Build Applications') {
+ container('gradle') {
+ sh """
+ echo "=== smarketing-java 디렉토리로 이동 ==="
+ cd smarketing-java
+
+ echo "=== gradlew 권한 설정 ==="
+ chmod +x gradlew
+
+ echo "=== 전체 서비스 빌드 ==="
+ ./gradlew :member:clean :member:build -x test
+ ./gradlew :store:clean :store:build -x test
+ ./gradlew :marketing-content:clean :marketing-content:build -x test
+ ./gradlew :ai-recommend:clean :ai-recommend:build -x test
+
+ echo "=== 빌드 결과 확인 ==="
+ find . -name "*.jar" -path "*/build/libs/*" | grep -v 'plain.jar'
+ """
+ }
+ }
+
+ stage('Build & Push Images') {
+ container('docker') {
+ sh """
+ echo "=== Docker 데몬 시작 대기 ==="
+ timeout 30 sh -c 'until docker info; do sleep 1; done'
+ """
+
+ // ACR Credential을 Jenkins에서 직접 사용
+ withCredentials([usernamePassword(
+ credentialsId: 'acr-credentials',
+ usernameVariable: 'ACR_USERNAME',
+ passwordVariable: 'ACR_PASSWORD'
+ )]) {
+ sh """
+ echo "=== Docker로 ACR 로그인 ==="
+ echo "\$ACR_PASSWORD" | docker login ${props.registry} --username \$ACR_USERNAME --password-stdin
+ """
+
+ services.each { service ->
+ script {
+ def buildDir = "smarketing-java/${service}"
+ def fullImageName = "${props.registry}/${props.image_org}/${service}:${imageTag}"
+
+ echo "Building image for ${service}: ${fullImageName}"
+
+ // 실제 JAR 파일명 동적 탐지
+ def actualJarFile = sh(
+ script: """
+ cd ${buildDir}/build/libs
+ ls *.jar | grep -v 'plain.jar' | head -1
+ """,
+ returnStdout: true
+ ).trim()
+
+ if (!actualJarFile) {
+ error "${service} JAR 파일을 찾을 수 없습니다"
+ }
+
+ echo "발견된 JAR 파일: ${actualJarFile}"
+
+ sh """
+ echo "=== ${service} 이미지 빌드 ==="
+ docker build \\
+ --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \\
+ --build-arg ARTIFACTORY_FILE="${actualJarFile}" \\
+ -f smarketing-java/deployment/container/Dockerfile \\
+ -t ${fullImageName} .
+
+ echo "=== ${service} 이미지 푸시 ==="
+ docker push ${fullImageName}
+
+ echo "Successfully built and pushed: ${fullImageName}"
+ """
+ }
+ }
+ }
+ }
+ }
+
+ stage('Generate & Apply Manifest') {
+ container('envsubst') {
+ sh """
+ echo "=== 환경변수 설정 ==="
+ export namespace=${namespace}
+ export allowed_origins='${props.allowed_origins}'
+ export jwt_secret_key='${props.jwt_secret_key}'
+ export postgres_user='${props.POSTGRES_USER}'
+ export postgres_password='${props.POSTGRES_PASSWORD}'
+ export replicas=${props.replicas}
+
+ # PostgreSQL 환경변수 추가 (올바른 DB명으로 수정)
+ export postgres_host='${props.POSTGRES_HOST}'
+ export postgres_port='5432'
+ export postgres_db_member='MemberDB'
+ export postgres_db_store='StoreDB'
+ export postgres_db_marketing_content='MarketingContentDB'
+ export postgres_db_ai_recommend='AiRecommendationDB'
+
+ # Redis 환경변수 추가
+ export redis_host='${props.REDIS_HOST}'
+ export redis_port='6380'
+ export redis_password='${props.REDIS_PASSWORD}'
+
+ # 리소스 요구사항
+ export resources_requests_cpu='${props.resources_requests_cpu}'
+ export resources_requests_memory='${props.resources_requests_memory}'
+ export resources_limits_cpu='${props.resources_limits_cpu}'
+ export resources_limits_memory='${props.resources_limits_memory}'
+
+ # 이미지 경로 환경변수 설정
+ export member_image_path='${props.registry}/${props.image_org}/member:${imageTag}'
+ export store_image_path='${props.registry}/${props.image_org}/store:${imageTag}'
+ export marketing_content_image_path='${props.registry}/${props.image_org}/marketing-content:${imageTag}'
+ export ai_recommend_image_path='${props.registry}/${props.image_org}/ai-recommend:${imageTag}'
+
+ echo "=== 환경변수 확인 ==="
+ echo "namespace: \$namespace"
+ echo "postgres_host: \$postgres_host"
+ echo "postgres_port: \$postgres_port"
+ echo "postgres_user: \$postgres_user"
+ echo "postgres_db_member: \$postgres_db_member"
+ echo "postgres_db_store: \$postgres_db_store"
+ echo "postgres_db_marketing_content: \$postgres_db_marketing_content"
+ echo "postgres_db_ai_recommend: \$postgres_db_ai_recommend"
+ echo "redis_host: \$redis_host"
+ echo "redis_port: \$redis_port"
+ echo "replicas: \$replicas"
+
+ echo "=== Manifest 생성 ==="
+ envsubst < smarketing-java/deployment/${manifest}.template > smarketing-java/deployment/${manifest}
+
+ echo "=== Generated Manifest File ==="
+ cat smarketing-java/deployment/${manifest}
+ echo "==============================="
+ """
+ }
+
+ container('azure-cli') {
+ sh """
+ echo "=== 현재 연결된 클러스터 재확인 ==="
+ kubectl config current-context
+ kubectl cluster-info | head -3
+
+ echo "=== 기존 ConfigMap 삭제 (타입 충돌 해결) ==="
+ kubectl delete configmap member-config store-config marketing-content-config ai-recommend-config -n ${namespace} --ignore-not-found=true
+
+ echo "=== PostgreSQL 서비스 확인 ==="
+ kubectl get svc -n ${namespace} | grep postgresql || echo "PostgreSQL 서비스를 찾을 수 없습니다."
+
+ echo "=== Redis 서비스 확인 ==="
+ kubectl get svc -n ${namespace} | grep redis || echo "Redis 서비스를 찾을 수 없습니다."
+
+ echo "=== Manifest 적용 ==="
+ kubectl apply -f smarketing-java/deployment/${manifest}
+
+ echo "=== 배포 상태 확인 (30초 대기) ==="
+ sleep 30
+ kubectl -n ${namespace} get deployments
+ kubectl -n ${namespace} get pods
+
+ echo "=== ConfigMap 확인 ==="
+ kubectl -n ${namespace} get configmap member-config -o yaml | grep -A 10 "data:"
+ kubectl -n ${namespace} get configmap ai-recommend-config -o yaml | grep -A 10 "data:"
+
+ echo "=== Secret 확인 ==="
+ kubectl -n ${namespace} get secret member-secret -o yaml | grep -A 5 "data:"
+
+ echo "=== 각 서비스 배포 대기 (120초 timeout) ==="
+ timeout 120 kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=120s || echo "member deployment 대기 타임아웃"
+ timeout 120 kubectl -n ${namespace} wait --for=condition=available deployment/store --timeout=120s || echo "store deployment 대기 타임아웃"
+ timeout 120 kubectl -n ${namespace} wait --for=condition=available deployment/marketing-content --timeout=120s || echo "marketing-content deployment 대기 타임아웃"
+ timeout 120 kubectl -n ${namespace} wait --for=condition=available deployment/ai-recommend --timeout=120s || echo "ai-recommend deployment 대기 타임아웃"
+
+ echo "=== 최종 배포 상태 ==="
+ kubectl -n ${namespace} get all
+
+ echo "=== 각 서비스 Pod 로그 확인 (최근 20라인) ==="
+ for service in member store marketing-content ai-recommend; do
+ echo "=== \$service 서비스 로그 ==="
+ kubectl -n ${namespace} logs deployment/\$service --tail=20 || echo "\$service 로그를 가져올 수 없습니다"
+ echo ""
+ done
+
+ echo "=== 실패한 Pod 상세 정보 ==="
+ for pod in \$(kubectl -n ${namespace} get pods --field-selector=status.phase!=Running -o name 2>/dev/null || true); do
+ if [ ! -z "\$pod" ]; then
+ echo "=== 실패한 Pod: \$pod ==="
+ kubectl -n ${namespace} describe \$pod | tail -30
+ echo "=== Pod 로그: \$pod ==="
+ kubectl -n ${namespace} logs \$pod --tail=50 || echo "로그를 가져올 수 없습니다"
+ echo "=========================================="
+ fi
+ done
+
+ echo "=== Ingress 상태 확인 ==="
+ kubectl -n ${namespace} get ingress
+ kubectl -n ${namespace} describe ingress smarketing-backend || echo "Ingress를 찾을 수 없습니다"
+
+ echo "=== 서비스 Endpoint 확인 ==="
+ kubectl -n ${namespace} get endpoints
+ """
+ }
+ }
+ }
+}
diff --git a/smarketing-java/deployment/argocd.yaml b/smarketing-java/deployment/argocd.yaml
new file mode 100644
index 0000000..21a6122
--- /dev/null
+++ b/smarketing-java/deployment/argocd.yaml
@@ -0,0 +1,28 @@
+## Globally shared configuration
+global:
+ # -- Default domain used by all components
+ ## Used for ingresses, certificates, SSO, notifications, etc.
+ ## IP는 외부에서 접근할 수 있는 ks8 node의 Public IP 또는
+ ## ingress-nginx-controller 서비스의 External IP이여야 함
+ domain: argo.20.249.184.228.nip.io
+
+ # -- 특정 노드에 배포시 지정
+ #nodeSelector:
+ #agentpool: argocd
+
+server:
+ ingress:
+ enabled: true
+ https: true
+ annotations:
+ kubernetes.io/ingress.class: nginx
+ tls:
+ - secretName: argocd-tls-smarketing-secret
+ extraArgs:
+ - --insecure # ArgoCD 서버가 TLS 종료를 Ingress에 위임
+
+configs:
+ params:
+ server.insecure: true # Ingress에서 TLS를 처리하므로 ArgoCD 서버는 HTTP로 통신
+certificate:
+ enabled: false # 자체 서명 인증서 사용 비활성화 (외부 인증서 사용 시)
diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template.backup
similarity index 63%
rename from smarketing-java/deployment/deploy.yaml.template
rename to smarketing-java/deployment/deploy.yaml.template.backup
index 92e1068..1f54a76 100644
--- a/smarketing-java/deployment/deploy.yaml.template
+++ b/smarketing-java/deployment/deploy.yaml.template.backup
@@ -8,16 +8,11 @@ data:
ALLOWED_ORIGINS: ${allowed_origins}
JPA_DDL_AUTO: update
JPA_SHOW_SQL: 'true'
- # 🔧 강화된 Actuator 설정
+ # Actuator 설정
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE: '*'
MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS: always
MANAGEMENT_ENDPOINT_HEALTH_ENABLED: 'true'
MANAGEMENT_ENDPOINTS_WEB_BASE_PATH: /actuator
- MANAGEMENT_SERVER_PORT: '8080'
- # Spring Security 비활성화 (Actuator용)
- SPRING_AUTOCONFIGURE_EXCLUDE: org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
- # 또는 Management port를 main port와 동일하게
- MANAGEMENT_SERVER_PORT: ''
---
apiVersion: v1
@@ -26,10 +21,14 @@ metadata:
name: member-config
namespace: ${namespace}
data:
- POSTGRES_DB: member
- POSTGRES_HOST: member-postgresql
- POSTGRES_PORT: '5432'
SERVER_PORT: '8081'
+ POSTGRES_HOST: ${postgres_host}
+ POSTGRES_PORT: '5432'
+ POSTGRES_DB: ${postgres_db_member}
+ REDIS_HOST: ${redis_host}
+ REDIS_PORT: '6380'
+ JPA_DDL_AUTO: 'create-drop'
+ JPA_SHOW_SQL: 'true'
---
apiVersion: v1
@@ -38,10 +37,14 @@ metadata:
name: store-config
namespace: ${namespace}
data:
- POSTGRES_DB: store
- POSTGRES_HOST: store-postgresql
- POSTGRES_PORT: '5432'
SERVER_PORT: '8082'
+ POSTGRES_HOST: ${postgres_host}
+ POSTGRES_PORT: '5432'
+ POSTGRES_DB: ${postgres_db_store}
+ REDIS_HOST: ${redis_host}
+ REDIS_PORT: '6380'
+ JPA_DDL_AUTO: 'create-drop'
+ JPA_SHOW_SQL: 'true'
---
apiVersion: v1
@@ -50,10 +53,14 @@ metadata:
name: marketing-content-config
namespace: ${namespace}
data:
- POSTGRES_DB: marketing_content
- POSTGRES_HOST: marketing-content-postgresql
- POSTGRES_PORT: '5432'
SERVER_PORT: '8083'
+ POSTGRES_HOST: ${postgres_host}
+ POSTGRES_PORT: '5432'
+ POSTGRES_DB: ${postgres_db_marketing_content}
+ REDIS_HOST: ${redis_host}
+ REDIS_PORT: '6380'
+ JPA_DDL_AUTO: 'create-drop'
+ JPA_SHOW_SQL: 'true'
---
apiVersion: v1
@@ -62,10 +69,14 @@ metadata:
name: ai-recommend-config
namespace: ${namespace}
data:
- POSTGRES_DB: ai_recommend
- POSTGRES_HOST: ai-recommend-postgresql
- POSTGRES_PORT: '5432'
SERVER_PORT: '8084'
+ POSTGRES_HOST: ${postgres_host}
+ POSTGRES_PORT: '5432'
+ POSTGRES_DB: ${postgres_db_ai_recommend}
+ REDIS_HOST: ${redis_host}
+ REDIS_PORT: '6380'
+ JPA_DDL_AUTO: 'create-drop'
+ JPA_SHOW_SQL: 'true'
---
# Secrets
@@ -87,8 +98,9 @@ metadata:
stringData:
JWT_ACCESS_TOKEN_VALIDITY: '3600000'
JWT_REFRESH_TOKEN_VALIDITY: '86400000'
- POSTGRES_PASSWORD: ${postgres_password}
POSTGRES_USER: ${postgres_user}
+ POSTGRES_PASSWORD: ${postgres_password}
+ REDIS_PASSWORD: ${redis_password}
type: Opaque
---
@@ -98,8 +110,9 @@ metadata:
name: store-secret
namespace: ${namespace}
stringData:
- POSTGRES_PASSWORD: ${postgres_password}
POSTGRES_USER: ${postgres_user}
+ POSTGRES_PASSWORD: ${postgres_password}
+ REDIS_PASSWORD: ${redis_password}
type: Opaque
---
@@ -109,8 +122,9 @@ metadata:
name: marketing-content-secret
namespace: ${namespace}
stringData:
- POSTGRES_PASSWORD: ${postgres_password}
POSTGRES_USER: ${postgres_user}
+ POSTGRES_PASSWORD: ${postgres_password}
+ REDIS_PASSWORD: ${redis_password}
type: Opaque
---
@@ -120,8 +134,9 @@ metadata:
name: ai-recommend-secret
namespace: ${namespace}
stringData:
- POSTGRES_PASSWORD: ${postgres_password}
POSTGRES_USER: ${postgres_user}
+ POSTGRES_PASSWORD: ${postgres_password}
+ REDIS_PASSWORD: ${redis_password}
type: Opaque
---
@@ -167,39 +182,6 @@ spec:
name: common-secret
- secretRef:
name: member-secret
- startupProbe:
- exec:
- command:
- - /bin/sh
- - -c
- - "nc -z member-postgresql 5432"
- initialDelaySeconds: 30
- periodSeconds: 10
- timeoutSeconds: 5
- failureThreshold: 10
- # 🔧 개선된 Health Check 설정
- livenessProbe:
- httpGet:
- path: /actuator/health
- port: 8081
- httpHeaders:
- - name: Accept
- value: application/json
- initialDelaySeconds: 120 # 2분으로 증가
- periodSeconds: 30
- timeoutSeconds: 10
- failureThreshold: 3
- readinessProbe:
- httpGet:
- path: /actuator/health/readiness
- port: 8081
- httpHeaders:
- - name: Accept
- value: application/json
- initialDelaySeconds: 60 # 1분으로 증가
- periodSeconds: 10
- timeoutSeconds: 5
- failureThreshold: 3
---
apiVersion: apps/v1
@@ -243,38 +225,7 @@ spec:
name: common-secret
- secretRef:
name: store-secret
- startupProbe:
- exec:
- command:
- - /bin/sh
- - -c
- - "nc -z store-postgresql 5432"
- initialDelaySeconds: 30
- periodSeconds: 10
- timeoutSeconds: 5
- failureThreshold: 10
- livenessProbe:
- httpGet:
- path: /actuator/health
- port: 8082
- httpHeaders:
- - name: Accept
- value: application/json
- initialDelaySeconds: 120
- periodSeconds: 30
- timeoutSeconds: 10
- failureThreshold: 3
- readinessProbe:
- httpGet:
- path: /actuator/health/readiness
- port: 8082
- httpHeaders:
- - name: Accept
- value: application/json
- initialDelaySeconds: 60
- periodSeconds: 10
- timeoutSeconds: 5
- failureThreshold: 3
+
---
apiVersion: apps/v1
@@ -318,38 +269,7 @@ spec:
name: common-secret
- secretRef:
name: marketing-content-secret
- startupProbe:
- exec:
- command:
- - /bin/sh
- - -c
- - "nc -z marketing-content-postgresql 5432"
- initialDelaySeconds: 30
- periodSeconds: 10
- timeoutSeconds: 5
- failureThreshold: 10
- livenessProbe:
- httpGet:
- path: /actuator/health
- port: 8083
- httpHeaders:
- - name: Accept
- value: application/json
- initialDelaySeconds: 120
- periodSeconds: 30
- timeoutSeconds: 10
- failureThreshold: 3
- readinessProbe:
- httpGet:
- path: /actuator/health/readiness
- port: 8083
- httpHeaders:
- - name: Accept
- value: application/json
- initialDelaySeconds: 60
- periodSeconds: 10
- timeoutSeconds: 5
- failureThreshold: 3
+
---
apiVersion: apps/v1
@@ -393,38 +313,7 @@ spec:
name: common-secret
- secretRef:
name: ai-recommend-secret
- startupProbe:
- exec:
- command:
- - /bin/sh
- - -c
- - "nc -z ai-recommend-postgresql 5432"
- initialDelaySeconds: 30
- periodSeconds: 10
- timeoutSeconds: 5
- failureThreshold: 10
- livenessProbe:
- httpGet:
- path: /actuator/health
- port: 8084
- httpHeaders:
- - name: Accept
- value: application/json
- initialDelaySeconds: 120
- periodSeconds: 30
- timeoutSeconds: 10
- failureThreshold: 3
- readinessProbe:
- httpGet:
- path: /actuator/health/readiness
- port: 8084
- httpHeaders:
- - name: Accept
- value: application/json
- initialDelaySeconds: 60
- periodSeconds: 10
- timeoutSeconds: 5
- failureThreshold: 3
+
---
# Services
@@ -487,14 +376,15 @@ spec:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
- name: smarketing-backend
+ name: smarketing-ingress
namespace: ${namespace}
annotations:
kubernetes.io/ingress.class: nginx
spec:
ingressClassName: nginx
rules:
- - http:
+ - host: smarketing.20.249.184.228.nip.io
+ http:
paths:
- path: /api/auth
pathType: Prefix
@@ -524,3 +414,4 @@ spec:
name: ai-recommend
port:
number: 80
+
diff --git a/smarketing-java/deployment/deploy_env_vars b/smarketing-java/deployment/deploy_env_vars
index 7a5d327..5e90919 100644
--- a/smarketing-java/deployment/deploy_env_vars
+++ b/smarketing-java/deployment/deploy_env_vars
@@ -8,8 +8,9 @@ registry=acrdigitalgarage02.azurecr.io
image_org=smarketing
# Application Settings
+ingress_host=smarketing.20.249.184.228.nip.io
replicas=1
-allowed_origins=http://20.249.171.38
+allowed_origins=http://20.249.154.194
# Security Settings
jwt_secret_key=8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ
diff --git a/smarketing-java/deployment/member/member-deployment.yaml b/smarketing-java/deployment/member/member-deployment.yaml
new file mode 100644
index 0000000..8a933c1
--- /dev/null
+++ b/smarketing-java/deployment/member/member-deployment.yaml
@@ -0,0 +1,30 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: member
+ namespace: smarketing
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: member
+ template:
+ metadata:
+ labels:
+ app: member
+ spec:
+ containers:
+ - name: member
+ image: acrdigitalgarage02.azurecr.io/member:latest
+ ports:
+ - containerPort: 8080
+ env:
+ - name: SPRING_PROFILES_ACTIVE
+ value: "k8s"
+ resources:
+ requests:
+ memory: "256Mi"
+ cpu: "256m"
+ limits:
+ memory: "1024Mi"
+ cpu: "1024m"
diff --git a/smarketing-java/deployment/member/member-service.yaml b/smarketing-java/deployment/member/member-service.yaml
new file mode 100644
index 0000000..8d8e892
--- /dev/null
+++ b/smarketing-java/deployment/member/member-service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: member
+ namespace: smarketing
+spec:
+ selector:
+ app: member
+ ports:
+ - port: 80
+ targetPort: 8080
+ type: ClusterIP
diff --git a/smarketing-java/marketing-content/build.gradle b/smarketing-java/marketing-content/build.gradle
index 715bc47..188d7bd 100644
--- a/smarketing-java/marketing-content/build.gradle
+++ b/smarketing-java/marketing-content/build.gradle
@@ -1,7 +1,4 @@
dependencies {
implementation project(':common')
runtimeOnly 'org.postgresql:postgresql'
-
- // WebClient를 위한 Spring WebFlux 의존성
- implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java
index a676c40..94c894d 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java
@@ -6,30 +6,41 @@ import com.won.smarketing.content.domain.model.ContentStatus;
import com.won.smarketing.content.domain.model.ContentType;
import com.won.smarketing.content.domain.model.CreationConditions;
import com.won.smarketing.content.domain.model.Platform;
+import com.won.smarketing.content.domain.model.store.StoreWithMenuData;
import com.won.smarketing.content.domain.repository.ContentRepository;
import com.won.smarketing.content.domain.service.AiPosterGenerator;
+import com.won.smarketing.content.domain.service.BlobStorageService;
+import com.won.smarketing.content.domain.service.StoreDataProvider;
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse;
import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
-import java.time.LocalDateTime;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.List;
/**
* 포스터 콘텐츠 서비스 구현체
* 홍보 포스터 생성 및 저장 기능 구현
*/
@Service
+@Slf4j
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PosterContentService implements PosterContentUseCase {
+ @Value("${azure.storage.container.poster-images:poster-images}")
+ private String posterImageContainer;
+
private final ContentRepository contentRepository;
private final AiPosterGenerator aiPosterGenerator;
+ private final BlobStorageService blobStorageService;
+ private final StoreDataProvider storeDataProvider;
/**
* 포스터 콘텐츠 생성
@@ -39,26 +50,24 @@ public class PosterContentService implements PosterContentUseCase {
*/
@Override
@Transactional
- public PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request) {
+ public PosterContentCreateResponse generatePosterContent(List images, PosterContentCreateRequest request) {
- String generatedPoster = aiPosterGenerator.generatePoster(request);
+ // 1. 이미지 blob storage에 저장하고 request 저장
+ List imageUrls = blobStorageService.uploadImage(images, posterImageContainer);
+ request.setImages(imageUrls);
+
+ // 매장 정보 호출
+ String userId = getCurrentUserId();
+ StoreWithMenuData storeWithMenuData = storeDataProvider.getStoreWithMenuData(userId);
- // 생성 조건 정보 구성
- CreationConditions conditions = CreationConditions.builder()
- .category(request.getCategory())
- .requirement(request.getRequirement())
- .eventName(request.getEventName())
- .startDate(request.getStartDate())
- .endDate(request.getEndDate())
- .photoStyle(request.getPhotoStyle())
- .build();
+ // 2. AI 요청
+ String generatedPoster = aiPosterGenerator.generatePoster(request, storeWithMenuData);
return PosterContentCreateResponse.builder()
.contentId(null) // 임시 생성이므로 ID 없음
.contentType(ContentType.POSTER.name())
.title(request.getTitle())
- .posterImage(generatedPoster)
- .posterSizes(new HashMap<>()) // 빈 맵 반환 (사이즈 변환 안함)
+ .content(generatedPoster)
.status(ContentStatus.DRAFT.name())
.build();
}
@@ -68,7 +77,6 @@ public class PosterContentService implements PosterContentUseCase {
*
* @param request 포스터 콘텐츠 저장 요청
*/
- @Override
@Transactional
public void savePosterContent(PosterContentSaveRequest request) {
// 생성 조건 구성
@@ -96,4 +104,11 @@ public class PosterContentService implements PosterContentUseCase {
// 저장
contentRepository.save(content);
}
+
+ /**
+ * 현재 로그인된 사용자 ID 조회
+ */
+ private String getCurrentUserId() {
+ return SecurityContextHolder.getContext().getAuthentication().getName();
+ }
}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java
index 753063b..2aa51d0 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java
@@ -14,6 +14,7 @@ import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest;
import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse;
import com.won.smarketing.content.presentation.dto.SnsContentSaveRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@@ -34,6 +35,9 @@ public class SnsContentService implements SnsContentUseCase {
private final AiContentGenerator aiContentGenerator;
private final BlobStorageService blobStorageService;
+ @Value("${azure.storage.container.poster-images:content-images}")
+ private String contentImageContainer;
+
/**
* SNS 콘텐츠 생성
*
@@ -44,8 +48,10 @@ public class SnsContentService implements SnsContentUseCase {
@Transactional
public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List files) {
//파일들 주소 가져옴
- List urls = blobStorageService.uploadImage(files);
- request.setImages(urls);
+ if(files != null) {
+ List urls = blobStorageService.uploadImage(files, contentImageContainer);
+ request.setImages(urls);
+ }
// AI를 사용하여 SNS 콘텐츠 생성
String content = aiContentGenerator.generateSnsContent(request);
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
index 6bf2960..77a7496 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
@@ -1,9 +1,13 @@
// marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
package com.won.smarketing.content.application.usecase;
+import com.won.smarketing.content.domain.model.Content;
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse;
import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
/**
* 포스터 콘텐츠 관련 UseCase 인터페이스
@@ -16,7 +20,7 @@ public interface PosterContentUseCase {
* @param request 포스터 콘텐츠 생성 요청
* @return 포스터 콘텐츠 생성 응답
*/
- PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request);
+ PosterContentCreateResponse generatePosterContent(List images, PosterContentCreateRequest request);
/**
* 포스터 콘텐츠 저장
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/SecurityConfig.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/SecurityConfig.java
new file mode 100644
index 0000000..ca74fc1
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/SecurityConfig.java
@@ -0,0 +1,88 @@
+package com.won.smarketing.content.config;
+
+import com.won.smarketing.common.security.JwtAuthenticationFilter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+import java.util.Arrays;
+
+/**
+ * Spring Security 설정 클래스
+ * JWT 기반 인증 및 CORS 설정
+ */
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig
+{
+
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
+ @Value("${allowed-origins}")
+ private String allowedOrigins;
+ /**
+ * Spring Security 필터 체인 설정
+ *
+ * @param http HttpSecurity 객체
+ * @return SecurityFilterChain
+ * @throws Exception 예외
+ */
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/api/auth/**", "/api/member/register", "/api/member/check-duplicate/**",
+ "/api/member/validate-password", "/swagger-ui/**", "/v3/api-docs/**",
+ "/swagger-resources/**", "/webjars/**", "/actuator/**", "/health/**", "/error"
+ ).permitAll()
+ .anyRequest().authenticated()
+ )
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
+
+ return http.build();
+ }
+
+ /**
+ * 패스워드 인코더 빈 등록
+ *
+ * @return BCryptPasswordEncoder
+ */
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ /**
+ * CORS 설정
+ *
+ * @return CorsConfigurationSource
+ */
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+ configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));
+ configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
+ configuration.setAllowedHeaders(Arrays.asList("*"));
+ configuration.setAllowCredentials(true);
+
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", configuration);
+ return source;
+ }
+}
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java
index 72e1a78..8fb41fc 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java
@@ -1,4 +1,3 @@
-// marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java
package com.won.smarketing.content.config;
import org.springframework.context.annotation.Bean;
@@ -20,8 +19,8 @@ public class WebClientConfig {
@Bean
public WebClient webClient() {
HttpClient httpClient = HttpClient.create()
- .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 50000)
- .responseTimeout(Duration.ofMillis(300000));
+ .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15000) // 연결 타임아웃: 15초
+ .responseTimeout(Duration.ofMinutes(5)); // 응답 타임아웃: 5분
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java
index 32b4231..1a453ef 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java
@@ -27,42 +27,37 @@ import java.util.List;
@Builder
public class Content {
- // ==================== 기본키 및 식별자 ====================
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "content_id")
private Long id;
- // ==================== 콘텐츠 분류 ====================
private ContentType contentType;
+
private Platform platform;
- // ==================== 콘텐츠 내용 ====================
private String title;
+
private String content;
- // ==================== 멀티미디어 및 메타데이터 ====================
@Builder.Default
private List hashtags = new ArrayList<>();
@Builder.Default
private List images = new ArrayList<>();
- // ==================== 상태 관리 ====================
private ContentStatus status;
- // ==================== 생성 조건 ====================
private CreationConditions creationConditions;
- // ==================== 매장 정보 ====================
private Long storeId;
- // ==================== 프로모션 기간 ====================
private LocalDateTime promotionStartDate;
+
private LocalDateTime promotionEndDate;
- // ==================== 메타데이터 ====================
private LocalDateTime createdAt;
+
private LocalDateTime updatedAt;
public Content(ContentId of, ContentType contentType, Platform platform, String title, String content, List strings, List strings1, ContentStatus contentStatus, CreationConditions conditions, Long storeId, LocalDateTime createdAt, LocalDateTime updatedAt) {
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java
index a284c2c..b90959e 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java
@@ -24,8 +24,6 @@ public class CreationConditions {
private String id;
private String category;
private String requirement;
-// private String toneAndManner;
-// private String emotionIntensity;
private String storeName;
private String storeType;
private String target;
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/MenuData.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/MenuData.java
new file mode 100644
index 0000000..d6597ad
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/MenuData.java
@@ -0,0 +1,21 @@
+package com.won.smarketing.content.domain.model.store;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * 메뉴 데이터 값 객체
+ */
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MenuData {
+ private Long menuId;
+ private String menuName;
+ private String category;
+ private Integer price;
+ private String description;
+}
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreData.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreData.java
new file mode 100644
index 0000000..8ae13f4
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreData.java
@@ -0,0 +1,22 @@
+package com.won.smarketing.content.domain.model.store;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * 매장 데이터 값 객체
+ */
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class StoreData {
+ private Long storeId;
+ private String storeName;
+ private String businessType;
+ private String location;
+ private String description;
+ private Integer seatCount;
+}
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreWithMenuData.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreWithMenuData.java
new file mode 100644
index 0000000..962969b
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreWithMenuData.java
@@ -0,0 +1,13 @@
+package com.won.smarketing.content.domain.model.store;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Builder
+public class StoreWithMenuData {
+ private StoreData storeData;
+ private List menuDataList;
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiPosterGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiPosterGenerator.java
index a689d30..c550b6c 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiPosterGenerator.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiPosterGenerator.java
@@ -1,5 +1,6 @@
package com.won.smarketing.content.domain.service;
+import com.won.smarketing.content.domain.model.store.StoreWithMenuData;
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
import java.util.Map;
@@ -16,5 +17,5 @@ public interface AiPosterGenerator {
* @param request 포스터 생성 요청
* @return 생성된 포스터 이미지 URL
*/
- String generatePoster(PosterContentCreateRequest request);
+ String generatePoster(PosterContentCreateRequest request, StoreWithMenuData storeWithMenuData);
}
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java
index 76ea929..92a6daf 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java
@@ -17,7 +17,7 @@ public interface BlobStorageService {
* @param file 업로드할 파일
* @return 업로드된 파일의 URL
*/
- List uploadImage(List file);
+ List uploadImage(List file, String containerName);
/**
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java
index 3bb63f1..c9b9d40 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java
@@ -34,12 +34,6 @@ public class BlobStorageServiceImpl implements BlobStorageService {
private final BlobServiceClient blobServiceClient;
- @Value("${azure.storage.container.poster-images:poster-images}")
- private String posterImageContainer;
-
- @Value("${azure.storage.container.content-images:content-images}")
- private String contentImageContainer;
-
@Value("${azure.storage.max-file-size:10485760}") // 10MB
private long maxFileSize;
@@ -60,7 +54,7 @@ public class BlobStorageServiceImpl implements BlobStorageService {
* @return 업로드된 파일의 URL
*/
@Override
- public List uploadImage(List files) {
+ public List uploadImage(List files, String containerName) {
// 파일 유효성 검증
validateImageFile(files);
List urls = new ArrayList<>();
@@ -70,10 +64,10 @@ public class BlobStorageServiceImpl implements BlobStorageService {
for(MultipartFile file : files) {
String fileName = generateMenuImageFileName(file.getOriginalFilename());
- ensureContainerExists(posterImageContainer);
+ ensureContainerExists(containerName);
// Blob 클라이언트 생성
- BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(posterImageContainer);
+ BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName);
BlobClient blobClient = containerClient.getBlobClient(fileName);
// 파일 업로드 (간단한 방식)
@@ -158,12 +152,12 @@ public class BlobStorageServiceImpl implements BlobStorageService {
* @param files 검증할 파일
*/
private void validateImageFile(List files) {
- for (MultipartFile file : files) {
- // 파일 존재 여부 확인
- if (file == null || file.isEmpty()) {
- throw new BusinessException(ErrorCode.FILE_NOT_FOUND);
- }
+ // 파일 존재 여부 확인
+ if (files == null || files.isEmpty()) {
+ throw new BusinessException(ErrorCode.FILE_NOT_FOUND);
+ }
+ for (MultipartFile file : files) {
// 파일 크기 확인
if (file.getSize() > maxFileSize) {
throw new BusinessException(ErrorCode.FILE_SIZE_EXCEEDED);
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/StoreDataProvider.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/StoreDataProvider.java
new file mode 100644
index 0000000..c28d33a
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/StoreDataProvider.java
@@ -0,0 +1,11 @@
+package com.won.smarketing.content.domain.service;
+
+import com.won.smarketing.content.domain.model.store.StoreWithMenuData;
+
+/**
+ * 매장 데이터 제공 도메인 서비스 인터페이스
+ */
+public interface StoreDataProvider {
+
+ StoreWithMenuData getStoreWithMenuData(String userId);
+}
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java
index 1fc2020..9227d85 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java
@@ -1,5 +1,8 @@
package com.won.smarketing.content.infrastructure.external;
+import com.won.smarketing.content.domain.model.store.MenuData;
+import com.won.smarketing.content.domain.model.store.StoreData;
+import com.won.smarketing.content.domain.model.store.StoreWithMenuData;
import com.won.smarketing.content.domain.service.AiPosterGenerator; // 도메인 인터페이스 import
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
import lombok.RequiredArgsConstructor;
@@ -11,7 +14,9 @@ import org.springframework.web.reactive.function.client.WebClient;
import java.time.Duration;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
/**
* Claude AI를 활용한 포스터 생성 구현체
@@ -34,12 +39,12 @@ public class PythonAiPosterGenerator implements AiPosterGenerator {
* @return 생성된 포스터 이미지 URL
*/
@Override
- public String generatePoster(PosterContentCreateRequest request) {
+ public String generatePoster(PosterContentCreateRequest request, StoreWithMenuData storeWithMenuData) {
try {
log.info("Python AI 포스터 서비스 호출: {}/api/ai/poster", aiServiceBaseUrl);
// 요청 데이터 구성
- Map requestBody = buildRequestBody(request);
+ Map requestBody = buildRequestBody(request, storeWithMenuData);
log.debug("포스터 생성 요청 데이터: {}", requestBody);
@@ -51,7 +56,7 @@ public class PythonAiPosterGenerator implements AiPosterGenerator {
.bodyValue(requestBody)
.retrieve()
.bodyToMono(Map.class)
- .timeout(Duration.ofSeconds(60)) // 포스터 생성은 시간이 오래 걸릴 수 있음
+ .timeout(Duration.ofSeconds(90))
.block();
// 응답에서 content(이미지 URL) 추출
@@ -75,9 +80,32 @@ public class PythonAiPosterGenerator implements AiPosterGenerator {
* Python 서비스의 PosterContentGetRequest 모델에 맞춤
* 카테고리,
*/
- private Map buildRequestBody(PosterContentCreateRequest request) {
+ private Map buildRequestBody(PosterContentCreateRequest request, StoreWithMenuData storeWithMenuData) {
Map requestBody = new HashMap<>();
+// TODO : 매장 정보 호출 후 request
+
+// StoreData storeData = storeWithMenuData.getStoreData();
+// List menuDataList = storeWithMenuData.getMenuDataList();
+//
+// List