Merge pull request #19 from ktds-dg0501/feature/content

Feature/content
This commit is contained in:
Cherry Kim 2025-10-28 16:22:49 +09:00 committed by GitHub
commit d36dc5be27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 511 additions and 973 deletions

View File

@ -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'

View File

@ -23,9 +23,9 @@ public class Content {
private final Long id;
/**
* 이벤트 ID (이벤트 초안 ID)
* 이벤트 ID
*/
private final Long eventDraftId;
private final String eventId;
/**
* 이벤트 제목

View File

@ -21,9 +21,9 @@ public class GeneratedImage {
private final Long id;
/**
* 이벤트 ID (이벤트 초안 ID)
* 이벤트 ID
*/
private final Long eventDraftId;
private final String eventId;
/**
* 이미지 스타일

View File

@ -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)

View File

@ -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;

View File

@ -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()

View File

@ -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())

View File

@ -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())

View File

@ -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;
/**
* 이벤트 제목

View File

@ -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)

View File

@ -29,9 +29,9 @@ public class RedisJobData {
private String id;
/**
* 이벤트 초안 ID
* 이벤트 ID
*/
private Long eventDraftId;
private String eventId;
/**
* Job 타입 (image-generation, image-regeneration)

View File

@ -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);

View File

@ -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()

View File

@ -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)

View File

@ -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())

View File

@ -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);
}
}
}

View File

@ -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)

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -10,8 +10,8 @@ public interface GetEventContentUseCase {
/**
* 이벤트 전체 콘텐츠 조회 (이미지 목록 포함)
*
* @param eventDraftId 이벤트 초안 ID
* @param eventId 이벤트 ID
* @return 콘텐츠 정보
*/
ContentInfo execute(Long eventDraftId);
ContentInfo execute(String eventId);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -24,6 +24,14 @@ public interface ContentWriter {
*/
GeneratedImage saveImage(GeneratedImage image);
/**
* 이미지 ID로 이미지 조회
*
* @param imageId 이미지 ID
* @return 이미지 도메인 모델
*/
GeneratedImage getImageById(Long imageId);
/**
* 이미지 ID로 이미지 삭제
*

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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}")

View File

@ -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);

View File

@ -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}")

View File

@ -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}")

View File

@ -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;
/**

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -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}

0
gradlew vendored Normal file → Executable file
View File