Merge pull request #19 from ktds-dg0501/feature/content
Feature/content
This commit is contained in:
commit
d36dc5be27
@ -2,9 +2,13 @@ configurations {
|
||||
// Exclude JPA and PostgreSQL from inherited dependencies (Phase 3: Redis migration)
|
||||
implementation.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-data-jpa'
|
||||
implementation.exclude group: 'org.postgresql', module: 'postgresql'
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
|
||||
// Redis for AI data reading and image URL caching
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
|
||||
|
||||
@ -23,9 +23,9 @@ public class Content {
|
||||
private final Long id;
|
||||
|
||||
/**
|
||||
* 이벤트 ID (이벤트 초안 ID)
|
||||
* 이벤트 ID
|
||||
*/
|
||||
private final Long eventDraftId;
|
||||
private final String eventId;
|
||||
|
||||
/**
|
||||
* 이벤트 제목
|
||||
|
||||
@ -21,9 +21,9 @@ public class GeneratedImage {
|
||||
private final Long id;
|
||||
|
||||
/**
|
||||
* 이벤트 ID (이벤트 초안 ID)
|
||||
* 이벤트 ID
|
||||
*/
|
||||
private final Long eventDraftId;
|
||||
private final String eventId;
|
||||
|
||||
/**
|
||||
* 이미지 스타일
|
||||
|
||||
@ -31,9 +31,9 @@ public class Job {
|
||||
private final String id;
|
||||
|
||||
/**
|
||||
* 이벤트 ID (이벤트 초안 ID)
|
||||
* 이벤트 ID
|
||||
*/
|
||||
private final Long eventDraftId;
|
||||
private final String eventId;
|
||||
|
||||
/**
|
||||
* Job 타입 (image-generation)
|
||||
|
||||
@ -20,7 +20,7 @@ public class ContentCommand {
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public static class GenerateImages {
|
||||
private Long eventDraftId;
|
||||
private String eventId;
|
||||
private String eventTitle;
|
||||
private String eventDescription;
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ import java.util.stream.Collectors;
|
||||
public class ContentInfo {
|
||||
|
||||
private Long id;
|
||||
private Long eventDraftId;
|
||||
private String eventId;
|
||||
private String eventTitle;
|
||||
private String eventDescription;
|
||||
private List<ImageInfo> images;
|
||||
@ -34,7 +34,7 @@ public class ContentInfo {
|
||||
public static ContentInfo from(Content content) {
|
||||
return ContentInfo.builder()
|
||||
.id(content.getId())
|
||||
.eventDraftId(content.getEventDraftId())
|
||||
.eventId(content.getEventId())
|
||||
.eventTitle(content.getEventTitle())
|
||||
.eventDescription(content.getEventDescription())
|
||||
.images(content.getImages().stream()
|
||||
|
||||
@ -18,7 +18,7 @@ import java.time.LocalDateTime;
|
||||
public class ImageInfo {
|
||||
|
||||
private Long id;
|
||||
private Long eventDraftId;
|
||||
private String eventId;
|
||||
private ImageStyle style;
|
||||
private Platform platform;
|
||||
private String cdnUrl;
|
||||
@ -36,7 +36,7 @@ public class ImageInfo {
|
||||
public static ImageInfo from(GeneratedImage image) {
|
||||
return ImageInfo.builder()
|
||||
.id(image.getId())
|
||||
.eventDraftId(image.getEventDraftId())
|
||||
.eventId(image.getEventId())
|
||||
.style(image.getStyle())
|
||||
.platform(image.getPlatform())
|
||||
.cdnUrl(image.getCdnUrl())
|
||||
|
||||
@ -16,7 +16,7 @@ import java.time.LocalDateTime;
|
||||
public class JobInfo {
|
||||
|
||||
private String id;
|
||||
private Long eventDraftId;
|
||||
private String eventId;
|
||||
private String jobType;
|
||||
private Job.Status status;
|
||||
private int progress;
|
||||
@ -34,7 +34,7 @@ public class JobInfo {
|
||||
public static JobInfo from(Job job) {
|
||||
return JobInfo.builder()
|
||||
.id(job.getId())
|
||||
.eventDraftId(job.getEventDraftId())
|
||||
.eventId(job.getEventId())
|
||||
.jobType(job.getJobType())
|
||||
.status(job.getStatus())
|
||||
.progress(job.getProgress())
|
||||
|
||||
@ -10,7 +10,7 @@ import java.util.Map;
|
||||
/**
|
||||
* AI Service가 Redis에 저장한 이벤트 데이터 (읽기 전용)
|
||||
*
|
||||
* Key Pattern: ai:event:{eventDraftId}
|
||||
* Key Pattern: ai:event:{eventId}
|
||||
* Data Type: Hash
|
||||
* TTL: 24시간 (86400초)
|
||||
*
|
||||
@ -25,9 +25,9 @@ import java.util.Map;
|
||||
@AllArgsConstructor
|
||||
public class RedisAIEventData {
|
||||
/**
|
||||
* 이벤트 초안 ID
|
||||
* 이벤트 ID
|
||||
*/
|
||||
private Long eventDraftId;
|
||||
private String eventId;
|
||||
|
||||
/**
|
||||
* 이벤트 제목
|
||||
|
||||
@ -12,7 +12,7 @@ import java.time.LocalDateTime;
|
||||
/**
|
||||
* Redis에 저장되는 이미지 데이터 구조
|
||||
*
|
||||
* Key Pattern: content:image:{eventDraftId}:{style}:{platform}
|
||||
* Key Pattern: content:image:{eventId}:{style}:{platform}
|
||||
* Data Type: String (JSON)
|
||||
* TTL: 7일 (604800초)
|
||||
*
|
||||
@ -31,9 +31,9 @@ public class RedisImageData {
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 이벤트 초안 ID
|
||||
* 이벤트 ID
|
||||
*/
|
||||
private Long eventDraftId;
|
||||
private String eventId;
|
||||
|
||||
/**
|
||||
* 이미지 스타일 (FANCY, SIMPLE, TRENDY)
|
||||
|
||||
@ -29,9 +29,9 @@ public class RedisJobData {
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 이벤트 초안 ID
|
||||
* 이벤트 ID
|
||||
*/
|
||||
private Long eventDraftId;
|
||||
private String eventId;
|
||||
|
||||
/**
|
||||
* Job 타입 (image-generation, image-regeneration)
|
||||
|
||||
@ -23,8 +23,8 @@ public class GetEventContentService implements GetEventContentUseCase {
|
||||
private final ContentReader contentReader;
|
||||
|
||||
@Override
|
||||
public ContentInfo execute(Long eventDraftId) {
|
||||
Content content = contentReader.findByEventDraftIdWithImages(eventDraftId)
|
||||
public ContentInfo execute(String eventId) {
|
||||
Content content = contentReader.findByEventDraftIdWithImages(eventId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.COMMON_001, "콘텐츠를 찾을 수 없습니다"));
|
||||
|
||||
return ContentInfo.from(content);
|
||||
|
||||
@ -26,10 +26,10 @@ public class GetImageListService implements GetImageListUseCase {
|
||||
private final ContentReader contentReader;
|
||||
|
||||
@Override
|
||||
public List<ImageInfo> execute(Long eventDraftId, ImageStyle style, Platform platform) {
|
||||
log.info("이미지 목록 조회: eventDraftId={}, style={}, platform={}", eventDraftId, style, platform);
|
||||
public List<ImageInfo> execute(String eventId, ImageStyle style, Platform platform) {
|
||||
log.info("이미지 목록 조회: eventId={}, style={}, platform={}", eventId, style, platform);
|
||||
|
||||
List<GeneratedImage> images = contentReader.findImagesByEventDraftId(eventDraftId);
|
||||
List<GeneratedImage> images = contentReader.findImagesByEventDraftId(eventId);
|
||||
|
||||
// 필터링 적용
|
||||
return images.stream()
|
||||
|
||||
@ -19,7 +19,6 @@ 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.context.annotation.Profile;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@ -34,7 +33,6 @@ import java.util.UUID;
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Profile({"prod", "dev"}) // production 및 dev 환경에서 활성화 (local은 Mock 사용)
|
||||
public class HuggingFaceImageGenerator implements GenerateImagesUseCase {
|
||||
|
||||
private final HuggingFaceApiClient huggingFaceClient;
|
||||
@ -58,15 +56,15 @@ public class HuggingFaceImageGenerator implements GenerateImagesUseCase {
|
||||
|
||||
@Override
|
||||
public JobInfo execute(ContentCommand.GenerateImages command) {
|
||||
log.info("Hugging Face 이미지 생성 요청: eventDraftId={}, styles={}, platforms={}",
|
||||
command.getEventDraftId(), command.getStyles(), command.getPlatforms());
|
||||
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)
|
||||
.eventDraftId(command.getEventDraftId())
|
||||
.eventId(command.getEventId())
|
||||
.jobType("image-generation")
|
||||
.status(Job.Status.PENDING)
|
||||
.progress(0)
|
||||
@ -77,7 +75,7 @@ public class HuggingFaceImageGenerator implements GenerateImagesUseCase {
|
||||
// Job 저장
|
||||
RedisJobData jobData = RedisJobData.builder()
|
||||
.id(job.getId())
|
||||
.eventDraftId(job.getEventDraftId())
|
||||
.eventId(job.getEventId())
|
||||
.jobType(job.getJobType())
|
||||
.status(job.getStatus().name())
|
||||
.progress(job.getProgress())
|
||||
@ -101,8 +99,8 @@ public class HuggingFaceImageGenerator implements GenerateImagesUseCase {
|
||||
|
||||
// Content 생성 또는 조회
|
||||
Content content = Content.builder()
|
||||
.eventDraftId(command.getEventDraftId())
|
||||
.eventTitle(command.getEventDraftId() + " 이벤트")
|
||||
.eventId(command.getEventId())
|
||||
.eventTitle(command.getEventId() + " 이벤트")
|
||||
.eventDescription("AI 생성 이벤트 이미지")
|
||||
.createdAt(java.time.LocalDateTime.now())
|
||||
.updatedAt(java.time.LocalDateTime.now())
|
||||
@ -137,7 +135,7 @@ public class HuggingFaceImageGenerator implements GenerateImagesUseCase {
|
||||
|
||||
// GeneratedImage 저장
|
||||
GeneratedImage image = GeneratedImage.builder()
|
||||
.eventDraftId(command.getEventDraftId())
|
||||
.eventId(command.getEventId())
|
||||
.style(style)
|
||||
.platform(platform)
|
||||
.cdnUrl(imageUrl)
|
||||
|
||||
@ -32,7 +32,7 @@ public class JobManagementService implements GetJobStatusUseCase {
|
||||
// RedisJobData를 Job 도메인 객체로 변환
|
||||
Job job = Job.builder()
|
||||
.id(jobData.getId())
|
||||
.eventDraftId(jobData.getEventDraftId())
|
||||
.eventId(jobData.getEventId())
|
||||
.jobType(jobData.getJobType())
|
||||
.status(Job.Status.valueOf(jobData.getStatus()))
|
||||
.progress(jobData.getProgress())
|
||||
|
||||
@ -0,0 +1,277 @@
|
||||
package com.kt.event.content.biz.service;
|
||||
|
||||
import com.kt.event.content.biz.domain.GeneratedImage;
|
||||
import com.kt.event.content.biz.domain.Job;
|
||||
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.RegenerateImageUseCase;
|
||||
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.ReplicateApiClient;
|
||||
import com.kt.event.content.infra.gateway.client.dto.ReplicateRequest;
|
||||
import com.kt.event.content.infra.gateway.client.dto.ReplicateResponse;
|
||||
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 이미지 재생성 서비스
|
||||
*
|
||||
* Stable Diffusion으로 기존 이미지를 새 프롬프트로 재생성
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class RegenerateImageService implements RegenerateImageUseCase {
|
||||
|
||||
private final ReplicateApiClient replicateClient;
|
||||
private final CDNUploader cdnUploader;
|
||||
private final JobWriter jobWriter;
|
||||
private final ContentWriter contentWriter;
|
||||
private final CircuitBreaker circuitBreaker;
|
||||
|
||||
@Value("${replicate.model.version:stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b}")
|
||||
private String modelVersion;
|
||||
|
||||
public RegenerateImageService(
|
||||
ReplicateApiClient replicateClient,
|
||||
CDNUploader cdnUploader,
|
||||
JobWriter jobWriter,
|
||||
ContentWriter contentWriter,
|
||||
@Qualifier("replicateCircuitBreaker") CircuitBreaker circuitBreaker) {
|
||||
this.replicateClient = replicateClient;
|
||||
this.cdnUploader = cdnUploader;
|
||||
this.jobWriter = jobWriter;
|
||||
this.contentWriter = contentWriter;
|
||||
this.circuitBreaker = circuitBreaker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JobInfo execute(ContentCommand.RegenerateImage command) {
|
||||
log.info("이미지 재생성 요청: imageId={}, newPrompt={}",
|
||||
command.getImageId(), command.getNewPrompt());
|
||||
|
||||
// Job 생성
|
||||
String jobId = "job-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
Job job = Job.builder()
|
||||
.id(jobId)
|
||||
.eventId("regenerate-" + command.getImageId())
|
||||
.jobType("image-regeneration")
|
||||
.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);
|
||||
|
||||
// 비동기로 이미지 재생성
|
||||
processImageRegeneration(jobId, command);
|
||||
|
||||
return JobInfo.from(job);
|
||||
}
|
||||
|
||||
@Async
|
||||
private void processImageRegeneration(String jobId, ContentCommand.RegenerateImage command) {
|
||||
try {
|
||||
log.info("이미지 재생성 시작: jobId={}, imageId={}", jobId, command.getImageId());
|
||||
|
||||
// 기존 이미지 조회
|
||||
GeneratedImage existingImage = contentWriter.getImageById(command.getImageId());
|
||||
if (existingImage == null) {
|
||||
throw new RuntimeException("이미지를 찾을 수 없습니다: imageId=" + command.getImageId());
|
||||
}
|
||||
|
||||
jobWriter.updateJobStatus(jobId, "IN_PROGRESS", 30);
|
||||
|
||||
// 새 프롬프트로 이미지 생성
|
||||
String newPrompt = command.getNewPrompt() != null && !command.getNewPrompt().trim().isEmpty()
|
||||
? command.getNewPrompt()
|
||||
: existingImage.getPrompt();
|
||||
|
||||
String imageUrl = generateImage(newPrompt, existingImage.getPlatform());
|
||||
|
||||
jobWriter.updateJobStatus(jobId, "IN_PROGRESS", 80);
|
||||
|
||||
// 기존 이미지를 기반으로 새 이미지 생성
|
||||
GeneratedImage updatedImage = GeneratedImage.builder()
|
||||
.id(existingImage.getId())
|
||||
.eventId(existingImage.getEventId())
|
||||
.style(existingImage.getStyle())
|
||||
.platform(existingImage.getPlatform())
|
||||
.cdnUrl(imageUrl) // 새 URL
|
||||
.prompt(newPrompt) // 새 프롬프트
|
||||
.selected(existingImage.isSelected())
|
||||
.createdAt(existingImage.getCreatedAt())
|
||||
.updatedAt(java.time.LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
contentWriter.saveImage(updatedImage);
|
||||
|
||||
log.info("이미지 재생성 완료: imageId={}, url={}", command.getImageId(), imageUrl);
|
||||
|
||||
// Job 완료
|
||||
jobWriter.updateJobStatus(jobId, "COMPLETED", 100);
|
||||
jobWriter.updateJobResult(jobId, "이미지가 성공적으로 재생성되었습니다.");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("이미지 재생성 실패: jobId={}", jobId, e);
|
||||
jobWriter.updateJobError(jobId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable Diffusion으로 이미지 생성
|
||||
*/
|
||||
private String generateImage(String prompt, com.kt.event.content.biz.domain.Platform platform) {
|
||||
try {
|
||||
int width = platform.getWidth();
|
||||
int height = platform.getHeight();
|
||||
|
||||
// Replicate API 요청
|
||||
ReplicateRequest request = ReplicateRequest.builder()
|
||||
.version(modelVersion)
|
||||
.input(ReplicateRequest.Input.builder()
|
||||
.prompt(prompt)
|
||||
.negativePrompt("blurry, bad quality, distorted, ugly, low resolution")
|
||||
.width(width)
|
||||
.height(height)
|
||||
.numOutputs(1)
|
||||
.guidanceScale(7.5)
|
||||
.numInferenceSteps(50)
|
||||
.seed(System.currentTimeMillis())
|
||||
.build())
|
||||
.build();
|
||||
|
||||
log.info("Replicate API 호출: prompt={}, size={}x{}", prompt, width, height);
|
||||
ReplicateResponse response = createPredictionWithCircuitBreaker(request);
|
||||
String predictionId = response.getId();
|
||||
|
||||
// 이미지 생성 완료까지 대기
|
||||
String replicateUrl = waitForCompletion(predictionId);
|
||||
log.info("이미지 생성 완료: url={}", replicateUrl);
|
||||
|
||||
// 이미지 다운로드
|
||||
byte[] imageData = downloadImage(replicateUrl);
|
||||
|
||||
// Azure Blob Storage에 업로드
|
||||
String fileName = String.format("regenerate-%s-%s.png",
|
||||
predictionId.substring(0, 8),
|
||||
System.currentTimeMillis());
|
||||
String azureCdnUrl = cdnUploader.upload(imageData, fileName);
|
||||
|
||||
return azureCdnUrl;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("이미지 생성 실패: prompt={}", prompt, e);
|
||||
throw new RuntimeException("이미지 생성 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replicate API 예측 완료 대기
|
||||
*/
|
||||
private String waitForCompletion(String predictionId) throws InterruptedException {
|
||||
int maxRetries = 60;
|
||||
int retryCount = 0;
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
ReplicateResponse response = getPredictionWithCircuitBreaker(predictionId);
|
||||
String status = response.getStatus();
|
||||
|
||||
if ("succeeded".equals(status)) {
|
||||
List<String> output = response.getOutput();
|
||||
if (output != null && !output.isEmpty()) {
|
||||
return output.get(0);
|
||||
}
|
||||
throw new RuntimeException("이미지 URL이 없습니다");
|
||||
} else if ("failed".equals(status) || "canceled".equals(status)) {
|
||||
String error = response.getError() != null ? response.getError() : "알 수 없는 오류";
|
||||
throw new RuntimeException("이미지 생성 실패: " + error);
|
||||
}
|
||||
|
||||
Thread.sleep(5000);
|
||||
retryCount++;
|
||||
}
|
||||
|
||||
throw new RuntimeException("이미지 생성 타임아웃 (5분 초과)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 다운로드
|
||||
*/
|
||||
private byte[] downloadImage(String imageUrl) throws Exception {
|
||||
URL url = new URL(imageUrl);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setConnectTimeout(30000);
|
||||
connection.setReadTimeout(30000);
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
if (responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw new RuntimeException("이미지 다운로드 실패: HTTP " + responseCode);
|
||||
}
|
||||
|
||||
try (InputStream inputStream = connection.getInputStream();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Circuit Breaker로 보호된 Replicate 예측 생성
|
||||
*/
|
||||
private ReplicateResponse createPredictionWithCircuitBreaker(ReplicateRequest request) {
|
||||
try {
|
||||
return circuitBreaker.executeSupplier(() -> replicateClient.createPrediction(request));
|
||||
} catch (CallNotPermittedException e) {
|
||||
log.error("Replicate Circuit Breaker가 OPEN 상태입니다");
|
||||
throw new RuntimeException("Replicate API에 일시적으로 접근할 수 없습니다", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Circuit Breaker로 보호된 Replicate 예측 조회
|
||||
*/
|
||||
private ReplicateResponse getPredictionWithCircuitBreaker(String predictionId) {
|
||||
try {
|
||||
return circuitBreaker.executeSupplier(() -> replicateClient.getPrediction(predictionId));
|
||||
} catch (CallNotPermittedException e) {
|
||||
log.error("Replicate Circuit Breaker가 OPEN 상태입니다");
|
||||
throw new RuntimeException("Replicate API에 일시적으로 접근할 수 없습니다", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -22,7 +22,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@ -42,7 +41,6 @@ import java.util.UUID;
|
||||
@Slf4j
|
||||
@Service
|
||||
@Primary
|
||||
@Profile({"prod", "dev"}) // production 및 dev 환경에서 활성화 (local은 Mock 사용)
|
||||
public class StableDiffusionImageGenerator implements GenerateImagesUseCase {
|
||||
|
||||
private final ReplicateApiClient replicateClient;
|
||||
@ -69,15 +67,15 @@ public class StableDiffusionImageGenerator implements GenerateImagesUseCase {
|
||||
|
||||
@Override
|
||||
public JobInfo execute(ContentCommand.GenerateImages command) {
|
||||
log.info("Stable Diffusion 이미지 생성 요청: eventDraftId={}, styles={}, platforms={}",
|
||||
command.getEventDraftId(), command.getStyles(), command.getPlatforms());
|
||||
log.info("Stable Diffusion 이미지 생성 요청: 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)
|
||||
.eventDraftId(command.getEventDraftId())
|
||||
.eventId(command.getEventId())
|
||||
.jobType("image-generation")
|
||||
.status(Job.Status.PENDING)
|
||||
.progress(0)
|
||||
@ -88,7 +86,7 @@ public class StableDiffusionImageGenerator implements GenerateImagesUseCase {
|
||||
// Job 저장
|
||||
RedisJobData jobData = RedisJobData.builder()
|
||||
.id(job.getId())
|
||||
.eventDraftId(job.getEventDraftId())
|
||||
.eventId(job.getEventId())
|
||||
.jobType(job.getJobType())
|
||||
.status(job.getStatus().name())
|
||||
.progress(job.getProgress())
|
||||
@ -112,8 +110,8 @@ public class StableDiffusionImageGenerator implements GenerateImagesUseCase {
|
||||
|
||||
// Content 생성 또는 조회
|
||||
Content content = Content.builder()
|
||||
.eventDraftId(command.getEventDraftId())
|
||||
.eventTitle(command.getEventDraftId() + " 이벤트")
|
||||
.eventId(command.getEventId())
|
||||
.eventTitle(command.getEventId() + " 이벤트")
|
||||
.eventDescription("AI 생성 이벤트 이미지")
|
||||
.createdAt(java.time.LocalDateTime.now())
|
||||
.updatedAt(java.time.LocalDateTime.now())
|
||||
@ -148,7 +146,7 @@ public class StableDiffusionImageGenerator implements GenerateImagesUseCase {
|
||||
|
||||
// GeneratedImage 저장
|
||||
GeneratedImage image = GeneratedImage.builder()
|
||||
.eventDraftId(command.getEventDraftId())
|
||||
.eventId(command.getEventId())
|
||||
.style(style)
|
||||
.platform(platform)
|
||||
.cdnUrl(imageUrl)
|
||||
|
||||
@ -1,154 +0,0 @@
|
||||
package com.kt.event.content.biz.service.mock;
|
||||
|
||||
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.ContentWriter;
|
||||
import com.kt.event.content.biz.usecase.out.JobWriter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Mock 이미지 생성 서비스 (테스트용)
|
||||
* local 및 test 환경에서만 사용
|
||||
*
|
||||
* 테스트를 위해 실제로 Content와 GeneratedImage를 생성합니다.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Profile({"local", "test"})
|
||||
@RequiredArgsConstructor
|
||||
public class MockGenerateImagesService implements GenerateImagesUseCase {
|
||||
|
||||
private final JobWriter jobWriter;
|
||||
private final ContentWriter contentWriter;
|
||||
|
||||
@Override
|
||||
public JobInfo execute(ContentCommand.GenerateImages command) {
|
||||
log.info("[MOCK] 이미지 생성 요청: eventDraftId={}, styles={}, platforms={}",
|
||||
command.getEventDraftId(), command.getStyles(), command.getPlatforms());
|
||||
|
||||
// Mock Job 생성
|
||||
String jobId = "job-mock-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
Job job = Job.builder()
|
||||
.id(jobId)
|
||||
.eventDraftId(command.getEventDraftId())
|
||||
.jobType("image-generation")
|
||||
.status(Job.Status.PENDING)
|
||||
.progress(0)
|
||||
.createdAt(java.time.LocalDateTime.now())
|
||||
.updatedAt(java.time.LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// Job 저장 (Job 도메인을 RedisJobData로 변환)
|
||||
RedisJobData jobData = RedisJobData.builder()
|
||||
.id(job.getId())
|
||||
.eventDraftId(job.getEventDraftId())
|
||||
.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("[MOCK] Job 생성 완료: jobId={}", jobId);
|
||||
|
||||
// 비동기로 이미지 생성 시뮬레이션
|
||||
processImageGeneration(jobId, command);
|
||||
|
||||
return JobInfo.from(job);
|
||||
}
|
||||
|
||||
@Async
|
||||
private void processImageGeneration(String jobId, ContentCommand.GenerateImages command) {
|
||||
try {
|
||||
log.info("[MOCK] 이미지 생성 시작: jobId={}", jobId);
|
||||
|
||||
// 1초 대기 (이미지 생성 시뮬레이션)
|
||||
Thread.sleep(1000);
|
||||
|
||||
// Content 생성 또는 조회
|
||||
Content content = Content.builder()
|
||||
.eventDraftId(command.getEventDraftId())
|
||||
.eventTitle("Mock 이벤트 제목 " + command.getEventDraftId())
|
||||
.eventDescription("Mock 이벤트 설명입니다. 테스트를 위한 Mock 데이터입니다.")
|
||||
.createdAt(java.time.LocalDateTime.now())
|
||||
.updatedAt(java.time.LocalDateTime.now())
|
||||
.build();
|
||||
Content savedContent = contentWriter.save(content);
|
||||
log.info("[MOCK] 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 count = 0;
|
||||
for (ImageStyle style : styles) {
|
||||
for (Platform platform : platforms) {
|
||||
count++;
|
||||
String mockCdnUrl = String.format(
|
||||
"https://mock-cdn.azure.com/images/%d/%s_%s_%s.png",
|
||||
command.getEventDraftId(),
|
||||
style.name().toLowerCase(),
|
||||
platform.name().toLowerCase(),
|
||||
UUID.randomUUID().toString().substring(0, 8)
|
||||
);
|
||||
|
||||
GeneratedImage image = GeneratedImage.builder()
|
||||
.eventDraftId(command.getEventDraftId())
|
||||
.style(style)
|
||||
.platform(platform)
|
||||
.cdnUrl(mockCdnUrl)
|
||||
.prompt(String.format("Mock prompt for %s style on %s platform", style, platform))
|
||||
.selected(false)
|
||||
.createdAt(java.time.LocalDateTime.now())
|
||||
.updatedAt(java.time.LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 첫 번째 이미지를 선택된 이미지로 설정
|
||||
if (count == 1) {
|
||||
image.select();
|
||||
}
|
||||
|
||||
GeneratedImage savedImage = contentWriter.saveImage(image);
|
||||
images.add(savedImage);
|
||||
log.info("[MOCK] 이미지 생성: imageId={}, style={}, platform={}",
|
||||
savedImage.getId(), style, platform);
|
||||
}
|
||||
}
|
||||
|
||||
// Job 상태 업데이트: COMPLETED
|
||||
String resultMessage = String.format("%d개의 이미지가 성공적으로 생성되었습니다.", images.size());
|
||||
jobWriter.updateJobStatus(jobId, "COMPLETED", 100);
|
||||
jobWriter.updateJobResult(jobId, resultMessage);
|
||||
log.info("[MOCK] Job 완료: jobId={}, 생성된 이미지 수={}", jobId, images.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] 이미지 생성 실패: jobId={}", jobId, e);
|
||||
|
||||
// Job 상태 업데이트: FAILED
|
||||
jobWriter.updateJobError(jobId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
package com.kt.event.content.biz.service.mock;
|
||||
|
||||
import com.kt.event.content.biz.domain.Job;
|
||||
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.RegenerateImageUseCase;
|
||||
import com.kt.event.content.biz.usecase.out.JobWriter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Mock 이미지 재생성 서비스 (테스트용)
|
||||
* 실제 구현 전까지 사용
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Profile({"local", "test", "dev"})
|
||||
@RequiredArgsConstructor
|
||||
public class MockRegenerateImageService implements RegenerateImageUseCase {
|
||||
|
||||
private final JobWriter jobWriter;
|
||||
|
||||
@Override
|
||||
public JobInfo execute(ContentCommand.RegenerateImage command) {
|
||||
log.info("[MOCK] 이미지 재생성 요청: imageId={}", command.getImageId());
|
||||
|
||||
// Mock Job 생성
|
||||
String jobId = "job-regen-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
Job job = Job.builder()
|
||||
.id(jobId)
|
||||
.eventDraftId(999L) // Mock event ID
|
||||
.jobType("image-regeneration")
|
||||
.status(Job.Status.PENDING)
|
||||
.progress(0)
|
||||
.createdAt(java.time.LocalDateTime.now())
|
||||
.updatedAt(java.time.LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// Job 저장 (Job 도메인을 RedisJobData로 변환)
|
||||
RedisJobData jobData = RedisJobData.builder()
|
||||
.id(job.getId())
|
||||
.eventDraftId(job.getEventDraftId())
|
||||
.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("[MOCK] 재생성 Job 생성 완료: jobId={}", jobId);
|
||||
|
||||
return JobInfo.from(job);
|
||||
}
|
||||
}
|
||||
@ -10,8 +10,8 @@ public interface GetEventContentUseCase {
|
||||
/**
|
||||
* 이벤트 전체 콘텐츠 조회 (이미지 목록 포함)
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param eventId 이벤트 ID
|
||||
* @return 콘텐츠 정보
|
||||
*/
|
||||
ContentInfo execute(Long eventDraftId);
|
||||
ContentInfo execute(String eventId);
|
||||
}
|
||||
|
||||
@ -14,10 +14,10 @@ public interface GetImageListUseCase {
|
||||
/**
|
||||
* 이벤트의 이미지 목록 조회 (필터링 지원)
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param eventId 이벤트 ID
|
||||
* @param style 이미지 스타일 필터 (null이면 전체)
|
||||
* @param platform 플랫폼 필터 (null이면 전체)
|
||||
* @return 이미지 정보 목록
|
||||
*/
|
||||
List<ImageInfo> execute(Long eventDraftId, ImageStyle style, Platform platform);
|
||||
List<ImageInfo> execute(String eventId, ImageStyle style, Platform platform);
|
||||
}
|
||||
|
||||
@ -14,10 +14,10 @@ public interface ContentReader {
|
||||
/**
|
||||
* 이벤트 초안 ID로 콘텐츠 조회 (이미지 목록 포함)
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param eventId 이벤트 초안 ID
|
||||
* @return 콘텐츠 도메인 모델
|
||||
*/
|
||||
Optional<Content> findByEventDraftIdWithImages(Long eventDraftId);
|
||||
Optional<Content> findByEventDraftIdWithImages(String eventId);
|
||||
|
||||
/**
|
||||
* 이미지 ID로 이미지 조회
|
||||
@ -30,8 +30,8 @@ public interface ContentReader {
|
||||
/**
|
||||
* 이벤트 초안 ID로 이미지 목록 조회
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param eventId 이벤트 초안 ID
|
||||
* @return 이미지 도메인 모델 목록
|
||||
*/
|
||||
List<GeneratedImage> findImagesByEventDraftId(Long eventDraftId);
|
||||
List<GeneratedImage> findImagesByEventDraftId(String eventId);
|
||||
}
|
||||
|
||||
@ -24,6 +24,14 @@ public interface ContentWriter {
|
||||
*/
|
||||
GeneratedImage saveImage(GeneratedImage image);
|
||||
|
||||
/**
|
||||
* 이미지 ID로 이미지 조회
|
||||
*
|
||||
* @param imageId 이미지 ID
|
||||
* @return 이미지 도메인 모델
|
||||
*/
|
||||
GeneratedImage getImageById(Long imageId);
|
||||
|
||||
/**
|
||||
* 이미지 ID로 이미지 삭제
|
||||
*
|
||||
|
||||
@ -15,18 +15,18 @@ public interface ImageReader {
|
||||
/**
|
||||
* 특정 이미지 조회
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param eventId 이벤트 초안 ID
|
||||
* @param style 이미지 스타일
|
||||
* @param platform 플랫폼
|
||||
* @return 이미지 데이터
|
||||
*/
|
||||
Optional<RedisImageData> getImage(Long eventDraftId, ImageStyle style, Platform platform);
|
||||
Optional<RedisImageData> getImage(String eventId, ImageStyle style, Platform platform);
|
||||
|
||||
/**
|
||||
* 이벤트의 모든 이미지 조회
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param eventId 이벤트 초안 ID
|
||||
* @return 이미지 목록
|
||||
*/
|
||||
List<RedisImageData> getImagesByEventId(Long eventDraftId);
|
||||
List<RedisImageData> getImagesByEventId(String eventId);
|
||||
}
|
||||
|
||||
@ -22,18 +22,18 @@ public interface ImageWriter {
|
||||
/**
|
||||
* 여러 이미지 저장
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param eventId 이벤트 초안 ID
|
||||
* @param images 이미지 목록
|
||||
* @param ttlSeconds TTL (초 단위)
|
||||
*/
|
||||
void saveImages(Long eventDraftId, List<RedisImageData> images, long ttlSeconds);
|
||||
void saveImages(String eventId, List<RedisImageData> images, long ttlSeconds);
|
||||
|
||||
/**
|
||||
* 이미지 삭제
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param eventId 이벤트 초안 ID
|
||||
* @param style 이미지 스타일
|
||||
* @param platform 플랫폼
|
||||
*/
|
||||
void deleteImage(Long eventDraftId, ImageStyle style, Platform platform);
|
||||
void deleteImage(String eventId, ImageStyle style, Platform platform);
|
||||
}
|
||||
|
||||
@ -12,8 +12,8 @@ public interface RedisAIDataReader {
|
||||
/**
|
||||
* AI 추천 데이터 조회
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param eventId 이벤트 초안 ID
|
||||
* @return AI 추천 데이터 (JSON 형태의 Map)
|
||||
*/
|
||||
Optional<Map<String, Object>> getAIRecommendation(Long eventDraftId);
|
||||
Optional<Map<String, Object>> getAIRecommendation(String eventId);
|
||||
}
|
||||
|
||||
@ -13,9 +13,9 @@ public interface RedisImageWriter {
|
||||
/**
|
||||
* 이미지 목록 캐싱
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param eventId 이벤트 초안 ID
|
||||
* @param images 이미지 목록
|
||||
* @param ttlSeconds TTL (초)
|
||||
*/
|
||||
void cacheImages(Long eventDraftId, List<GeneratedImage> images, long ttlSeconds);
|
||||
void cacheImages(String eventId, List<GeneratedImage> images, long ttlSeconds);
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ package com.kt.event.content.infra.config;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
@ -12,11 +11,9 @@ import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSeriali
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 설정 (Production 환경용)
|
||||
* Local/Test 환경에서는 Mock Gateway 사용
|
||||
* Redis 설정
|
||||
*/
|
||||
@Configuration
|
||||
@Profile({"!local", "!test"})
|
||||
public class RedisConfig {
|
||||
|
||||
@Value("${spring.data.redis.host}")
|
||||
|
||||
@ -18,7 +18,6 @@ import com.kt.event.content.biz.usecase.out.RedisAIDataReader;
|
||||
import com.kt.event.content.biz.usecase.out.RedisImageWriter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@ -31,13 +30,10 @@ import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Redis Gateway 구현체 (Production 환경용)
|
||||
*
|
||||
* Local/Test 환경에서는 MockRedisGateway 사용
|
||||
* Redis Gateway 구현체
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Profile({"!local", "!test"})
|
||||
@RequiredArgsConstructor
|
||||
public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageWriter, ImageReader, JobWriter, JobReader, ContentReader, ContentWriter {
|
||||
|
||||
@ -49,13 +45,13 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
private static final Duration DEFAULT_TTL = Duration.ofHours(24);
|
||||
|
||||
@Override
|
||||
public Optional<Map<String, Object>> getAIRecommendation(Long eventDraftId) {
|
||||
public Optional<Map<String, Object>> getAIRecommendation(String eventId) {
|
||||
try {
|
||||
String key = AI_DATA_KEY_PREFIX + eventDraftId;
|
||||
String key = AI_DATA_KEY_PREFIX + eventId;
|
||||
Object data = redisTemplate.opsForValue().get(key);
|
||||
|
||||
if (data == null) {
|
||||
log.warn("AI 이벤트 데이터를 찾을 수 없음: eventDraftId={}", eventDraftId);
|
||||
log.warn("AI 이벤트 데이터를 찾을 수 없음: eventId={}", eventId);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@ -63,48 +59,48 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
Map<String, Object> aiData = objectMapper.convertValue(data, Map.class);
|
||||
return Optional.of(aiData);
|
||||
} catch (Exception e) {
|
||||
log.error("AI 이벤트 데이터 조회 실패: eventDraftId={}", eventDraftId, e);
|
||||
log.error("AI 이벤트 데이터 조회 실패: eventId={}", eventId, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cacheImages(Long eventDraftId, List<GeneratedImage> images, long ttlSeconds) {
|
||||
public void cacheImages(String eventId, List<GeneratedImage> images, long ttlSeconds) {
|
||||
try {
|
||||
String key = IMAGE_URL_KEY_PREFIX + eventDraftId;
|
||||
String key = IMAGE_URL_KEY_PREFIX + eventId;
|
||||
|
||||
// 이미지 목록을 캐싱
|
||||
redisTemplate.opsForValue().set(key, images, Duration.ofSeconds(ttlSeconds));
|
||||
log.info("이미지 목록 캐싱 완료: eventDraftId={}, count={}, ttl={}초",
|
||||
eventDraftId, images.size(), ttlSeconds);
|
||||
log.info("이미지 목록 캐싱 완료: eventId={}, count={}, ttl={}초",
|
||||
eventId, images.size(), ttlSeconds);
|
||||
} catch (Exception e) {
|
||||
log.error("이미지 목록 캐싱 실패: eventDraftId={}", eventDraftId, e);
|
||||
log.error("이미지 목록 캐싱 실패: eventId={}", eventId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 URL 캐시 삭제
|
||||
*/
|
||||
public void deleteImageUrl(Long eventDraftId) {
|
||||
public void deleteImageUrl(String eventId) {
|
||||
try {
|
||||
String key = IMAGE_URL_KEY_PREFIX + eventDraftId;
|
||||
String key = IMAGE_URL_KEY_PREFIX + eventId;
|
||||
redisTemplate.delete(key);
|
||||
log.info("이미지 URL 캐시 삭제: eventDraftId={}", eventDraftId);
|
||||
log.info("이미지 URL 캐시 삭제: eventId={}", eventId);
|
||||
} catch (Exception e) {
|
||||
log.error("이미지 URL 캐시 삭제 실패: eventDraftId={}", eventDraftId, e);
|
||||
log.error("이미지 URL 캐시 삭제 실패: eventId={}", eventId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 이벤트 데이터 캐시 삭제
|
||||
*/
|
||||
public void deleteAIEventData(Long eventDraftId) {
|
||||
public void deleteAIEventData(String eventId) {
|
||||
try {
|
||||
String key = AI_DATA_KEY_PREFIX + eventDraftId;
|
||||
String key = AI_DATA_KEY_PREFIX + eventId;
|
||||
redisTemplate.delete(key);
|
||||
log.info("AI 이벤트 데이터 캐시 삭제: eventDraftId={}", eventDraftId);
|
||||
log.info("AI 이벤트 데이터 캐시 삭제: eventId={}", eventId);
|
||||
} catch (Exception e) {
|
||||
log.error("AI 이벤트 데이터 캐시 삭제 실패: eventDraftId={}", eventDraftId, e);
|
||||
log.error("AI 이벤트 데이터 캐시 삭제 실패: eventId={}", eventId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,26 +110,26 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
|
||||
/**
|
||||
* 이미지 저장
|
||||
* Key: content:image:{eventDraftId}:{style}:{platform}
|
||||
* Key: content:image:{eventId}:{style}:{platform}
|
||||
*/
|
||||
public void saveImage(RedisImageData imageData, long ttlSeconds) {
|
||||
try {
|
||||
String key = buildImageKey(imageData.getEventDraftId(), imageData.getStyle(), imageData.getPlatform());
|
||||
String key = buildImageKey(imageData.getEventId(), imageData.getStyle(), imageData.getPlatform());
|
||||
String json = objectMapper.writeValueAsString(imageData);
|
||||
redisTemplate.opsForValue().set(key, json, Duration.ofSeconds(ttlSeconds));
|
||||
log.info("이미지 저장 완료: key={}, ttl={}초", key, ttlSeconds);
|
||||
} catch (Exception e) {
|
||||
log.error("이미지 저장 실패: eventDraftId={}, style={}, platform={}",
|
||||
imageData.getEventDraftId(), imageData.getStyle(), imageData.getPlatform(), e);
|
||||
log.error("이미지 저장 실패: eventId={}, style={}, platform={}",
|
||||
imageData.getEventId(), imageData.getStyle(), imageData.getPlatform(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 이미지 조회
|
||||
*/
|
||||
public Optional<RedisImageData> getImage(Long eventDraftId, ImageStyle style, Platform platform) {
|
||||
public Optional<RedisImageData> getImage(String eventId, ImageStyle style, Platform platform) {
|
||||
try {
|
||||
String key = buildImageKey(eventDraftId, style, platform);
|
||||
String key = buildImageKey(eventId, style, platform);
|
||||
Object data = redisTemplate.opsForValue().get(key);
|
||||
|
||||
if (data == null) {
|
||||
@ -144,7 +140,7 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
RedisImageData imageData = objectMapper.readValue(data.toString(), RedisImageData.class);
|
||||
return Optional.of(imageData);
|
||||
} catch (Exception e) {
|
||||
log.error("이미지 조회 실패: eventDraftId={}, style={}, platform={}", eventDraftId, style, platform, e);
|
||||
log.error("이미지 조회 실패: eventId={}, style={}, platform={}", eventId, style, platform, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@ -152,13 +148,13 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
/**
|
||||
* 이벤트의 모든 이미지 조회
|
||||
*/
|
||||
public List<RedisImageData> getImagesByEventId(Long eventDraftId) {
|
||||
public List<RedisImageData> getImagesByEventId(String eventId) {
|
||||
try {
|
||||
String pattern = IMAGE_KEY_PREFIX + eventDraftId + ":*";
|
||||
String pattern = IMAGE_KEY_PREFIX + eventId + ":*";
|
||||
var keys = redisTemplate.keys(pattern);
|
||||
|
||||
if (keys == null || keys.isEmpty()) {
|
||||
log.warn("이벤트 이미지를 찾을 수 없음: eventDraftId={}", eventDraftId);
|
||||
log.warn("이벤트 이미지를 찾을 수 없음: eventId={}", eventId);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@ -171,10 +167,10 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
}
|
||||
}
|
||||
|
||||
log.info("이벤트 이미지 조회 완료: eventDraftId={}, count={}", eventDraftId, images.size());
|
||||
log.info("이벤트 이미지 조회 완료: eventId={}, count={}", eventId, images.size());
|
||||
return images;
|
||||
} catch (Exception e) {
|
||||
log.error("이벤트 이미지 조회 실패: eventDraftId={}", eventDraftId, e);
|
||||
log.error("이벤트 이미지 조회 실패: eventId={}", eventId, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@ -182,29 +178,29 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
/**
|
||||
* 이미지 삭제
|
||||
*/
|
||||
public void deleteImage(Long eventDraftId, ImageStyle style, Platform platform) {
|
||||
public void deleteImage(String eventId, ImageStyle style, Platform platform) {
|
||||
try {
|
||||
String key = buildImageKey(eventDraftId, style, platform);
|
||||
String key = buildImageKey(eventId, style, platform);
|
||||
redisTemplate.delete(key);
|
||||
log.info("이미지 삭제 완료: key={}", key);
|
||||
} catch (Exception e) {
|
||||
log.error("이미지 삭제 실패: eventDraftId={}, style={}, platform={}", eventDraftId, style, platform, e);
|
||||
log.error("이미지 삭제 실패: eventId={}, style={}, platform={}", eventId, style, platform, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 이미지 저장
|
||||
*/
|
||||
public void saveImages(Long eventDraftId, List<RedisImageData> images, long ttlSeconds) {
|
||||
public void saveImages(String eventId, List<RedisImageData> images, long ttlSeconds) {
|
||||
images.forEach(image -> saveImage(image, ttlSeconds));
|
||||
log.info("여러 이미지 저장 완료: eventDraftId={}, count={}", eventDraftId, images.size());
|
||||
log.info("여러 이미지 저장 완료: eventId={}, count={}", eventId, images.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 Key 생성
|
||||
*/
|
||||
private String buildImageKey(Long eventDraftId, ImageStyle style, Platform platform) {
|
||||
return IMAGE_KEY_PREFIX + eventDraftId + ":" + style.name() + ":" + platform.name();
|
||||
private String buildImageKey(String eventId, ImageStyle style, Platform platform) {
|
||||
return IMAGE_KEY_PREFIX + eventId + ":" + style.name() + ":" + platform.name();
|
||||
}
|
||||
|
||||
// ==================== Job 상태 관리 ====================
|
||||
@ -222,7 +218,7 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
// Hash 형태로 저장
|
||||
Map<String, String> jobFields = Map.of(
|
||||
"id", jobData.getId(),
|
||||
"eventDraftId", String.valueOf(jobData.getEventDraftId()),
|
||||
"eventId", jobData.getEventId(),
|
||||
"jobType", jobData.getJobType(),
|
||||
"status", jobData.getStatus(),
|
||||
"progress", String.valueOf(jobData.getProgress()),
|
||||
@ -256,7 +252,7 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
|
||||
RedisJobData jobData = RedisJobData.builder()
|
||||
.id(getString(jobFields, "id"))
|
||||
.eventDraftId(getLong(jobFields, "eventDraftId"))
|
||||
.eventId(getString(jobFields, "eventId"))
|
||||
.jobType(getString(jobFields, "jobType"))
|
||||
.status(getString(jobFields, "status"))
|
||||
.progress(getInteger(jobFields, "progress"))
|
||||
@ -349,23 +345,23 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
private static final String IMAGE_IDS_SET_KEY_PREFIX = "content:images:";
|
||||
|
||||
@Override
|
||||
public Optional<Content> findByEventDraftIdWithImages(Long eventDraftId) {
|
||||
public Optional<Content> findByEventDraftIdWithImages(String eventId) {
|
||||
try {
|
||||
String contentKey = CONTENT_META_KEY_PREFIX + eventDraftId;
|
||||
String contentKey = CONTENT_META_KEY_PREFIX + eventId;
|
||||
Map<Object, Object> contentFields = redisTemplate.opsForHash().entries(contentKey);
|
||||
|
||||
if (contentFields.isEmpty()) {
|
||||
log.warn("Content를 찾을 수 없음: eventDraftId={}", eventDraftId);
|
||||
log.warn("Content를 찾을 수 없음: eventId={}", eventId);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// 이미지 목록 조회
|
||||
List<GeneratedImage> images = findImagesByEventDraftId(eventDraftId);
|
||||
List<GeneratedImage> images = findImagesByEventDraftId(eventId);
|
||||
|
||||
// Content 재구성
|
||||
Content content = Content.builder()
|
||||
.id(getLong(contentFields, "id"))
|
||||
.eventDraftId(getLong(contentFields, "eventDraftId"))
|
||||
.eventId(getString(contentFields, "eventId"))
|
||||
.eventTitle(getString(contentFields, "eventTitle"))
|
||||
.eventDescription(getString(contentFields, "eventDescription"))
|
||||
.images(images)
|
||||
@ -375,7 +371,7 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
|
||||
return Optional.of(content);
|
||||
} catch (Exception e) {
|
||||
log.error("Content 조회 실패: eventDraftId={}", eventDraftId, e);
|
||||
log.error("Content 조회 실패: eventId={}", eventId, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@ -400,13 +396,13 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GeneratedImage> findImagesByEventDraftId(Long eventDraftId) {
|
||||
public List<GeneratedImage> findImagesByEventDraftId(String eventId) {
|
||||
try {
|
||||
String setKey = IMAGE_IDS_SET_KEY_PREFIX + eventDraftId;
|
||||
String setKey = IMAGE_IDS_SET_KEY_PREFIX + eventId;
|
||||
var imageIdSet = redisTemplate.opsForSet().members(setKey);
|
||||
|
||||
if (imageIdSet == null || imageIdSet.isEmpty()) {
|
||||
log.info("이미지 목록이 비어있음: eventDraftId={}", eventDraftId);
|
||||
log.info("이미지 목록이 비어있음: eventId={}", eventId);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@ -416,10 +412,10 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
findImageById(imageId).ifPresent(images::add);
|
||||
}
|
||||
|
||||
log.info("이미지 목록 조회 완료: eventDraftId={}, count={}", eventDraftId, images.size());
|
||||
log.info("이미지 목록 조회 완료: eventId={}, count={}", eventId, images.size());
|
||||
return images;
|
||||
} catch (Exception e) {
|
||||
log.error("이미지 목록 조회 실패: eventDraftId={}", eventDraftId, e);
|
||||
log.error("이미지 목록 조회 실패: eventId={}", eventId, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@ -433,12 +429,12 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
public Content save(Content content) {
|
||||
try {
|
||||
Long id = content.getId() != null ? content.getId() : nextContentId++;
|
||||
String contentKey = CONTENT_META_KEY_PREFIX + content.getEventDraftId();
|
||||
String contentKey = CONTENT_META_KEY_PREFIX + content.getEventId();
|
||||
|
||||
// Content 메타 정보 저장
|
||||
Map<String, String> contentFields = new java.util.HashMap<>();
|
||||
contentFields.put("id", String.valueOf(id));
|
||||
contentFields.put("eventDraftId", String.valueOf(content.getEventDraftId()));
|
||||
contentFields.put("eventId", String.valueOf(content.getEventId()));
|
||||
contentFields.put("eventTitle", content.getEventTitle() != null ? content.getEventTitle() : "");
|
||||
contentFields.put("eventDescription", content.getEventDescription() != null ? content.getEventDescription() : "");
|
||||
contentFields.put("createdAt", content.getCreatedAt() != null ? content.getCreatedAt().toString() : LocalDateTime.now().toString());
|
||||
@ -450,7 +446,7 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
// Content 재구성하여 반환
|
||||
Content savedContent = Content.builder()
|
||||
.id(id)
|
||||
.eventDraftId(content.getEventDraftId())
|
||||
.eventId(content.getEventId())
|
||||
.eventTitle(content.getEventTitle())
|
||||
.eventDescription(content.getEventDescription())
|
||||
.images(content.getImages())
|
||||
@ -458,10 +454,10 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
.updatedAt(content.getUpdatedAt())
|
||||
.build();
|
||||
|
||||
log.info("Content 저장 완료: contentId={}, eventDraftId={}", id, content.getEventDraftId());
|
||||
log.info("Content 저장 완료: contentId={}, eventId={}", id, content.getEventId());
|
||||
return savedContent;
|
||||
} catch (Exception e) {
|
||||
log.error("Content 저장 실패: eventDraftId={}", content.getEventDraftId(), e);
|
||||
log.error("Content 저장 실패: eventId={}", content.getEventId(), e);
|
||||
throw new RuntimeException("Content 저장 실패", e);
|
||||
}
|
||||
}
|
||||
@ -475,7 +471,7 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
String imageKey = IMAGE_BY_ID_KEY_PREFIX + imageId;
|
||||
GeneratedImage savedImage = GeneratedImage.builder()
|
||||
.id(imageId)
|
||||
.eventDraftId(image.getEventDraftId())
|
||||
.eventId(image.getEventId())
|
||||
.style(image.getStyle())
|
||||
.platform(image.getPlatform())
|
||||
.cdnUrl(image.getCdnUrl())
|
||||
@ -489,18 +485,29 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
redisTemplate.opsForValue().set(imageKey, json, DEFAULT_TTL);
|
||||
|
||||
// Image ID를 Set에 추가
|
||||
String setKey = IMAGE_IDS_SET_KEY_PREFIX + image.getEventDraftId();
|
||||
String setKey = IMAGE_IDS_SET_KEY_PREFIX + image.getEventId();
|
||||
redisTemplate.opsForSet().add(setKey, imageId);
|
||||
redisTemplate.expire(setKey, DEFAULT_TTL);
|
||||
|
||||
log.info("이미지 저장 완료: imageId={}, eventDraftId={}", imageId, image.getEventDraftId());
|
||||
log.info("이미지 저장 완료: imageId={}, eventId={}", imageId, image.getEventId());
|
||||
return savedImage;
|
||||
} catch (Exception e) {
|
||||
log.error("이미지 저장 실패: eventDraftId={}", image.getEventDraftId(), e);
|
||||
log.error("이미지 저장 실패: eventId={}", image.getEventId(), e);
|
||||
throw new RuntimeException("이미지 저장 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GeneratedImage getImageById(Long imageId) {
|
||||
try {
|
||||
Optional<GeneratedImage> imageOpt = findImageById(imageId);
|
||||
return imageOpt.orElse(null);
|
||||
} catch (Exception e) {
|
||||
log.error("이미지 조회 실패: imageId={}", imageId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteImageById(Long imageId) {
|
||||
try {
|
||||
@ -518,10 +525,10 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageW
|
||||
redisTemplate.delete(imageKey);
|
||||
|
||||
// Set에서 Image ID 제거
|
||||
String setKey = IMAGE_IDS_SET_KEY_PREFIX + image.getEventDraftId();
|
||||
String setKey = IMAGE_IDS_SET_KEY_PREFIX + image.getEventId();
|
||||
redisTemplate.opsForSet().remove(setKey, imageId);
|
||||
|
||||
log.info("이미지 삭제 완료: imageId={}, eventDraftId={}", imageId, image.getEventDraftId());
|
||||
log.info("이미지 삭제 완료: imageId={}, eventId={}", imageId, image.getEventId());
|
||||
} catch (Exception e) {
|
||||
log.error("이미지 삭제 실패: imageId={}", imageId, e);
|
||||
throw new RuntimeException("이미지 삭제 실패", e);
|
||||
|
||||
@ -11,7 +11,6 @@ import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@ -26,7 +25,6 @@ import java.util.UUID;
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Profile({"prod", "dev"}) // production 및 dev 환경에서 활성화 (local은 Mock 사용)
|
||||
public class AzureBlobStorageUploader implements CDNUploader {
|
||||
|
||||
@Value("${azure.storage.connection-string}")
|
||||
|
||||
@ -2,7 +2,6 @@ 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.context.annotation.Profile;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
@ -15,7 +14,6 @@ import org.springframework.web.client.RestClient;
|
||||
* Stable Diffusion 모델: stabilityai/stable-diffusion-2-1
|
||||
*/
|
||||
@Component
|
||||
@Profile({"prod", "dev"})
|
||||
public class HuggingFaceApiClient {
|
||||
|
||||
private final RestClient restClient;
|
||||
@ -23,7 +21,7 @@ public class HuggingFaceApiClient {
|
||||
@Value("${huggingface.api.url:https://api-inference.huggingface.co}")
|
||||
private String apiUrl;
|
||||
|
||||
@Value("${huggingface.api.token}")
|
||||
@Value("${huggingface.api.token:}")
|
||||
private String apiToken;
|
||||
|
||||
@Value("${huggingface.model:stabilityai/stable-diffusion-2-1}")
|
||||
|
||||
@ -16,7 +16,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
@Configuration
|
||||
public class ReplicateApiConfig {
|
||||
|
||||
@Value("${replicate.api.token}")
|
||||
@Value("${replicate.api.token:}")
|
||||
private String apiToken;
|
||||
|
||||
/**
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
package com.kt.event.content.infra.gateway.mock;
|
||||
|
||||
import com.kt.event.content.biz.usecase.out.CDNUploader;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Mock CDN Uploader (테스트용)
|
||||
* 실제 Azure Blob Storage 연동 전까지 사용
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Profile({"local", "test"})
|
||||
public class MockCDNUploader implements CDNUploader {
|
||||
|
||||
private static final String MOCK_CDN_BASE_URL = "https://cdn.kt-event.com/images/mock";
|
||||
|
||||
@Override
|
||||
public String upload(byte[] imageData, String fileName) {
|
||||
log.info("[MOCK] CDN에 이미지 업로드: fileName={}, size={} bytes",
|
||||
fileName, imageData.length);
|
||||
|
||||
// Mock CDN URL 생성
|
||||
String mockUrl = String.format("%s/%s", MOCK_CDN_BASE_URL, fileName);
|
||||
|
||||
log.info("[MOCK] 업로드된 CDN URL: {}", mockUrl);
|
||||
|
||||
return mockUrl;
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
package com.kt.event.content.infra.gateway.mock;
|
||||
|
||||
import com.kt.event.content.biz.domain.ImageStyle;
|
||||
import com.kt.event.content.biz.domain.Platform;
|
||||
import com.kt.event.content.biz.usecase.out.ImageGeneratorCaller;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Mock Image Generator (테스트용)
|
||||
* 실제 AI 이미지 생성 API 연동 전까지 사용
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Profile({"local", "test"})
|
||||
public class MockImageGenerator implements ImageGeneratorCaller {
|
||||
|
||||
@Override
|
||||
public byte[] generateImage(String prompt, ImageStyle style, Platform platform) {
|
||||
log.info("[MOCK] AI 이미지 생성: prompt='{}', style={}, platform={}",
|
||||
prompt, style, platform);
|
||||
|
||||
// Mock: 빈 바이트 배열 반환 (실제로는 AI가 생성한 이미지 데이터)
|
||||
byte[] mockImageData = createMockImageData(style, platform);
|
||||
|
||||
log.info("[MOCK] 이미지 생성 완료: size={} bytes", mockImageData.length);
|
||||
|
||||
return mockImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 이미지 데이터 생성
|
||||
* 실제로는 PNG/JPEG 이미지 바이너리 데이터
|
||||
*/
|
||||
private byte[] createMockImageData(ImageStyle style, Platform platform) {
|
||||
// 간단한 Mock 데이터 생성 (실제로는 이미지 바이너리)
|
||||
String mockContent = String.format("MOCK_IMAGE_DATA[style=%s,platform=%s]", style, platform);
|
||||
return mockContent.getBytes();
|
||||
}
|
||||
}
|
||||
@ -1,430 +0,0 @@
|
||||
package com.kt.event.content.infra.gateway.mock;
|
||||
|
||||
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.RedisImageData;
|
||||
import com.kt.event.content.biz.dto.RedisJobData;
|
||||
import com.kt.event.content.biz.usecase.out.ContentReader;
|
||||
import com.kt.event.content.biz.usecase.out.ContentWriter;
|
||||
import com.kt.event.content.biz.usecase.out.ImageReader;
|
||||
import com.kt.event.content.biz.usecase.out.ImageWriter;
|
||||
import com.kt.event.content.biz.usecase.out.JobReader;
|
||||
import com.kt.event.content.biz.usecase.out.JobWriter;
|
||||
import com.kt.event.content.biz.usecase.out.RedisAIDataReader;
|
||||
import com.kt.event.content.biz.usecase.out.RedisImageWriter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Mock Redis Gateway (테스트용)
|
||||
* 실제 Redis 연동 전까지 사용
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Primary
|
||||
@Profile({"local", "test"})
|
||||
public class MockRedisGateway implements RedisAIDataReader, RedisImageWriter, ImageWriter, ImageReader, JobWriter, JobReader, ContentReader, ContentWriter {
|
||||
|
||||
private final Map<Long, Map<String, Object>> aiDataCache = new HashMap<>();
|
||||
|
||||
// In-memory storage for contents, images, and jobs
|
||||
private final Map<Long, Content> contentStorage = new ConcurrentHashMap<>();
|
||||
private final Map<Long, GeneratedImage> imageByIdStorage = new ConcurrentHashMap<>();
|
||||
private final Map<String, RedisImageData> imageStorage = new ConcurrentHashMap<>();
|
||||
private final Map<String, RedisJobData> jobStorage = new ConcurrentHashMap<>();
|
||||
|
||||
// ========================================
|
||||
// RedisAIDataReader 구현
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
public Optional<Map<String, Object>> getAIRecommendation(Long eventDraftId) {
|
||||
log.info("[MOCK] Redis에서 AI 추천 데이터 조회: eventDraftId={}", eventDraftId);
|
||||
|
||||
// Mock 데이터 반환
|
||||
Map<String, Object> mockData = new HashMap<>();
|
||||
mockData.put("title", "테스트 이벤트 제목");
|
||||
mockData.put("description", "테스트 이벤트 설명");
|
||||
mockData.put("brandColor", "#FF5733");
|
||||
|
||||
return Optional.of(mockData);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// RedisImageWriter 구현
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
public void cacheImages(Long eventDraftId, List<GeneratedImage> images, long ttlSeconds) {
|
||||
log.info("[MOCK] Redis에 이미지 캐싱: eventDraftId={}, count={}, ttl={}초",
|
||||
eventDraftId, images.size(), ttlSeconds);
|
||||
}
|
||||
|
||||
// ==================== 이미지 CRUD ====================
|
||||
|
||||
private static final String IMAGE_KEY_PREFIX = "content:image:";
|
||||
|
||||
/**
|
||||
* 이미지 저장
|
||||
*/
|
||||
public void saveImage(RedisImageData imageData, long ttlSeconds) {
|
||||
try {
|
||||
String key = buildImageKey(imageData.getEventDraftId(), imageData.getStyle(), imageData.getPlatform());
|
||||
imageStorage.put(key, imageData);
|
||||
log.info("[MOCK] 이미지 저장 완료: key={}, ttl={}초", key, ttlSeconds);
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] 이미지 저장 실패: eventDraftId={}, style={}, platform={}",
|
||||
imageData.getEventDraftId(), imageData.getStyle(), imageData.getPlatform(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 이미지 조회
|
||||
*/
|
||||
public Optional<RedisImageData> getImage(Long eventDraftId, ImageStyle style, Platform platform) {
|
||||
try {
|
||||
String key = buildImageKey(eventDraftId, style, platform);
|
||||
RedisImageData imageData = imageStorage.get(key);
|
||||
|
||||
if (imageData == null) {
|
||||
log.warn("[MOCK] 이미지를 찾을 수 없음: key={}", key);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(imageData);
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] 이미지 조회 실패: eventDraftId={}, style={}, platform={}",
|
||||
eventDraftId, style, platform, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트의 모든 이미지 조회
|
||||
*/
|
||||
public List<RedisImageData> getImagesByEventId(Long eventDraftId) {
|
||||
try {
|
||||
String pattern = IMAGE_KEY_PREFIX + eventDraftId + ":";
|
||||
|
||||
List<RedisImageData> images = imageStorage.entrySet().stream()
|
||||
.filter(entry -> entry.getKey().startsWith(pattern))
|
||||
.map(Map.Entry::getValue)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.info("[MOCK] 이벤트 이미지 조회 완료: eventDraftId={}, count={}", eventDraftId, images.size());
|
||||
return images;
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] 이벤트 이미지 조회 실패: eventDraftId={}", eventDraftId, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 삭제
|
||||
*/
|
||||
public void deleteImage(Long eventDraftId, ImageStyle style, Platform platform) {
|
||||
try {
|
||||
String key = buildImageKey(eventDraftId, style, platform);
|
||||
imageStorage.remove(key);
|
||||
log.info("[MOCK] 이미지 삭제 완료: key={}", key);
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] 이미지 삭제 실패: eventDraftId={}, style={}, platform={}",
|
||||
eventDraftId, style, platform, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 이미지 저장
|
||||
*/
|
||||
public void saveImages(Long eventDraftId, List<RedisImageData> images, long ttlSeconds) {
|
||||
images.forEach(image -> saveImage(image, ttlSeconds));
|
||||
log.info("[MOCK] 여러 이미지 저장 완료: eventDraftId={}, count={}", eventDraftId, images.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 Key 생성
|
||||
*/
|
||||
private String buildImageKey(Long eventDraftId, ImageStyle style, Platform platform) {
|
||||
return IMAGE_KEY_PREFIX + eventDraftId + ":" + style.name() + ":" + platform.name();
|
||||
}
|
||||
|
||||
// ==================== Job 상태 관리 ====================
|
||||
|
||||
private static final String JOB_KEY_PREFIX = "job:";
|
||||
|
||||
/**
|
||||
* Job 생성/저장
|
||||
*/
|
||||
public void saveJob(RedisJobData jobData, long ttlSeconds) {
|
||||
try {
|
||||
String key = JOB_KEY_PREFIX + jobData.getId();
|
||||
jobStorage.put(key, jobData);
|
||||
log.info("[MOCK] Job 저장 완료: jobId={}, status={}, ttl={}초",
|
||||
jobData.getId(), jobData.getStatus(), ttlSeconds);
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] Job 저장 실패: jobId={}", jobData.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job 조회
|
||||
*/
|
||||
public Optional<RedisJobData> getJob(String jobId) {
|
||||
try {
|
||||
String key = JOB_KEY_PREFIX + jobId;
|
||||
RedisJobData jobData = jobStorage.get(key);
|
||||
|
||||
if (jobData == null) {
|
||||
log.warn("[MOCK] Job을 찾을 수 없음: jobId={}", jobId);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(jobData);
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] Job 조회 실패: jobId={}", jobId, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job 상태 업데이트
|
||||
*/
|
||||
public void updateJobStatus(String jobId, String status, Integer progress) {
|
||||
try {
|
||||
String key = JOB_KEY_PREFIX + jobId;
|
||||
RedisJobData jobData = jobStorage.get(key);
|
||||
|
||||
if (jobData != null) {
|
||||
jobData.setStatus(status);
|
||||
jobData.setProgress(progress);
|
||||
jobData.setUpdatedAt(LocalDateTime.now());
|
||||
jobStorage.put(key, jobData);
|
||||
log.info("[MOCK] Job 상태 업데이트: jobId={}, status={}, progress={}",
|
||||
jobId, status, progress);
|
||||
} else {
|
||||
log.warn("[MOCK] Job을 찾을 수 없어 상태 업데이트 실패: jobId={}", jobId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] Job 상태 업데이트 실패: jobId={}", jobId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job 결과 메시지 업데이트
|
||||
*/
|
||||
public void updateJobResult(String jobId, String resultMessage) {
|
||||
try {
|
||||
String key = JOB_KEY_PREFIX + jobId;
|
||||
RedisJobData jobData = jobStorage.get(key);
|
||||
|
||||
if (jobData != null) {
|
||||
jobData.setResultMessage(resultMessage);
|
||||
jobData.setUpdatedAt(LocalDateTime.now());
|
||||
jobStorage.put(key, jobData);
|
||||
log.info("[MOCK] Job 결과 업데이트: jobId={}, resultMessage={}", jobId, resultMessage);
|
||||
} else {
|
||||
log.warn("[MOCK] Job을 찾을 수 없어 결과 업데이트 실패: jobId={}", jobId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] Job 결과 업데이트 실패: jobId={}", jobId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job 에러 메시지 업데이트
|
||||
*/
|
||||
public void updateJobError(String jobId, String errorMessage) {
|
||||
try {
|
||||
String key = JOB_KEY_PREFIX + jobId;
|
||||
RedisJobData jobData = jobStorage.get(key);
|
||||
|
||||
if (jobData != null) {
|
||||
jobData.setErrorMessage(errorMessage);
|
||||
jobData.setStatus("FAILED");
|
||||
jobData.setUpdatedAt(LocalDateTime.now());
|
||||
jobStorage.put(key, jobData);
|
||||
log.info("[MOCK] Job 에러 업데이트: jobId={}, errorMessage={}", jobId, errorMessage);
|
||||
} else {
|
||||
log.warn("[MOCK] Job을 찾을 수 없어 에러 업데이트 실패: jobId={}", jobId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] Job 에러 업데이트 실패: jobId={}", jobId, e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ContentReader 구현 ====================
|
||||
|
||||
/**
|
||||
* 이벤트 초안 ID로 콘텐츠 조회 (이미지 목록 포함)
|
||||
*/
|
||||
@Override
|
||||
public Optional<Content> findByEventDraftIdWithImages(Long eventDraftId) {
|
||||
try {
|
||||
Content content = contentStorage.get(eventDraftId);
|
||||
if (content == null) {
|
||||
log.warn("[MOCK] Content를 찾을 수 없음: eventDraftId={}", eventDraftId);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// 이미지 목록 조회 및 Content 재생성 (immutable pattern)
|
||||
List<GeneratedImage> images = findImagesByEventDraftId(eventDraftId);
|
||||
Content contentWithImages = Content.builder()
|
||||
.id(content.getId())
|
||||
.eventDraftId(content.getEventDraftId())
|
||||
.eventTitle(content.getEventTitle())
|
||||
.eventDescription(content.getEventDescription())
|
||||
.images(images)
|
||||
.createdAt(content.getCreatedAt())
|
||||
.updatedAt(content.getUpdatedAt())
|
||||
.build();
|
||||
|
||||
return Optional.of(contentWithImages);
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] Content 조회 실패: eventDraftId={}", eventDraftId, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 ID로 이미지 조회
|
||||
*/
|
||||
@Override
|
||||
public Optional<GeneratedImage> findImageById(Long imageId) {
|
||||
try {
|
||||
GeneratedImage image = imageByIdStorage.get(imageId);
|
||||
if (image == null) {
|
||||
log.warn("[MOCK] 이미지를 찾을 수 없음: imageId={}", imageId);
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(image);
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] 이미지 조회 실패: imageId={}", imageId, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 초안 ID로 이미지 목록 조회
|
||||
*/
|
||||
@Override
|
||||
public List<GeneratedImage> findImagesByEventDraftId(Long eventDraftId) {
|
||||
try {
|
||||
return imageByIdStorage.values().stream()
|
||||
.filter(image -> image.getEventDraftId().equals(eventDraftId))
|
||||
.collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] 이미지 목록 조회 실패: eventDraftId={}", eventDraftId, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ContentWriter 구현 ====================
|
||||
|
||||
private static Long nextContentId = 1L;
|
||||
private static Long nextImageId = 1L;
|
||||
|
||||
/**
|
||||
* 콘텐츠 저장
|
||||
*/
|
||||
@Override
|
||||
public Content save(Content content) {
|
||||
try {
|
||||
// ID가 없으면 생성하여 새 Content 객체 생성 (immutable pattern)
|
||||
Long id = content.getId() != null ? content.getId() : nextContentId++;
|
||||
|
||||
Content savedContent = Content.builder()
|
||||
.id(id)
|
||||
.eventDraftId(content.getEventDraftId())
|
||||
.eventTitle(content.getEventTitle())
|
||||
.eventDescription(content.getEventDescription())
|
||||
.images(content.getImages())
|
||||
.createdAt(content.getCreatedAt())
|
||||
.updatedAt(content.getUpdatedAt())
|
||||
.build();
|
||||
|
||||
contentStorage.put(savedContent.getEventDraftId(), savedContent);
|
||||
log.info("[MOCK] Content 저장 완료: contentId={}, eventDraftId={}",
|
||||
savedContent.getId(), savedContent.getEventDraftId());
|
||||
|
||||
return savedContent;
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] Content 저장 실패: eventDraftId={}", content.getEventDraftId(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 저장
|
||||
*/
|
||||
@Override
|
||||
public GeneratedImage saveImage(GeneratedImage image) {
|
||||
try {
|
||||
// ID가 없으면 생성하여 새 GeneratedImage 객체 생성 (immutable pattern)
|
||||
Long id = image.getId() != null ? image.getId() : nextImageId++;
|
||||
|
||||
GeneratedImage savedImage = GeneratedImage.builder()
|
||||
.id(id)
|
||||
.eventDraftId(image.getEventDraftId())
|
||||
.style(image.getStyle())
|
||||
.platform(image.getPlatform())
|
||||
.cdnUrl(image.getCdnUrl())
|
||||
.prompt(image.getPrompt())
|
||||
.selected(image.isSelected())
|
||||
.createdAt(image.getCreatedAt())
|
||||
.updatedAt(image.getUpdatedAt())
|
||||
.build();
|
||||
|
||||
imageByIdStorage.put(savedImage.getId(), savedImage);
|
||||
log.info("[MOCK] 이미지 저장 완료: imageId={}, eventDraftId={}, style={}, platform={}",
|
||||
savedImage.getId(), savedImage.getEventDraftId(), savedImage.getStyle(), savedImage.getPlatform());
|
||||
|
||||
return savedImage;
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] 이미지 저장 실패: eventDraftId={}", image.getEventDraftId(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 ID로 이미지 삭제
|
||||
*/
|
||||
@Override
|
||||
public void deleteImageById(Long imageId) {
|
||||
try {
|
||||
// imageByIdStorage에서 이미지 조회
|
||||
GeneratedImage image = imageByIdStorage.get(imageId);
|
||||
|
||||
if (image == null) {
|
||||
log.warn("[MOCK] 삭제할 이미지를 찾을 수 없음: imageId={}", imageId);
|
||||
return;
|
||||
}
|
||||
|
||||
// imageByIdStorage에서 삭제
|
||||
imageByIdStorage.remove(imageId);
|
||||
|
||||
// imageStorage에서도 삭제 (Redis 캐시 스토리지)
|
||||
String key = buildImageKey(image.getEventDraftId(), image.getStyle(), image.getPlatform());
|
||||
imageStorage.remove(key);
|
||||
|
||||
log.info("[MOCK] 이미지 삭제 완료: imageId={}, eventDraftId={}, style={}, platform={}",
|
||||
imageId, image.getEventDraftId(), image.getStyle(), image.getPlatform());
|
||||
} catch (Exception e) {
|
||||
log.error("[MOCK] 이미지 삭제 실패: imageId={}", imageId, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -52,8 +52,8 @@ public class ContentController {
|
||||
*/
|
||||
@PostMapping("/images/generate")
|
||||
public ResponseEntity<JobInfo> generateImages(@RequestBody ContentCommand.GenerateImages command) {
|
||||
log.info("이미지 생성 요청: eventDraftId={}, styles={}, platforms={}",
|
||||
command.getEventDraftId(), command.getStyles(), command.getPlatforms());
|
||||
log.info("이미지 생성 요청: eventId={}, styles={}, platforms={}",
|
||||
command.getEventId(), command.getStyles(), command.getPlatforms());
|
||||
|
||||
JobInfo jobInfo = generateImagesUseCase.execute(command);
|
||||
|
||||
@ -77,42 +77,42 @@ public class ContentController {
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/content/events/{eventDraftId}
|
||||
* GET /api/v1/content/events/{eventId}
|
||||
* 이벤트의 생성된 콘텐츠 조회
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param eventId 이벤트 ID
|
||||
* @return 200 OK - 콘텐츠 정보 (이미지 목록 포함)
|
||||
*/
|
||||
@GetMapping("/events/{eventDraftId}")
|
||||
public ResponseEntity<ContentInfo> getContentByEventId(@PathVariable Long eventDraftId) {
|
||||
log.info("이벤트 콘텐츠 조회: eventDraftId={}", eventDraftId);
|
||||
@GetMapping("/events/{eventId}")
|
||||
public ResponseEntity<ContentInfo> getContentByEventId(@PathVariable String eventId) {
|
||||
log.info("이벤트 콘텐츠 조회: eventId={}", eventId);
|
||||
|
||||
ContentInfo contentInfo = getEventContentUseCase.execute(eventDraftId);
|
||||
ContentInfo contentInfo = getEventContentUseCase.execute(eventId);
|
||||
|
||||
return ResponseEntity.ok(contentInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/content/events/{eventDraftId}/images
|
||||
* GET /api/v1/content/events/{eventId}/images
|
||||
* 이벤트의 이미지 목록 조회 (필터링)
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param eventId 이벤트 ID
|
||||
* @param style 이미지 스타일 필터 (선택)
|
||||
* @param platform 플랫폼 필터 (선택)
|
||||
* @return 200 OK - 이미지 목록
|
||||
*/
|
||||
@GetMapping("/events/{eventDraftId}/images")
|
||||
@GetMapping("/events/{eventId}/images")
|
||||
public ResponseEntity<List<ImageInfo>> getImages(
|
||||
@PathVariable Long eventDraftId,
|
||||
@PathVariable String eventId,
|
||||
@RequestParam(required = false) String style,
|
||||
@RequestParam(required = false) String platform) {
|
||||
log.info("이미지 목록 조회: eventDraftId={}, style={}, platform={}", eventDraftId, style, platform);
|
||||
log.info("이미지 목록 조회: eventId={}, style={}, platform={}", eventId, style, platform);
|
||||
|
||||
// String -> Enum 변환
|
||||
ImageStyle imageStyle = style != null ? ImageStyle.valueOf(style.toUpperCase()) : null;
|
||||
Platform imagePlatform = platform != null ? Platform.valueOf(platform.toUpperCase()) : null;
|
||||
|
||||
List<ImageInfo> images = getImageListUseCase.execute(eventDraftId, imageStyle, imagePlatform);
|
||||
List<ImageInfo> images = getImageListUseCase.execute(eventId, imageStyle, imagePlatform);
|
||||
|
||||
return ResponseEntity.ok(images);
|
||||
}
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
spring:
|
||||
application:
|
||||
name: content-service
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:20.214.210.71}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
|
||||
server:
|
||||
port: ${SERVER_PORT:8084}
|
||||
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:kt-event-marketing-jwt-secret-key-for-authentication-and-authorization-2025}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600000}
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800000}
|
||||
|
||||
azure:
|
||||
storage:
|
||||
connection-string: ${AZURE_STORAGE_CONNECTION_STRING:}
|
||||
container-name: ${AZURE_CONTAINER_NAME:event-images}
|
||||
|
||||
replicate:
|
||||
api:
|
||||
url: ${REPLICATE_API_URL:https://api.replicate.com}
|
||||
token: ${REPLICATE_API_TOKEN:r8_Q33U00fSnpjYlHNIRglwurV446h7g8V2wkFFa}
|
||||
|
||||
huggingface:
|
||||
api:
|
||||
url: ${HUGGINGFACE_API_URL:https://api-inference.huggingface.co}
|
||||
token: ${HUGGINGFACE_API_TOKEN:}
|
||||
model: ${HUGGINGFACE_MODEL:runwayml/stable-diffusion-v1-5}
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.kt.event: ${LOG_LEVEL_APP:DEBUG}
|
||||
root: ${LOG_LEVEL_ROOT:INFO}
|
||||
file:
|
||||
name: ${LOG_FILE:logs/content-service.log}
|
||||
logback:
|
||||
rollingpolicy:
|
||||
max-file-size: 10MB
|
||||
max-history: 7
|
||||
total-size-cap: 100MB
|
||||
@ -1,43 +0,0 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:contentdb
|
||||
username: sa
|
||||
password:
|
||||
driver-class-name: org.h2.Driver
|
||||
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.H2Dialect
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
dialect: org.hibernate.dialect.H2Dialect
|
||||
|
||||
data:
|
||||
redis:
|
||||
# Redis 연결 비활성화 (Mock 사용)
|
||||
repositories:
|
||||
enabled: false
|
||||
host: localhost
|
||||
port: 6379
|
||||
|
||||
autoconfigure:
|
||||
exclude:
|
||||
- org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
|
||||
- org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
|
||||
|
||||
server:
|
||||
port: 8084
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.kt.event: DEBUG
|
||||
org.hibernate.SQL: DEBUG
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
||||
@ -2,38 +2,97 @@ spring:
|
||||
application:
|
||||
name: content-service
|
||||
|
||||
# Redis Configuration
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
enabled: ${REDIS_ENABLED:true}
|
||||
host: ${REDIS_HOST:20.214.210.71}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
|
||||
server:
|
||||
port: ${SERVER_PORT:8084}
|
||||
password: ${REDIS_PASSWORD:Hi5Jessica!}
|
||||
timeout: ${REDIS_TIMEOUT:2000ms}
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: ${REDIS_POOL_MAX:8}
|
||||
max-idle: ${REDIS_POOL_IDLE:8}
|
||||
min-idle: ${REDIS_POOL_MIN:0}
|
||||
max-wait: ${REDIS_POOL_WAIT:-1ms}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
|
||||
# JWT Configuration
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:dev-jwt-secret-key}
|
||||
secret: ${JWT_SECRET:kt-event-marketing-jwt-secret-key-for-authentication-and-authorization-2025}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600000}
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800000}
|
||||
|
||||
# Azure Blob Storage Configuration
|
||||
azure:
|
||||
storage:
|
||||
connection-string: ${AZURE_STORAGE_CONNECTION_STRING:}
|
||||
container-name: ${AZURE_CONTAINER_NAME:event-images}
|
||||
connection-string: ${AZURE_STORAGE_CONNECTION_STRING:DefaultEndpointsProtocol=https;AccountName=blobkteventstorage;AccountKey=tcBN7mAfojbl0uGsOpU7RNuKNhHnzmwDiWjN31liSMVSrWaEK+HHnYKZrjBXXAC6ZPsuxUDlsf8x+AStd++QYg==;EndpointSuffix=core.windows.net}
|
||||
container-name: ${AZURE_CONTAINER_NAME:content-images}
|
||||
|
||||
# Replicate API Configuration (Stable Diffusion)
|
||||
replicate:
|
||||
api:
|
||||
url: ${REPLICATE_API_URL:https://api.replicate.com}
|
||||
token: ${REPLICATE_API_TOKEN:}
|
||||
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:*}
|
||||
|
||||
# Actuator
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
base-path: /actuator
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
show-components: always
|
||||
health:
|
||||
livenessState:
|
||||
enabled: true
|
||||
readinessState:
|
||||
enabled: true
|
||||
|
||||
# OpenAPI Documentation
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
tags-sorter: alpha
|
||||
operations-sorter: alpha
|
||||
show-actuator: false
|
||||
|
||||
# Logging
|
||||
logging:
|
||||
level:
|
||||
com.kt.event: ${LOG_LEVEL_APP:DEBUG}
|
||||
com.kt.event.content: ${LOG_LEVEL_APP:DEBUG}
|
||||
org.springframework.web: ${LOG_LEVEL_WEB:INFO}
|
||||
root: ${LOG_LEVEL_ROOT:INFO}
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: ${LOG_FILE:logs/content-service.log}
|
||||
name: ${LOG_FILE_PATH:logs/content-service.log}
|
||||
logback:
|
||||
rollingpolicy:
|
||||
max-file-size: 10MB
|
||||
max-history: 7
|
||||
total-size-cap: 100MB
|
||||
max-file-size: ${LOG_FILE_MAX_SIZE:10MB}
|
||||
max-history: ${LOG_FILE_MAX_HISTORY:7}
|
||||
total-size-cap: ${LOG_FILE_TOTAL_CAP:100MB}
|
||||
|
||||
# Server Configuration
|
||||
server:
|
||||
port: ${SERVER_PORT:8084}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user