From 1faf5ced89c5d326ba646ff5e8279b0da5f5cdc9 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 16:38:13 +0900 Subject: [PATCH 01/61] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index ce96650..685d9b3 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -181,6 +181,21 @@ podTemplate( export postgres_user=${props.postgres_user} export postgres_password=${props.postgres_password} export replicas=${props.replicas} + + # PostgreSQL 환경변수 추가 + export postgres_host=${props.POSTGRES_HOST} + export postgres_port=${props.POSTGRES_PORT:-5432} + export postgres_db_member=member + export postgres_db_store=store + export postgres_db_marketing_content=marketing_content + export postgres_db_ai_recommend=AiRecommendationDB + + # Redis 환경변수 추가 + export redis_host=${props.REDIS_HOST} + export redis_port=${props.REDIS_PORT} + export redis_password=${props.REDIS_PASSWORD} + + # 리소스 요구사항 조정 (작게) export resources_requests_cpu=100m export resources_requests_memory=128Mi From 04b46ebedcd8db0689680f08493b81c45ca4d0c6 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 16:40:17 +0900 Subject: [PATCH 02/61] Update deploy.yaml.template --- smarketing-java/deployment/deploy.yaml.template | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index 92e1068..a2e5c4f 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -494,7 +494,8 @@ metadata: spec: ingressClassName: nginx rules: - - http: + - host: smarketing + http: paths: - path: /api/auth pathType: Prefix From 26f82f79b30ebea9d6c6210245a7f1068422b5c5 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 16:41:30 +0900 Subject: [PATCH 03/61] Update deploy.yaml.template --- smarketing-java/deployment/deploy.yaml.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index a2e5c4f..377be44 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -494,7 +494,7 @@ metadata: spec: ingressClassName: nginx rules: - - host: smarketing + - host: smarketing.20.249.184.228.nip.io http: paths: - path: /api/auth From fd5ede7a4da4602724431ec810cb38afa3459f35 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 16:47:16 +0900 Subject: [PATCH 04/61] Update deploy.yaml.template --- .../deployment/deploy.yaml.template | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index 377be44..0a45408 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -26,10 +26,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: ${postgres_port} + POSTGRES_DB: ${postgres_db_ai_recommend} + REDIS_HOST: ${redis_host} + REDIS_PORT: ${redis_port} + JPA_DDL_AUTO: 'create-drop' + JPA_SHOW_SQL: 'true' --- apiVersion: v1 @@ -38,10 +42,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: ${postgres_port} + POSTGRES_DB: ${postgres_db_ai_recommend} + REDIS_HOST: ${redis_host} + REDIS_PORT: ${redis_port} + JPA_DDL_AUTO: 'create-drop' + JPA_SHOW_SQL: 'true' --- apiVersion: v1 @@ -50,10 +58,15 @@ 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: ${postgres_port} + POSTGRES_DB: ${postgres_db_ai_recommend} + REDIS_HOST: ${redis_host} + REDIS_PORT: ${redis_port} + JPA_DDL_AUTO: 'create-drop' + JPA_SHOW_SQL: 'true' + --- apiVersion: v1 @@ -62,10 +75,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: ${postgres_port} + POSTGRES_DB: ${postgres_db_ai_recommend} + REDIS_HOST: ${redis_host} + REDIS_PORT: ${redis_port} + JPA_DDL_AUTO: 'create-drop' + JPA_SHOW_SQL: 'true' --- # Secrets @@ -87,8 +104,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 +116,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 +128,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 +140,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 --- From a87032e7a6b6861c5439421064d4f6a8532aff43 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 16:52:51 +0900 Subject: [PATCH 05/61] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 82 ++++++++++++++------------ 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index 685d9b3..675d3fa 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -173,48 +173,52 @@ podTemplate( 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 환경변수 추가 - export postgres_host=${props.POSTGRES_HOST} - export postgres_port=${props.POSTGRES_PORT:-5432} - export postgres_db_member=member - export postgres_db_store=store - export postgres_db_marketing_content=marketing_content - export postgres_db_ai_recommend=AiRecommendationDB - - # Redis 환경변수 추가 - export redis_host=${props.REDIS_HOST} - export redis_port=${props.REDIS_PORT} - export redis_password=${props.REDIS_PASSWORD} - + script { + def postgresPort = props.POSTGRES_PORT ?: '5432' + def redisPort = props.REDIS_PORT ?: '6379' - # 리소스 요구사항 조정 (작게) - export resources_requests_cpu=100m - export resources_requests_memory=128Mi - export resources_limits_cpu=500m - export resources_limits_memory=512Mi + 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 환경변수 추가 + export postgres_host=${props.POSTGRES_HOST} + export postgres_port=${postgresPort} + export postgres_db_member=member + export postgres_db_store=store + export postgres_db_marketing_content=marketing_content + export postgres_db_ai_recommend=AiRecommendationDB + + # Redis 환경변수 추가 + export redis_host=${props.REDIS_HOST} + export redis_port=${redisPort} + 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} + # 이미지 경로 환경변수 설정 + 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 "=== Manifest 생성 ===" - envsubst < smarketing-java/deployment/${manifest}.template > smarketing-java/deployment/${manifest} - - echo "=== Generated Manifest File ===" - cat smarketing-java/deployment/${manifest} - echo "===============================" - """ + 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') { From 135264dd952bc1542a1eb74cb47985de5a9b1fcf Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 17:03:00 +0900 Subject: [PATCH 06/61] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 336 ++++++------------------- 1 file changed, 82 insertions(+), 254 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index 675d3fa..14bd4c3 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -1,260 +1,88 @@ -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 +stage('Generate & Apply Manifest') { + container('envsubst') { + script { + def postgresPort = "'" + (props.POSTGRES_PORT ?: '5432') + "'" + def redisPort = "'" + (props.REDIS_PORT ?: '6379') + "'" - // smarketing-java 하위에 있는 설정 파일 읽기 - props = readProperties file: "smarketing-java/deployment/deploy_env_vars" - namespace = "${props.namespace}" + 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 환경변수 추가 (문자열로) + export postgres_host=${props.POSTGRES_HOST} + export postgres_port=${postgresPort} + export postgres_db_member=member + export postgres_db_store=store + export postgres_db_marketing_content=marketing_content + export postgres_db_ai_recommend=AiRecommendationDB + + # Redis 환경변수 추가 (문자열로) + export redis_host=${props.REDIS_HOST} + export redis_port=${redisPort} + 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} - echo "=== Build Information ===" - echo "Services: ${services}" - echo "Namespace: ${namespace}" - echo "Image Tag: ${imageTag}" - } + # 이미지 경로 환경변수 설정 + 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} - 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') { - script { - def postgresPort = props.POSTGRES_PORT ?: '5432' - def redisPort = props.REDIS_PORT ?: '6379' - - 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 환경변수 추가 - export postgres_host=${props.POSTGRES_HOST} - export postgres_port=${postgresPort} - export postgres_db_member=member - export postgres_db_store=store - export postgres_db_marketing_content=marketing_content - export postgres_db_ai_recommend=AiRecommendationDB - - # Redis 환경변수 추가 - export redis_host=${props.REDIS_HOST} - export redis_port=${redisPort} - 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 "=== 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 "=== PostgreSQL 서비스 확인 ===" - kubectl get svc -n ${namespace} | grep postgresql || echo "PostgreSQL 서비스가 없습니다. 먼저 설치해주세요." - - echo "=== Manifest 적용 ===" - kubectl apply -f smarketing-java/deployment/${manifest} - - 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 - """ - } + 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 "=== 기존 ConfigMap 삭제 (타입 충돌 해결) ===" + kubectl delete configmap member-config store-config marketing-content-config ai-recommend-config -n ${namespace} --ignore-not-found=true + + 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 "=== 배포 상태 확인 (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 + """ + } } From e70821edfe37e2dfc1d178d8c457c66f7892b549 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 17:04:38 +0900 Subject: [PATCH 07/61] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 350 +++++++++++++++++++------ 1 file changed, 268 insertions(+), 82 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index 14bd4c3..b2934e3 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -1,88 +1,274 @@ -stage('Generate & Apply Manifest') { - container('envsubst') { - script { - def postgresPort = "'" + (props.POSTGRES_PORT ?: '5432') + "'" - def redisPort = "'" + (props.REDIS_PORT ?: '6379') + "'" +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 - 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 환경변수 추가 (문자열로) - export postgres_host=${props.POSTGRES_HOST} - export postgres_port=${postgresPort} - export postgres_db_member=member - export postgres_db_store=store - export postgres_db_marketing_content=marketing_content - export postgres_db_ai_recommend=AiRecommendationDB - - # Redis 환경변수 추가 (문자열로) - export redis_host=${props.REDIS_HOST} - export redis_port=${redisPort} - 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} + // smarketing-java 하위에 있는 설정 파일 읽기 + props = readProperties file: "smarketing-java/deployment/deploy_env_vars" + namespace = "${props.namespace}" - # 이미지 경로 환경변수 설정 - 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 "=== Build Information ===" + echo "Services: ${services}" + echo "Namespace: ${namespace}" + echo "Image Tag: ${imageTag}" + } - echo "=== Manifest 생성 ===" - envsubst < smarketing-java/deployment/${manifest}.template > smarketing-java/deployment/${manifest} - - echo "=== Generated Manifest File ===" - cat smarketing-java/deployment/${manifest} - echo "===============================" - """ + 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 환경변수 추가 (문자열로) + export postgres_host='${props.POSTGRES_HOST}' + export postgres_port='5432' + export postgres_db_member='member' + export postgres_db_store='store' + export postgres_db_marketing_content='marketing_content' + 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 "postgres_host: \$postgres_host" + echo "postgres_port: \$postgres_port" + echo "redis_host: \$redis_host" + echo "redis_port: \$redis_port" + echo "postgres_user: \$postgres_user" + + 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 "=== 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 ai-recommend-config -o yaml + + echo "=== 각 서비스 배포 대기 (90초 timeout) ===" + timeout 90 kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=90s || echo "member deployment 대기 타임아웃" + timeout 90 kubectl -n ${namespace} wait --for=condition=available deployment/store --timeout=90s || echo "store deployment 대기 타임아웃" + timeout 90 kubectl -n ${namespace} wait --for=condition=available deployment/marketing-content --timeout=90s || echo "marketing-content deployment 대기 타임아웃" + timeout 90 kubectl -n ${namespace} wait --for=condition=available deployment/ai-recommend --timeout=90s || echo "ai-recommend deployment 대기 타임아웃" + + echo "=== 최종 상태 ===" + kubectl -n ${namespace} get all + + echo "=== Pod 로그 확인 (ai-recommend) ===" + kubectl -n ${namespace} logs deployment/ai-recommend --tail=50 || echo "ai-recommend 로그를 가져올 수 없습니다" + + 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=20 || echo "로그를 가져올 수 없습니다" + fi + done + """ + } } } - - container('azure-cli') { - sh """ - echo "=== 기존 ConfigMap 삭제 (타입 충돌 해결) ===" - kubectl delete configmap member-config store-config marketing-content-config ai-recommend-config -n ${namespace} --ignore-not-found=true - - 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 "=== 배포 상태 확인 (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 - """ - } } From 37a69d88340766ec5ef896f5252633ff9fb57371 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 17:07:16 +0900 Subject: [PATCH 08/61] Update deploy.yaml.template --- .../deployment/deploy.yaml.template | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index 0a45408..7920afb 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -19,6 +19,23 @@ data: # 또는 Management port를 main port와 동일하게 MANAGEMENT_SERVER_PORT: '' +--- +# ConfigMap +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: ${namespace} +data: + ALLOWED_ORIGINS: ${allowed_origins} + JPA_DDL_AUTO: update + JPA_SHOW_SQL: 'true' + # Actuator 설정 + MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE: '*' + MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS: always + MANAGEMENT_ENDPOINT_HEALTH_ENABLED: 'true' + MANAGEMENT_ENDPOINTS_WEB_BASE_PATH: /actuator + --- apiVersion: v1 kind: ConfigMap @@ -29,7 +46,7 @@ data: SERVER_PORT: '8081' POSTGRES_HOST: ${postgres_host} POSTGRES_PORT: ${postgres_port} - POSTGRES_DB: ${postgres_db_ai_recommend} + POSTGRES_DB: ${postgres_db_member} REDIS_HOST: ${redis_host} REDIS_PORT: ${redis_port} JPA_DDL_AUTO: 'create-drop' @@ -45,7 +62,7 @@ data: SERVER_PORT: '8082' POSTGRES_HOST: ${postgres_host} POSTGRES_PORT: ${postgres_port} - POSTGRES_DB: ${postgres_db_ai_recommend} + POSTGRES_DB: ${postgres_db_store} REDIS_HOST: ${redis_host} REDIS_PORT: ${redis_port} JPA_DDL_AUTO: 'create-drop' @@ -61,13 +78,12 @@ data: SERVER_PORT: '8083' POSTGRES_HOST: ${postgres_host} POSTGRES_PORT: ${postgres_port} - POSTGRES_DB: ${postgres_db_ai_recommend} + POSTGRES_DB: ${postgres_db_marketing_content} REDIS_HOST: ${redis_host} REDIS_PORT: ${redis_port} JPA_DDL_AUTO: 'create-drop' JPA_SHOW_SQL: 'true' - --- apiVersion: v1 kind: ConfigMap @@ -84,6 +100,7 @@ data: JPA_DDL_AUTO: 'create-drop' JPA_SHOW_SQL: 'true' + --- # Secrets apiVersion: v1 From c2244293ff4cf5a418fbdbaf690ef089c6972bbd Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 17:15:01 +0900 Subject: [PATCH 09/61] Update deploy.yaml.template --- .../deployment/deploy.yaml.template | 104 ++++-------------- 1 file changed, 22 insertions(+), 82 deletions(-) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index 7920afb..0416d61 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -1,28 +1,6 @@ # ConfigMap apiVersion: v1 kind: ConfigMap -metadata: - name: common-config - namespace: ${namespace} -data: - ALLOWED_ORIGINS: ${allowed_origins} - JPA_DDL_AUTO: update - JPA_SHOW_SQL: 'true' - # 🔧 강화된 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: '' - ---- -# ConfigMap -apiVersion: v1 -kind: ConfigMap metadata: name: common-config namespace: ${namespace} @@ -45,10 +23,10 @@ metadata: data: SERVER_PORT: '8081' POSTGRES_HOST: ${postgres_host} - POSTGRES_PORT: ${postgres_port} + POSTGRES_PORT: '5432' POSTGRES_DB: ${postgres_db_member} REDIS_HOST: ${redis_host} - REDIS_PORT: ${redis_port} + REDIS_PORT: '6380' JPA_DDL_AUTO: 'create-drop' JPA_SHOW_SQL: 'true' @@ -61,10 +39,10 @@ metadata: data: SERVER_PORT: '8082' POSTGRES_HOST: ${postgres_host} - POSTGRES_PORT: ${postgres_port} + POSTGRES_PORT: '5432' POSTGRES_DB: ${postgres_db_store} REDIS_HOST: ${redis_host} - REDIS_PORT: ${redis_port} + REDIS_PORT: '6380' JPA_DDL_AUTO: 'create-drop' JPA_SHOW_SQL: 'true' @@ -77,10 +55,10 @@ metadata: data: SERVER_PORT: '8083' POSTGRES_HOST: ${postgres_host} - POSTGRES_PORT: ${postgres_port} + POSTGRES_PORT: '5432' POSTGRES_DB: ${postgres_db_marketing_content} REDIS_HOST: ${redis_host} - REDIS_PORT: ${redis_port} + REDIS_PORT: '6380' JPA_DDL_AUTO: 'create-drop' JPA_SHOW_SQL: 'true' @@ -93,14 +71,13 @@ metadata: data: SERVER_PORT: '8084' POSTGRES_HOST: ${postgres_host} - POSTGRES_PORT: ${postgres_port} + POSTGRES_PORT: '5432' POSTGRES_DB: ${postgres_db_ai_recommend} REDIS_HOST: ${redis_host} - REDIS_PORT: ${redis_port} + REDIS_PORT: '6380' JPA_DDL_AUTO: 'create-drop' JPA_SHOW_SQL: 'true' - --- # Secrets apiVersion: v1 @@ -206,35 +183,25 @@ spec: - secretRef: name: member-secret startupProbe: - exec: - command: - - /bin/sh - - -c - - "nc -z member-postgresql 5432" + tcpSocket: + port: 8081 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분으로 증가 + initialDelaySeconds: 120 periodSeconds: 30 timeoutSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: - path: /actuator/health/readiness + path: /actuator/health port: 8081 - httpHeaders: - - name: Accept - value: application/json - initialDelaySeconds: 60 # 1분으로 증가 + initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 @@ -282,11 +249,8 @@ spec: - secretRef: name: store-secret startupProbe: - exec: - command: - - /bin/sh - - -c - - "nc -z store-postgresql 5432" + tcpSocket: + port: 8082 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 @@ -295,20 +259,14 @@ spec: 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 + path: /actuator/health port: 8082 - httpHeaders: - - name: Accept - value: application/json initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 5 @@ -357,11 +315,8 @@ spec: - secretRef: name: marketing-content-secret startupProbe: - exec: - command: - - /bin/sh - - -c - - "nc -z marketing-content-postgresql 5432" + tcpSocket: + port: 8083 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 @@ -370,20 +325,14 @@ spec: 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 + path: /actuator/health port: 8083 - httpHeaders: - - name: Accept - value: application/json initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 5 @@ -432,11 +381,8 @@ spec: - secretRef: name: ai-recommend-secret startupProbe: - exec: - command: - - /bin/sh - - -c - - "nc -z ai-recommend-postgresql 5432" + tcpSocket: + port: 8084 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 @@ -445,20 +391,14 @@ spec: 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 + path: /actuator/health port: 8084 - httpHeaders: - - name: Accept - value: application/json initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 5 From 1d548a5f5c3ccf76bcfc5e59b4c08bd21585d992 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Tue, 17 Jun 2025 17:26:36 +0900 Subject: [PATCH 10/61] feat: poster content --- .../service/PosterContentService.java | 25 +++++++--- .../usecase/PosterContentUseCase.java | 12 ++--- .../content/domain/model/Content.java | 13 ++--- .../domain/model/CreationConditions.java | 2 - .../controller/ContentController.java | 47 ++++++++++++------- .../dto/PosterContentCreateRequest.java | 4 +- .../dto/PosterContentSaveRequest.java | 15 +----- .../dto/SnsContentCreateRequest.java | 12 ----- 8 files changed, 60 insertions(+), 70 deletions(-) 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 69c6213..a2d6cc0 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 @@ -8,16 +8,17 @@ import com.won.smarketing.content.domain.model.CreationConditions; import com.won.smarketing.content.domain.model.Platform; 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.presentation.dto.PosterContentCreateRequest; import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse; import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest; import lombok.RequiredArgsConstructor; 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; /** * 포스터 콘텐츠 서비스 구현체 @@ -30,6 +31,7 @@ public class PosterContentService implements PosterContentUseCase { private final ContentRepository contentRepository; private final AiPosterGenerator aiPosterGenerator; + private final BlobStorageService blobStorageService; /** * 포스터 콘텐츠 생성 @@ -39,10 +41,18 @@ public class PosterContentService implements PosterContentUseCase { */ @Override @Transactional - public PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request) { + public PosterContentCreateResponse generatePosterContent(List images, PosterContentCreateRequest request) { + // 1. 이미지 blob storage에 저장하고 request 저장 + List imageUrls = blobStorageService.uploadImage(images, "poster-content-original"); + request.setImages(imageUrls); + + // 2. AI 요청 String generatedPoster = aiPosterGenerator.generatePoster(request); + // 3. 저장 + Content savedContent = savePosterContent(request, generatedPoster); + // 생성 조건 정보 구성 CreationConditions conditions = CreationConditions.builder() .category(request.getCategory()) @@ -68,9 +78,8 @@ public class PosterContentService implements PosterContentUseCase { * * @param request 포스터 콘텐츠 저장 요청 */ - @Override @Transactional - public void savePosterContent(PosterContentSaveRequest request) { + public Content savePosterContent(PosterContentCreateRequest request, String generatedPoster) { // 생성 조건 구성 CreationConditions conditions = CreationConditions.builder() .category(request.getCategory()) @@ -86,7 +95,7 @@ public class PosterContentService implements PosterContentUseCase { .contentType(ContentType.POSTER) .platform(Platform.GENERAL) .title(request.getTitle()) - .content(request.getContent()) + .content(generatedPoster) .images(request.getImages()) .status(ContentStatus.PUBLISHED) .creationConditions(conditions) @@ -94,6 +103,8 @@ public class PosterContentService implements PosterContentUseCase { .build(); // 저장 - contentRepository.save(content); + Content savedContent = contentRepository.save(content); + + return savedContent; } } \ No newline at end of file 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..101482b 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 @@ -4,6 +4,9 @@ package com.won.smarketing.content.application.usecase; 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 인터페이스 @@ -12,15 +15,10 @@ import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest; public interface PosterContentUseCase { /** - * 포스터 콘텐츠 생성 + * 포스터 콘텐츠 생성 및 저장 * @param request 포스터 콘텐츠 생성 요청 * @return 포스터 콘텐츠 생성 응답 */ - PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request); + PosterContentCreateResponse generatePosterContent(List images, PosterContentCreateRequest request); - /** - * 포스터 콘텐츠 저장 - * @param request 포스터 콘텐츠 저장 요청 - */ - void savePosterContent(PosterContentSaveRequest request); } \ No newline at end of file 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/presentation/controller/ContentController.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java index b454fb1..0bd8e61 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java @@ -7,12 +7,17 @@ import com.won.smarketing.content.application.usecase.SnsContentUseCase; import com.won.smarketing.content.presentation.dto.*; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; +import org.springframework.web.multipart.MultipartFile; + import java.util.List; /** @@ -62,23 +67,33 @@ public class ContentController { * @return 생성된 포스터 콘텐츠 정보 */ @Operation(summary = "홍보 포스터 생성", description = "AI를 활용하여 홍보 포스터를 생성합니다.") - @PostMapping("/poster/generate") - public ResponseEntity> generatePosterContent(@Valid @RequestBody PosterContentCreateRequest request) { - PosterContentCreateResponse response = posterContentUseCase.generatePosterContent(request); - return ResponseEntity.ok(ApiResponse.success(response, "포스터 콘텐츠가 성공적으로 생성되었습니다.")); - } + @PostMapping(value = "/poster/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> generatePosterContent( + @Parameter( + description = "참고할 이미지 파일들 (선택사항, 최대 5개)", + required = false, + content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE) + ) + @RequestPart(value = "images", required = false) List images, + @Parameter( + description = "포스터 생성 요청 정보", + required = true, + example = """ + { + "title": "신메뉴 출시 이벤트", + "category": "이벤트", + "requirement": "밝고 화사한 분위기로 만들어주세요", + "eventName": "아메리카노 할인 이벤트", + "startDate": "2024-01-15", + "endDate": "2024-01-31", + "photoStyle": "밝고 화사한" + } + """ + ) + @RequestPart(value = "request") @Valid PosterContentCreateRequest request) { - /** - * 홍보 포스터 저장 - * - * @param request 포스터 콘텐츠 저장 요청 - * @return 저장 성공 응답 - */ - @Operation(summary = "홍보 포스터 저장", description = "생성된 홍보 포스터를 저장합니다.") - @PostMapping("/poster/save") - public ResponseEntity> savePosterContent(@Valid @RequestBody PosterContentSaveRequest request) { - posterContentUseCase.savePosterContent(request); - return ResponseEntity.ok(ApiResponse.success(null, "포스터 콘텐츠가 성공적으로 저장되었습니다.")); + PosterContentCreateResponse response = posterContentUseCase.generatePosterContent(images, request); + return ResponseEntity.ok(ApiResponse.success(response, "포스터 콘텐츠가 성공적으로 생성되었습니다.")); } /** diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateRequest.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateRequest.java index 1cbf87d..65508ce 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateRequest.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateRequest.java @@ -50,9 +50,7 @@ public class PosterContentCreateRequest { @Schema(description = "이미지 스타일", example = "모던") private String imageStyle; - @Schema(description = "업로드된 이미지 URL 목록", required = true) - @NotNull(message = "이미지는 1개 이상 필수입니다") - @Size(min = 1, message = "이미지는 1개 이상 업로드해야 합니다") + @Schema(description = "업로드된 이미지 URL 목록") private List images; @Schema(description = "콘텐츠 카테고리", example = "이벤트") diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java index 9cdf9e1..f3c5877 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java @@ -1,8 +1,6 @@ -// smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java package com.won.smarketing.content.presentation.dto; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -19,12 +17,7 @@ import java.util.List; @Schema(description = "포스터 콘텐츠 저장 요청") public class PosterContentSaveRequest { -// @Schema(description = "콘텐츠 ID", example = "1", required = true) -// @NotNull(message = "콘텐츠 ID는 필수입니다") -// private Long contentId; - - @Schema(description = "매장 ID", example = "1", required = true) - @NotNull(message = "매장 ID는 필수입니다") + @Schema(description = "매장 ID", example = "1") private Long storeId; @Schema(description = "제목", example = "특별 이벤트 안내") @@ -46,12 +39,6 @@ public class PosterContentSaveRequest { @Schema(description = "구체적인 요구사항", example = "신메뉴 출시 이벤트 포스터를 만들어주세요") private String requirement; - @Schema(description = "톤앤매너", example = "전문적") - private String toneAndManner; - - @Schema(description = "감정 강도", example = "보통") - private String emotionIntensity; - @Schema(description = "이벤트명", example = "신메뉴 출시 이벤트") private String eventName; diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateRequest.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateRequest.java index f8bcdeb..271d604 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateRequest.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateRequest.java @@ -68,18 +68,6 @@ public class SnsContentCreateRequest { @Schema(description = "콘텐츠 타입", example = "SNS 게시물") private String contentType; -// @Schema(description = "톤앤매너", -// example = "친근함", -// allowableValues = {"친근함", "전문적", "유머러스", "감성적", "트렌디"}) -// private String toneAndManner; - -// @Schema(description = "감정 강도", -// example = "보통", -// allowableValues = {"약함", "보통", "강함"}) -// private String emotionIntensity; - - // ==================== 이벤트 정보 ==================== - @Schema(description = "이벤트명 (이벤트 콘텐츠인 경우)", example = "신메뉴 출시 이벤트") @Size(max = 200, message = "이벤트명은 200자 이하로 입력해주세요") From 0663018fe3f3cc8341d718110a5965ceebfaabfa Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Tue, 17 Jun 2025 17:33:26 +0900 Subject: [PATCH 11/61] Update Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 61 +++++++++++++++++++------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index b2934e3..a84cf56 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -39,6 +39,8 @@ podTemplate( echo "Services: ${services}" echo "Namespace: ${namespace}" echo "Image Tag: ${imageTag}" + echo "Registry: ${props.registry}" + echo "Image Org: ${props.image_org}" } stage("Check Changes") { @@ -182,15 +184,15 @@ podTemplate( export postgres_password='${props.POSTGRES_PASSWORD}' export replicas=${props.replicas} - # PostgreSQL 환경변수 추가 (문자열로) + # PostgreSQL 환경변수 추가 (올바른 DB명으로 수정) export postgres_host='${props.POSTGRES_HOST}' export postgres_port='5432' - export postgres_db_member='member' - export postgres_db_store='store' - export postgres_db_marketing_content='marketing_content' + export postgres_db_member='MemberDB' + export postgres_db_store='StoreDB' + export postgres_db_marketing_content='MarketingContentDB' export postgres_db_ai_recommend='AiRecommendationDB' - # Redis 환경변수 추가 (문자열로) + # Redis 환경변수 추가 export redis_host='${props.REDIS_HOST}' export redis_port='6380' export redis_password='${props.REDIS_PASSWORD}' @@ -208,11 +210,17 @@ podTemplate( 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 "postgres_user: \$postgres_user" + echo "replicas: \$replicas" echo "=== Manifest 생성 ===" envsubst < smarketing-java/deployment/${manifest}.template > smarketing-java/deployment/${manifest} @@ -233,7 +241,10 @@ podTemplate( 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 서비스가 없습니다." + 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} @@ -244,19 +255,27 @@ podTemplate( kubectl -n ${namespace} get pods echo "=== ConfigMap 확인 ===" - kubectl -n ${namespace} get configmap ai-recommend-config -o yaml + 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 "=== 각 서비스 배포 대기 (90초 timeout) ===" - timeout 90 kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=90s || echo "member deployment 대기 타임아웃" - timeout 90 kubectl -n ${namespace} wait --for=condition=available deployment/store --timeout=90s || echo "store deployment 대기 타임아웃" - timeout 90 kubectl -n ${namespace} wait --for=condition=available deployment/marketing-content --timeout=90s || echo "marketing-content deployment 대기 타임아웃" - timeout 90 kubectl -n ${namespace} wait --for=condition=available deployment/ai-recommend --timeout=90s || echo "ai-recommend deployment 대기 타임아웃" + echo "=== Secret 확인 ===" + kubectl -n ${namespace} get secret member-secret -o yaml | grep -A 5 "data:" - echo "=== 최종 상태 ===" + 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 로그 확인 (ai-recommend) ===" - kubectl -n ${namespace} logs deployment/ai-recommend --tail=50 || echo "ai-recommend 로그를 가져올 수 없습니다" + 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 @@ -264,9 +283,17 @@ podTemplate( echo "=== 실패한 Pod: \$pod ===" kubectl -n ${namespace} describe \$pod | tail -30 echo "=== Pod 로그: \$pod ===" - kubectl -n ${namespace} logs \$pod --tail=20 || echo "로그를 가져올 수 없습니다" + 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 """ } } From ffa4a7f0a96e1bc89d42c5b9e9416b892af8a5e5 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 09:50:11 +0900 Subject: [PATCH 12/61] feat: generate poster --- .../service/PosterContentService.java | 12 +++++-- .../service/SnsContentService.java | 2 +- .../domain/service/BlobStorageService.java | 2 +- .../service/BlobStorageServiceImpl.java | 12 ++----- .../controller/ContentController.java | 31 ++++++------------- 5 files changed, 24 insertions(+), 35 deletions(-) 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 a2d6cc0..4af4ddc 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 @@ -13,6 +13,8 @@ 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.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -25,10 +27,14 @@ 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; @@ -42,11 +48,11 @@ public class PosterContentService implements PosterContentUseCase { @Override @Transactional public PosterContentCreateResponse generatePosterContent(List images, PosterContentCreateRequest request) { - + log.info("지점1-1"); // 1. 이미지 blob storage에 저장하고 request 저장 - List imageUrls = blobStorageService.uploadImage(images, "poster-content-original"); + List imageUrls = blobStorageService.uploadImage(images, posterImageContainer); request.setImages(imageUrls); - + log.info("지점2-1"); // 2. AI 요청 String generatedPoster = aiPosterGenerator.generatePoster(request); 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 6119226..233bd5d 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 @@ -44,7 +44,7 @@ public class SnsContentService implements SnsContentUseCase { @Transactional public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List files) { //파일들 주소 가져옴 - List urls = blobStorageService.uploadImage(files); + List urls = blobStorageService.uploadImage(files, "containerName"); request.setImages(urls); // AI를 사용하여 SNS 콘텐츠 생성 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..d37a0ea 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); // 파일 업로드 (간단한 방식) diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java index d83f488..48a516b 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java @@ -1,5 +1,7 @@ package com.won.smarketing.content.presentation.controller; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.won.smarketing.common.dto.ApiResponse; import com.won.smarketing.content.application.usecase.ContentQueryUseCase; import com.won.smarketing.content.application.usecase.PosterContentUseCase; @@ -11,6 +13,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -25,6 +28,7 @@ import java.util.List; * SNS 콘텐츠 생성, 포스터 생성, 콘텐츠 관리 기능 제공 */ @Tag(name = "마케팅 콘텐츠 관리", description = "AI 기반 마케팅 콘텐츠 생성 및 관리 API") +@Slf4j @RestController @RequestMapping("/api/content") @RequiredArgsConstructor @@ -33,6 +37,7 @@ public class ContentController { private final SnsContentUseCase snsContentUseCase; private final PosterContentUseCase posterContentUseCase; private final ContentQueryUseCase contentQueryUseCase; + private final ObjectMapper objectMapper; /** * SNS 게시물 생성 @@ -70,28 +75,12 @@ public class ContentController { @Operation(summary = "홍보 포스터 생성", description = "AI를 활용하여 홍보 포스터를 생성합니다.") @PostMapping(value = "/poster/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> generatePosterContent( - @Parameter( - description = "참고할 이미지 파일들 (선택사항, 최대 5개)", - required = false, - content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE) - ) + @Parameter(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)) @RequestPart(value = "images", required = false) List images, - @Parameter( - description = "포스터 생성 요청 정보", - required = true, - example = """ - { - "title": "신메뉴 출시 이벤트", - "category": "이벤트", - "requirement": "밝고 화사한 분위기로 만들어주세요", - "eventName": "아메리카노 할인 이벤트", - "startDate": "2024-01-15", - "endDate": "2024-01-31", - "photoStyle": "밝고 화사한" - } - """ - ) - @RequestPart(value = "request") @Valid PosterContentCreateRequest request) { + @RequestPart("request") String requestJson) throws JsonProcessingException { + + // JSON 파싱 + PosterContentCreateRequest request = objectMapper.readValue(requestJson, PosterContentCreateRequest.class); PosterContentCreateResponse response = posterContentUseCase.generatePosterContent(images, request); return ResponseEntity.ok(ApiResponse.success(response, "포스터 콘텐츠가 성공적으로 생성되었습니다.")); From ee9736bb9f111743a0756dd1ded50618ffa1b50d Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 09:56:48 +0900 Subject: [PATCH 13/61] =?UTF-8?q?refactor:=20build.gradle=20=EC=B5=9C?= =?UTF-8?q?=EC=83=81=EB=8B=A8=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- smarketing-java/build.gradle | 7 +++++++ smarketing-java/marketing-content/build.gradle | 3 --- smarketing-java/store/build.gradle | 6 ------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/smarketing-java/build.gradle b/smarketing-java/build.gradle index e917ca4..3bd1308 100644 --- a/smarketing-java/build.gradle +++ b/smarketing-java/build.gradle @@ -53,6 +53,13 @@ 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-databind:2.12.3' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + } tasks.named('test') { 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/store/build.gradle b/smarketing-java/store/build.gradle index ef65d80..771a2fc 100644 --- a/smarketing-java/store/build.gradle +++ b/smarketing-java/store/build.gradle @@ -1,10 +1,4 @@ dependencies { implementation project(':common') runtimeOnly 'com.mysql:mysql-connector-j' - - // 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-databind:2.12.3' } \ No newline at end of file From 937bf520517d662d99962539e6ccffead4dcc340 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Wed, 18 Jun 2025 10:12:14 +0900 Subject: [PATCH 14/61] Update deploy.yaml.template --- .../deployment/deploy.yaml.template | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index 0416d61..9b8d945 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -469,35 +469,38 @@ metadata: namespace: ${namespace} annotations: kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/rewrite-target: /$2 + nginx.ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/use-regex: "true" spec: ingressClassName: nginx rules: - host: smarketing.20.249.184.228.nip.io http: paths: - - path: /api/auth - pathType: Prefix + - path: /api/auth(/|$)(.*) + pathType: ImplementationSpecific backend: service: name: member port: number: 80 - - path: /api/store - pathType: Prefix + - path: /api/store(/|$)(.*) + pathType: ImplementationSpecific backend: service: name: store port: number: 80 - - path: /api/content - pathType: Prefix + - path: /api/content(/|$)(.*) + pathType: ImplementationSpecific backend: service: name: marketing-content port: number: 80 - - path: /api/recommend - pathType: Prefix + - path: /api/recommend(/|$)(.*) + pathType: ImplementationSpecific backend: service: name: ai-recommend From 379f690cdce0dce54392da41b974bde90ba12060 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 10:48:45 +0900 Subject: [PATCH 15/61] refactor: poster content --- smarketing-java/build.gradle | 4 +++- .../content/domain/service/BlobStorageServiceImpl.java | 10 +++++----- .../external/PythonAiPosterGenerator.java | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/smarketing-java/build.gradle b/smarketing-java/build.gradle index 3bd1308..01426b7 100644 --- a/smarketing-java/build.gradle +++ b/smarketing-java/build.gradle @@ -57,7 +57,9 @@ subprojects { implementation 'com.azure:azure-storage-blob:12.25.0' implementation 'com.azure:azure-identity:1.11.1' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3' + 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' } 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 d37a0ea..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 @@ -152,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/infrastructure/external/PythonAiPosterGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java index 1fc2020..fc1405c 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 @@ -46,7 +46,7 @@ public class PythonAiPosterGenerator implements AiPosterGenerator { // Python AI 서비스 호출 Map response = webClient .post() - .uri(aiServiceBaseUrl + "/api/ai/poster") + .uri("http://localhost:5001" + "/api/ai/poster") .header("Content-Type", "application/json") .bodyValue(requestBody) .retrieve() From 2e8055df2d6f45a2cf43b952b0c43d1f540b09a5 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 10:59:31 +0900 Subject: [PATCH 16/61] =?UTF-8?q?refactor:=20SpringConfig=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/won/smarketing/common/config/SecurityConfig.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java b/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java index 7b8f4f2..e71887c 100644 --- a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java +++ b/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java @@ -45,7 +45,14 @@ public class SecurityConfig { .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() + "/swagger-resources/**", "/webjars/**", "/actuator/**", "/health/**", "/error" + ).permitAll() + // Rewrite된 경로들 허용 (새로 추가!) + .requestMatchers("/login", "/register", "/auth/**").permitAll() // Member 서비스 + .requestMatchers("/stores", "/stores/**").permitAll() // Store 서비스 + .requestMatchers("/content", "/content/**").permitAll() // Content 서비스 + .requestMatchers("/recommend", "/recommend/**").permitAll() // AI Recommend 서비스 + .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); From 7da375e038e502498bb36ff0c9afc29a77ae34ef Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Wed, 18 Jun 2025 11:32:59 +0900 Subject: [PATCH 17/61] Update deploy.yaml.template --- .../deployment/deploy.yaml.template | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index 9b8d945..2a2defd 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -462,6 +462,7 @@ spec: type: ClusterIP --- +# deploy.yaml.template의 Ingress 부분 - 완전한 설정 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: @@ -478,6 +479,7 @@ spec: - host: smarketing.20.249.184.228.nip.io http: paths: + # Member 서비스 - 인증 관련 - path: /api/auth(/|$)(.*) pathType: ImplementationSpecific backend: @@ -485,6 +487,15 @@ spec: name: member port: number: 80 + # Member 서비스 - 회원 관리 (누락된 경로!) + - path: /api/member(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: member + port: + number: 80 + # Store 서비스 - path: /api/store(/|$)(.*) pathType: ImplementationSpecific backend: @@ -492,6 +503,31 @@ spec: name: store port: number: 80 + # Store 서비스 - 매출 관련 + - path: /api/sales(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: store + port: + number: 80 + # Store 서비스 - 메뉴 관련 + - path: /api/menu(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: store + port: + number: 80 + # Store 서비스 - 이미지 업로드 + - path: /api/images(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: store + port: + number: 80 + # Marketing Content 서비스 - path: /api/content(/|$)(.*) pathType: ImplementationSpecific backend: @@ -499,6 +535,7 @@ spec: name: marketing-content port: number: 80 + # AI Recommend 서비스 - path: /api/recommend(/|$)(.*) pathType: ImplementationSpecific backend: From 9108232c5a5013f18be285506a51b7196c31416a Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 11:33:53 +0900 Subject: [PATCH 18/61] feat: ai-poster content --- smarketing-ai/app.py | 7 ++----- smarketing-ai/models/request_models.py | 3 --- smarketing-ai/services/poster_service.py | 1 - 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/smarketing-ai/app.py b/smarketing-ai/app.py index d3c91da..e96f33a 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,12 +140,9 @@ 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, diff --git a/smarketing-ai/models/request_models.py b/smarketing-ai/models/request_models.py index 3f6952d..b21a4e1 100644 --- a/smarketing-ai/models/request_models.py +++ b/smarketing-ai/models/request_models.py @@ -33,12 +33,9 @@ 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 diff --git a/smarketing-ai/services/poster_service.py b/smarketing-ai/services/poster_service.py index c90119c..3dec55e 100644 --- a/smarketing-ai/services/poster_service.py +++ b/smarketing-ai/services/poster_service.py @@ -154,7 +154,6 @@ class PosterService: ### 📋 기본 정보 카테고리: {request.category} - 콘텐츠 타입: {request.contentType} 메뉴명: {request.menuName or '없음'} 메뉴 정보: {main_description} From b2a40bcee896f4d5ef40d10383081beb2a1b5884 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 11:34:54 +0900 Subject: [PATCH 19/61] =?UTF-8?q?refactor:=20webClientConfig=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PosterContentService.java | 28 ++++--------------- .../usecase/PosterContentUseCase.java | 8 +++++- .../content/config/WebClientConfig.java | 5 ++-- .../external/PythonAiPosterGenerator.java | 2 +- .../controller/ContentController.java | 13 +++++++++ .../dto/PosterContentCreateResponse.java | 10 ------- .../dto/PosterContentSaveRequest.java | 1 - 7 files changed, 29 insertions(+), 38 deletions(-) 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 4af4ddc..94c5362 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 @@ -48,33 +48,19 @@ public class PosterContentService implements PosterContentUseCase { @Override @Transactional public PosterContentCreateResponse generatePosterContent(List images, PosterContentCreateRequest request) { - log.info("지점1-1"); + // 1. 이미지 blob storage에 저장하고 request 저장 List imageUrls = blobStorageService.uploadImage(images, posterImageContainer); request.setImages(imageUrls); - log.info("지점2-1"); + // 2. AI 요청 String generatedPoster = aiPosterGenerator.generatePoster(request); - // 3. 저장 - Content savedContent = savePosterContent(request, generatedPoster); - - // 생성 조건 정보 구성 - CreationConditions conditions = CreationConditions.builder() - .category(request.getCategory()) - .requirement(request.getRequirement()) - .eventName(request.getEventName()) - .startDate(request.getStartDate()) - .endDate(request.getEndDate()) - .photoStyle(request.getPhotoStyle()) - .build(); - 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(); } @@ -85,7 +71,7 @@ public class PosterContentService implements PosterContentUseCase { * @param request 포스터 콘텐츠 저장 요청 */ @Transactional - public Content savePosterContent(PosterContentCreateRequest request, String generatedPoster) { + public Content savePosterContent(PosterContentSaveRequest request) { // 생성 조건 구성 CreationConditions conditions = CreationConditions.builder() .category(request.getCategory()) @@ -101,7 +87,7 @@ public class PosterContentService implements PosterContentUseCase { .contentType(ContentType.POSTER) .platform(Platform.GENERAL) .title(request.getTitle()) - .content(generatedPoster) +// .content(request.gen) .images(request.getImages()) .status(ContentStatus.PUBLISHED) .creationConditions(conditions) @@ -109,8 +95,6 @@ public class PosterContentService implements PosterContentUseCase { .build(); // 저장 - Content savedContent = contentRepository.save(content); - - return savedContent; + return contentRepository.save(content); } } \ No newline at end of file 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 101482b..7f346e3 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,6 +1,7 @@ // 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; @@ -15,10 +16,15 @@ import java.util.List; public interface PosterContentUseCase { /** - * 포스터 콘텐츠 생성 및 저장 + * 포스터 콘텐츠 생성 * @param request 포스터 콘텐츠 생성 요청 * @return 포스터 콘텐츠 생성 응답 */ PosterContentCreateResponse generatePosterContent(List images, PosterContentCreateRequest request); + /** + * 포스터 콘텐츠 저장 + * @param request 포스터 콘텐츠 저장 요청 + */ + Content savePosterContent(PosterContentSaveRequest request); } \ No newline at end of file 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 7f7cf08..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, 10000) - .responseTimeout(Duration.ofMillis(30000)); + .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/infrastructure/external/PythonAiPosterGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java index fc1405c..4ea396a 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 @@ -51,7 +51,7 @@ public class PythonAiPosterGenerator implements AiPosterGenerator { .bodyValue(requestBody) .retrieve() .bodyToMono(Map.class) - .timeout(Duration.ofSeconds(60)) // 포스터 생성은 시간이 오래 걸릴 수 있음 + .timeout(Duration.ofSeconds(90)) // 포스터 생성은 시간이 오래 걸릴 수 있음 .block(); // 응답에서 content(이미지 URL) 추출 diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java index f306abc..ab7afa1 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java @@ -85,6 +85,19 @@ public class ContentController { return ResponseEntity.ok(ApiResponse.success(response, "포스터 콘텐츠가 성공적으로 생성되었습니다.")); } + /** + * 홍보 포스터 저장 + * + * @param request 포스터 콘텐츠 저장 요청 + * @return 저장 성공 응답 + */ + @Operation(summary = "홍보 포스터 저장", description = "생성된 홍보 포스터를 저장합니다.") + @PostMapping("/poster/save") + public ResponseEntity> savePosterContent(@Valid @RequestBody PosterContentSaveRequest request) { + posterContentUseCase.savePosterContent(request); + return ResponseEntity.ok(ApiResponse.success(null, "포스터 콘텐츠가 성공적으로 저장되었습니다.")); + } + /** * 콘텐츠 수정 * diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateResponse.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateResponse.java index 0c02b68..5fa5c53 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateResponse.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateResponse.java @@ -31,19 +31,9 @@ public class PosterContentCreateResponse { @Schema(description = "생성된 포스터 타입") private String contentType; - @Schema(description = "포스터 이미지 URL") - private String posterImage; - - @Schema(description = "원본 이미지 URL 목록") - private List originalImages; - @Schema(description = "이미지 스타일", example = "모던") private String imageStyle; @Schema(description = "생성 상태", example = "DRAFT") private String status; - - @Schema(description = "포스터사이즈", example = "800x600") - private Map posterSizes; - } \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java index f3c5877..eb549f0 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java @@ -32,7 +32,6 @@ public class PosterContentSaveRequest { @Schema(description = "발행 상태", example = "PUBLISHED") private String status; - // CreationConditions에 필요한 필드들 @Schema(description = "콘텐츠 카테고리", example = "이벤트") private String category; From 82516174d33f7e3fe39d9c162ad7a03f86e22e3c Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 13:53:51 +0900 Subject: [PATCH 20/61] fix: Security Config --- .../won/smarketing/common/config/SecurityConfig.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java b/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java index e71887c..89f1436 100644 --- a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java +++ b/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java @@ -43,17 +43,7 @@ public class SecurityConfig { .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() - // Rewrite된 경로들 허용 (새로 추가!) - .requestMatchers("/login", "/register", "/auth/**").permitAll() // Member 서비스 - .requestMatchers("/stores", "/stores/**").permitAll() // Store 서비스 - .requestMatchers("/content", "/content/**").permitAll() // Content 서비스 - .requestMatchers("/recommend", "/recommend/**").permitAll() // AI Recommend 서비스 - - .anyRequest().authenticated() + .anyRequest().permitAll() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); From 934ad742d49fb664fc848a01d728c7e993a40397 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 14:03:55 +0900 Subject: [PATCH 21/61] feat: save poster content --- .../content/application/service/PosterContentService.java | 7 +++---- .../content/application/usecase/PosterContentUseCase.java | 2 +- .../content/presentation/dto/PosterContentSaveRequest.java | 3 --- 3 files changed, 4 insertions(+), 8 deletions(-) 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 94c5362..944f98a 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 @@ -19,7 +19,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.util.HashMap; import java.util.List; /** @@ -71,7 +70,7 @@ public class PosterContentService implements PosterContentUseCase { * @param request 포스터 콘텐츠 저장 요청 */ @Transactional - public Content savePosterContent(PosterContentSaveRequest request) { + public void savePosterContent(PosterContentSaveRequest request) { // 생성 조건 구성 CreationConditions conditions = CreationConditions.builder() .category(request.getCategory()) @@ -87,7 +86,7 @@ public class PosterContentService implements PosterContentUseCase { .contentType(ContentType.POSTER) .platform(Platform.GENERAL) .title(request.getTitle()) -// .content(request.gen) + .content(request.getContent()) .images(request.getImages()) .status(ContentStatus.PUBLISHED) .creationConditions(conditions) @@ -95,6 +94,6 @@ public class PosterContentService implements PosterContentUseCase { .build(); // 저장 - return contentRepository.save(content); + contentRepository.save(content); } } \ No newline at end of file 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 7f346e3..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 @@ -26,5 +26,5 @@ public interface PosterContentUseCase { * 포스터 콘텐츠 저장 * @param request 포스터 콘텐츠 저장 요청 */ - Content savePosterContent(PosterContentSaveRequest request); + void savePosterContent(PosterContentSaveRequest request); } \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java index eb549f0..e05612c 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java @@ -29,9 +29,6 @@ public class PosterContentSaveRequest { @Schema(description = "선택된 포스터 이미지 URL") private List images; - @Schema(description = "발행 상태", example = "PUBLISHED") - private String status; - @Schema(description = "콘텐츠 카테고리", example = "이벤트") private String category; From 8ec1625e14b66b0bfbeb42d59b1358b9326f9da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:26:35 +0900 Subject: [PATCH 22/61] change from GENRAL to POSTER --- .../content/application/service/PosterContentService.java | 2 +- .../content/application/service/SnsContentService.java | 3 --- .../java/com/won/smarketing/content/domain/model/Platform.java | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) 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 69c6213..a676c40 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 @@ -84,7 +84,7 @@ public class PosterContentService implements PosterContentUseCase { // 콘텐츠 엔티티 생성 Content content = Content.builder() .contentType(ContentType.POSTER) - .platform(Platform.GENERAL) + .platform(Platform.POSTER) .title(request.getTitle()) .content(request.getContent()) .images(request.getImages()) 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 6119226..753063b 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 @@ -67,8 +67,6 @@ public class SnsContentService implements SnsContentUseCase { CreationConditions conditions = CreationConditions.builder() .category(request.getCategory()) .requirement(request.getRequirement()) - //.toneAndManner(request.getToneAndManner()) - //.emotionIntensity(request.getEmotionIntensity()) .eventName(request.getEventName()) .startDate(request.getStartDate()) .endDate(request.getEndDate()) @@ -76,7 +74,6 @@ public class SnsContentService implements SnsContentUseCase { // 콘텐츠 엔티티 생성 및 저장 Content content = Content.builder() -// .contentType(ContentType.SNS_POST) .platform(Platform.fromString(request.getPlatform())) .title(request.getTitle()) .content(request.getContent()) diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java index 66e266c..aea9446 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java @@ -17,7 +17,7 @@ public enum Platform { FACEBOOK("페이스북"), KAKAO_STORY("카카오스토리"), YOUTUBE("유튜브"), - GENERAL("일반"); + POSTER("포스터"); private final String displayName; From b6721846476f73f1ebcd2ed68fbd56db9a98ecdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:43:36 +0900 Subject: [PATCH 23/61] add sns_contents prompt --- smarketing-ai/services/sns_content_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index 16fd8ba..7554474 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1792,6 +1792,7 @@ class SnsContentService: 네이버 검색에서 상위 노출되고, 실제로 도움이 되는 정보를 제공하는 블로그 포스트를 작성해주세요. 필수 요구사항을 반드시 참고하여 작성해주세요. 이미지 배치 위치를 [IMAGE_X] 태그로 명확히 표시해주세요. +이미지는 제공된 것만 사용하시고, 추가하진 말아주세요. """ return prompt From 5a1cdfbd8af4e936f813eadba4f11ff5dd2931f0 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 14:45:53 +0900 Subject: [PATCH 24/61] fix: Security Config --- .../java/com/won/smarketing/common/config/SecurityConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java b/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java index 89f1436..b5179bb 100644 --- a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java +++ b/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java @@ -45,7 +45,8 @@ public class SecurityConfig { .authorizeHttpRequests(auth -> auth .anyRequest().permitAll() ) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); +// .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + ; return http.build(); } From 4e19db7c684fc47639a27c0293a0aa064e126e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:57:03 +0900 Subject: [PATCH 25/61] change images required false --- .../content/presentation/controller/ContentController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java index ab7afa1..bb4bfc6 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java @@ -46,7 +46,7 @@ public class ContentController { @Operation(summary = "SNS 게시물 생성", description = "AI를 활용하여 SNS 게시물을 생성합니다.") @PostMapping(path = "/sns/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> generateSnsContent(@Valid @RequestPart("request") String requestJson, - @Valid @RequestPart("files") List images) throws JsonProcessingException { + @Valid @RequestPart(name = "files", required = false) List images) throws JsonProcessingException { SnsContentCreateRequest request = objectMapper.readValue(requestJson, SnsContentCreateRequest.class); SnsContentCreateResponse response = snsContentUseCase.generateSnsContent(request, images); return ResponseEntity.ok(ApiResponse.success(response, "SNS 콘텐츠가 성공적으로 생성되었습니다.")); From 09a637337115ab64766838de1cb644c388d2ae5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:06:32 +0900 Subject: [PATCH 26/61] add if --- .../content/application/service/SnsContentService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 3332fbf..86c279c 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 @@ -44,8 +44,10 @@ public class SnsContentService implements SnsContentUseCase { @Transactional public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List files) { //파일들 주소 가져옴 - List urls = blobStorageService.uploadImage(files, "containerName"); - request.setImages(urls); + if(files != null) { + List urls = blobStorageService.uploadImage(files, "containerName"); + request.setImages(urls); + } // AI를 사용하여 SNS 콘텐츠 생성 String content = aiContentGenerator.generateSnsContent(request); From 5c6b5a732b854c428e00b080d0dccb05db37e1b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:09:57 +0900 Subject: [PATCH 27/61] chg option --- smarketing-ai/services/sns_content_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index 7554474..2f0b98d 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문장마다 이미지 배치', '이미지 설명은 간결하고 매력적으로', '마지막에 대표 이미지로 마무리' ] From aed79a7f966b3812a594fbec94c98871bec02e38 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 15:13:48 +0900 Subject: [PATCH 28/61] refactor: Spring security config --- .../com/won/smarketing/common/config/SecurityConfig.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java b/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java index b5179bb..dcc1f20 100644 --- a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java +++ b/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java @@ -43,10 +43,13 @@ public class SecurityConfig { .cors(cors -> cors.configurationSource(corsConfigurationSource())) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() + .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) - ; + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } From 17e6235c99e372436d38bf8a027b6323b6f78275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:35:02 +0900 Subject: [PATCH 29/61] set image container & chg blog --- smarketing-ai/services/sns_content_service.py | 188 +++++++++--------- .../service/SnsContentService.java | 6 +- 2 files changed, 99 insertions(+), 95 deletions(-) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index 2f0b98d..f8e837f 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1450,8 +1450,8 @@ class SnsContentService: # 네이버 블로그인 경우 이미지 배치 계획 생성 image_placement_plan = None - if request.platform == '네이버 블로그': - image_placement_plan = self._create_image_placement_plan(image_analysis, request) + # if request.platform == '네이버 블로그': + # image_placement_plan = self._create_image_placement_plan(image_analysis, request) # 플랫폼별 특화 프롬프트 생성 prompt = self._create_platform_specific_prompt(request, image_analysis, image_placement_plan) @@ -1551,98 +1551,98 @@ class SnsContentService: return '기타' - def _create_image_placement_plan(self, image_analysis: Dict[str, Any], request: SnsContentGetRequest) -> Dict[ - str, Any]: - """ - 네이버 블로그용 이미지 배치 계획 생성 - """ - images = image_analysis.get('results', []) - if not images: - return None - - # 이미지 타입별 분류 - categorized_images = { - '매장외관': [], - '인테리어': [], - '메뉴판': [], - '음식': [], - '사람': [], - '기타': [] - } - - for img in images: - img_type = img.get('type', '기타') - categorized_images[img_type].append(img) - - # 블로그 구조에 따른 이미지 배치 계획 - placement_plan = { - 'structure': [ - { - 'section': '인트로', - 'description': '첫인상과 방문 동기', - 'recommended_images': [], - 'placement_guide': '매장 외관이나 대표적인 음식 사진으로 시작' - }, - { - 'section': '매장 정보', - 'description': '위치, 분위기, 인테리어 소개', - 'recommended_images': [], - 'placement_guide': '매장 외관 → 내부 인테리어 순서로 배치' - }, - { - 'section': '메뉴 소개', - 'description': '주문한 메뉴와 상세 후기', - 'recommended_images': [], - 'placement_guide': '메뉴판 → 실제 음식 사진 순서로 배치' - }, - { - 'section': '총평', - 'description': '재방문 의향과 추천 이유', - 'recommended_images': [], - 'placement_guide': '가장 매력적인 음식 사진이나 전체 분위기 사진' - } - ], - 'image_sequence': [], - 'usage_guide': [] - } - - # 각 섹션에 적절한 이미지 배정 - # 인트로: 매장외관 또는 대표 음식 - 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]) - - # 매장 정보: 외관 + 인테리어 - placement_plan['structure'][1]['recommended_images'].extend(categorized_images['매장외관']) - placement_plan['structure'][1]['recommended_images'].extend(categorized_images['인테리어']) - - # 메뉴 소개: 메뉴판 + 음식 - placement_plan['structure'][2]['recommended_images'].extend(categorized_images['메뉴판']) - placement_plan['structure'][2]['recommended_images'].extend(categorized_images['음식']) - - # 총평: 남은 음식 사진 또는 기타 - 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]) - - # 전체 이미지 순서 생성 - 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['usage_guide'] = [ - "📸 이미지 배치 가이드라인:", - "1. 각 섹션마다 2-3문장의 설명 후 이미지 삽입", - "2. 이미지마다 간단한 설명 텍스트 추가", - "3. 음식 사진은 가장 맛있어 보이는 각도로 배치", - "4. 마지막에 전체적인 분위기를 보여주는 사진으로 마무리" - ] - - return placement_plan + # def _create_image_placement_plan(self, image_analysis: Dict[str, Any], request: SnsContentGetRequest) -> Dict[ + # str, Any]: + # """ + # 네이버 블로그용 이미지 배치 계획 생성 + # """ + # images = image_analysis.get('results', []) + # if not images: + # return None + # + # # 이미지 타입별 분류 + # categorized_images = { + # '매장외관': [], + # '인테리어': [], + # '메뉴판': [], + # '음식': [], + # '사람': [], + # '기타': [] + # } + # + # for img in images: + # img_type = img.get('type', '기타') + # categorized_images[img_type].append(img) + # + # # 블로그 구조에 따른 이미지 배치 계획 + # #placement_plan = { + # # 'structure': [ + # # { + # # 'section': '인트로', + # # 'description': '첫인상과 방문 동기', + # # 'recommended_images': [], + # # 'placement_guide': '매장 외관이나 대표적인 음식 사진으로 시작' + # # }, + # # { + # # 'section': '매장 정보', + # # 'description': '위치, 분위기, 인테리어 소개', + # # 'recommended_images': [], + # # 'placement_guide': '매장 외관 → 내부 인테리어 순서로 배치' + # # }, + # # { + # # 'section': '메뉴 소개', + # # 'description': '주문한 메뉴와 상세 후기', + # # 'recommended_images': [], + # # 'placement_guide': '메뉴판 → 실제 음식 사진 순서로 배치' + # # }, + # # { + # # 'section': '총평', + # # 'description': '재방문 의향과 추천 이유', + # # 'recommended_images': [], + # # 'placement_guide': '가장 매력적인 음식 사진이나 전체 분위기 사진' + # # } + # # ], + # # 'image_sequence': [], + # # 'usage_guide': [] + # # } + # + # # 각 섹션에 적절한 이미지 배정 + # # 인트로: 매장외관 또는 대표 음식 + # 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]) + # + # # 매장 정보: 외관 + 인테리어 + # placement_plan['structure'][1]['recommended_images'].extend(categorized_images['매장외관']) + # placement_plan['structure'][1]['recommended_images'].extend(categorized_images['인테리어']) + # + # # 메뉴 소개: 메뉴판 + 음식 + # placement_plan['structure'][2]['recommended_images'].extend(categorized_images['메뉴판']) + # placement_plan['structure'][2]['recommended_images'].extend(categorized_images['음식']) + # + # # 총평: 남은 음식 사진 또는 기타 + # 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]) + # + # # 전체 이미지 순서 생성 + # 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['usage_guide'] = [ + # "📸 이미지 배치 가이드라인:", + # "1. 각 섹션마다 2-3문장의 설명 후 이미지 삽입", + # "2. 이미지마다 간단한 설명 텍스트 추가", + # "3. 음식 사진은 가장 맛있어 보이는 각도로 배치", + # "4. 마지막에 전체적인 분위기를 보여주는 사진으로 마무리" + # ] + # + # return placement_plan def _create_platform_specific_prompt(self, request: SnsContentGetRequest, image_analysis: Dict[str, Any], image_placement_plan: Dict[str, Any] = None) -> str: 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 86c279c..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 콘텐츠 생성 * @@ -45,7 +49,7 @@ public class SnsContentService implements SnsContentUseCase { public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List files) { //파일들 주소 가져옴 if(files != null) { - List urls = blobStorageService.uploadImage(files, "containerName"); + List urls = blobStorageService.uploadImage(files, contentImageContainer); request.setImages(urls); } From 69eaf6d8bd6da3cd991f96b2535e5a6c95b78bf5 Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Wed, 18 Jun 2025 15:38:42 +0900 Subject: [PATCH 30/61] Update deploy.yaml.template --- .../deployment/deploy.yaml.template | 70 ++++--------------- 1 file changed, 15 insertions(+), 55 deletions(-) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index 2a2defd..15bd41d 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -462,84 +462,44 @@ spec: type: ClusterIP --- -# deploy.yaml.template의 Ingress 부분 - 완전한 설정 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: smarketing-backend - namespace: ${namespace} + name: smarketing-ingress annotations: kubernetes.io/ingress.class: nginx - nginx.ingress.kubernetes.io/rewrite-target: /$2 - nginx.ingress.kubernetes.io/ssl-redirect: "false" - nginx.ingress.kubernetes.io/use-regex: "true" + # nginx.ingress.kubernetes.io/rewrite-target: /$2 # 이 줄 제거 + # nginx.ingress.kubernetes.io/use-regex: "true" # 이 줄 제거 spec: - ingressClassName: nginx rules: - host: smarketing.20.249.184.228.nip.io http: paths: - # Member 서비스 - 인증 관련 - - path: /api/auth(/|$)(.*) - pathType: ImplementationSpecific + - path: /api/auth + pathType: Prefix backend: service: - name: member + name: auth-service port: number: 80 - # Member 서비스 - 회원 관리 (누락된 경로!) - - path: /api/member(/|$)(.*) - pathType: ImplementationSpecific + - path: /api/store + pathType: Prefix backend: service: - name: member + name: store-service port: number: 80 - # Store 서비스 - - path: /api/store(/|$)(.*) - pathType: ImplementationSpecific + - path: /api/content + pathType: Prefix backend: service: - name: store + name: content-service port: number: 80 - # Store 서비스 - 매출 관련 - - path: /api/sales(/|$)(.*) - pathType: ImplementationSpecific + - path: /api/recommend + pathType: Prefix backend: service: - name: store - port: - number: 80 - # Store 서비스 - 메뉴 관련 - - path: /api/menu(/|$)(.*) - pathType: ImplementationSpecific - backend: - service: - name: store - port: - number: 80 - # Store 서비스 - 이미지 업로드 - - path: /api/images(/|$)(.*) - pathType: ImplementationSpecific - backend: - service: - name: store - port: - number: 80 - # Marketing Content 서비스 - - path: /api/content(/|$)(.*) - pathType: ImplementationSpecific - backend: - service: - name: marketing-content - port: - number: 80 - # AI Recommend 서비스 - - path: /api/recommend(/|$)(.*) - pathType: ImplementationSpecific - backend: - service: - name: ai-recommend + name: recommend-service port: number: 80 From bab2769e0849e894f8699df6dceff4c1f6fc4976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:51:55 +0900 Subject: [PATCH 31/61] chg blog setting --- smarketing-ai/services/sns_content_service.py | 195 +++++++++--------- 1 file changed, 96 insertions(+), 99 deletions(-) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index f8e837f..9f35f2c 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1450,8 +1450,8 @@ class SnsContentService: # 네이버 블로그인 경우 이미지 배치 계획 생성 image_placement_plan = None - # if request.platform == '네이버 블로그': - # image_placement_plan = self._create_image_placement_plan(image_analysis, request) + if request.platform == '네이버 블로그': + image_placement_plan = self._create_image_placement_plan(image_analysis, request) # 플랫폼별 특화 프롬프트 생성 prompt = self._create_platform_specific_prompt(request, image_analysis, image_placement_plan) @@ -1551,98 +1551,98 @@ class SnsContentService: return '기타' - # def _create_image_placement_plan(self, image_analysis: Dict[str, Any], request: SnsContentGetRequest) -> Dict[ - # str, Any]: - # """ - # 네이버 블로그용 이미지 배치 계획 생성 - # """ - # images = image_analysis.get('results', []) - # if not images: - # return None - # - # # 이미지 타입별 분류 - # categorized_images = { - # '매장외관': [], - # '인테리어': [], - # '메뉴판': [], - # '음식': [], - # '사람': [], - # '기타': [] - # } - # - # for img in images: - # img_type = img.get('type', '기타') - # categorized_images[img_type].append(img) - # - # # 블로그 구조에 따른 이미지 배치 계획 - # #placement_plan = { - # # 'structure': [ - # # { - # # 'section': '인트로', - # # 'description': '첫인상과 방문 동기', - # # 'recommended_images': [], - # # 'placement_guide': '매장 외관이나 대표적인 음식 사진으로 시작' - # # }, - # # { - # # 'section': '매장 정보', - # # 'description': '위치, 분위기, 인테리어 소개', - # # 'recommended_images': [], - # # 'placement_guide': '매장 외관 → 내부 인테리어 순서로 배치' - # # }, - # # { - # # 'section': '메뉴 소개', - # # 'description': '주문한 메뉴와 상세 후기', - # # 'recommended_images': [], - # # 'placement_guide': '메뉴판 → 실제 음식 사진 순서로 배치' - # # }, - # # { - # # 'section': '총평', - # # 'description': '재방문 의향과 추천 이유', - # # 'recommended_images': [], - # # 'placement_guide': '가장 매력적인 음식 사진이나 전체 분위기 사진' - # # } - # # ], - # # 'image_sequence': [], - # # 'usage_guide': [] - # # } - # - # # 각 섹션에 적절한 이미지 배정 - # # 인트로: 매장외관 또는 대표 음식 - # 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]) - # - # # 매장 정보: 외관 + 인테리어 - # placement_plan['structure'][1]['recommended_images'].extend(categorized_images['매장외관']) - # placement_plan['structure'][1]['recommended_images'].extend(categorized_images['인테리어']) - # - # # 메뉴 소개: 메뉴판 + 음식 - # placement_plan['structure'][2]['recommended_images'].extend(categorized_images['메뉴판']) - # placement_plan['structure'][2]['recommended_images'].extend(categorized_images['음식']) - # - # # 총평: 남은 음식 사진 또는 기타 - # 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]) - # - # # 전체 이미지 순서 생성 - # 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['usage_guide'] = [ - # "📸 이미지 배치 가이드라인:", - # "1. 각 섹션마다 2-3문장의 설명 후 이미지 삽입", - # "2. 이미지마다 간단한 설명 텍스트 추가", - # "3. 음식 사진은 가장 맛있어 보이는 각도로 배치", - # "4. 마지막에 전체적인 분위기를 보여주는 사진으로 마무리" - # ] - # - # return placement_plan + def _create_image_placement_plan(self, image_analysis: Dict[str, Any], request: SnsContentGetRequest) -> Dict[ + str, Any]: + """ + 네이버 블로그용 이미지 배치 계획 생성 + """ + images = image_analysis.get('results', []) + if not images: + return None + + # 이미지 타입별 분류 + categorized_images = { + '매장외관': [], + '인테리어': [], + '메뉴판': [], + '음식': [], + '사람': [], + '기타': [] + } + + for img in images: + img_type = img.get('type', '기타') + categorized_images[img_type].append(img) + + # 블로그 구조에 따른 이미지 배치 계획 + placement_plan = { + 'structure': [ + { + 'section': '인트로', + 'description': '첫인상과 방문 동기', + 'recommended_images': [], + 'placement_guide': '매장 외관이나 대표적인 음식 사진으로 시작' + }, + { + 'section': '매장 정보', + 'description': '위치, 분위기, 인테리어 소개', + 'recommended_images': [], + 'placement_guide': '매장 외관 → 내부 인테리어 순서로 배치' + }, + { + 'section': '메뉴 소개', + 'description': '주문한 메뉴와 상세 후기', + 'recommended_images': [], + 'placement_guide': '메뉴판 → 실제 음식 사진 순서로 배치' + }, + { + 'section': '총평', + 'description': '재방문 의향과 추천 이유', + 'recommended_images': [], + 'placement_guide': '가장 매력적인 음식 사진이나 전체 분위기 사진' + } + ], + 'image_sequence': [], + 'usage_guide': [] + } + + # 각 섹션에 적절한 이미지 배정 + # 인트로: 매장외관 또는 대표 음식 + 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]) + + # 매장 정보: 외관 + 인테리어 + placement_plan['structure'][1]['recommended_images'].extend(categorized_images['매장외관']) + placement_plan['structure'][1]['recommended_images'].extend(categorized_images['인테리어']) + + # 메뉴 소개: 메뉴판 + 음식 + placement_plan['structure'][2]['recommended_images'].extend(categorized_images['메뉴판']) + placement_plan['structure'][2]['recommended_images'].extend(categorized_images['음식']) + + # 총평: 남은 음식 사진 또는 기타 + 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]) + + # 전체 이미지 순서 생성 + 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['usage_guide'] = [ + "📸 이미지 배치 가이드라인:", + "1. 각 섹션마다 2-3문장의 설명 후 이미지 삽입", + "2. 이미지마다 간단한 설명 텍스트 추가", + "3. 음식 사진은 가장 맛있어 보이는 각도로 배치", + "4. 마지막에 전체적인 분위기를 보여주는 사진으로 마무리" + ] + + return placement_plan def _create_platform_specific_prompt(self, request: SnsContentGetRequest, image_analysis: Dict[str, Any], image_placement_plan: Dict[str, Any] = None) -> str: @@ -1777,9 +1777,8 @@ class SnsContentService: 1. 검색자의 궁금증을 해결하는 정보 중심 작성 2. 구체적인 가격, 위치, 운영시간 등 실용 정보 포함 3. 개인적인 경험과 솔직한 후기 작성 -4. 각 섹션마다 적절한 위치에 [IMAGE_X] 태그로 이미지 배치 위치 표시 -5. 이미지마다 간단한 설명 문구 추가 -6. 지역 정보와 접근성 정보 포함 +4. 이미지마다 간단한 설명 문구 추가 +5. 지역 정보와 접근성 정보 포함 **이미지 태그 사용법:** - [IMAGE_1]: 첫 번째 이미지 배치 위치 @@ -1791,8 +1790,6 @@ class SnsContentService: 네이버 검색에서 상위 노출되고, 실제로 도움이 되는 정보를 제공하는 블로그 포스트를 작성해주세요. 필수 요구사항을 반드시 참고하여 작성해주세요. -이미지 배치 위치를 [IMAGE_X] 태그로 명확히 표시해주세요. -이미지는 제공된 것만 사용하시고, 추가하진 말아주세요. """ return prompt From fe7f4a4b6fcbbf63376b95a4f2da03dce13af7d3 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 15:57:06 +0900 Subject: [PATCH 32/61] =?UTF-8?q?refactor:=20probe=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deployment/deploy.yaml.template | 95 +------------------ 1 file changed, 3 insertions(+), 92 deletions(-) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index 15bd41d..c9d65a6 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -182,29 +182,6 @@ spec: name: common-secret - secretRef: name: member-secret - startupProbe: - tcpSocket: - port: 8081 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 10 - livenessProbe: - httpGet: - path: /actuator/health - port: 8081 - initialDelaySeconds: 120 - periodSeconds: 30 - timeoutSeconds: 10 - failureThreshold: 3 - readinessProbe: - httpGet: - path: /actuator/health - port: 8081 - initialDelaySeconds: 60 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 --- apiVersion: apps/v1 @@ -248,29 +225,7 @@ spec: name: common-secret - secretRef: name: store-secret - startupProbe: - tcpSocket: - port: 8082 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 10 - livenessProbe: - httpGet: - path: /actuator/health - port: 8082 - initialDelaySeconds: 120 - periodSeconds: 30 - timeoutSeconds: 10 - failureThreshold: 3 - readinessProbe: - httpGet: - path: /actuator/health - port: 8082 - initialDelaySeconds: 60 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 + --- apiVersion: apps/v1 @@ -314,29 +269,7 @@ spec: name: common-secret - secretRef: name: marketing-content-secret - startupProbe: - tcpSocket: - port: 8083 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 10 - livenessProbe: - httpGet: - path: /actuator/health - port: 8083 - initialDelaySeconds: 120 - periodSeconds: 30 - timeoutSeconds: 10 - failureThreshold: 3 - readinessProbe: - httpGet: - path: /actuator/health - port: 8083 - initialDelaySeconds: 60 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 + --- apiVersion: apps/v1 @@ -380,29 +313,7 @@ spec: name: common-secret - secretRef: name: ai-recommend-secret - startupProbe: - tcpSocket: - port: 8084 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 10 - livenessProbe: - httpGet: - path: /actuator/health - port: 8084 - initialDelaySeconds: 120 - periodSeconds: 30 - timeoutSeconds: 10 - failureThreshold: 3 - readinessProbe: - httpGet: - path: /actuator/health - port: 8084 - initialDelaySeconds: 60 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 + --- # Services From e969189ce7fecc2f8378c5b4cb0485c424ffad2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:02:08 +0900 Subject: [PATCH 33/61] delete prompt --- smarketing-ai/services/sns_content_service.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index 9f35f2c..2956240 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1780,11 +1780,6 @@ class SnsContentService: 4. 이미지마다 간단한 설명 문구 추가 5. 지역 정보와 접근성 정보 포함 -**이미지 태그 사용법:** -- [IMAGE_1]: 첫 번째 이미지 배치 위치 -- [IMAGE_2]: 두 번째 이미지 배치 위치 -- 각 이미지 태그 다음 줄에 이미지 설명 문구 작성 - **필수 요구사항:** {request.requirement} or '유용한 정보를 제공하여 방문을 유도하는 신뢰성 있는 후기' From 3f6a6b603446840220c8801c58f0eef1533db604 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 16:04:09 +0900 Subject: [PATCH 34/61] =?UTF-8?q?refactor:=20probe=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- smarketing-java/deployment/deploy.yaml.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index c9d65a6..04836c2 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -413,4 +413,4 @@ spec: service: name: recommend-service port: - number: 80 + number: 80 \ No newline at end of file From d75f1f71fbd8bb24e8d329834ed92e0e699fe4db Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 16:09:54 +0900 Subject: [PATCH 35/61] refactor: Jenkinsfile --- smarketing-ai/deployment/Jenkinsfile | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/smarketing-ai/deployment/Jenkinsfile b/smarketing-ai/deployment/Jenkinsfile index e55f855..912d946 100644 --- a/smarketing-ai/deployment/Jenkinsfile +++ b/smarketing-ai/deployment/Jenkinsfile @@ -35,24 +35,24 @@ podTemplate( 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("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') { From 534698ca93fb45169806d29c1d8d5ac7bc9eef31 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 16:10:36 +0900 Subject: [PATCH 36/61] refactor: Jenkinsfile --- smarketing-java/deployment/Jenkinsfile | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index a84cf56..26ced99 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -43,22 +43,22 @@ podTemplate( 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("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') { From 0315e0b89de0fade8bb98fa4eb63f7299b1ae431 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 16:52:20 +0900 Subject: [PATCH 37/61] =?UTF-8?q?refactor:=20env=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- smarketing-java/deployment/deploy_env_vars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-java/deployment/deploy_env_vars b/smarketing-java/deployment/deploy_env_vars index 7a5d327..6cfb19e 100644 --- a/smarketing-java/deployment/deploy_env_vars +++ b/smarketing-java/deployment/deploy_env_vars @@ -9,7 +9,7 @@ image_org=smarketing # Application Settings replicas=1 -allowed_origins=http://20.249.171.38 +allowed_origins=http://20.249.154.194 # Security Settings jwt_secret_key=8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ From 3fdd233a70edf02fb2d5db6981338e3906bd118c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:54:08 +0900 Subject: [PATCH 38/61] Update sns_content_service.py --- smarketing-ai/services/sns_content_service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index 2956240..ac8bc77 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1713,6 +1713,11 @@ class SnsContentService: 5. 줄바꿈을 활용하여 가독성 향상 6. 해시태그는 본문과 자연스럽게 연결되도록 배치 +**이미지 태그 사용법:** +- [IMAGE_1]: 첫 번째 이미지 배치 위치 +- [IMAGE_2]: 두 번째 이미지 배치 위치 +- 각 이미지 태그 다음 줄에 이미지 설명 문구 작성 + **필수 요구사항:** {request.requirement} or '고객의 관심을 끌고 방문을 유도하는 매력적인 게시물' From 6313ef78e68dbc86c64086ac9be9bd84edbfa582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:03:20 +0900 Subject: [PATCH 39/61] Update sns_content_service.py --- smarketing-ai/services/sns_content_service.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index ac8bc77..f4a1d5e 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1709,9 +1709,10 @@ class SnsContentService: 1. 첫 문장은 반드시 관심을 끄는 후킹 문장으로 시작 2. 이모티콘을 적절히 활용하여 시각적 재미 추가 3. 스토리텔링을 통해 감정적 연결 유도 -4. 명확한 행동 유도 문구 포함 (팔로우, 댓글, 저장, 방문 등) -5. 줄바꿈을 활용하여 가독성 향상 -6. 해시태그는 본문과 자연스럽게 연결되도록 배치 +4. 각 섹션마다 적절한 위치에 [IMAGE_X] 태그로 이미지 배치 위치 표시 +5. 명확한 행동 유도 문구 포함 (팔로우, 댓글, 저장, 방문 등) +6. 줄바꿈을 활용하여 가독성 향상 +7. 해시태그는 본문과 자연스럽게 연결되도록 배치 **이미지 태그 사용법:** - [IMAGE_1]: 첫 번째 이미지 배치 위치 From 5f020d3f0b7e5fb6ac64d96fa7afe6d7f03ebf2b Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 17:13:44 +0900 Subject: [PATCH 40/61] =?UTF-8?q?refactor:=20allowed=20origins=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recommend/config/SecurityConfig.java | 88 +++++++++++++++++++ .../src/main/resources/application.yml | 4 +- .../content/config/SecurityConfig.java | 88 +++++++++++++++++++ .../src/main/resources/application.yml | 5 +- .../member/config/SecurityConfig.java | 88 +++++++++++++++++++ .../member/src/main/resources/application.yml | 4 +- .../store}/config/SecurityConfig.java | 10 ++- .../store/src/main/resources/application.yml | 4 +- 8 files changed, 284 insertions(+), 7 deletions(-) create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/SecurityConfig.java create mode 100644 smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/SecurityConfig.java create mode 100644 smarketing-java/member/src/main/java/com/won/smarketing/member/config/SecurityConfig.java rename smarketing-java/{common/src/main/java/com/won/smarketing/common => store/src/main/java/com/won/smarketing/store}/config/SecurityConfig.java (91%) 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..16fd541 100644 --- a/smarketing-java/ai-recommend/src/main/resources/application.yml +++ b/smarketing-java/ai-recommend/src/main/resources/application.yml @@ -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/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/resources/application.yml b/smarketing-java/marketing-content/src/main/resources/application.yml index 819d127..64a7bac 100644 --- a/smarketing-java/marketing-content/src/main/resources/application.yml +++ b/smarketing-java/marketing-content/src/main/resources/application.yml @@ -67,4 +67,7 @@ info: app: name: ${APP_NAME:smarketing-content} version: "1.0.0-MVP" - description: "AI 마케팅 서비스 MVP - content" \ No newline at end of file + description: "AI 마케팅 서비스 MVP - content" + + +allowed-origins: ${ALLOWED_ORIGINS:http://localhost:3000} \ No newline at end of file diff --git a/smarketing-java/member/src/main/java/com/won/smarketing/member/config/SecurityConfig.java b/smarketing-java/member/src/main/java/com/won/smarketing/member/config/SecurityConfig.java new file mode 100644 index 0000000..293caad --- /dev/null +++ b/smarketing-java/member/src/main/java/com/won/smarketing/member/config/SecurityConfig.java @@ -0,0 +1,88 @@ +package com.won.smarketing.member.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/member/src/main/resources/application.yml b/smarketing-java/member/src/main/resources/application.yml index 92741bc..6306701 100644 --- a/smarketing-java/member/src/main/resources/application.yml +++ b/smarketing-java/member/src/main/resources/application.yml @@ -53,4 +53,6 @@ info: app: name: ${APP_NAME:smarketing-member} version: "1.0.0-MVP" - description: "AI 마케팅 서비스 MVP - member" \ No newline at end of file + description: "AI 마케팅 서비스 MVP - member" + +allowed-origins: ${ALLOWED_ORIGINS:http://localhost:3000} \ No newline at end of file diff --git a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java b/smarketing-java/store/src/main/java/com/won/smarketing/store/config/SecurityConfig.java similarity index 91% rename from smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java rename to smarketing-java/store/src/main/java/com/won/smarketing/store/config/SecurityConfig.java index dcc1f20..69fe47a 100644 --- a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java +++ b/smarketing-java/store/src/main/java/com/won/smarketing/store/config/SecurityConfig.java @@ -1,7 +1,8 @@ -package com.won.smarketing.common.config; +package com.won.smarketing.store.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; @@ -25,10 +26,13 @@ import java.util.Arrays; @Configuration @EnableWebSecurity @RequiredArgsConstructor -public class SecurityConfig { +public class SecurityConfig +{ private final JwtAuthenticationFilter jwtAuthenticationFilter; + @Value("${allowed-origins}") + private String allowedOrigins; /** * Spring Security 필터 체인 설정 * @@ -72,7 +76,7 @@ public class SecurityConfig { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOriginPatterns(Arrays.asList("*")); + configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(","))); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setAllowCredentials(true); diff --git a/smarketing-java/store/src/main/resources/application.yml b/smarketing-java/store/src/main/resources/application.yml index 18a8934..42a2488 100644 --- a/smarketing-java/store/src/main/resources/application.yml +++ b/smarketing-java/store/src/main/resources/application.yml @@ -68,4 +68,6 @@ info: app: name: ${APP_NAME:smarketing-content} version: "1.0.0-MVP" - description: "AI 마케팅 서비스 MVP - content" \ No newline at end of file + description: "AI 마케팅 서비스 MVP - content" + +allowed-origins: ${ALLOWED_ORIGINS:http://localhost:3000} \ No newline at end of file From 446fe062a2490eb03b5fc9acd3a8d38afdfd84a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:14:56 +0900 Subject: [PATCH 41/61] Update sns_content_service.py --- smarketing-ai/services/sns_content_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index f4a1d5e..07da33a 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문장마다 입력받은 이미지 배치', '이미지 설명은 간결하고 매력적으로', '마지막에 대표 이미지로 마무리' ] From 2c1833ebe1d6be0746597e960dc5217605b2f867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:26:22 +0900 Subject: [PATCH 42/61] Update sns_content_service.py --- smarketing-ai/services/sns_content_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index 07da33a..c62baad 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1791,6 +1791,7 @@ class SnsContentService: 네이버 검색에서 상위 노출되고, 실제로 도움이 되는 정보를 제공하는 블로그 포스트를 작성해주세요. 필수 요구사항을 반드시 참고하여 작성해주세요. +이미지 배치 위치를 [IMAGE_X] 태그로 명확히 표시해주세요. """ return prompt From 9434de944c4503f8c7ca27c9c59dedfe4668c369 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 17:34:48 +0900 Subject: [PATCH 43/61] refactor --- .../common/config/SecurityConfig.java | 2 +- .../service/PosterContentService.java | 17 +- .../content/domain/model/store/MenuData.java | 21 ++ .../content/domain/model/store/StoreData.java | 22 ++ .../domain/model/store/StoreWithMenuData.java | 13 + .../domain/service/AiPosterGenerator.java | 3 +- .../domain/service/StoreDataProvider.java | 11 + .../external/PythonAiPosterGenerator.java | 7 +- .../external/StoreApiDataProvider.java | 310 ++++++++++++++++++ .../member/src/main/resources/application.yml | 2 +- 10 files changed, 401 insertions(+), 7 deletions(-) create mode 100644 smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/MenuData.java create mode 100644 smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreData.java create mode 100644 smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreWithMenuData.java create mode 100644 smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/StoreDataProvider.java create mode 100644 smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/StoreApiDataProvider.java diff --git a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java b/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java index dcc1f20..8bc7076 100644 --- a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java +++ b/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java @@ -76,7 +76,7 @@ public class SecurityConfig { 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/application/service/PosterContentService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java index 043a8b2..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,15 +6,18 @@ 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; @@ -37,6 +40,7 @@ public class PosterContentService implements PosterContentUseCase { private final ContentRepository contentRepository; private final AiPosterGenerator aiPosterGenerator; private final BlobStorageService blobStorageService; + private final StoreDataProvider storeDataProvider; /** * 포스터 콘텐츠 생성 @@ -51,9 +55,13 @@ public class PosterContentService implements PosterContentUseCase { // 1. 이미지 blob storage에 저장하고 request 저장 List imageUrls = blobStorageService.uploadImage(images, posterImageContainer); request.setImages(imageUrls); + + // 매장 정보 호출 + String userId = getCurrentUserId(); + StoreWithMenuData storeWithMenuData = storeDataProvider.getStoreWithMenuData(userId); // 2. AI 요청 - String generatedPoster = aiPosterGenerator.generatePoster(request); + String generatedPoster = aiPosterGenerator.generatePoster(request, storeWithMenuData); return PosterContentCreateResponse.builder() .contentId(null) // 임시 생성이므로 ID 없음 @@ -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/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/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 4ea396a..1220a5e 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,6 @@ package com.won.smarketing.content.infrastructure.external; +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; @@ -34,12 +35,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); @@ -75,7 +76,7 @@ public class PythonAiPosterGenerator implements AiPosterGenerator { * Python 서비스의 PosterContentGetRequest 모델에 맞춤 * 카테고리, */ - private Map buildRequestBody(PosterContentCreateRequest request) { + private Map buildRequestBody(PosterContentCreateRequest request, StoreWithMenuData storeWithMenuData) { Map requestBody = new HashMap<>(); // 기본 정보 diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/StoreApiDataProvider.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/StoreApiDataProvider.java new file mode 100644 index 0000000..8480161 --- /dev/null +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/StoreApiDataProvider.java @@ -0,0 +1,310 @@ +package com.won.smarketing.content.infrastructure.external; + +import com.won.smarketing.common.exception.BusinessException; +import com.won.smarketing.common.exception.ErrorCode; +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.StoreDataProvider; +import jakarta.servlet.http.HttpServletRequest; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 매장 API 데이터 제공자 구현체 + */ +@Slf4j +@Service // 추가된 어노테이션 +@RequiredArgsConstructor +public class StoreApiDataProvider implements StoreDataProvider { + + private final WebClient webClient; + + @Value("${external.store-service.base-url}") + private String storeServiceBaseUrl; + + @Value("${external.store-service.timeout}") + private int timeout; + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER_PREFIX = "Bearer "; + + public StoreWithMenuData getStoreWithMenuData(String userId) { + log.info("매장 정보와 메뉴 정보 통합 조회 시작: userId={}", userId); + + try { + // 매장 정보와 메뉴 정보를 병렬로 조회 + StoreData storeData = getStoreDataByUserId(userId); + List menuDataList = getMenusByStoreId(storeData.getStoreId()); + + StoreWithMenuData result = StoreWithMenuData.builder() + .storeData(storeData) + .menuDataList(menuDataList) + .build(); + + log.info("매장 정보와 메뉴 정보 통합 조회 완료: storeId={}, storeName={}, menuCount={}", + storeData.getStoreId(), storeData.getStoreName(), menuDataList.size()); + + return result; + + } catch (Exception e) { + log.error("매장 정보와 메뉴 정보 통합 조회 실패, Mock 데이터 반환: storeId={}", userId, e); + + // 실패 시 Mock 데이터 반환 + return StoreWithMenuData.builder() + .storeData(createMockStoreData(userId)) + .menuDataList(createMockMenuData(6L)) + .build(); + } + } + + public StoreData getStoreDataByUserId(String userId) { + try { + log.debug("매장 정보 실시간 조회: userId={}", userId); + return callStoreServiceByUserId(userId); + + } catch (Exception e) { + log.error("매장 정보 조회 실패, Mock 데이터 반환: userId={}, error={}", userId, e.getMessage()); + return createMockStoreData(userId); + } + } + + + public List getMenusByStoreId(Long storeId) { + log.info("매장 메뉴 조회 시작: storeId={}", storeId); + + try { + return callMenuService(storeId); + } catch (Exception e) { + log.error("메뉴 조회 실패, Mock 데이터 반환: storeId={}", storeId, e); + return createMockMenuData(storeId); + } + } + + private StoreData callStoreServiceByUserId(String userId) { + + try { + StoreApiResponse response = webClient + .get() + .uri(storeServiceBaseUrl + "/api/store") + .header("Authorization", "Bearer " + getCurrentJwtToken()) // JWT 토큰 추가 + .retrieve() + .bodyToMono(StoreApiResponse.class) + .timeout(Duration.ofMillis(timeout)) + .block(); + + log.info("response : {}", response.getData().getStoreName()); + log.info("response : {}", response.getData().getStoreId()); + + if (response != null && response.getData() != null) { + StoreApiResponse.StoreInfo storeInfo = response.getData(); + return StoreData.builder() + .storeId(storeInfo.getStoreId()) + .storeName(storeInfo.getStoreName()) + .businessType(storeInfo.getBusinessType()) + .location(storeInfo.getAddress()) + .description(storeInfo.getDescription()) + .seatCount(storeInfo.getSeatCount()) + .build(); + } + } catch (WebClientResponseException e) { + if (e.getStatusCode().value() == 404) { + throw new BusinessException(ErrorCode.STORE_NOT_FOUND); + } + log.error("매장 서비스 호출 실패: {}", e.getMessage()); + } + + return createMockStoreData(userId); + } + + private String getCurrentJwtToken() { + try { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + + if (attributes == null) { + log.warn("RequestAttributes를 찾을 수 없음 - HTTP 요청 컨텍스트 없음"); + return null; + } + + HttpServletRequest request = attributes.getRequest(); + String bearerToken = request.getHeader(AUTHORIZATION_HEADER); + + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) { + String token = bearerToken.substring(BEARER_PREFIX.length()); + log.debug("JWT 토큰 추출 성공: {}...", token.substring(0, Math.min(10, token.length()))); + return token; + } else { + log.warn("Authorization 헤더에서 Bearer 토큰을 찾을 수 없음: {}", bearerToken); + return null; + } + + } catch (Exception e) { + log.error("JWT 토큰 추출 중 오류 발생: {}", e.getMessage()); + return null; + } + } + + private List callMenuService(Long storeId) { + try { + MenuApiResponse response = webClient + .get() + .uri(storeServiceBaseUrl + "/api/menu/store/" + storeId) + .retrieve() + .bodyToMono(MenuApiResponse.class) + .timeout(Duration.ofMillis(timeout)) + .block(); + + if (response != null && response.getData() != null && !response.getData().isEmpty()) { + List menuDataList = response.getData().stream() + .map(this::toMenuData) + .collect(Collectors.toList()); + + log.info("매장 메뉴 조회 성공: storeId={}, menuCount={}", storeId, menuDataList.size()); + return menuDataList; + } + } catch (WebClientResponseException e) { + if (e.getStatusCode().value() == 404) { + log.warn("매장의 메뉴 정보가 없습니다: storeId={}", storeId); + return Collections.emptyList(); + } + log.error("메뉴 서비스 호출 실패: storeId={}, error={}", storeId, e.getMessage()); + } catch (WebClientException e) { + log.error("메뉴 서비스 연결 실패: storeId={}, error={}", storeId, e.getMessage()); + } + + return createMockMenuData(storeId); + } + + /** + * MenuResponse를 MenuData로 변환 + */ + private MenuData toMenuData(MenuApiResponse.MenuInfo menuInfo) { + return MenuData.builder() + .menuId(menuInfo.getMenuId()) + .menuName(menuInfo.getMenuName()) + .category(menuInfo.getCategory()) + .price(menuInfo.getPrice()) + .description(menuInfo.getDescription()) + .build(); + } + + private StoreData createMockStoreData(String userId) { + return StoreData.builder() + .storeName("테스트 카페 " + userId) + .businessType("카페") + .location("서울시 강남구") + .build(); + } + + private List createMockMenuData(Long storeId) { + log.info("Mock 메뉴 데이터 생성: storeId={}", storeId); + + return List.of( + MenuData.builder() + .menuId(1L) + .menuName("아메리카노") + .category("음료") + .price(4000) + .description("깊고 진한 맛의 아메리카노") + .build(), + MenuData.builder() + .menuId(2L) + .menuName("카페라떼") + .category("음료") + .price(4500) + .description("부드러운 우유 거품이 올라간 카페라떼") + .build(), + MenuData.builder() + .menuId(3L) + .menuName("치즈케이크") + .category("디저트") + .price(6000) + .description("진한 치즈 맛의 수제 케이크") + + .build() + ); + } + + @Getter + private static class StoreApiResponse { + private int status; + private String message; + private StoreInfo data; + + public int getStatus() { return status; } + public void setStatus(int status) { this.status = status; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + public StoreInfo getData() { return data; } + public void setData(StoreInfo data) { this.data = data; } + + @Getter + static class StoreInfo { + private Long storeId; + private String storeName; + private String businessType; + private String address; + private String description; + private Integer seatCount; + } + } + + /** + * Menu API 응답 DTO (새로 추가) + */ + private static class MenuApiResponse { + private List data; + private String message; + private boolean success; + + public List getData() { return data; } + public void setData(List data) { this.data = data; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + public boolean isSuccess() { return success; } + public void setSuccess(boolean success) { this.success = success; } + + public static class MenuInfo { + private Long menuId; + private String menuName; + private String category; + private Integer price; + private String description; + private String image; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public Long getMenuId() { return menuId; } + public void setMenuId(Long menuId) { this.menuId = menuId; } + public String getMenuName() { return menuName; } + public void setMenuName(String menuName) { this.menuName = menuName; } + public String getCategory() { return category; } + public void setCategory(String category) { this.category = category; } + public Integer getPrice() { return price; } + public void setPrice(Integer price) { this.price = price; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public String getImage() { return image; } + public void setImage(String image) { this.image = image; } + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + public LocalDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } + } + } +} \ No newline at end of file diff --git a/smarketing-java/member/src/main/resources/application.yml b/smarketing-java/member/src/main/resources/application.yml index 92741bc..46d38e4 100644 --- a/smarketing-java/member/src/main/resources/application.yml +++ b/smarketing-java/member/src/main/resources/application.yml @@ -53,4 +53,4 @@ info: app: name: ${APP_NAME:smarketing-member} version: "1.0.0-MVP" - description: "AI 마케팅 서비스 MVP - member" \ No newline at end of file + description: "AI 마케팅 서비스 MVP - member" From dc5d31442bac6198c662e6ea5e1c29b97ed92e22 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 17:47:36 +0900 Subject: [PATCH 44/61] refactor: Jenkinsfile v2 --- smarketing-ai/deployment/Jenkinsfile | 36 +++++++++++++------------- smarketing-java/deployment/Jenkinsfile | 32 +++++++++++------------ 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/smarketing-ai/deployment/Jenkinsfile b/smarketing-ai/deployment/Jenkinsfile index 912d946..e55f855 100644 --- a/smarketing-ai/deployment/Jenkinsfile +++ b/smarketing-ai/deployment/Jenkinsfile @@ -35,24 +35,24 @@ podTemplate( 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("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') { diff --git a/smarketing-java/deployment/Jenkinsfile b/smarketing-java/deployment/Jenkinsfile index 26ced99..a84cf56 100644 --- a/smarketing-java/deployment/Jenkinsfile +++ b/smarketing-java/deployment/Jenkinsfile @@ -43,22 +43,22 @@ podTemplate( 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("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') { From de9b6c7142ffa664f24e17031aff94d2b2355f0e Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 18:12:11 +0900 Subject: [PATCH 45/61] refactor: ingress namespace --- smarketing-java/deployment/deploy.yaml.template | 3 ++- smarketing-java/deployment/deploy_env_vars | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index 04836c2..fd5df45 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -377,13 +377,14 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: smarketing-ingress + namespace: ${namespace} annotations: kubernetes.io/ingress.class: nginx # nginx.ingress.kubernetes.io/rewrite-target: /$2 # 이 줄 제거 # nginx.ingress.kubernetes.io/use-regex: "true" # 이 줄 제거 spec: rules: - - host: smarketing.20.249.184.228.nip.io + - host: ${ingress_host} http: paths: - path: /api/auth diff --git a/smarketing-java/deployment/deploy_env_vars b/smarketing-java/deployment/deploy_env_vars index 6cfb19e..5e90919 100644 --- a/smarketing-java/deployment/deploy_env_vars +++ b/smarketing-java/deployment/deploy_env_vars @@ -8,6 +8,7 @@ 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.154.194 From a6157bff26a5550862aff3ca70966bd6a3f72ca7 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 18:24:09 +0900 Subject: [PATCH 46/61] =?UTF-8?q?refactor:=20ingress=20=EC=B5=9C=EC=A2=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deployment/deploy.yaml.template | 76 +++++++++---------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index fd5df45..ee6cf84 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -373,45 +373,37 @@ spec: type: ClusterIP --- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: smarketing-ingress - namespace: ${namespace} - annotations: - kubernetes.io/ingress.class: nginx - # nginx.ingress.kubernetes.io/rewrite-target: /$2 # 이 줄 제거 - # nginx.ingress.kubernetes.io/use-regex: "true" # 이 줄 제거 -spec: - rules: - - host: ${ingress_host} - http: - paths: - - path: /api/auth - pathType: Prefix - backend: - service: - name: auth-service - port: - number: 80 - - path: /api/store - pathType: Prefix - backend: - service: - name: store-service - port: - number: 80 - - path: /api/content - pathType: Prefix - backend: - service: - name: content-service - port: - number: 80 - - path: /api/recommend - pathType: Prefix - backend: - service: - name: recommend-service - port: - number: 80 \ No newline at end of file + + paths: + - backend: + service: + name: member + port: + number: 80 + path: /api/auth + pathType: Prefix + - backend: + service: + name: store + port: + number: 80 + path: /api/store + pathType: Prefix + - backend: + service: + name: marketing-content + port: + number: 80 + path: /api/content + pathType: Prefix + - backend: + service: + name: ai-recommend + port: + number: 80 + path: /api/recommend + pathType: Prefix +status: + loadBalancer: + ingress: + - ip: 20.249.184.228 From 90922194993c8c5eeebd989033ee7c003eb0e546 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 18:31:53 +0900 Subject: [PATCH 47/61] =?UTF-8?q?refactor:=20ingress=20=EC=B5=9C=EC=A2=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deployment/deploy.yaml.template | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index ee6cf84..a17dae6 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -373,37 +373,43 @@ spec: type: ClusterIP --- - - paths: - - backend: - service: - name: member - port: - number: 80 - path: /api/auth - pathType: Prefix - - backend: - service: - name: store - port: - number: 80 - path: /api/store - pathType: Prefix - - backend: - service: - name: marketing-content - port: - number: 80 - path: /api/content - pathType: Prefix - - backend: - service: - name: ai-recommend - port: - number: 80 - path: /api/recommend - pathType: Prefix -status: - loadBalancer: - ingress: - - ip: 20.249.184.228 +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: smarketing-ingress + annotations: + kubernetes.io/ingress.class: nginx +spec: + ingressClassName: nginx + rules: + - host: smarketing.20.249.184.228.nip.io + http: + paths: + - path: /api/auth + pathType: Prefix + backend: + service: + name: member + port: + number: 80 + - path: /api/store + pathType: Prefix + backend: + service: + name: store + port: + number: 80 + - path: /api/content + pathType: Prefix + backend: + service: + name: marketing-content + port: + number: 80 + - path: /api/recommend + pathType: Prefix + backend: + service: + name: ai-recommend + port: + number: 80 From a6599438e9d7f053ef06c629289a11acc80ceb4a Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 18:39:24 +0900 Subject: [PATCH 48/61] =?UTF-8?q?refactor:=20ingress=20=EC=A7=84=EC=A7=9C?= =?UTF-8?q?=EC=B5=9C=EC=A2=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- smarketing-java/deployment/deploy.yaml.template | 1 + 1 file changed, 1 insertion(+) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index a17dae6..78cc468 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -413,3 +413,4 @@ spec: name: ai-recommend port: number: 80 + From 8e87c05af0fa75d1be1da62457336a23249d3061 Mon Sep 17 00:00:00 2001 From: yuhalog <62270401+yuhalog@users.noreply.github.com> Date: Thu, 19 Jun 2025 09:09:55 +0900 Subject: [PATCH 49/61] Update deploy.yaml.template --- smarketing-java/deployment/deploy.yaml.template | 1 + 1 file changed, 1 insertion(+) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index 78cc468..1f54a76 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -377,6 +377,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: smarketing-ingress + namespace: ${namespace} annotations: kubernetes.io/ingress.class: nginx spec: From 8717659c0427e19576b39ddc7e290df9637ae719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Thu, 19 Jun 2025 09:39:30 +0900 Subject: [PATCH 50/61] set image cnt --- smarketing-ai/services/sns_content_service.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index c62baad..8067403 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -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,7 +1606,8 @@ class SnsContentService: } ], 'image_sequence': [], - 'usage_guide': [] + 'usage_guide': [], + 'actual_image_count': actual_image_count # 🔥 실제 이미지 수 추가 } # 각 섹션에 적절한 이미지 배정 @@ -1633,6 +1637,9 @@ class SnsContentService: 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'] = [ "📸 이미지 배치 가이드라인:", @@ -1735,6 +1742,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: @@ -1786,12 +1796,18 @@ class SnsContentService: 4. 이미지마다 간단한 설명 문구 추가 5. 지역 정보와 접근성 정보 포함 +**⚠️ 중요한 제약사항:** +- 반드시 제공된 {actual_image_count}개의 이미지 개수를 초과하지 마세요 +- [IMAGE_{actual_image_count}]까지만 사용하세요 +- {actual_image_count}개를 초과하는 [IMAGE_X] 태그는 절대 사용 금지 + **필수 요구사항:** {request.requirement} or '유용한 정보를 제공하여 방문을 유도하는 신뢰성 있는 후기' 네이버 검색에서 상위 노출되고, 실제로 도움이 되는 정보를 제공하는 블로그 포스트를 작성해주세요. 필수 요구사항을 반드시 참고하여 작성해주세요. 이미지 배치 위치를 [IMAGE_X] 태그로 명확히 표시해주세요. + """ return prompt From a897b10851c14df6fd9e673f0edfab4a1765696a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Thu, 19 Jun 2025 09:43:41 +0900 Subject: [PATCH 51/61] Update sns_content_service.py --- smarketing-ai/services/sns_content_service.py | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index 8067403..72e49ce 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1612,11 +1612,57 @@ class SnsContentService: # 각 섹션에 적절한 이미지 배정 # 인트로: 매장외관 또는 대표 음식 - 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]) + 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]) + + 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]) + + 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 + + # 매장 정보: 외관 + 인테리어 placement_plan['structure'][1]['recommended_images'].extend(categorized_images['매장외관']) placement_plan['structure'][1]['recommended_images'].extend(categorized_images['인테리어']) From d497ffd8b504506170fe8f9f37f6061d0935b681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Thu, 19 Jun 2025 09:46:16 +0900 Subject: [PATCH 52/61] Update sns_content_service.py --- smarketing-ai/services/sns_content_service.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index 72e49ce..111ce79 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1610,8 +1610,6 @@ class SnsContentService: 'actual_image_count': actual_image_count # 🔥 실제 이미지 수 추가 } - # 각 섹션에 적절한 이미지 배정 - # 인트로: 매장외관 또는 대표 음식 # 🔥 핵심: 실제 이미지 수에 따라 배치 전략 조정 if actual_image_count == 1: # 이미지 1개: 가장 대표적인 위치에 배치 @@ -1662,22 +1660,7 @@ class SnsContentService: else: break - - # 매장 정보: 외관 + 인테리어 - placement_plan['structure'][1]['recommended_images'].extend(categorized_images['매장외관']) - placement_plan['structure'][1]['recommended_images'].extend(categorized_images['인테리어']) - - # 메뉴 소개: 메뉴판 + 음식 - placement_plan['structure'][2]['recommended_images'].extend(categorized_images['메뉴판']) - placement_plan['structure'][2]['recommended_images'].extend(categorized_images['음식']) - - # 총평: 남은 음식 사진 또는 기타 - 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]) - - # 전체 이미지 순서 생성 + # 전체 이미지 순서 생성 (실제 사용될 이미지만) for section in placement_plan['structure']: for img in section['recommended_images']: if img not in placement_plan['image_sequence']: From 62683407cf483dfa6cd1dfbab23d7737c306d43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Thu, 19 Jun 2025 09:50:56 +0900 Subject: [PATCH 53/61] Update sns_content_service.py --- smarketing-ai/services/sns_content_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index 111ce79..7a1d82f 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -2007,7 +2007,7 @@ class SnsContentService: image_description = f"🏠 {description}" elif img_type == '메뉴판': image_description = f"📋 {description}" - else: + else: image_description = f"📸 {description}" # HTML 이미지 태그로 변환 From 7537f862e96bfbdcdc6f7ce44a9f406feba19948 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Thu, 19 Jun 2025 09:53:12 +0900 Subject: [PATCH 54/61] =?UTF-8?q?refactor:=20python=20ai=20api=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=20=EC=A3=BC=EC=86=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 4 +-- .../external/PythonAiPosterGenerator.java | 31 +++++++++++++++++-- .../src/main/resources/application.yml | 4 +++ .../member/src/main/resources/application.yml | 3 -- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/smarketing-java/ai-recommend/src/main/resources/application.yml b/smarketing-java/ai-recommend/src/main/resources/application.yml index 16fd541..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} 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 1220a5e..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,7 @@ 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; @@ -12,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를 활용한 포스터 생성 구현체 @@ -47,12 +51,12 @@ public class PythonAiPosterGenerator implements AiPosterGenerator { // Python AI 서비스 호출 Map response = webClient .post() - .uri("http://localhost:5001" + "/api/ai/poster") + .uri(aiServiceBaseUrl + "/api/ai/poster") .header("Content-Type", "application/json") .bodyValue(requestBody) .retrieve() .bodyToMono(Map.class) - .timeout(Duration.ofSeconds(90)) // 포스터 생성은 시간이 오래 걸릴 수 있음 + .timeout(Duration.ofSeconds(90)) .block(); // 응답에서 content(이미지 URL) 추출 @@ -79,6 +83,29 @@ public class PythonAiPosterGenerator implements AiPosterGenerator { private Map buildRequestBody(PosterContentCreateRequest request, StoreWithMenuData storeWithMenuData) { Map requestBody = new HashMap<>(); +// TODO : 매장 정보 호출 후 request + +// StoreData storeData = storeWithMenuData.getStoreData(); +// List menuDataList = storeWithMenuData.getMenuDataList(); +// +// List> menuList = menuDataList.stream() +// .map(menu -> { +// Map menuMap = new HashMap<>(); +// menuMap.put("menu_id", menu.getMenuId()); +// menuMap.put("menu_name", menu.getMenuName()); +// menuMap.put("category", menu.getCategory()); +// menuMap.put("price", menu.getPrice()); +// menuMap.put("description", menu.getDescription()); +// return menuMap; +// }) +// .collect(Collectors.toList()); +// +// requestBody.put("store_name", storeData.getStoreName()); +// requestBody.put("business_type", storeData.getBusinessType()); +// requestBody.put("location", storeData.getLocation()); +// requestBody.put("seat_count", storeData.getSeatCount()); +// requestBody.put("menu_list", menuList); + // 기본 정보 requestBody.put("title", request.getTitle()); requestBody.put("category", request.getCategory()); diff --git a/smarketing-java/marketing-content/src/main/resources/application.yml b/smarketing-java/marketing-content/src/main/resources/application.yml index 64a7bac..bab1983 100644 --- a/smarketing-java/marketing-content/src/main/resources/application.yml +++ b/smarketing-java/marketing-content/src/main/resources/application.yml @@ -37,6 +37,10 @@ logging: external: ai-service: base-url: ${AI_SERVICE_BASE_URL:http://20.249.113.247:5001} + store-service: + base-url: ${STORE_SERVICE_URL:http://smarketing.20.249.184.228.nip.io} + timeout: ${STORE_SERVICE_TIMEOUT:5000} + azure: storage: account-name: ${AZURE_STORAGE_ACCOUNT_NAME:stdigitalgarage02} diff --git a/smarketing-java/member/src/main/resources/application.yml b/smarketing-java/member/src/main/resources/application.yml index 6812fc3..912bca3 100644 --- a/smarketing-java/member/src/main/resources/application.yml +++ b/smarketing-java/member/src/main/resources/application.yml @@ -54,8 +54,5 @@ info: name: ${APP_NAME:smarketing-member} version: "1.0.0-MVP" description: "AI 마케팅 서비스 MVP - member" -<<<<<<< HEAD -======= allowed-origins: ${ALLOWED_ORIGINS:http://localhost:3000} ->>>>>>> 2c1833ebe1d6be0746597e960dc5217605b2f867 From ac69f230ffd9e3612ac9366bc21c0191bf2ca171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:21:48 +0900 Subject: [PATCH 55/61] chg insta prompt --- smarketing-ai/services/sns_content_service.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index 7a1d82f..8dd860c 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1710,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""" 당신은 인스타그램 마케팅 전문가입니다. 소상공인 음식점을 위한 매력적인 인스타그램 게시물을 작성해주세요. **🍸 가게 정보:** @@ -1724,6 +1733,8 @@ class SnsContentService: - 이벤트: {request.eventName or '특별 이벤트'} - 독자층: {request.target} +{image_tag_usage} + **📱 인스타그램 특화 요구사항:** - 글 구조: {platform_spec['content_structure']} - 최대 길이: {platform_spec['max_length']}자 @@ -1750,6 +1761,11 @@ class SnsContentService: 6. 줄바꿈을 활용하여 가독성 향상 7. 해시태그는 본문과 자연스럽게 연결되도록 배치 +**⚠️ 중요한 제약사항:** +- 반드시 제공된 {actual_image_count}개의 이미지 개수를 초과하지 마세요 +- [IMAGE_{actual_image_count}]까지만 사용하세요 +- 더 많은 이미지 태그를 사용하면 오류가 발생합니다 + **이미지 태그 사용법:** - [IMAGE_1]: 첫 번째 이미지 배치 위치 - [IMAGE_2]: 두 번째 이미지 배치 위치 @@ -1856,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: @@ -2007,7 +2031,7 @@ class SnsContentService: image_description = f"🏠 {description}" elif img_type == '메뉴판': image_description = f"📋 {description}" - else: + else: image_description = f"📸 {description}" # HTML 이미지 태그로 변환 From 67188a9e7a75d2582e12540f503088bdbbc8ac2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:24:50 +0900 Subject: [PATCH 56/61] chg sns format --- .../java/com/won/smarketing/store/dto/StoreCreateRequest.java | 4 ++-- .../main/java/com/won/smarketing/store/dto/StoreResponse.java | 4 ++-- .../java/com/won/smarketing/store/dto/StoreUpdateRequest.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/StoreCreateRequest.java b/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/StoreCreateRequest.java index ebb72c6..0cdbbf0 100644 --- a/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/StoreCreateRequest.java +++ b/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/StoreCreateRequest.java @@ -46,12 +46,12 @@ public class StoreCreateRequest { @Schema(description = "좌석 수", example = "20") private Integer seatCount; - @Schema(description = "SNS 계정 정보", example = "인스타그램: @mystore") + @Schema(description = "SNS 계정 정보", example = "@mystore") @Size(max = 500, message = "SNS 계정 정보는 500자 이하여야 합니다") private String instaAccounts; @Size(max = 500, message = "SNS 계정 정보는 500자 이하여야 합니다") - @Schema(description = "블로그 계정 정보", example = "블로그: mystore") + @Schema(description = "블로그 계정 정보", example = "mystore") private String blogAccounts; @Schema(description = "매장 설명", example = "따뜻한 분위기의 동네 카페입니다.") diff --git a/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/StoreResponse.java b/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/StoreResponse.java index 9c0bce0..8bc2c95 100644 --- a/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/StoreResponse.java +++ b/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/StoreResponse.java @@ -47,10 +47,10 @@ public class StoreResponse { @Schema(description = "좌석 수", example = "20") private Integer seatCount; - @Schema(description = "블로그 계정 정보", example = "블로그: mystore") + @Schema(description = "블로그 계정 정보", example = "mystore") private String blogAccounts; - @Schema(description = "인스타 계정 정보", example = "인스타그램: @mystore") + @Schema(description = "인스타 계정 정보", example = "@mystore") private String instaAccounts; @Schema(description = "매장 설명", example = "따뜻한 분위기의 동네 카페입니다.") diff --git a/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/StoreUpdateRequest.java b/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/StoreUpdateRequest.java index 1d235b7..acb119d 100644 --- a/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/StoreUpdateRequest.java +++ b/smarketing-java/store/src/main/java/com/won/smarketing/store/dto/StoreUpdateRequest.java @@ -43,11 +43,11 @@ public class StoreUpdateRequest { @Schema(description = "좌석 수", example = "20") private Integer seatCount; - @Schema(description = "인스타 계정 정보", example = "인스타그램: @mystore") + @Schema(description = "인스타 계정 정보", example = "@mystore") @Size(max = 500, message = "인스타 계정 정보는 500자 이하여야 합니다") private String instaAccounts; - @Schema(description = "블로그 계정 정보", example = "블로그: mystore") + @Schema(description = "블로그 계정 정보", example = "mystore") @Size(max = 500, message = "SNS 계정 정보는 500자 이하여야 합니다") private String blogAccounts; From c7f258aa7f858b2d5fcb099548f507e370f8a64e Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Thu, 19 Jun 2025 10:31:15 +0900 Subject: [PATCH 57/61] Create argocd.yaml --- smarketing-java/deployment/argocd.yaml | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 smarketing-java/deployment/argocd.yaml diff --git a/smarketing-java/deployment/argocd.yaml b/smarketing-java/deployment/argocd.yaml new file mode 100644 index 0000000..4e79018 --- /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-secret + extraArgs: + - --insecure # ArgoCD 서버가 TLS 종료를 Ingress에 위임 + +configs: + params: + server.insecure: true # Ingress에서 TLS를 처리하므로 ArgoCD 서버는 HTTP로 통신 +certificate: + enabled: false # 자체 서명 인증서 사용 비활성화 (외부 인증서 사용 시) From 7374762c1cb0b7059ddce93fa3f5a85745ebf1aa Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Thu, 19 Jun 2025 10:32:12 +0900 Subject: [PATCH 58/61] Update argocd.yaml --- smarketing-java/deployment/argocd.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-java/deployment/argocd.yaml b/smarketing-java/deployment/argocd.yaml index 4e79018..21a6122 100644 --- a/smarketing-java/deployment/argocd.yaml +++ b/smarketing-java/deployment/argocd.yaml @@ -17,7 +17,7 @@ server: annotations: kubernetes.io/ingress.class: nginx tls: - - secretName: argocd-tls-secret + - secretName: argocd-tls-smarketing-secret extraArgs: - --insecure # ArgoCD 서버가 TLS 종료를 Ingress에 위임 From 40eb216ee7cbbbbbe2f4ef9c03f11906e5463615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:47:17 +0900 Subject: [PATCH 59/61] Update sns_content_service.py --- smarketing-ai/services/sns_content_service.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index 8dd860c..db58e88 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1936,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) From 838e0b1d6de3e0ab22caa77831c23ac0f97a05a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:49:51 +0900 Subject: [PATCH 60/61] Update sns_content_service.py --- smarketing-ai/services/sns_content_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py index db58e88..3e37bb6 100644 --- a/smarketing-ai/services/sns_content_service.py +++ b/smarketing-ai/services/sns_content_service.py @@ -1942,7 +1942,7 @@ class SnsContentService: # 🔥 추가: 태그 제거 후 남은 빈 줄 정리 content = re.sub(r'\n\s*\n\s*\n', '\n\n', content) # 3개 이상의 연속 줄바꿈을 2개로 - content = re.sub(r'
\s*
\s*
', '

', content) # 3개 이상의 연속
을 2개로 + content = re.sub(r'
\s*
\s*
', '

', content) # 3개 이상의 연속
을 2개로 # 2. 네이버 블로그인 경우 이미지 태그를 실제 이미지로 변환 elif request.platform == '네이버 블로그' and image_placement_plan: From b86f1213c4ec09f513ad5f24ae3b8d7e425add3b Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Thu, 19 Jun 2025 13:10:00 +0900 Subject: [PATCH 61/61] Create member --- smarketing-java/deployment/member | 1 + 1 file changed, 1 insertion(+) create mode 100644 smarketing-java/deployment/member diff --git a/smarketing-java/deployment/member b/smarketing-java/deployment/member new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/smarketing-java/deployment/member @@ -0,0 +1 @@ +