From 16a91c85bf8a006424d91e9908b2260751c85290 Mon Sep 17 00:00:00 2001 From: cherry2250 Date: Tue, 28 Oct 2025 10:46:47 +0900 Subject: [PATCH 1/3] =?UTF-8?q?gradlew=20=EC=8B=A4=ED=96=89=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From 5a82fe3610c06fabb786c5d5b7a74f42c4791d04 Mon Sep 17 00:00:00 2001 From: cherry2250 Date: Tue, 28 Oct 2025 15:52:57 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Mock=20=EA=B5=AC=ED=98=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20=EC=9B=90=EA=B2=A9=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=97=B0=EA=B2=B0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mock 디렉토리 완전 제거 (biz/service/mock, infra/gateway/mock) - @Profile 조건부 어노테이션 모두 제거 - Redis 원격 서버 연결 (20.214.210.71:6379) - RegenerateImageService 실제 구현 추가 - ContentWriter.getImageById() 메서드 추가 - JWT Secret 보안 강화 (32자 이상) - API 토큰 기본값 설정 추가 - AKS 배포 준비 완료 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../kt/event/content/biz/domain/Content.java | 4 +- .../content/biz/domain/GeneratedImage.java | 4 +- .../com/kt/event/content/biz/domain/Job.java | 4 +- .../event/content/biz/dto/ContentCommand.java | 2 +- .../kt/event/content/biz/dto/ContentInfo.java | 4 +- .../kt/event/content/biz/dto/ImageInfo.java | 4 +- .../com/kt/event/content/biz/dto/JobInfo.java | 4 +- .../content/biz/dto/RedisAIEventData.java | 6 +- .../event/content/biz/dto/RedisImageData.java | 6 +- .../event/content/biz/dto/RedisJobData.java | 4 +- .../biz/service/GetEventContentService.java | 4 +- .../biz/service/GetImageListService.java | 6 +- .../service/HuggingFaceImageGenerator.java | 16 +- .../biz/service/JobManagementService.java | 2 +- .../biz/service/RegenerateImageService.java | 277 +++++++++++ .../StableDiffusionImageGenerator.java | 16 +- .../mock/MockGenerateImagesService.java | 154 ------- .../mock/MockRegenerateImageService.java | 62 --- .../usecase/in/GetEventContentUseCase.java | 4 +- .../biz/usecase/in/GetImageListUseCase.java | 4 +- .../biz/usecase/out/ContentReader.java | 8 +- .../biz/usecase/out/ContentWriter.java | 8 + .../content/biz/usecase/out/ImageReader.java | 8 +- .../content/biz/usecase/out/ImageWriter.java | 8 +- .../biz/usecase/out/RedisAIDataReader.java | 4 +- .../biz/usecase/out/RedisImageWriter.java | 4 +- .../content/infra/config/RedisConfig.java | 5 +- .../content/infra/gateway/RedisGateway.java | 137 +++--- .../client/AzureBlobStorageUploader.java | 2 - .../gateway/client/HuggingFaceApiClient.java | 4 +- .../gateway/client/ReplicateApiConfig.java | 2 +- .../infra/gateway/mock/MockCDNUploader.java | 31 -- .../gateway/mock/MockImageGenerator.java | 41 -- .../infra/gateway/mock/MockRedisGateway.java | 430 ------------------ .../web/controller/ContentController.java | 28 +- .../src/main/resources/application.yml | 6 +- 36 files changed, 438 insertions(+), 875 deletions(-) create mode 100644 content-service/src/main/java/com/kt/event/content/biz/service/RegenerateImageService.java delete mode 100644 content-service/src/main/java/com/kt/event/content/biz/service/mock/MockGenerateImagesService.java delete mode 100644 content-service/src/main/java/com/kt/event/content/biz/service/mock/MockRegenerateImageService.java delete mode 100644 content-service/src/main/java/com/kt/event/content/infra/gateway/mock/MockCDNUploader.java delete mode 100644 content-service/src/main/java/com/kt/event/content/infra/gateway/mock/MockImageGenerator.java delete mode 100644 content-service/src/main/java/com/kt/event/content/infra/gateway/mock/MockRedisGateway.java diff --git a/content-service/src/main/java/com/kt/event/content/biz/domain/Content.java b/content-service/src/main/java/com/kt/event/content/biz/domain/Content.java index 278c110..6b3eb5a 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/domain/Content.java +++ b/content-service/src/main/java/com/kt/event/content/biz/domain/Content.java @@ -23,9 +23,9 @@ public class Content { private final Long id; /** - * 이벤트 ID (이벤트 초안 ID) + * 이벤트 ID */ - private final Long eventDraftId; + private final String eventId; /** * 이벤트 제목 diff --git a/content-service/src/main/java/com/kt/event/content/biz/domain/GeneratedImage.java b/content-service/src/main/java/com/kt/event/content/biz/domain/GeneratedImage.java index 2d08b1e..2da63ad 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/domain/GeneratedImage.java +++ b/content-service/src/main/java/com/kt/event/content/biz/domain/GeneratedImage.java @@ -21,9 +21,9 @@ public class GeneratedImage { private final Long id; /** - * 이벤트 ID (이벤트 초안 ID) + * 이벤트 ID */ - private final Long eventDraftId; + private final String eventId; /** * 이미지 스타일 diff --git a/content-service/src/main/java/com/kt/event/content/biz/domain/Job.java b/content-service/src/main/java/com/kt/event/content/biz/domain/Job.java index cc67600..2e3047d 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/domain/Job.java +++ b/content-service/src/main/java/com/kt/event/content/biz/domain/Job.java @@ -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) diff --git a/content-service/src/main/java/com/kt/event/content/biz/dto/ContentCommand.java b/content-service/src/main/java/com/kt/event/content/biz/dto/ContentCommand.java index 6b06318..bfa7d81 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/dto/ContentCommand.java +++ b/content-service/src/main/java/com/kt/event/content/biz/dto/ContentCommand.java @@ -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; diff --git a/content-service/src/main/java/com/kt/event/content/biz/dto/ContentInfo.java b/content-service/src/main/java/com/kt/event/content/biz/dto/ContentInfo.java index 727b9ec..f7cbe7a 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/dto/ContentInfo.java +++ b/content-service/src/main/java/com/kt/event/content/biz/dto/ContentInfo.java @@ -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 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() diff --git a/content-service/src/main/java/com/kt/event/content/biz/dto/ImageInfo.java b/content-service/src/main/java/com/kt/event/content/biz/dto/ImageInfo.java index 5aed268..8c179aa 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/dto/ImageInfo.java +++ b/content-service/src/main/java/com/kt/event/content/biz/dto/ImageInfo.java @@ -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()) diff --git a/content-service/src/main/java/com/kt/event/content/biz/dto/JobInfo.java b/content-service/src/main/java/com/kt/event/content/biz/dto/JobInfo.java index 48e4909..336a3a2 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/dto/JobInfo.java +++ b/content-service/src/main/java/com/kt/event/content/biz/dto/JobInfo.java @@ -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()) diff --git a/content-service/src/main/java/com/kt/event/content/biz/dto/RedisAIEventData.java b/content-service/src/main/java/com/kt/event/content/biz/dto/RedisAIEventData.java index a624bc9..8c0be71 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/dto/RedisAIEventData.java +++ b/content-service/src/main/java/com/kt/event/content/biz/dto/RedisAIEventData.java @@ -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; /** * 이벤트 제목 diff --git a/content-service/src/main/java/com/kt/event/content/biz/dto/RedisImageData.java b/content-service/src/main/java/com/kt/event/content/biz/dto/RedisImageData.java index 58fdce2..a73b110 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/dto/RedisImageData.java +++ b/content-service/src/main/java/com/kt/event/content/biz/dto/RedisImageData.java @@ -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) diff --git a/content-service/src/main/java/com/kt/event/content/biz/dto/RedisJobData.java b/content-service/src/main/java/com/kt/event/content/biz/dto/RedisJobData.java index d65f3f6..d53223e 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/dto/RedisJobData.java +++ b/content-service/src/main/java/com/kt/event/content/biz/dto/RedisJobData.java @@ -29,9 +29,9 @@ public class RedisJobData { private String id; /** - * 이벤트 초안 ID + * 이벤트 ID */ - private Long eventDraftId; + private String eventId; /** * Job 타입 (image-generation, image-regeneration) diff --git a/content-service/src/main/java/com/kt/event/content/biz/service/GetEventContentService.java b/content-service/src/main/java/com/kt/event/content/biz/service/GetEventContentService.java index 8ac84bb..2df84bb 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/service/GetEventContentService.java +++ b/content-service/src/main/java/com/kt/event/content/biz/service/GetEventContentService.java @@ -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); diff --git a/content-service/src/main/java/com/kt/event/content/biz/service/GetImageListService.java b/content-service/src/main/java/com/kt/event/content/biz/service/GetImageListService.java index e1c48b5..705c919 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/service/GetImageListService.java +++ b/content-service/src/main/java/com/kt/event/content/biz/service/GetImageListService.java @@ -26,10 +26,10 @@ public class GetImageListService implements GetImageListUseCase { private final ContentReader contentReader; @Override - public List execute(Long eventDraftId, ImageStyle style, Platform platform) { - log.info("이미지 목록 조회: eventDraftId={}, style={}, platform={}", eventDraftId, style, platform); + public List execute(String eventId, ImageStyle style, Platform platform) { + log.info("이미지 목록 조회: eventId={}, style={}, platform={}", eventId, style, platform); - List images = contentReader.findImagesByEventDraftId(eventDraftId); + List images = contentReader.findImagesByEventDraftId(eventId); // 필터링 적용 return images.stream() diff --git a/content-service/src/main/java/com/kt/event/content/biz/service/HuggingFaceImageGenerator.java b/content-service/src/main/java/com/kt/event/content/biz/service/HuggingFaceImageGenerator.java index 09c864a..106b5c3 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/service/HuggingFaceImageGenerator.java +++ b/content-service/src/main/java/com/kt/event/content/biz/service/HuggingFaceImageGenerator.java @@ -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) diff --git a/content-service/src/main/java/com/kt/event/content/biz/service/JobManagementService.java b/content-service/src/main/java/com/kt/event/content/biz/service/JobManagementService.java index 798dfdb..8a7cf9a 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/service/JobManagementService.java +++ b/content-service/src/main/java/com/kt/event/content/biz/service/JobManagementService.java @@ -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()) diff --git a/content-service/src/main/java/com/kt/event/content/biz/service/RegenerateImageService.java b/content-service/src/main/java/com/kt/event/content/biz/service/RegenerateImageService.java new file mode 100644 index 0000000..67f5320 --- /dev/null +++ b/content-service/src/main/java/com/kt/event/content/biz/service/RegenerateImageService.java @@ -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 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); + } + } +} diff --git a/content-service/src/main/java/com/kt/event/content/biz/service/StableDiffusionImageGenerator.java b/content-service/src/main/java/com/kt/event/content/biz/service/StableDiffusionImageGenerator.java index f1d6058..0d1775e 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/service/StableDiffusionImageGenerator.java +++ b/content-service/src/main/java/com/kt/event/content/biz/service/StableDiffusionImageGenerator.java @@ -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) diff --git a/content-service/src/main/java/com/kt/event/content/biz/service/mock/MockGenerateImagesService.java b/content-service/src/main/java/com/kt/event/content/biz/service/mock/MockGenerateImagesService.java deleted file mode 100644 index 5fd1ab7..0000000 --- a/content-service/src/main/java/com/kt/event/content/biz/service/mock/MockGenerateImagesService.java +++ /dev/null @@ -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 styles = command.getStyles() != null && !command.getStyles().isEmpty() - ? command.getStyles() - : List.of(ImageStyle.FANCY, ImageStyle.SIMPLE); - - List platforms = command.getPlatforms() != null && !command.getPlatforms().isEmpty() - ? command.getPlatforms() - : List.of(Platform.INSTAGRAM, Platform.KAKAO); - - List images = new ArrayList<>(); - int 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()); - } - } -} diff --git a/content-service/src/main/java/com/kt/event/content/biz/service/mock/MockRegenerateImageService.java b/content-service/src/main/java/com/kt/event/content/biz/service/mock/MockRegenerateImageService.java deleted file mode 100644 index 01c9699..0000000 --- a/content-service/src/main/java/com/kt/event/content/biz/service/mock/MockRegenerateImageService.java +++ /dev/null @@ -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); - } -} diff --git a/content-service/src/main/java/com/kt/event/content/biz/usecase/in/GetEventContentUseCase.java b/content-service/src/main/java/com/kt/event/content/biz/usecase/in/GetEventContentUseCase.java index 9b29d21..747aed1 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/usecase/in/GetEventContentUseCase.java +++ b/content-service/src/main/java/com/kt/event/content/biz/usecase/in/GetEventContentUseCase.java @@ -10,8 +10,8 @@ public interface GetEventContentUseCase { /** * 이벤트 전체 콘텐츠 조회 (이미지 목록 포함) * - * @param eventDraftId 이벤트 초안 ID + * @param eventId 이벤트 ID * @return 콘텐츠 정보 */ - ContentInfo execute(Long eventDraftId); + ContentInfo execute(String eventId); } diff --git a/content-service/src/main/java/com/kt/event/content/biz/usecase/in/GetImageListUseCase.java b/content-service/src/main/java/com/kt/event/content/biz/usecase/in/GetImageListUseCase.java index 59e426b..e756897 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/usecase/in/GetImageListUseCase.java +++ b/content-service/src/main/java/com/kt/event/content/biz/usecase/in/GetImageListUseCase.java @@ -14,10 +14,10 @@ public interface GetImageListUseCase { /** * 이벤트의 이미지 목록 조회 (필터링 지원) * - * @param eventDraftId 이벤트 초안 ID + * @param eventId 이벤트 ID * @param style 이미지 스타일 필터 (null이면 전체) * @param platform 플랫폼 필터 (null이면 전체) * @return 이미지 정보 목록 */ - List execute(Long eventDraftId, ImageStyle style, Platform platform); + List execute(String eventId, ImageStyle style, Platform platform); } diff --git a/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ContentReader.java b/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ContentReader.java index 1847e1d..b5d01cb 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ContentReader.java +++ b/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ContentReader.java @@ -14,10 +14,10 @@ public interface ContentReader { /** * 이벤트 초안 ID로 콘텐츠 조회 (이미지 목록 포함) * - * @param eventDraftId 이벤트 초안 ID + * @param eventId 이벤트 초안 ID * @return 콘텐츠 도메인 모델 */ - Optional findByEventDraftIdWithImages(Long eventDraftId); + Optional findByEventDraftIdWithImages(String eventId); /** * 이미지 ID로 이미지 조회 @@ -30,8 +30,8 @@ public interface ContentReader { /** * 이벤트 초안 ID로 이미지 목록 조회 * - * @param eventDraftId 이벤트 초안 ID + * @param eventId 이벤트 초안 ID * @return 이미지 도메인 모델 목록 */ - List findImagesByEventDraftId(Long eventDraftId); + List findImagesByEventDraftId(String eventId); } diff --git a/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ContentWriter.java b/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ContentWriter.java index 62bfb47..4b243c0 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ContentWriter.java +++ b/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ContentWriter.java @@ -24,6 +24,14 @@ public interface ContentWriter { */ GeneratedImage saveImage(GeneratedImage image); + /** + * 이미지 ID로 이미지 조회 + * + * @param imageId 이미지 ID + * @return 이미지 도메인 모델 + */ + GeneratedImage getImageById(Long imageId); + /** * 이미지 ID로 이미지 삭제 * diff --git a/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ImageReader.java b/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ImageReader.java index fe7c384..be76861 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ImageReader.java +++ b/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ImageReader.java @@ -15,18 +15,18 @@ public interface ImageReader { /** * 특정 이미지 조회 * - * @param eventDraftId 이벤트 초안 ID + * @param eventId 이벤트 초안 ID * @param style 이미지 스타일 * @param platform 플랫폼 * @return 이미지 데이터 */ - Optional getImage(Long eventDraftId, ImageStyle style, Platform platform); + Optional getImage(String eventId, ImageStyle style, Platform platform); /** * 이벤트의 모든 이미지 조회 * - * @param eventDraftId 이벤트 초안 ID + * @param eventId 이벤트 초안 ID * @return 이미지 목록 */ - List getImagesByEventId(Long eventDraftId); + List getImagesByEventId(String eventId); } diff --git a/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ImageWriter.java b/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ImageWriter.java index 9c8f167..01e2259 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ImageWriter.java +++ b/content-service/src/main/java/com/kt/event/content/biz/usecase/out/ImageWriter.java @@ -22,18 +22,18 @@ public interface ImageWriter { /** * 여러 이미지 저장 * - * @param eventDraftId 이벤트 초안 ID + * @param eventId 이벤트 초안 ID * @param images 이미지 목록 * @param ttlSeconds TTL (초 단위) */ - void saveImages(Long eventDraftId, List images, long ttlSeconds); + void saveImages(String eventId, List 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); } diff --git a/content-service/src/main/java/com/kt/event/content/biz/usecase/out/RedisAIDataReader.java b/content-service/src/main/java/com/kt/event/content/biz/usecase/out/RedisAIDataReader.java index ee66f12..3f39620 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/usecase/out/RedisAIDataReader.java +++ b/content-service/src/main/java/com/kt/event/content/biz/usecase/out/RedisAIDataReader.java @@ -12,8 +12,8 @@ public interface RedisAIDataReader { /** * AI 추천 데이터 조회 * - * @param eventDraftId 이벤트 초안 ID + * @param eventId 이벤트 초안 ID * @return AI 추천 데이터 (JSON 형태의 Map) */ - Optional> getAIRecommendation(Long eventDraftId); + Optional> getAIRecommendation(String eventId); } diff --git a/content-service/src/main/java/com/kt/event/content/biz/usecase/out/RedisImageWriter.java b/content-service/src/main/java/com/kt/event/content/biz/usecase/out/RedisImageWriter.java index 2ccd7ba..c18e3e2 100644 --- a/content-service/src/main/java/com/kt/event/content/biz/usecase/out/RedisImageWriter.java +++ b/content-service/src/main/java/com/kt/event/content/biz/usecase/out/RedisImageWriter.java @@ -13,9 +13,9 @@ public interface RedisImageWriter { /** * 이미지 목록 캐싱 * - * @param eventDraftId 이벤트 초안 ID + * @param eventId 이벤트 초안 ID * @param images 이미지 목록 * @param ttlSeconds TTL (초) */ - void cacheImages(Long eventDraftId, List images, long ttlSeconds); + void cacheImages(String eventId, List images, long ttlSeconds); } diff --git a/content-service/src/main/java/com/kt/event/content/infra/config/RedisConfig.java b/content-service/src/main/java/com/kt/event/content/infra/config/RedisConfig.java index 8036711..238b278 100644 --- a/content-service/src/main/java/com/kt/event/content/infra/config/RedisConfig.java +++ b/content-service/src/main/java/com/kt/event/content/infra/config/RedisConfig.java @@ -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}") diff --git a/content-service/src/main/java/com/kt/event/content/infra/gateway/RedisGateway.java b/content-service/src/main/java/com/kt/event/content/infra/gateway/RedisGateway.java index 1f8953c..fde56e5 100644 --- a/content-service/src/main/java/com/kt/event/content/infra/gateway/RedisGateway.java +++ b/content-service/src/main/java/com/kt/event/content/infra/gateway/RedisGateway.java @@ -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> getAIRecommendation(Long eventDraftId) { + public Optional> 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 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 images, long ttlSeconds) { + public void cacheImages(String eventId, List 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 getImage(Long eventDraftId, ImageStyle style, Platform platform) { + public Optional 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 getImagesByEventId(Long eventDraftId) { + public List 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 images, long ttlSeconds) { + public void saveImages(String eventId, List 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 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 findByEventDraftIdWithImages(Long eventDraftId) { + public Optional findByEventDraftIdWithImages(String eventId) { try { - String contentKey = CONTENT_META_KEY_PREFIX + eventDraftId; + String contentKey = CONTENT_META_KEY_PREFIX + eventId; Map contentFields = redisTemplate.opsForHash().entries(contentKey); if (contentFields.isEmpty()) { - log.warn("Content를 찾을 수 없음: eventDraftId={}", eventDraftId); + log.warn("Content를 찾을 수 없음: eventId={}", eventId); return Optional.empty(); } // 이미지 목록 조회 - List images = findImagesByEventDraftId(eventDraftId); + List 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 findImagesByEventDraftId(Long eventDraftId) { + public List 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 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 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); diff --git a/content-service/src/main/java/com/kt/event/content/infra/gateway/client/AzureBlobStorageUploader.java b/content-service/src/main/java/com/kt/event/content/infra/gateway/client/AzureBlobStorageUploader.java index a977eeb..3d065db 100644 --- a/content-service/src/main/java/com/kt/event/content/infra/gateway/client/AzureBlobStorageUploader.java +++ b/content-service/src/main/java/com/kt/event/content/infra/gateway/client/AzureBlobStorageUploader.java @@ -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}") diff --git a/content-service/src/main/java/com/kt/event/content/infra/gateway/client/HuggingFaceApiClient.java b/content-service/src/main/java/com/kt/event/content/infra/gateway/client/HuggingFaceApiClient.java index dd53ef0..2e87a38 100644 --- a/content-service/src/main/java/com/kt/event/content/infra/gateway/client/HuggingFaceApiClient.java +++ b/content-service/src/main/java/com/kt/event/content/infra/gateway/client/HuggingFaceApiClient.java @@ -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}") diff --git a/content-service/src/main/java/com/kt/event/content/infra/gateway/client/ReplicateApiConfig.java b/content-service/src/main/java/com/kt/event/content/infra/gateway/client/ReplicateApiConfig.java index a017082..2eab9e5 100644 --- a/content-service/src/main/java/com/kt/event/content/infra/gateway/client/ReplicateApiConfig.java +++ b/content-service/src/main/java/com/kt/event/content/infra/gateway/client/ReplicateApiConfig.java @@ -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; /** diff --git a/content-service/src/main/java/com/kt/event/content/infra/gateway/mock/MockCDNUploader.java b/content-service/src/main/java/com/kt/event/content/infra/gateway/mock/MockCDNUploader.java deleted file mode 100644 index c11bc31..0000000 --- a/content-service/src/main/java/com/kt/event/content/infra/gateway/mock/MockCDNUploader.java +++ /dev/null @@ -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; - } -} diff --git a/content-service/src/main/java/com/kt/event/content/infra/gateway/mock/MockImageGenerator.java b/content-service/src/main/java/com/kt/event/content/infra/gateway/mock/MockImageGenerator.java deleted file mode 100644 index 85d42bc..0000000 --- a/content-service/src/main/java/com/kt/event/content/infra/gateway/mock/MockImageGenerator.java +++ /dev/null @@ -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(); - } -} diff --git a/content-service/src/main/java/com/kt/event/content/infra/gateway/mock/MockRedisGateway.java b/content-service/src/main/java/com/kt/event/content/infra/gateway/mock/MockRedisGateway.java deleted file mode 100644 index 7fdae20..0000000 --- a/content-service/src/main/java/com/kt/event/content/infra/gateway/mock/MockRedisGateway.java +++ /dev/null @@ -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> aiDataCache = new HashMap<>(); - - // In-memory storage for contents, images, and jobs - private final Map contentStorage = new ConcurrentHashMap<>(); - private final Map imageByIdStorage = new ConcurrentHashMap<>(); - private final Map imageStorage = new ConcurrentHashMap<>(); - private final Map jobStorage = new ConcurrentHashMap<>(); - - // ======================================== - // RedisAIDataReader 구현 - // ======================================== - - @Override - public Optional> getAIRecommendation(Long eventDraftId) { - log.info("[MOCK] Redis에서 AI 추천 데이터 조회: eventDraftId={}", eventDraftId); - - // Mock 데이터 반환 - Map 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 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 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 getImagesByEventId(Long eventDraftId) { - try { - String pattern = IMAGE_KEY_PREFIX + eventDraftId + ":"; - - List 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 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 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 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 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 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 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; - } - } -} diff --git a/content-service/src/main/java/com/kt/event/content/infra/web/controller/ContentController.java b/content-service/src/main/java/com/kt/event/content/infra/web/controller/ContentController.java index bf528fd..7fd2997 100644 --- a/content-service/src/main/java/com/kt/event/content/infra/web/controller/ContentController.java +++ b/content-service/src/main/java/com/kt/event/content/infra/web/controller/ContentController.java @@ -52,8 +52,8 @@ public class ContentController { */ @PostMapping("/images/generate") public ResponseEntity 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 getContentByEventId(@PathVariable Long eventDraftId) { - log.info("이벤트 콘텐츠 조회: eventDraftId={}", eventDraftId); + @GetMapping("/events/{eventId}") + public ResponseEntity 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> 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 images = getImageListUseCase.execute(eventDraftId, imageStyle, imagePlatform); + List images = getImageListUseCase.execute(eventId, imageStyle, imagePlatform); return ResponseEntity.ok(images); } diff --git a/content-service/src/main/resources/application.yml b/content-service/src/main/resources/application.yml index e5d3f3d..193e898 100644 --- a/content-service/src/main/resources/application.yml +++ b/content-service/src/main/resources/application.yml @@ -4,15 +4,15 @@ spring: data: redis: - host: ${REDIS_HOST:localhost} + host: ${REDIS_HOST:20.214.210.71} port: ${REDIS_PORT:6379} - password: ${REDIS_PASSWORD:} + password: ${REDIS_PASSWORD:Hi5Jessica!} server: port: ${SERVER_PORT:8084} jwt: - secret: ${JWT_SECRET:dev-jwt-secret-key} + secret: ${JWT_SECRET:dev-jwt-secret-key-minimum-32-characters-required-for-hmac-sha256} access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600000} refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800000} From 9305dfdb7f413c49748459cfedfbc8f2b388d97d Mon Sep 17 00:00:00 2001 From: cherry2250 Date: Tue, 28 Oct 2025 16:19:51 +0900 Subject: [PATCH 3/3] =?UTF-8?q?application.yml=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=EB=B0=8F=20Azure=20Blob=20Storage=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - application-dev.yml, application-local.yml 삭제 - 단일 application.yml로 통합 (user-service 형식 참고) - Azure Blob Storage connection string 기본값 추가 - Redis, Actuator, Logging 상세 설정 추가 - OpenAPI/Swagger 설정 추가 - CORS 설정 추가 - 모든 설정을 환경 변수로 관리 가능하도록 구성 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- content-service/build.gradle | 4 + .../src/main/resources/application-dev.yml | 45 ----------- .../src/main/resources/application-local.yml | 43 ---------- .../src/main/resources/application.yml | 81 ++++++++++++++++--- 4 files changed, 74 insertions(+), 99 deletions(-) delete mode 100644 content-service/src/main/resources/application-dev.yml delete mode 100644 content-service/src/main/resources/application-local.yml diff --git a/content-service/build.gradle b/content-service/build.gradle index 17c9c23..fa8e188 100644 --- a/content-service/build.gradle +++ b/content-service/build.gradle @@ -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' diff --git a/content-service/src/main/resources/application-dev.yml b/content-service/src/main/resources/application-dev.yml deleted file mode 100644 index 6c0abb8..0000000 --- a/content-service/src/main/resources/application-dev.yml +++ /dev/null @@ -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 diff --git a/content-service/src/main/resources/application-local.yml b/content-service/src/main/resources/application-local.yml deleted file mode 100644 index eb843f8..0000000 --- a/content-service/src/main/resources/application-local.yml +++ /dev/null @@ -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 diff --git a/content-service/src/main/resources/application.yml b/content-service/src/main/resources/application.yml index 193e898..a115a4b 100644 --- a/content-service/src/main/resources/application.yml +++ b/content-service/src/main/resources/application.yml @@ -2,38 +2,97 @@ spring: application: name: content-service + # Redis Configuration data: redis: + enabled: ${REDIS_ENABLED:true} host: ${REDIS_HOST:20.214.210.71} port: ${REDIS_PORT:6379} 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} -server: - port: ${SERVER_PORT:8084} - +# JWT Configuration jwt: - secret: ${JWT_SECRET:dev-jwt-secret-key-minimum-32-characters-required-for-hmac-sha256} + 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}