mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2025-12-06 11:26:26 +00:00
Merge pull request #22 from ktds-dg0501/feature/content
Feature/content
This commit is contained in:
commit
436c0bf2b8
@ -1,10 +1,13 @@
|
|||||||
---
|
---
|
||||||
command: "/deploy-actions-cicd-guide-back"
|
command: "/deploy-actions-cicd-guide-back"
|
||||||
|
description: "백엔드 GitHub Actions CI/CD 파이프라인 가이드 작성"
|
||||||
---
|
---
|
||||||
|
|
||||||
@cicd
|
@cicd
|
||||||
'백엔드GitHubActions파이프라인작성가이드'에 따라 GitHub Actions를 이용한 CI/CD 가이드를 작성해 주세요.
|
'백엔드GitHubActions파이프라인작성가이드'에 따라 GitHub Actions를 이용한 CI/CD 가이드를 작성해 주세요.
|
||||||
|
|
||||||
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
||||||
|
|
||||||
{안내메시지}
|
{안내메시지}
|
||||||
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
||||||
[실행정보]
|
[실행정보]
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
---
|
---
|
||||||
command: "/deploy-actions-cicd-guide-front"
|
command: "/deploy-actions-cicd-guide-front"
|
||||||
|
description: "프론트엔드 GitHub Actions CI/CD 파이프라인 가이드 작성"
|
||||||
---
|
---
|
||||||
|
|
||||||
@cicd
|
@cicd
|
||||||
'프론트엔드GitHubActions파이프라인작성가이드'에 따라 GitHub Actions를 이용한 CI/CD 가이드를 작성해 주세요.
|
'프론트엔드GitHubActions파이프라인작성가이드'에 따라 GitHub Actions를 이용한 CI/CD 가이드를 작성해 주세요.
|
||||||
|
|
||||||
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
||||||
|
|
||||||
{안내메시지}
|
{안내메시지}
|
||||||
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
||||||
[실행정보]
|
[실행정보]
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
command: "/deploy-build-image-back"
|
command: "/deploy-build-image-back"
|
||||||
|
description: "백엔드 컨테이너 이미지 작성"
|
||||||
---
|
---
|
||||||
|
|
||||||
@cicd
|
@cicd
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
command: "/deploy-build-image-front"
|
command: "/deploy-build-image-front"
|
||||||
|
description: "프론트엔드 컨테이너 이미지 작성"
|
||||||
---
|
---
|
||||||
|
|
||||||
@cicd
|
@cicd
|
||||||
|
|||||||
@ -1,81 +1,64 @@
|
|||||||
---
|
---
|
||||||
command: "/deploy-help"
|
command: "/deploy-help"
|
||||||
|
description: "배포 작업 순서 및 명령어 안내"
|
||||||
---
|
---
|
||||||
|
|
||||||
# 배포 작업 순서
|
# 배포 작업 순서
|
||||||
|
|
||||||
## 1단계: 컨테이너 이미지 작성
|
## 컨테이너 이미지 작성
|
||||||
### 백엔드
|
### 백엔드
|
||||||
```
|
|
||||||
/deploy-build-image-back
|
/deploy-build-image-back
|
||||||
```
|
- 백엔드 서비스들의 컨테이너 이미지를 작성합니다
|
||||||
- 백엔드컨테이너이미지작성가이드를 참고하여 컨테이너 이미지를 빌드합니다
|
|
||||||
|
|
||||||
### 프론트엔드
|
### 프론트엔드
|
||||||
```
|
|
||||||
/deploy-build-image-front
|
/deploy-build-image-front
|
||||||
```
|
- 프론트엔드 서비스의 컨테이너 이미지를 작성합니다
|
||||||
- 프론트엔드컨테이너이미지작성가이드를 참고하여 컨테이너 이미지를 빌드합니다
|
|
||||||
|
|
||||||
## 2단계: 컨테이너 실행 가이드 작성
|
## 컨테이너 실행 가이드 작성
|
||||||
### 백엔드
|
### 백엔드
|
||||||
```
|
|
||||||
/deploy-run-container-guide-back
|
/deploy-run-container-guide-back
|
||||||
```
|
- 백엔드 컨테이너 실행 가이드를 작성합니다
|
||||||
- 백엔드컨테이너실행방법가이드를 참고하여 컨테이너 실행 방법을 작성합니다
|
- [실행정보] 섹션에 ACR명, VM 접속 정보 제공 필요
|
||||||
- 실행정보(ACR명, VM정보)가 필요합니다
|
|
||||||
|
|
||||||
### 프론트엔드
|
### 프론트엔드
|
||||||
```
|
|
||||||
/deploy-run-container-guide-front
|
/deploy-run-container-guide-front
|
||||||
```
|
- 프론트엔드 컨테이너 실행 가이드를 작성합니다
|
||||||
- 프론트엔드컨테이너실행방법가이드를 참고하여 컨테이너 실행 방법을 작성합니다
|
- [실행정보] 섹션에 시스템명, ACR명, VM 접속 정보 제공 필요
|
||||||
- 실행정보(시스템명, ACR명, VM정보)가 필요합니다
|
|
||||||
|
|
||||||
## 3단계: Kubernetes 배포 가이드 작성
|
## Kubernetes 배포 가이드 작성
|
||||||
### 백엔드
|
### 백엔드
|
||||||
```
|
|
||||||
/deploy-k8s-guide-back
|
/deploy-k8s-guide-back
|
||||||
```
|
- 백엔드 서비스 Kubernetes 배포 가이드를 작성합니다
|
||||||
- 백엔드배포가이드를 참고하여 쿠버네티스 배포 방법을 작성합니다
|
- [실행정보] 섹션에 ACR명, k8s명, 네임스페이스, 리소스 정보 제공 필요
|
||||||
- 실행정보(ACR명, k8s명, 네임스페이스, 리소스 설정)가 필요합니다
|
|
||||||
|
|
||||||
### 프론트엔드
|
### 프론트엔드
|
||||||
```
|
|
||||||
/deploy-k8s-guide-front
|
/deploy-k8s-guide-front
|
||||||
```
|
- 프론트엔드 서비스 Kubernetes 배포 가이드를 작성합니다
|
||||||
- 프론트엔드배포가이드를 참고하여 쿠버네티스 배포 방법을 작성합니다
|
- [실행정보] 섹션에 시스템명, ACR명, k8s명, 네임스페이스, Gateway Host 정보 제공 필요
|
||||||
- 실행정보(시스템명, ACR명, k8s명, 네임스페이스, Gateway Host, 리소스 설정)가 필요합니다
|
|
||||||
|
|
||||||
## 4단계: CI/CD 파이프라인 구성
|
## CI/CD 파이프라인 작성
|
||||||
|
### Jenkins CI/CD
|
||||||
### Jenkins 사용 시
|
|
||||||
#### 백엔드
|
#### 백엔드
|
||||||
```
|
|
||||||
/deploy-jenkins-cicd-guide-back
|
/deploy-jenkins-cicd-guide-back
|
||||||
```
|
- Jenkins를 이용한 백엔드 CI/CD 파이프라인 가이드를 작성합니다
|
||||||
- 백엔드Jenkins파이프라인작성가이드를 참고하여 Jenkins CI/CD 파이프라인을 구성합니다
|
- [실행정보] 섹션에 ACR_NAME, RESOURCE_GROUP, AKS_CLUSTER, NAMESPACE 제공 필요
|
||||||
|
|
||||||
#### 프론트엔드
|
#### 프론트엔드
|
||||||
```
|
|
||||||
/deploy-jenkins-cicd-guide-front
|
/deploy-jenkins-cicd-guide-front
|
||||||
```
|
- Jenkins를 이용한 프론트엔드 CI/CD 파이프라인 가이드를 작성합니다
|
||||||
- 프론트엔드Jenkins파이프라인작성가이드를 참고하여 Jenkins CI/CD 파이프라인을 구성합니다
|
- [실행정보] 섹션에 SYSTEM_NAME, ACR_NAME, RESOURCE_GROUP, AKS_CLUSTER, NAMESPACE 제공 필요
|
||||||
|
|
||||||
### GitHub Actions 사용 시
|
### GitHub Actions CI/CD
|
||||||
#### 백엔드
|
#### 백엔드
|
||||||
```
|
|
||||||
/deploy-actions-cicd-guide-back
|
/deploy-actions-cicd-guide-back
|
||||||
```
|
- GitHub Actions를 이용한 백엔드 CI/CD 파이프라인 가이드를 작성합니다
|
||||||
- 백엔드GitHubActions파이프라인작성가이드를 참고하여 GitHub Actions CI/CD 파이프라인을 구성합니다
|
- [실행정보] 섹션에 ACR_NAME, RESOURCE_GROUP, AKS_CLUSTER, NAMESPACE 제공 필요
|
||||||
|
|
||||||
#### 프론트엔드
|
#### 프론트엔드
|
||||||
```
|
|
||||||
/deploy-actions-cicd-guide-front
|
/deploy-actions-cicd-guide-front
|
||||||
```
|
- GitHub Actions를 이용한 프론트엔드 CI/CD 파이프라인 가이드를 작성합니다
|
||||||
- 프론트엔드GitHubActions파이프라인작성가이드를 참고하여 GitHub Actions CI/CD 파이프라인을 구성합니다
|
- [실행정보] 섹션에 SYSTEM_NAME, ACR_NAME, RESOURCE_GROUP, AKS_CLUSTER, NAMESPACE 제공 필요
|
||||||
|
|
||||||
## 참고사항
|
---
|
||||||
- 각 명령 실행 전 필요한 실행정보를 프롬프트에 포함해야 합니다
|
|
||||||
- 실행정보가 없으면 안내 메시지가 표시되며 작업이 중단됩니다
|
**참고**: 각 명령어 실행 시 [실행정보] 섹션에 필요한 정보를 함께 제공해야 합니다.
|
||||||
- CI/CD 도구는 Jenkins 또는 GitHub Actions 중 선택하여 사용합니다
|
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
---
|
---
|
||||||
command: "/deploy-jenkins-cicd-guide-back"
|
command: "/deploy-jenkins-cicd-guide-back"
|
||||||
|
description: "백엔드 Jenkins CI/CD 파이프라인 가이드 작성"
|
||||||
---
|
---
|
||||||
|
|
||||||
@cicd
|
@cicd
|
||||||
'백엔드Jenkins파이프라인작성가이드'에 따라 Jenkins를 이용한 CI/CD 가이드를 작성해 주세요.
|
'백엔드Jenkins파이프라인작성가이드'에 따라 Jenkins를 이용한 CI/CD 가이드를 작성해 주세요.
|
||||||
|
|
||||||
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
||||||
|
|
||||||
{안내메시지}
|
{안내메시지}
|
||||||
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
||||||
[실행정보]
|
[실행정보]
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
---
|
---
|
||||||
command: "/deploy-jenkins-cicd-guide-front"
|
command: "/deploy-jenkins-cicd-guide-front"
|
||||||
|
description: "프론트엔드 Jenkins CI/CD 파이프라인 가이드 작성"
|
||||||
---
|
---
|
||||||
|
|
||||||
@cicd
|
@cicd
|
||||||
'프론트엔드Jenkins파이프라인작성가이드'에 따라 Jenkins를 이용한 CI/CD 가이드를 작성해 주세요.
|
'프론트엔드Jenkins파이프라인작성가이드'에 따라 Jenkins를 이용한 CI/CD 가이드를 작성해 주세요.
|
||||||
|
|
||||||
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
||||||
|
|
||||||
{안내메시지}
|
{안내메시지}
|
||||||
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
||||||
[실행정보]
|
[실행정보]
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
---
|
---
|
||||||
command: "/deploy-k8s-guide-back"
|
command: "/deploy-k8s-guide-back"
|
||||||
|
description: "백엔드 Kubernetes 배포 가이드 작성"
|
||||||
---
|
---
|
||||||
|
|
||||||
@cicd
|
@cicd
|
||||||
'백엔드배포가이드'에 따라 백엔드 서비스 배포 방법을 작성해 주세요.
|
'백엔드배포가이드'에 따라 백엔드 서비스 배포 방법을 작성해 주세요.
|
||||||
|
|
||||||
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
||||||
|
|
||||||
{안내메시지}
|
{안내메시지}
|
||||||
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
||||||
[실행정보]
|
[실행정보]
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
---
|
---
|
||||||
command: "/deploy-k8s-guide-front"
|
command: "/deploy-k8s-guide-front"
|
||||||
|
description: "프론트엔드 Kubernetes 배포 가이드 작성"
|
||||||
---
|
---
|
||||||
|
|
||||||
@cicd
|
@cicd
|
||||||
'프론트엔드배포가이드'에 따라 프론트엔드 서비스 배포 방법을 작성해 주세요.
|
'프론트엔드배포가이드'에 따라 프론트엔드 서비스 배포 방법을 작성해 주세요.
|
||||||
|
|
||||||
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
||||||
|
|
||||||
{안내메시지}
|
{안내메시지}
|
||||||
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
||||||
[실행정보]
|
[실행정보]
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
---
|
---
|
||||||
command: "/deploy-run-container-guide-back"
|
command: "/deploy-run-container-guide-back"
|
||||||
|
description: "백엔드 컨테이너 실행방법 가이드 작성"
|
||||||
---
|
---
|
||||||
|
|
||||||
@cicd
|
@cicd
|
||||||
'백엔드컨테이너실행방법가이드'에 따라 컨테이너 실행 가이드를 작성해 주세요.
|
'백엔드컨테이너실행방법가이드'에 따라 컨테이너 실행 가이드를 작성해 주세요.
|
||||||
|
|
||||||
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
||||||
|
|
||||||
{안내메시지}
|
{안내메시지}
|
||||||
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
||||||
[실행정보]
|
[실행정보]
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
---
|
---
|
||||||
command: "/deploy-run-container-guide-front"
|
command: "/deploy-run-container-guide-front"
|
||||||
|
description: "프론트엔드 컨테이너 실행방법 가이드 작성"
|
||||||
---
|
---
|
||||||
|
|
||||||
@cicd
|
@cicd
|
||||||
'프론트엔드컨테이너실행방법가이드'에 따라 컨테이너 실행 가이드를 작성해 주세요.
|
'프론트엔드컨테이너실행방법가이드'에 따라 컨테이너 실행 가이드를 작성해 주세요.
|
||||||
|
|
||||||
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
프롬프트에 '[실행정보]'항목이 없으면 수행을 중단하고 안내 메시지를 표시해 주세요.
|
||||||
|
|
||||||
{안내메시지}
|
{안내메시지}
|
||||||
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
'[실행정보]'섹션 하위에 아래 예와 같이 필요한 정보를 제시해 주세요.
|
||||||
[실행정보]
|
[실행정보]
|
||||||
|
|||||||
64
content-service-deployment.yaml
Normal file
64
content-service-deployment.yaml
Normal file
@ -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
|
||||||
16
content-service-service.yaml
Normal file
16
content-service-service.yaml
Normal file
@ -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
|
||||||
24
content-service/Dockerfile
Normal file
24
content-service/Dockerfile
Normal file
@ -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"]
|
||||||
@ -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<ImageStyle> styles = command.getStyles() != null && !command.getStyles().isEmpty()
|
|
||||||
? command.getStyles()
|
|
||||||
: List.of(ImageStyle.FANCY, ImageStyle.SIMPLE);
|
|
||||||
|
|
||||||
List<Platform> platforms = command.getPlatforms() != null && !command.getPlatforms().isEmpty()
|
|
||||||
? command.getPlatforms()
|
|
||||||
: List.of(Platform.INSTAGRAM, Platform.KAKAO);
|
|
||||||
|
|
||||||
List<GeneratedImage> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +1,13 @@
|
|||||||
package com.kt.event.content.infra;
|
package com.kt.event.content.infra;
|
||||||
|
|
||||||
|
import com.kt.event.common.security.JwtAuthenticationFilter;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
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;
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -13,6 +18,16 @@ import org.springframework.scheduling.annotation.EnableAsync;
|
|||||||
"com.kt.event.content",
|
"com.kt.event.content",
|
||||||
"com.kt.event.common"
|
"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
|
@EnableAsync
|
||||||
@EnableFeignClients(basePackages = "com.kt.event.content.infra.gateway.client")
|
@EnableFeignClients(basePackages = "com.kt.event.content.infra.gateway.client")
|
||||||
public class ContentApplication {
|
public class ContentApplication {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import java.time.Duration;
|
|||||||
/**
|
/**
|
||||||
* Resilience4j Circuit Breaker 설정
|
* Resilience4j Circuit Breaker 설정
|
||||||
*
|
*
|
||||||
* Hugging Face API, Replicate API 및 Azure Blob Storage에 대한 Circuit Breaker 패턴 적용
|
* Replicate API 및 Azure Blob Storage에 대한 Circuit Breaker 패턴 적용
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
@ -89,40 +89,4 @@ public class Resilience4jConfig {
|
|||||||
|
|
||||||
return circuitBreaker;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,13 +4,14 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
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.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spring Security 설정
|
* Spring Security 설정
|
||||||
* API 테스트를 위해 일단 모든 요청 허용 (추후 JWT 인증 추가)
|
* API 테스트를 위해 일단 모든 요청 허용
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@ -27,13 +28,20 @@ public class SecurityConfig {
|
|||||||
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 모든 요청 허용 (테스트용, 추후 JWT 필터 추가 필요)
|
// 모든 요청 허용 (테스트용)
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
.anyRequest().permitAll()
|
||||||
.requestMatchers("/actuator/**").permitAll()
|
|
||||||
.anyRequest().permitAll() // TODO: 추후 authenticated()로 변경
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chrome DevTools 요청 등 정적 리소스 요청을 Spring Security에서 제외
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||||
|
return (web) -> web.ignoring()
|
||||||
|
.requestMatchers("/.well-known/**");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,9 @@ public class SwaggerConfig {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.servers(List.of(
|
.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()
|
new Server()
|
||||||
.url("http://localhost:8084")
|
.url("http://localhost:8084")
|
||||||
.description("Local Development Server"),
|
.description("Local Development Server"),
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -31,7 +31,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/content")
|
@RequestMapping
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ContentController {
|
public class ContentController {
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ public class ContentController {
|
|||||||
* @param imageId 이미지 ID
|
* @param imageId 이미지 ID
|
||||||
* @return 200 OK - 이미지 상세 정보
|
* @return 200 OK - 이미지 상세 정보
|
||||||
*/
|
*/
|
||||||
@GetMapping("/images/{imageId}")
|
@GetMapping("/images/{imageId:[0-9]+}")
|
||||||
public ResponseEntity<ImageInfo> getImageById(@PathVariable Long imageId) {
|
public ResponseEntity<ImageInfo> getImageById(@PathVariable Long imageId) {
|
||||||
log.info("이미지 상세 조회: imageId={}", imageId);
|
log.info("이미지 상세 조회: imageId={}", imageId);
|
||||||
|
|
||||||
@ -140,7 +140,7 @@ public class ContentController {
|
|||||||
* @param imageId 이미지 ID
|
* @param imageId 이미지 ID
|
||||||
* @return 204 NO CONTENT
|
* @return 204 NO CONTENT
|
||||||
*/
|
*/
|
||||||
@DeleteMapping("/images/{imageId}")
|
@DeleteMapping("/images/{imageId:[0-9]+}")
|
||||||
public ResponseEntity<Void> deleteImage(@PathVariable Long imageId) {
|
public ResponseEntity<Void> deleteImage(@PathVariable Long imageId) {
|
||||||
log.info("이미지 삭제 요청: imageId={}", imageId);
|
log.info("이미지 삭제 요청: imageId={}", imageId);
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ public class ContentController {
|
|||||||
* @param requestBody 재생성 요청 정보 (선택)
|
* @param requestBody 재생성 요청 정보 (선택)
|
||||||
* @return 202 ACCEPTED - Job ID 반환
|
* @return 202 ACCEPTED - Job ID 반환
|
||||||
*/
|
*/
|
||||||
@PostMapping("/images/{imageId}/regenerate")
|
@PostMapping("/images/{imageId:[0-9]+}/regenerate")
|
||||||
public ResponseEntity<JobInfo> regenerateImage(
|
public ResponseEntity<JobInfo> regenerateImage(
|
||||||
@PathVariable Long imageId,
|
@PathVariable Long imageId,
|
||||||
@RequestBody(required = false) ContentCommand.RegenerateImage requestBody) {
|
@RequestBody(required = false) ContentCommand.RegenerateImage requestBody) {
|
||||||
|
|||||||
@ -38,13 +38,6 @@ replicate:
|
|||||||
model:
|
model:
|
||||||
version: ${REPLICATE_MODEL_VERSION:stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b}
|
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 Configuration
|
||||||
cors:
|
cors:
|
||||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*}
|
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*}
|
||||||
@ -96,3 +89,5 @@ logging:
|
|||||||
# Server Configuration
|
# Server Configuration
|
||||||
server:
|
server:
|
||||||
port: ${SERVER_PORT:8084}
|
port: ${SERVER_PORT:8084}
|
||||||
|
servlet:
|
||||||
|
context-path: /api/v1/content
|
||||||
|
|||||||
@ -262,15 +262,60 @@ http://localhost:8082/swagger-ui/index.html
|
|||||||
|
|
||||||
## 10. 다음 단계
|
## 10. 다음 단계
|
||||||
|
|
||||||
1. **컨테이너 테스트**: 로컬 환경에서 컨테이너 실행 및 API 테스트
|
### 빌드 수행 이력
|
||||||
2. **환경변수 설정**: 운영 환경에 맞는 환경변수 구성
|
|
||||||
3. **통합 테스트**: 다른 마이크로서비스들과의 통합 테스트
|
|
||||||
4. **이미지 레지스트리 푸시**: Docker Hub 또는 사설 레지스트리에 이미지 업로드
|
|
||||||
5. **Kubernetes 배포**: K8s 클러스터에 배포
|
|
||||||
|
|
||||||
## 11. 참고사항
|
#### 최신 빌드 (2025-10-28)
|
||||||
|
|
||||||
- **개발 환경 인증**: DevAuthenticationFilter가 자동으로 테스트용 UserPrincipal 생성
|
**1단계: JAR 빌드**
|
||||||
- **프로덕션 배포**: DevAuthenticationFilter 비활성화 및 실제 JWT 인증 필터 활성화 필요
|
```bash
|
||||||
- **보안**: JWT_SECRET은 안전하게 관리하고 최소 32자 이상 사용
|
./gradlew content-service:clean content-service:bootJar
|
||||||
- **성능**: JAVA_OPTS를 통해 JVM 메모리 설정 최적화 권장
|
```
|
||||||
|
|
||||||
|
빌드 결과:
|
||||||
|
```
|
||||||
|
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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user