diff --git a/.claude/commands/deploy-actions-cicd-guide-back.md b/.claude/commands/deploy-actions-cicd-guide-back.md index 0ec39e4..ae97c1d 100644 --- a/.claude/commands/deploy-actions-cicd-guide-back.md +++ b/.claude/commands/deploy-actions-cicd-guide-back.md @@ -1,10 +1,13 @@ --- command: "/deploy-actions-cicd-guide-back" +description: "백엔드 GitHub Actions CI/CD 파이프라인 가이드 작성" --- @cicd '백엔드GitHubActions파이프라인작성가이드'에 따라 GitHub Actions를 이용한 CI/CD 가이드를 작성해 주세요. + 프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요. + {안내메시지} '[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요. [실행정보] diff --git a/.claude/commands/deploy-actions-cicd-guide-front.md b/.claude/commands/deploy-actions-cicd-guide-front.md index 0975422..b4e818a 100644 --- a/.claude/commands/deploy-actions-cicd-guide-front.md +++ b/.claude/commands/deploy-actions-cicd-guide-front.md @@ -1,10 +1,13 @@ --- command: "/deploy-actions-cicd-guide-front" +description: "프론트엔드 GitHub Actions CI/CD 파이프라인 가이드 작성" --- @cicd '프론트엔드GitHubActions파이프라인작성가이드'에 따라 GitHub Actions를 이용한 CI/CD 가이드를 작성해 주세요. + 프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요. + {안내메시지} '[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요. [실행정보] diff --git a/.claude/commands/deploy-build-image-back.md b/.claude/commands/deploy-build-image-back.md index 5305a1b..242c4a4 100644 --- a/.claude/commands/deploy-build-image-back.md +++ b/.claude/commands/deploy-build-image-back.md @@ -1,5 +1,6 @@ --- command: "/deploy-build-image-back" +description: "백엔드 컨테이너 이미지 작성" --- @cicd diff --git a/.claude/commands/deploy-build-image-front.md b/.claude/commands/deploy-build-image-front.md index 1cfe9d1..e2d8426 100644 --- a/.claude/commands/deploy-build-image-front.md +++ b/.claude/commands/deploy-build-image-front.md @@ -1,5 +1,6 @@ --- command: "/deploy-build-image-front" +description: "프론트엔드 컨테이너 이미지 작성" --- @cicd diff --git a/.claude/commands/deploy-help.md b/.claude/commands/deploy-help.md index d6ec88f..660195b 100644 --- a/.claude/commands/deploy-help.md +++ b/.claude/commands/deploy-help.md @@ -1,81 +1,64 @@ --- command: "/deploy-help" +description: "배포 작업 순서 및 명령어 안내" --- # 배포 작업 순서 -## 1단계: 컨테이너 이미지 작성 +## 컨테이너 이미지 작성 ### 백엔드 -``` /deploy-build-image-back -``` -- 백엔드컨테이너이미지작성가이드를 참고하여 컨테이너 이미지를 빌드합니다 +- 백엔드 서비스들의 컨테이너 이미지를 작성합니다 ### 프론트엔드 -``` /deploy-build-image-front -``` -- 프론트엔드컨테이너이미지작성가이드를 참고하여 컨테이너 이미지를 빌드합니다 +- 프론트엔드 서비스의 컨테이너 이미지를 작성합니다 -## 2단계: 컨테이너 실행 가이드 작성 +## 컨테이너 실행 가이드 작성 ### 백엔드 -``` /deploy-run-container-guide-back -``` -- 백엔드컨테이너실행방법가이드를 참고하여 컨테이너 실행 방법을 작성합니다 -- 실행정보(ACR명, VM정보)가 필요합니다 +- 백엔드 컨테이너 실행 가이드를 작성합니다 +- [실행정보] 섹션에 ACR명, VM 접속 정보 제공 필요 ### 프론트엔드 -``` /deploy-run-container-guide-front -``` -- 프론트엔드컨테이너실행방법가이드를 참고하여 컨테이너 실행 방법을 작성합니다 -- 실행정보(시스템명, ACR명, VM정보)가 필요합니다 +- 프론트엔드 컨테이너 실행 가이드를 작성합니다 +- [실행정보] 섹션에 시스템명, ACR명, VM 접속 정보 제공 필요 -## 3단계: Kubernetes 배포 가이드 작성 +## Kubernetes 배포 가이드 작성 ### 백엔드 -``` /deploy-k8s-guide-back -``` -- 백엔드배포가이드를 참고하여 쿠버네티스 배포 방법을 작성합니다 -- 실행정보(ACR명, k8s명, 네임스페이스, 리소스 설정)가 필요합니다 +- 백엔드 서비스 Kubernetes 배포 가이드를 작성합니다 +- [실행정보] 섹션에 ACR명, k8s명, 네임스페이스, 리소스 정보 제공 필요 ### 프론트엔드 -``` /deploy-k8s-guide-front -``` -- 프론트엔드배포가이드를 참고하여 쿠버네티스 배포 방법을 작성합니다 -- 실행정보(시스템명, ACR명, k8s명, 네임스페이스, Gateway Host, 리소스 설정)가 필요합니다 +- 프론트엔드 서비스 Kubernetes 배포 가이드를 작성합니다 +- [실행정보] 섹션에 시스템명, ACR명, k8s명, 네임스페이스, Gateway Host 정보 제공 필요 -## 4단계: CI/CD 파이프라인 구성 - -### Jenkins 사용 시 +## CI/CD 파이프라인 작성 +### Jenkins CI/CD #### 백엔드 -``` /deploy-jenkins-cicd-guide-back -``` -- 백엔드Jenkins파이프라인작성가이드를 참고하여 Jenkins CI/CD 파이프라인을 구성합니다 +- Jenkins를 이용한 백엔드 CI/CD 파이프라인 가이드를 작성합니다 +- [실행정보] 섹션에 ACR_NAME, RESOURCE_GROUP, AKS_CLUSTER, NAMESPACE 제공 필요 #### 프론트엔드 -``` /deploy-jenkins-cicd-guide-front -``` -- 프론트엔드Jenkins파이프라인작성가이드를 참고하여 Jenkins CI/CD 파이프라인을 구성합니다 +- Jenkins를 이용한 프론트엔드 CI/CD 파이프라인 가이드를 작성합니다 +- [실행정보] 섹션에 SYSTEM_NAME, ACR_NAME, RESOURCE_GROUP, AKS_CLUSTER, NAMESPACE 제공 필요 -### GitHub Actions 사용 시 +### GitHub Actions CI/CD #### 백엔드 -``` /deploy-actions-cicd-guide-back -``` -- 백엔드GitHubActions파이프라인작성가이드를 참고하여 GitHub Actions CI/CD 파이프라인을 구성합니다 +- GitHub Actions를 이용한 백엔드 CI/CD 파이프라인 가이드를 작성합니다 +- [실행정보] 섹션에 ACR_NAME, RESOURCE_GROUP, AKS_CLUSTER, NAMESPACE 제공 필요 #### 프론트엔드 -``` /deploy-actions-cicd-guide-front -``` -- 프론트엔드GitHubActions파이프라인작성가이드를 참고하여 GitHub Actions CI/CD 파이프라인을 구성합니다 +- GitHub Actions를 이용한 프론트엔드 CI/CD 파이프라인 가이드를 작성합니다 +- [실행정보] 섹션에 SYSTEM_NAME, ACR_NAME, RESOURCE_GROUP, AKS_CLUSTER, NAMESPACE 제공 필요 -## 참고사항 -- 각 명령 실행 전 필요한 실행정보를 프롬프트에 포함해야 합니다 -- 실행정보가 없으면 안내 메시지가 표시되며 작업이 중단됩니다 -- CI/CD 도구는 Jenkins 또는 GitHub Actions 중 선택하여 사용합니다 +--- + +**참고**: 각 명령어 실행 시 [실행정보] 섹션에 필요한 정보를 함께 제공해야 합니다. diff --git a/.claude/commands/deploy-jenkins-cicd-guide-back.md b/.claude/commands/deploy-jenkins-cicd-guide-back.md index dbd3e8b..96a9093 100644 --- a/.claude/commands/deploy-jenkins-cicd-guide-back.md +++ b/.claude/commands/deploy-jenkins-cicd-guide-back.md @@ -1,10 +1,13 @@ --- command: "/deploy-jenkins-cicd-guide-back" +description: "백엔드 Jenkins CI/CD 파이프라인 가이드 작성" --- @cicd '백엔드Jenkins파이프라인작성가이드'에 따라 Jenkins를 이용한 CI/CD 가이드를 작성해 주세요. + 프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요. + {안내메시지} '[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요. [실행정보] diff --git a/.claude/commands/deploy-jenkins-cicd-guide-front.md b/.claude/commands/deploy-jenkins-cicd-guide-front.md index 5df6fad..af3807d 100644 --- a/.claude/commands/deploy-jenkins-cicd-guide-front.md +++ b/.claude/commands/deploy-jenkins-cicd-guide-front.md @@ -1,10 +1,13 @@ --- command: "/deploy-jenkins-cicd-guide-front" +description: "프론트엔드 Jenkins CI/CD 파이프라인 가이드 작성" --- @cicd '프론트엔드Jenkins파이프라인작성가이드'에 따라 Jenkins를 이용한 CI/CD 가이드를 작성해 주세요. + 프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요. + {안내메시지} '[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요. [실행정보] diff --git a/.claude/commands/deploy-k8s-guide-back.md b/.claude/commands/deploy-k8s-guide-back.md index 8fccb04..e5f4009 100644 --- a/.claude/commands/deploy-k8s-guide-back.md +++ b/.claude/commands/deploy-k8s-guide-back.md @@ -1,10 +1,13 @@ --- command: "/deploy-k8s-guide-back" +description: "백엔드 Kubernetes 배포 가이드 작성" --- @cicd '백엔드배포가이드'에 따라 백엔드 서비스 배포 방법을 작성해 주세요. + 프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요. + {안내메시지} '[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요. [실행정보] diff --git a/.claude/commands/deploy-k8s-guide-front.md b/.claude/commands/deploy-k8s-guide-front.md index 54a069d..0d62215 100644 --- a/.claude/commands/deploy-k8s-guide-front.md +++ b/.claude/commands/deploy-k8s-guide-front.md @@ -1,10 +1,13 @@ --- command: "/deploy-k8s-guide-front" +description: "프론트엔드 Kubernetes 배포 가이드 작성" --- @cicd '프론트엔드배포가이드'에 따라 프론트엔드 서비스 배포 방법을 작성해 주세요. + 프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요. + {안내메시지} '[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요. [실행정보] diff --git a/.claude/commands/deploy-run-container-guide-back.md b/.claude/commands/deploy-run-container-guide-back.md index c93388f..47dc409 100644 --- a/.claude/commands/deploy-run-container-guide-back.md +++ b/.claude/commands/deploy-run-container-guide-back.md @@ -1,10 +1,13 @@ --- command: "/deploy-run-container-guide-back" +description: "백엔드 컨테이너 실행방법 가이드 작성" --- @cicd '백엔드컨테이너실행방법가이드'에 따라 컨테이너 실행 가이드를 작성해 주세요. + 프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요. + {안내메시지} '[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요. [실행정보] diff --git a/.claude/commands/deploy-run-container-guide-front.md b/.claude/commands/deploy-run-container-guide-front.md index eb68f9a..ff3f3d4 100644 --- a/.claude/commands/deploy-run-container-guide-front.md +++ b/.claude/commands/deploy-run-container-guide-front.md @@ -1,10 +1,13 @@ --- command: "/deploy-run-container-guide-front" +description: "프론트엔드 컨테이너 실행방법 가이드 작성" --- @cicd '프론트엔드컨테이너실행방법가이드'에 따라 컨테이너 실행 가이드를 작성해 주세요. + 프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요. + {안내메시지} '[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요. [실행정보] diff --git a/content-service-deployment.yaml b/content-service-deployment.yaml new file mode 100644 index 0000000..7b84cf2 --- /dev/null +++ b/content-service-deployment.yaml @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: content-service + namespace: kt-event-marketing + labels: + app: content-service +spec: + replicas: 1 + selector: + matchLabels: + app: content-service + template: + metadata: + labels: + app: content-service + spec: + containers: + - name: content-service + image: acrdigitalgarage01.azurecr.io/content-service:latest + imagePullPolicy: Always + ports: + - containerPort: 8084 + name: http + protocol: TCP + envFrom: + - configMapRef: + name: cm-common + - configMapRef: + name: cm-content-service + - secretRef: + name: secret-common + - secretRef: + name: secret-content-service + resources: + requests: + cpu: 256m + memory: 512Mi + limits: + cpu: 1024m + memory: 1024Mi + startupProbe: + httpGet: + path: /api/v1/content/actuator/health + port: 8084 + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 30 + livenessProbe: + httpGet: + path: /api/v1/content/actuator/health/liveness + port: 8084 + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /api/v1/content/actuator/health/readiness + port: 8084 + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 3 + imagePullSecrets: + - name: kt-event-marketing diff --git a/content-service-service.yaml b/content-service-service.yaml new file mode 100644 index 0000000..6249e7c --- /dev/null +++ b/content-service-service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: content-service + namespace: kt-event-marketing + labels: + app: content-service +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 8084 + protocol: TCP + name: http + selector: + app: content-service diff --git a/content-service/Dockerfile b/content-service/Dockerfile new file mode 100644 index 0000000..9925cb3 --- /dev/null +++ b/content-service/Dockerfile @@ -0,0 +1,24 @@ +# Multi-stage build for Spring Boot application +FROM eclipse-temurin:21-jre-alpine AS builder +WORKDIR /app +COPY build/libs/*.jar app.jar +RUN java -Djarmode=layertools -jar app.jar extract + +FROM eclipse-temurin:21-jre-alpine +WORKDIR /app + +# Create non-root user +RUN addgroup -S spring && adduser -S spring -G spring +USER spring:spring + +# Copy layers from builder +COPY --from=builder /app/dependencies/ ./ +COPY --from=builder /app/spring-boot-loader/ ./ +COPY --from=builder /app/snapshot-dependencies/ ./ +COPY --from=builder /app/application/ ./ + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8084/actuator/health || exit 1 + +ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] diff --git a/content-service/src/main/java/com/kt/event/content/biz/service/HuggingFaceImageGenerator.java b/content-service/src/main/java/com/kt/event/content/biz/service/HuggingFaceImageGenerator.java deleted file mode 100644 index 106b5c3..0000000 --- a/content-service/src/main/java/com/kt/event/content/biz/service/HuggingFaceImageGenerator.java +++ /dev/null @@ -1,286 +0,0 @@ -package com.kt.event.content.biz.service; - -import com.kt.event.content.biz.domain.Content; -import com.kt.event.content.biz.domain.GeneratedImage; -import com.kt.event.content.biz.domain.ImageStyle; -import com.kt.event.content.biz.domain.Job; -import com.kt.event.content.biz.domain.Platform; -import com.kt.event.content.biz.dto.ContentCommand; -import com.kt.event.content.biz.dto.JobInfo; -import com.kt.event.content.biz.dto.RedisJobData; -import com.kt.event.content.biz.usecase.in.GenerateImagesUseCase; -import com.kt.event.content.biz.usecase.out.CDNUploader; -import com.kt.event.content.biz.usecase.out.ContentWriter; -import com.kt.event.content.biz.usecase.out.JobWriter; -import com.kt.event.content.infra.gateway.client.HuggingFaceApiClient; -import com.kt.event.content.infra.gateway.client.dto.HuggingFaceRequest; -import io.github.resilience4j.circuitbreaker.CallNotPermittedException; -import io.github.resilience4j.circuitbreaker.CircuitBreaker; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Primary; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -/** - * Hugging Face Inference API 이미지 생성 서비스 - * - * Hugging Face Inference API를 사용하여 Stable Diffusion으로 이미지 생성 (무료) - */ -@Slf4j -@Service -public class HuggingFaceImageGenerator implements GenerateImagesUseCase { - - private final HuggingFaceApiClient huggingFaceClient; - private final CDNUploader cdnUploader; - private final JobWriter jobWriter; - private final ContentWriter contentWriter; - private final CircuitBreaker circuitBreaker; - - public HuggingFaceImageGenerator( - HuggingFaceApiClient huggingFaceClient, - CDNUploader cdnUploader, - JobWriter jobWriter, - ContentWriter contentWriter, - @Qualifier("huggingfaceCircuitBreaker") CircuitBreaker circuitBreaker) { - this.huggingFaceClient = huggingFaceClient; - this.cdnUploader = cdnUploader; - this.jobWriter = jobWriter; - this.contentWriter = contentWriter; - this.circuitBreaker = circuitBreaker; - } - - @Override - public JobInfo execute(ContentCommand.GenerateImages command) { - log.info("Hugging Face 이미지 생성 요청: eventId={}, styles={}, platforms={}", - command.getEventId(), command.getStyles(), command.getPlatforms()); - - // Job 생성 - String jobId = "job-" + UUID.randomUUID().toString().substring(0, 8); - - Job job = Job.builder() - .id(jobId) - .eventId(command.getEventId()) - .jobType("image-generation") - .status(Job.Status.PENDING) - .progress(0) - .createdAt(java.time.LocalDateTime.now()) - .updatedAt(java.time.LocalDateTime.now()) - .build(); - - // Job 저장 - RedisJobData jobData = RedisJobData.builder() - .id(job.getId()) - .eventId(job.getEventId()) - .jobType(job.getJobType()) - .status(job.getStatus().name()) - .progress(job.getProgress()) - .createdAt(job.getCreatedAt()) - .updatedAt(job.getUpdatedAt()) - .build(); - - jobWriter.saveJob(jobData, 3600); // TTL 1시간 - log.info("Job 생성 완료: jobId={}", jobId); - - // 비동기로 이미지 생성 - processImageGeneration(jobId, command); - - return JobInfo.from(job); - } - - @Async - private void processImageGeneration(String jobId, ContentCommand.GenerateImages command) { - try { - log.info("Hugging Face 이미지 생성 시작: jobId={}", jobId); - - // Content 생성 또는 조회 - Content content = Content.builder() - .eventId(command.getEventId()) - .eventTitle(command.getEventId() + " 이벤트") - .eventDescription("AI 생성 이벤트 이미지") - .createdAt(java.time.LocalDateTime.now()) - .updatedAt(java.time.LocalDateTime.now()) - .build(); - Content savedContent = contentWriter.save(content); - log.info("Content 생성 완료: contentId={}", savedContent.getId()); - - // 스타일 x 플랫폼 조합으로 이미지 생성 - List styles = command.getStyles() != null && !command.getStyles().isEmpty() - ? command.getStyles() - : List.of(ImageStyle.FANCY, ImageStyle.SIMPLE); - - List platforms = command.getPlatforms() != null && !command.getPlatforms().isEmpty() - ? command.getPlatforms() - : List.of(Platform.INSTAGRAM, Platform.KAKAO); - - List images = new ArrayList<>(); - int totalCount = styles.size() * platforms.size(); - int currentCount = 0; - - for (ImageStyle style : styles) { - for (Platform platform : platforms) { - currentCount++; - - // 진행률 업데이트 - int progress = (currentCount * 100) / totalCount; - jobWriter.updateJobStatus(jobId, "IN_PROGRESS", progress); - - // Hugging Face로 이미지 생성 - String prompt = buildPrompt(command, style, platform); - String imageUrl = generateImage(prompt, platform); - - // GeneratedImage 저장 - GeneratedImage image = GeneratedImage.builder() - .eventId(command.getEventId()) - .style(style) - .platform(platform) - .cdnUrl(imageUrl) - .prompt(prompt) - .selected(currentCount == 1) // 첫 번째 이미지를 선택 - .createdAt(java.time.LocalDateTime.now()) - .updatedAt(java.time.LocalDateTime.now()) - .build(); - - if (currentCount == 1) { - image.select(); - } - - GeneratedImage savedImage = contentWriter.saveImage(image); - images.add(savedImage); - log.info("이미지 생성 완료: imageId={}, style={}, platform={}, url={}", - savedImage.getId(), style, platform, imageUrl); - } - } - - // Job 완료 - String resultMessage = String.format("%d개의 이미지가 성공적으로 생성되었습니다.", images.size()); - jobWriter.updateJobStatus(jobId, "COMPLETED", 100); - jobWriter.updateJobResult(jobId, resultMessage); - log.info("Hugging Face Job 완료: jobId={}, 생성된 이미지 수={}", jobId, images.size()); - - } catch (Exception e) { - log.error("Hugging Face 이미지 생성 실패: jobId={}", jobId, e); - jobWriter.updateJobError(jobId, e.getMessage()); - } - } - - /** - * Hugging Face로 이미지 생성 - * - * @param prompt 이미지 생성 프롬프트 - * @param platform 플랫폼 (이미지 크기 결정) - * @return 생성된 이미지 URL - */ - private String generateImage(String prompt, Platform platform) { - try { - // 플랫폼별 이미지 크기 설정 - int width = platform.getWidth(); - int height = platform.getHeight(); - - // Hugging Face API 요청 - HuggingFaceRequest request = HuggingFaceRequest.builder() - .inputs(prompt) - .parameters(HuggingFaceRequest.Parameters.builder() - .negative_prompt("blurry, bad quality, distorted, ugly, low resolution") - .width(width) - .height(height) - .guidance_scale(7.5) - .num_inference_steps(50) - .build()) - .build(); - - log.info("Hugging Face API 호출: prompt={}, size={}x{}", prompt, width, height); - - // 이미지 생성 (동기 방식) - byte[] imageData = generateImageWithCircuitBreaker(request); - log.info("Hugging Face 이미지 생성 완료: size={} bytes", imageData.length); - - // Azure Blob Storage에 업로드 - String fileName = String.format("event-%s-%s-%s.png", - platform.name().toLowerCase(), - UUID.randomUUID().toString().substring(0, 8), - System.currentTimeMillis()); - String azureCdnUrl = cdnUploader.upload(imageData, fileName); - log.info("Azure CDN 업로드 완료: fileName={}, url={}", fileName, azureCdnUrl); - - return azureCdnUrl; - - } catch (Exception e) { - log.error("Hugging Face 이미지 생성 실패: prompt={}", prompt, e); - throw new RuntimeException("이미지 생성 실패: " + e.getMessage(), e); - } - } - - /** - * 이미지 생성 프롬프트 구성 - */ - private String buildPrompt(ContentCommand.GenerateImages command, ImageStyle style, Platform platform) { - StringBuilder prompt = new StringBuilder(); - - // 업종 정보 추가 - if (command.getIndustry() != null && !command.getIndustry().trim().isEmpty()) { - prompt.append(command.getIndustry()).append(" "); - } - - // 기본 프롬프트 - prompt.append("event promotion image"); - - // 지역 정보 추가 - if (command.getLocation() != null && !command.getLocation().trim().isEmpty()) { - prompt.append(" in ").append(command.getLocation()); - } - - // 트렌드 키워드 추가 (최대 3개) - if (command.getTrends() != null && !command.getTrends().isEmpty()) { - prompt.append(", featuring "); - int count = Math.min(3, command.getTrends().size()); - for (int i = 0; i < count; i++) { - if (i > 0) prompt.append(", "); - prompt.append(command.getTrends().get(i)); - } - } - - prompt.append(", "); - - // 스타일별 프롬프트 - switch (style) { - case FANCY: - prompt.append("elegant, luxurious, premium design, vibrant colors, "); - break; - case SIMPLE: - prompt.append("minimalist, clean design, simple layout, modern, "); - break; - case TRENDY: - prompt.append("trendy, contemporary, stylish, modern design, "); - break; - } - - // 플랫폼별 특성 추가 - prompt.append("optimized for ").append(platform.name().toLowerCase()).append(" platform, "); - prompt.append("high quality, detailed, 4k resolution"); - - return prompt.toString(); - } - - /** - * Circuit Breaker로 보호된 Hugging Face 이미지 생성 - * - * @param request Hugging Face 요청 - * @return 생성된 이미지 바이트 데이터 - */ - private byte[] generateImageWithCircuitBreaker(HuggingFaceRequest request) { - try { - return circuitBreaker.executeSupplier(() -> huggingFaceClient.generateImage(request)); - } catch (CallNotPermittedException e) { - log.error("Hugging Face Circuit Breaker가 OPEN 상태입니다. 이미지 생성 차단"); - throw new RuntimeException("Hugging Face API에 일시적으로 접근할 수 없습니다. 잠시 후 다시 시도해주세요.", e); - } catch (Exception e) { - log.error("Hugging Face 이미지 생성 실패", e); - throw new RuntimeException("이미지 생성 실패: " + e.getMessage(), e); - } - } -} diff --git a/content-service/src/main/java/com/kt/event/content/infra/ContentApplication.java b/content-service/src/main/java/com/kt/event/content/infra/ContentApplication.java index 31a8d57..3c6cbed 100644 --- a/content-service/src/main/java/com/kt/event/content/infra/ContentApplication.java +++ b/content-service/src/main/java/com/kt/event/content/infra/ContentApplication.java @@ -1,8 +1,13 @@ package com.kt.event.content.infra; +import com.kt.event.common.security.JwtAuthenticationFilter; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; import org.springframework.scheduling.annotation.EnableAsync; /** @@ -13,6 +18,16 @@ import org.springframework.scheduling.annotation.EnableAsync; "com.kt.event.content", "com.kt.event.common" }) +@ComponentScan( + basePackages = { + "com.kt.event.content", + "com.kt.event.common" + }, + excludeFilters = @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + classes = JwtAuthenticationFilter.class + ) +) @EnableAsync @EnableFeignClients(basePackages = "com.kt.event.content.infra.gateway.client") public class ContentApplication { diff --git a/content-service/src/main/java/com/kt/event/content/infra/config/Resilience4jConfig.java b/content-service/src/main/java/com/kt/event/content/infra/config/Resilience4jConfig.java index 93ec6a7..506a60f 100644 --- a/content-service/src/main/java/com/kt/event/content/infra/config/Resilience4jConfig.java +++ b/content-service/src/main/java/com/kt/event/content/infra/config/Resilience4jConfig.java @@ -12,7 +12,7 @@ import java.time.Duration; /** * Resilience4j Circuit Breaker 설정 * - * Hugging Face API, Replicate API 및 Azure Blob Storage에 대한 Circuit Breaker 패턴 적용 + * Replicate API 및 Azure Blob Storage에 대한 Circuit Breaker 패턴 적용 */ @Slf4j @Configuration @@ -89,40 +89,4 @@ public class Resilience4jConfig { return circuitBreaker; } - - /** - * Hugging Face API Circuit Breaker - * - * - 실패율 50% 이상 시 Open - * - 최소 3개 요청 후 평가 - * - Open 후 30초 대기 (Half-Open 전환) - * - Half-Open 상태에서 2개 요청으로 평가 - */ - @Bean - public CircuitBreaker huggingfaceCircuitBreaker() { - CircuitBreakerConfig config = CircuitBreakerConfig.custom() - .failureRateThreshold(50) // 실패율 50% 초과 시 Open - .slowCallRateThreshold(50) // 느린 호출 50% 초과 시 Open - .slowCallDurationThreshold(Duration.ofSeconds(60)) // 60초 이상 걸리면 느린 호출로 판단 - .waitDurationInOpenState(Duration.ofSeconds(30)) // Open 후 30초 대기 - .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) // 횟수 기반 평가 - .slidingWindowSize(10) // 최근 10개 요청 평가 - .minimumNumberOfCalls(3) // 최소 3개 요청 후 평가 - .permittedNumberOfCallsInHalfOpenState(2) // Half-Open에서 2개 요청으로 평가 - .automaticTransitionFromOpenToHalfOpenEnabled(true) // 자동 Half-Open 전환 - .build(); - - CircuitBreaker circuitBreaker = CircuitBreakerRegistry.of(config).circuitBreaker("huggingface"); - - // Circuit Breaker 이벤트 로깅 - circuitBreaker.getEventPublisher() - .onSuccess(event -> log.debug("Hugging Face Circuit Breaker: Success")) - .onError(event -> log.warn("Hugging Face Circuit Breaker: Error - {}", event.getThrowable().getMessage())) - .onStateTransition(event -> log.warn("Hugging Face Circuit Breaker: State transition from {} to {}", - event.getStateTransition().getFromState(), event.getStateTransition().getToState())) - .onSlowCallRateExceeded(event -> log.warn("Hugging Face Circuit Breaker: Slow call rate exceeded")) - .onFailureRateExceeded(event -> log.warn("Hugging Face Circuit Breaker: Failure rate exceeded")); - - return circuitBreaker; - } } diff --git a/content-service/src/main/java/com/kt/event/content/infra/config/SecurityConfig.java b/content-service/src/main/java/com/kt/event/content/infra/config/SecurityConfig.java index 9b78a69..23c002c 100644 --- a/content-service/src/main/java/com/kt/event/content/infra/config/SecurityConfig.java +++ b/content-service/src/main/java/com/kt/event/content/infra/config/SecurityConfig.java @@ -4,13 +4,14 @@ 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.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; /** * Spring Security 설정 - * API 테스트를 위해 일단 모든 요청 허용 (추후 JWT 인증 추가) + * API 테스트를 위해 일단 모든 요청 허용 */ @Configuration @EnableWebSecurity @@ -27,13 +28,20 @@ public class SecurityConfig { session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) - // 모든 요청 허용 (테스트용, 추후 JWT 필터 추가 필요) + // 모든 요청 허용 (테스트용) .authorizeHttpRequests(auth -> auth - .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() - .requestMatchers("/actuator/**").permitAll() - .anyRequest().permitAll() // TODO: 추후 authenticated()로 변경 + .anyRequest().permitAll() ); return http.build(); } + + /** + * Chrome DevTools 요청 등 정적 리소스 요청을 Spring Security에서 제외 + */ + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring() + .requestMatchers("/.well-known/**"); + } } diff --git a/content-service/src/main/java/com/kt/event/content/infra/config/SwaggerConfig.java b/content-service/src/main/java/com/kt/event/content/infra/config/SwaggerConfig.java index 8a0f63a..38d2248 100644 --- a/content-service/src/main/java/com/kt/event/content/infra/config/SwaggerConfig.java +++ b/content-service/src/main/java/com/kt/event/content/infra/config/SwaggerConfig.java @@ -36,6 +36,9 @@ public class SwaggerConfig { ) ) .servers(List.of( + new Server() + .url("http://kt-event-marketing-api.20.214.196.128.nip.io/api/v1/content") + .description("VM Development Server"), new Server() .url("http://localhost:8084") .description("Local Development Server"), diff --git a/content-service/src/main/java/com/kt/event/content/infra/gateway/client/HuggingFaceApiClient.java b/content-service/src/main/java/com/kt/event/content/infra/gateway/client/HuggingFaceApiClient.java deleted file mode 100644 index 2e87a38..0000000 --- a/content-service/src/main/java/com/kt/event/content/infra/gateway/client/HuggingFaceApiClient.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.kt.event.content.infra.gateway.client; - -import com.kt.event.content.infra.gateway.client.dto.HuggingFaceRequest; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestClient; - -/** - * Hugging Face Inference API 클라이언트 - * - * API 문서: https://huggingface.co/docs/api-inference/index - * Stable Diffusion 모델: stabilityai/stable-diffusion-2-1 - */ -@Component -public class HuggingFaceApiClient { - - private final RestClient restClient; - - @Value("${huggingface.api.url:https://api-inference.huggingface.co}") - private String apiUrl; - - @Value("${huggingface.api.token:}") - private String apiToken; - - @Value("${huggingface.model:stabilityai/stable-diffusion-2-1}") - private String modelId; - - public HuggingFaceApiClient(RestClient.Builder restClientBuilder) { - this.restClient = restClientBuilder.build(); - } - - /** - * 이미지 생성 요청 (동기 방식) - * - * @param request Hugging Face 요청 - * @return 생성된 이미지 바이트 데이터 - */ - public byte[] generateImage(HuggingFaceRequest request) { - String url = String.format("%s/models/%s", apiUrl, modelId); - - return restClient.post() - .uri(url) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiToken) - .contentType(MediaType.APPLICATION_JSON) - .body(request) - .retrieve() - .body(byte[].class); - } -} diff --git a/content-service/src/main/java/com/kt/event/content/infra/gateway/client/dto/HuggingFaceRequest.java b/content-service/src/main/java/com/kt/event/content/infra/gateway/client/dto/HuggingFaceRequest.java deleted file mode 100644 index 94827c8..0000000 --- a/content-service/src/main/java/com/kt/event/content/infra/gateway/client/dto/HuggingFaceRequest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.kt.event.content.infra.gateway.client.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -/** - * Hugging Face Inference API 요청 DTO - * - * API 문서: https://huggingface.co/docs/api-inference/index - */ -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class HuggingFaceRequest { - - /** - * 이미지 생성 프롬프트 - */ - private String inputs; - - /** - * 생성 파라미터 - */ - private Parameters parameters; - - @Getter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class Parameters { - /** - * Negative prompt (생성하지 않을 내용) - */ - private String negative_prompt; - - /** - * 이미지 너비 - */ - private Integer width; - - /** - * 이미지 높이 - */ - private Integer height; - - /** - * Guidance scale (프롬프트 준수 정도, 기본: 7.5) - */ - private Double guidance_scale; - - /** - * Inference steps (품질, 기본: 50) - */ - private Integer num_inference_steps; - } -} diff --git a/content-service/src/main/java/com/kt/event/content/infra/web/controller/ContentController.java b/content-service/src/main/java/com/kt/event/content/infra/web/controller/ContentController.java index 7fd2997..4bde7ce 100644 --- a/content-service/src/main/java/com/kt/event/content/infra/web/controller/ContentController.java +++ b/content-service/src/main/java/com/kt/event/content/infra/web/controller/ContentController.java @@ -31,7 +31,7 @@ import java.util.List; */ @Slf4j @RestController -@RequestMapping("/api/v1/content") +@RequestMapping @RequiredArgsConstructor public class ContentController { @@ -124,7 +124,7 @@ public class ContentController { * @param imageId 이미지 ID * @return 200 OK - 이미지 상세 정보 */ - @GetMapping("/images/{imageId}") + @GetMapping("/images/{imageId:[0-9]+}") public ResponseEntity getImageById(@PathVariable Long imageId) { log.info("이미지 상세 조회: imageId={}", imageId); @@ -140,7 +140,7 @@ public class ContentController { * @param imageId 이미지 ID * @return 204 NO CONTENT */ - @DeleteMapping("/images/{imageId}") + @DeleteMapping("/images/{imageId:[0-9]+}") public ResponseEntity deleteImage(@PathVariable Long imageId) { log.info("이미지 삭제 요청: imageId={}", imageId); @@ -157,7 +157,7 @@ public class ContentController { * @param requestBody 재생성 요청 정보 (선택) * @return 202 ACCEPTED - Job ID 반환 */ - @PostMapping("/images/{imageId}/regenerate") + @PostMapping("/images/{imageId:[0-9]+}/regenerate") public ResponseEntity regenerateImage( @PathVariable Long imageId, @RequestBody(required = false) ContentCommand.RegenerateImage requestBody) { diff --git a/content-service/src/main/resources/application.yml b/content-service/src/main/resources/application.yml index a115a4b..1ff0b87 100644 --- a/content-service/src/main/resources/application.yml +++ b/content-service/src/main/resources/application.yml @@ -38,13 +38,6 @@ replicate: model: version: ${REPLICATE_MODEL_VERSION:stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b} -# HuggingFace API Configuration -huggingface: - api: - url: ${HUGGINGFACE_API_URL:https://api-inference.huggingface.co} - token: ${HUGGINGFACE_API_TOKEN:} - model: ${HUGGINGFACE_MODEL:runwayml/stable-diffusion-v1-5} - # CORS Configuration cors: allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*} @@ -96,3 +89,5 @@ logging: # Server Configuration server: port: ${SERVER_PORT:8084} + servlet: + context-path: /api/v1/content diff --git a/deployment/container/build-image.md b/deployment/container/build-image.md index 702b951..d1038c5 100644 --- a/deployment/container/build-image.md +++ b/deployment/container/build-image.md @@ -262,15 +262,60 @@ http://localhost:8082/swagger-ui/index.html ## 10. 다음 단계 -1. **컨테이너 테스트**: 로컬 환경에서 컨테이너 실행 및 API 테스트 -2. **환경변수 설정**: 운영 환경에 맞는 환경변수 구성 -3. **통합 테스트**: 다른 마이크로서비스들과의 통합 테스트 -4. **이미지 레지스트리 푸시**: Docker Hub 또는 사설 레지스트리에 이미지 업로드 -5. **Kubernetes 배포**: K8s 클러스터에 배포 +### 빌드 수행 이력 -## 11. 참고사항 +#### 최신 빌드 (2025-10-28) -- **개발 환경 인증**: DevAuthenticationFilter가 자동으로 테스트용 UserPrincipal 생성 -- **프로덕션 배포**: DevAuthenticationFilter 비활성화 및 실제 JWT 인증 필터 활성화 필요 -- **보안**: JWT_SECRET은 안전하게 관리하고 최소 32자 이상 사용 -- **성능**: JAVA_OPTS를 통해 JVM 메모리 설정 최적화 권장 +**1단계: JAR 빌드** +```bash +./gradlew content-service:clean content-service:bootJar +``` + +빌드 결과: +``` +BUILD SUCCESSFUL in 8s +9 actionable tasks: 6 executed, 3 up-to-date +``` + +**2단계: Docker 이미지 빌드** +```bash +docker build \ + --platform linux/amd64 \ + --build-arg BUILD_LIB_DIR="content-service/build/libs" \ + --build-arg ARTIFACTORY_FILE="content-service.jar" \ + -f deployment/container/Dockerfile-backend \ + -t content-service:latest . +``` + +빌드 결과: +- ✅ Build stage 완료 (openjdk:23-oraclelinux8) +- ✅ Run stage 완료 (openjdk:23-slim) +- ✅ 이미지 생성 완료 + +**3단계: 이미지 확인** +```bash +docker images | grep content-service +``` + +확인 결과: +``` +content-service latest ff73258c94cc 15 seconds ago 393MB +``` + +### 빌드 정보 +- **서비스명**: content-service +- **JAR 파일**: content-service.jar +- **Docker 이미지**: content-service:latest +- **이미지 ID**: ff73258c94cc +- **이미지 크기**: 393MB +- **노출 포트**: 8084 + +### 빌드 일시 +- **최신 빌드**: 2025-10-28 +- **이전 빌드**: 2025-10-27 + +### 환경 +- **Base Image**: openjdk:23-slim +- **Platform**: linux/amd64 +- **User**: k8s (non-root) +- **Java Version**: 23