Content Service Phase 2: Port 인터페이스 구현 및 Gateway 통합
Phase 2 작업으로 Clean Architecture의 의존성 역전 원칙을 적용하여 Service 계층이 Port 인터페이스에만 의존하도록 구조를 개선했습니다. 주요 변경사항: 1. Redis DTO 생성 (Phase 1) - RedisAIEventData: AI 이벤트 데이터 DTO - RedisImageData: 이미지 데이터 DTO - RedisJobData: Job 데이터 DTO 2. Port 인터페이스 생성 - ImageWriter: 이미지 저장 Port - ImageReader: 이미지 조회 Port - JobWriter: Job 저장 Port - JobReader: Job 조회 Port 3. Gateway 구현 - RedisGateway: 4개 Port 인터페이스 구현 (Production용) - MockRedisGateway: 4개 Port 인터페이스 구현 (Local/Test용) - JobGateway: 2개 Port 인터페이스 구현 + @Primary 추가 (Phase 3 삭제 예정) 4. 하위 호환성 유지 - Port 인터페이스에 레거시 메서드 추가 (save, findById) - Service 계층 코드 변경 없이 점진적 마이그레이션 - "Phase 3에서 삭제 예정" 주석 표시 검증 완료: - 컴파일 성공 - 서비스 정상 시작 (포트 8084) - API 정상 작동 확인 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
package com.kt.event.content.biz.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI Service가 Redis에 저장한 이벤트 데이터 (읽기 전용)
|
||||
*
|
||||
* Key Pattern: ai:event:{eventDraftId}
|
||||
* Data Type: Hash
|
||||
* TTL: 24시간 (86400초)
|
||||
*
|
||||
* 예시:
|
||||
* - ai:event:1
|
||||
*
|
||||
* Note: 이 데이터는 AI Service가 생성하고 Content Service는 읽기만 합니다.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RedisAIEventData {
|
||||
/**
|
||||
* 이벤트 초안 ID
|
||||
*/
|
||||
private Long eventDraftId;
|
||||
|
||||
/**
|
||||
* 이벤트 제목
|
||||
*/
|
||||
private String eventTitle;
|
||||
|
||||
/**
|
||||
* 이벤트 설명
|
||||
*/
|
||||
private String eventDescription;
|
||||
|
||||
/**
|
||||
* 타겟 고객
|
||||
*/
|
||||
private String targetAudience;
|
||||
|
||||
/**
|
||||
* 이벤트 목적
|
||||
*/
|
||||
private String eventObjective;
|
||||
|
||||
/**
|
||||
* AI가 생성한 추가 데이터
|
||||
*/
|
||||
private Map<String, Object> additionalData;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.kt.event.content.biz.dto;
|
||||
|
||||
import com.kt.event.content.biz.domain.ImageStyle;
|
||||
import com.kt.event.content.biz.domain.Platform;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Redis에 저장되는 이미지 데이터 구조
|
||||
*
|
||||
* Key Pattern: content:image:{eventDraftId}:{style}:{platform}
|
||||
* Data Type: String (JSON)
|
||||
* TTL: 7일 (604800초)
|
||||
*
|
||||
* 예시:
|
||||
* - content:image:1:FANCY:INSTAGRAM
|
||||
* - content:image:1:SIMPLE:KAKAO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RedisImageData {
|
||||
/**
|
||||
* 이미지 고유 ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 이벤트 초안 ID
|
||||
*/
|
||||
private Long eventDraftId;
|
||||
|
||||
/**
|
||||
* 이미지 스타일 (FANCY, SIMPLE, TRENDY)
|
||||
*/
|
||||
private ImageStyle style;
|
||||
|
||||
/**
|
||||
* 플랫폼 (INSTAGRAM, KAKAO, NAVER)
|
||||
*/
|
||||
private Platform platform;
|
||||
|
||||
/**
|
||||
* CDN 이미지 URL
|
||||
*/
|
||||
private String cdnUrl;
|
||||
|
||||
/**
|
||||
* 이미지 생성 프롬프트
|
||||
*/
|
||||
private String prompt;
|
||||
|
||||
/**
|
||||
* 선택 여부
|
||||
*/
|
||||
private Boolean selected;
|
||||
|
||||
/**
|
||||
* 생성 일시
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 수정 일시
|
||||
*/
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.kt.event.content.biz.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Redis에 저장되는 Job 상태 정보
|
||||
*
|
||||
* Key Pattern: job:{jobId}
|
||||
* Data Type: Hash
|
||||
* TTL: 1시간 (3600초)
|
||||
*
|
||||
* 예시:
|
||||
* - job:job-mock-7ada8bd3
|
||||
* - job:job-regen-df2bb3a3
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RedisJobData {
|
||||
/**
|
||||
* Job ID (예: job-mock-7ada8bd3)
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 이벤트 초안 ID
|
||||
*/
|
||||
private Long eventDraftId;
|
||||
|
||||
/**
|
||||
* Job 타입 (image-generation, image-regeneration)
|
||||
*/
|
||||
private String jobType;
|
||||
|
||||
/**
|
||||
* 상태 (PENDING, IN_PROGRESS, COMPLETED, FAILED)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 진행률 (0-100)
|
||||
*/
|
||||
private Integer progress;
|
||||
|
||||
/**
|
||||
* 결과 메시지
|
||||
*/
|
||||
private String resultMessage;
|
||||
|
||||
/**
|
||||
* 에러 메시지
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 생성 일시
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 수정 일시
|
||||
*/
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.kt.event.content.biz.usecase.out;
|
||||
|
||||
import com.kt.event.content.biz.domain.ImageStyle;
|
||||
import com.kt.event.content.biz.domain.Platform;
|
||||
import com.kt.event.content.biz.dto.RedisImageData;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 이미지 조회 Port (Output Port)
|
||||
*/
|
||||
public interface ImageReader {
|
||||
|
||||
/**
|
||||
* 특정 이미지 조회
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param style 이미지 스타일
|
||||
* @param platform 플랫폼
|
||||
* @return 이미지 데이터
|
||||
*/
|
||||
Optional<RedisImageData> getImage(Long eventDraftId, ImageStyle style, Platform platform);
|
||||
|
||||
/**
|
||||
* 이벤트의 모든 이미지 조회
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @return 이미지 목록
|
||||
*/
|
||||
List<RedisImageData> getImagesByEventId(Long eventDraftId);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.kt.event.content.biz.usecase.out;
|
||||
|
||||
import com.kt.event.content.biz.domain.ImageStyle;
|
||||
import com.kt.event.content.biz.domain.Platform;
|
||||
import com.kt.event.content.biz.dto.RedisImageData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 이미지 저장 Port (Output Port)
|
||||
*/
|
||||
public interface ImageWriter {
|
||||
|
||||
/**
|
||||
* 단일 이미지 저장
|
||||
*
|
||||
* @param imageData 이미지 데이터
|
||||
* @param ttlSeconds TTL (초 단위)
|
||||
*/
|
||||
void saveImage(RedisImageData imageData, long ttlSeconds);
|
||||
|
||||
/**
|
||||
* 여러 이미지 저장
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param images 이미지 목록
|
||||
* @param ttlSeconds TTL (초 단위)
|
||||
*/
|
||||
void saveImages(Long eventDraftId, List<RedisImageData> images, long ttlSeconds);
|
||||
|
||||
/**
|
||||
* 이미지 삭제
|
||||
*
|
||||
* @param eventDraftId 이벤트 초안 ID
|
||||
* @param style 이미지 스타일
|
||||
* @param platform 플랫폼
|
||||
*/
|
||||
void deleteImage(Long eventDraftId, ImageStyle style, Platform platform);
|
||||
}
|
||||
@@ -1,19 +1,29 @@
|
||||
package com.kt.event.content.biz.usecase.out;
|
||||
|
||||
import com.kt.event.content.biz.domain.Job;
|
||||
import com.kt.event.content.biz.dto.RedisJobData;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Job 조회 포트
|
||||
* Job 조회 Port (Output Port)
|
||||
*/
|
||||
public interface JobReader {
|
||||
|
||||
/**
|
||||
* Job ID로 조회
|
||||
* Job 조회
|
||||
*
|
||||
* @param jobId Job ID
|
||||
* @return Job 도메인 모델
|
||||
* @return Job 데이터
|
||||
*/
|
||||
Optional<RedisJobData> getJob(String jobId);
|
||||
|
||||
/**
|
||||
* 기존 호환성을 위한 메서드 (Phase 3에서 삭제 예정)
|
||||
* JPA 기반 JobGateway에서만 사용
|
||||
*
|
||||
* @param jobId Job ID
|
||||
* @return Job 도메인 객체
|
||||
*/
|
||||
Optional<Job> findById(String jobId);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,51 @@
|
||||
package com.kt.event.content.biz.usecase.out;
|
||||
|
||||
import com.kt.event.content.biz.domain.Job;
|
||||
import com.kt.event.content.biz.dto.RedisJobData;
|
||||
|
||||
/**
|
||||
* Job 저장 포트
|
||||
* Job 저장 Port (Output Port)
|
||||
*/
|
||||
public interface JobWriter {
|
||||
|
||||
/**
|
||||
* Job 저장
|
||||
* Job 생성/저장
|
||||
*
|
||||
* @param job Job 도메인 모델
|
||||
* @param jobData Job 데이터
|
||||
* @param ttlSeconds TTL (초 단위)
|
||||
*/
|
||||
void saveJob(RedisJobData jobData, long ttlSeconds);
|
||||
|
||||
/**
|
||||
* Job 상태 업데이트
|
||||
*
|
||||
* @param jobId Job ID
|
||||
* @param status 상태
|
||||
* @param progress 진행률 (0-100)
|
||||
*/
|
||||
void updateJobStatus(String jobId, String status, Integer progress);
|
||||
|
||||
/**
|
||||
* Job 결과 메시지 업데이트
|
||||
*
|
||||
* @param jobId Job ID
|
||||
* @param resultMessage 결과 메시지
|
||||
*/
|
||||
void updateJobResult(String jobId, String resultMessage);
|
||||
|
||||
/**
|
||||
* Job 에러 메시지 업데이트
|
||||
*
|
||||
* @param jobId Job ID
|
||||
* @param errorMessage 에러 메시지
|
||||
*/
|
||||
void updateJobError(String jobId, String errorMessage);
|
||||
|
||||
/**
|
||||
* 기존 호환성을 위한 메서드 (Phase 3에서 삭제 예정)
|
||||
* JPA 기반 JobGateway에서만 사용
|
||||
*
|
||||
* @param job Job 도메인 객체
|
||||
* @return 저장된 Job
|
||||
*/
|
||||
Job save(Job job);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.kt.event.content.infra.gateway;
|
||||
|
||||
import com.kt.event.content.biz.domain.Job;
|
||||
import com.kt.event.content.biz.dto.RedisJobData;
|
||||
import com.kt.event.content.biz.usecase.out.JobReader;
|
||||
import com.kt.event.content.biz.usecase.out.JobWriter;
|
||||
import com.kt.event.content.infra.gateway.entity.JobEntity;
|
||||
import com.kt.event.content.infra.gateway.repository.JobJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -17,9 +19,11 @@ import java.util.stream.Collectors;
|
||||
/**
|
||||
* Job 영속성 Gateway
|
||||
* JobReader, JobWriter outbound port 구현
|
||||
* Phase 3에서 삭제 예정
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Primary
|
||||
@RequiredArgsConstructor
|
||||
public class JobGateway implements JobReader, JobWriter {
|
||||
|
||||
@@ -31,6 +35,26 @@ public class JobGateway implements JobReader, JobWriter {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<RedisJobData> getJob(String jobId) {
|
||||
log.debug("[JPA] Job 조회: jobId={}", jobId);
|
||||
return jobRepository.findById(jobId)
|
||||
.map(entity -> RedisJobData.builder()
|
||||
.id(entity.getId())
|
||||
.eventDraftId(entity.getEventDraftId())
|
||||
.jobType(entity.getJobType())
|
||||
.status(entity.getStatus().name())
|
||||
.progress(entity.getProgress())
|
||||
.resultMessage(entity.getResultMessage())
|
||||
.errorMessage(entity.getErrorMessage())
|
||||
.createdAt(entity.getCreatedAt())
|
||||
.updatedAt(entity.getUpdatedAt())
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 호환성을 위한 메서드 (Phase 3에서 삭제 예정)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Job> findById(String jobId) {
|
||||
log.debug("Job 조회: jobId={}", jobId);
|
||||
return jobRepository.findById(jobId)
|
||||
@@ -64,6 +88,64 @@ public class JobGateway implements JobReader, JobWriter {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void saveJob(RedisJobData jobData, long ttlSeconds) {
|
||||
log.debug("[JPA] Job 저장: jobId={}, status={}, ttl={}초", jobData.getId(), jobData.getStatus(), ttlSeconds);
|
||||
|
||||
JobEntity entity = jobRepository.findById(jobData.getId())
|
||||
.orElseGet(() -> JobEntity.create(
|
||||
jobData.getId(),
|
||||
jobData.getEventDraftId(),
|
||||
jobData.getJobType()
|
||||
));
|
||||
|
||||
// Job 상태 업데이트
|
||||
entity.updateStatus(Job.Status.valueOf(jobData.getStatus()), jobData.getProgress());
|
||||
if (jobData.getResultMessage() != null) {
|
||||
entity.setResultMessage(jobData.getResultMessage());
|
||||
}
|
||||
if (jobData.getErrorMessage() != null) {
|
||||
entity.setErrorMessage(jobData.getErrorMessage());
|
||||
}
|
||||
|
||||
jobRepository.save(entity);
|
||||
// Note: TTL은 JPA에서는 무시됨 (Redis 전용)
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateJobStatus(String jobId, String status, Integer progress) {
|
||||
log.debug("[JPA] Job 상태 업데이트: jobId={}, status={}, progress={}", jobId, status, progress);
|
||||
jobRepository.findById(jobId).ifPresent(entity -> {
|
||||
entity.updateStatus(Job.Status.valueOf(status), progress);
|
||||
jobRepository.save(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateJobResult(String jobId, String resultMessage) {
|
||||
log.debug("[JPA] Job 결과 업데이트: jobId={}, resultMessage={}", jobId, resultMessage);
|
||||
jobRepository.findById(jobId).ifPresent(entity -> {
|
||||
entity.setResultMessage(resultMessage);
|
||||
jobRepository.save(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateJobError(String jobId, String errorMessage) {
|
||||
log.debug("[JPA] Job 에러 업데이트: jobId={}, errorMessage={}", jobId, errorMessage);
|
||||
jobRepository.findById(jobId).ifPresent(entity -> {
|
||||
entity.setErrorMessage(errorMessage);
|
||||
entity.updateStatus(Job.Status.FAILED, entity.getProgress());
|
||||
jobRepository.save(entity);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 호환성을 위한 메서드 (Phase 3에서 삭제 예정)
|
||||
*/
|
||||
@Transactional
|
||||
public Job save(Job job) {
|
||||
log.debug("Job 저장: jobId={}, status={}", job.getId(), job.getStatus());
|
||||
|
||||
|
||||
+296
-1
@@ -2,6 +2,15 @@ package com.kt.event.content.infra.gateway;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.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.RequiredArgsConstructor;
|
||||
@@ -11,9 +20,12 @@ import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Redis Gateway 구현체 (Production 환경용)
|
||||
@@ -24,7 +36,7 @@ import java.util.Optional;
|
||||
@Component
|
||||
@Profile({"!local", "!test"})
|
||||
@RequiredArgsConstructor
|
||||
public class RedisGateway implements RedisAIDataReader, RedisImageWriter {
|
||||
public class RedisGateway implements RedisAIDataReader, RedisImageWriter, ImageWriter, ImageReader, JobWriter, JobReader {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
@@ -92,4 +104,287 @@ public class RedisGateway implements RedisAIDataReader, RedisImageWriter {
|
||||
log.error("AI 이벤트 데이터 캐시 삭제 실패: eventDraftId={}", eventDraftId, e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 이미지 CRUD ====================
|
||||
|
||||
private static final String IMAGE_KEY_PREFIX = "content:image:";
|
||||
|
||||
/**
|
||||
* 이미지 저장
|
||||
* Key: content:image:{eventDraftId}:{style}:{platform}
|
||||
*/
|
||||
public void saveImage(RedisImageData imageData, long ttlSeconds) {
|
||||
try {
|
||||
String key = buildImageKey(imageData.getEventDraftId(), 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 이미지 조회
|
||||
*/
|
||||
public Optional<RedisImageData> getImage(Long eventDraftId, ImageStyle style, Platform platform) {
|
||||
try {
|
||||
String key = buildImageKey(eventDraftId, style, platform);
|
||||
Object data = redisTemplate.opsForValue().get(key);
|
||||
|
||||
if (data == null) {
|
||||
log.warn("이미지를 찾을 수 없음: key={}", key);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
RedisImageData imageData = objectMapper.readValue(data.toString(), RedisImageData.class);
|
||||
return Optional.of(imageData);
|
||||
} catch (Exception e) {
|
||||
log.error("이미지 조회 실패: eventDraftId={}, style={}, platform={}", eventDraftId, style, platform, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트의 모든 이미지 조회
|
||||
*/
|
||||
public List<RedisImageData> getImagesByEventId(Long eventDraftId) {
|
||||
try {
|
||||
String pattern = IMAGE_KEY_PREFIX + eventDraftId + ":*";
|
||||
var keys = redisTemplate.keys(pattern);
|
||||
|
||||
if (keys == null || keys.isEmpty()) {
|
||||
log.warn("이벤트 이미지를 찾을 수 없음: eventDraftId={}", eventDraftId);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<RedisImageData> images = new ArrayList<>();
|
||||
for (Object key : keys) {
|
||||
Object data = redisTemplate.opsForValue().get(key);
|
||||
if (data != null) {
|
||||
RedisImageData imageData = objectMapper.readValue(data.toString(), RedisImageData.class);
|
||||
images.add(imageData);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("이벤트 이미지 조회 완료: eventDraftId={}, count={}", eventDraftId, images.size());
|
||||
return images;
|
||||
} catch (Exception e) {
|
||||
log.error("이벤트 이미지 조회 실패: eventDraftId={}", eventDraftId, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 삭제
|
||||
*/
|
||||
public void deleteImage(Long eventDraftId, ImageStyle style, Platform platform) {
|
||||
try {
|
||||
String key = buildImageKey(eventDraftId, style, platform);
|
||||
redisTemplate.delete(key);
|
||||
log.info("이미지 삭제 완료: key={}", key);
|
||||
} catch (Exception e) {
|
||||
log.error("이미지 삭제 실패: 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("여러 이미지 저장 완료: 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 생성/저장
|
||||
* Key: job:{jobId}
|
||||
*/
|
||||
public void saveJob(RedisJobData jobData, long ttlSeconds) {
|
||||
try {
|
||||
String key = JOB_KEY_PREFIX + jobData.getId();
|
||||
|
||||
// Hash 형태로 저장
|
||||
Map<String, String> jobFields = Map.of(
|
||||
"id", jobData.getId(),
|
||||
"eventDraftId", String.valueOf(jobData.getEventDraftId()),
|
||||
"jobType", jobData.getJobType(),
|
||||
"status", jobData.getStatus(),
|
||||
"progress", String.valueOf(jobData.getProgress()),
|
||||
"resultMessage", jobData.getResultMessage() != null ? jobData.getResultMessage() : "",
|
||||
"errorMessage", jobData.getErrorMessage() != null ? jobData.getErrorMessage() : "",
|
||||
"createdAt", jobData.getCreatedAt().toString(),
|
||||
"updatedAt", jobData.getUpdatedAt().toString()
|
||||
);
|
||||
|
||||
redisTemplate.opsForHash().putAll(key, jobFields);
|
||||
redisTemplate.expire(key, Duration.ofSeconds(ttlSeconds));
|
||||
|
||||
log.info("Job 저장 완료: jobId={}, status={}, ttl={}초", jobData.getId(), jobData.getStatus(), ttlSeconds);
|
||||
} catch (Exception e) {
|
||||
log.error("Job 저장 실패: jobId={}", jobData.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job 조회
|
||||
*/
|
||||
public Optional<RedisJobData> getJob(String jobId) {
|
||||
try {
|
||||
String key = JOB_KEY_PREFIX + jobId;
|
||||
Map<Object, Object> jobFields = redisTemplate.opsForHash().entries(key);
|
||||
|
||||
if (jobFields.isEmpty()) {
|
||||
log.warn("Job을 찾을 수 없음: jobId={}", jobId);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
RedisJobData jobData = RedisJobData.builder()
|
||||
.id(getString(jobFields, "id"))
|
||||
.eventDraftId(getLong(jobFields, "eventDraftId"))
|
||||
.jobType(getString(jobFields, "jobType"))
|
||||
.status(getString(jobFields, "status"))
|
||||
.progress(getInteger(jobFields, "progress"))
|
||||
.resultMessage(getString(jobFields, "resultMessage"))
|
||||
.errorMessage(getString(jobFields, "errorMessage"))
|
||||
.createdAt(getLocalDateTime(jobFields, "createdAt"))
|
||||
.updatedAt(getLocalDateTime(jobFields, "updatedAt"))
|
||||
.build();
|
||||
|
||||
return Optional.of(jobData);
|
||||
} catch (Exception e) {
|
||||
log.error("Job 조회 실패: jobId={}", jobId, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job 상태 업데이트
|
||||
*/
|
||||
public void updateJobStatus(String jobId, String status, Integer progress) {
|
||||
try {
|
||||
String key = JOB_KEY_PREFIX + jobId;
|
||||
redisTemplate.opsForHash().put(key, "status", status);
|
||||
redisTemplate.opsForHash().put(key, "progress", String.valueOf(progress));
|
||||
redisTemplate.opsForHash().put(key, "updatedAt", LocalDateTime.now().toString());
|
||||
|
||||
log.info("Job 상태 업데이트: jobId={}, status={}, progress={}", jobId, status, progress);
|
||||
} catch (Exception e) {
|
||||
log.error("Job 상태 업데이트 실패: jobId={}", jobId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job 결과 메시지 업데이트
|
||||
*/
|
||||
public void updateJobResult(String jobId, String resultMessage) {
|
||||
try {
|
||||
String key = JOB_KEY_PREFIX + jobId;
|
||||
redisTemplate.opsForHash().put(key, "resultMessage", resultMessage);
|
||||
redisTemplate.opsForHash().put(key, "updatedAt", LocalDateTime.now().toString());
|
||||
|
||||
log.info("Job 결과 업데이트: jobId={}, resultMessage={}", jobId, resultMessage);
|
||||
} catch (Exception e) {
|
||||
log.error("Job 결과 업데이트 실패: jobId={}", jobId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job 에러 메시지 업데이트
|
||||
*/
|
||||
public void updateJobError(String jobId, String errorMessage) {
|
||||
try {
|
||||
String key = JOB_KEY_PREFIX + jobId;
|
||||
redisTemplate.opsForHash().put(key, "errorMessage", errorMessage);
|
||||
redisTemplate.opsForHash().put(key, "status", "FAILED");
|
||||
redisTemplate.opsForHash().put(key, "updatedAt", LocalDateTime.now().toString());
|
||||
|
||||
log.info("Job 에러 업데이트: jobId={}, errorMessage={}", jobId, errorMessage);
|
||||
} catch (Exception e) {
|
||||
log.error("Job 에러 업데이트 실패: jobId={}", jobId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 호환성을 위한 메서드 (Phase 3에서 삭제 예정)
|
||||
* Job 도메인 객체를 RedisJobData로 변환하여 저장
|
||||
*
|
||||
* @param job Job 도메인 객체
|
||||
* @return 저장된 Job
|
||||
*/
|
||||
@Override
|
||||
public Job save(Job job) {
|
||||
log.debug("[Redis] Job 저장 (호환성): jobId={}, status={}", job.getId(), job.getStatus());
|
||||
|
||||
RedisJobData jobData = RedisJobData.builder()
|
||||
.id(job.getId())
|
||||
.eventDraftId(job.getEventDraftId())
|
||||
.jobType(job.getJobType())
|
||||
.status(job.getStatus().name())
|
||||
.progress(job.getProgress())
|
||||
.resultMessage(job.getResultMessage())
|
||||
.errorMessage(job.getErrorMessage())
|
||||
.createdAt(job.getCreatedAt())
|
||||
.updatedAt(job.getUpdatedAt())
|
||||
.build();
|
||||
|
||||
saveJob(jobData, 86400); // 24시간 TTL
|
||||
return job;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 호환성을 위한 메서드 (Phase 3에서 삭제 예정)
|
||||
*
|
||||
* @param jobId Job ID
|
||||
* @return Job 도메인 객체
|
||||
*/
|
||||
@Override
|
||||
public Optional<Job> findById(String jobId) {
|
||||
log.debug("[Redis] Job 조회 (호환성): jobId={}", jobId);
|
||||
return getJob(jobId).map(data -> Job.builder()
|
||||
.id(data.getId())
|
||||
.eventDraftId(data.getEventDraftId())
|
||||
.jobType(data.getJobType())
|
||||
.status(Job.Status.valueOf(data.getStatus()))
|
||||
.progress(data.getProgress())
|
||||
.resultMessage(data.getResultMessage())
|
||||
.errorMessage(data.getErrorMessage())
|
||||
.createdAt(data.getCreatedAt())
|
||||
.updatedAt(data.getUpdatedAt())
|
||||
.build());
|
||||
}
|
||||
|
||||
// ==================== Helper Methods ====================
|
||||
|
||||
private String getString(Map<Object, Object> map, String key) {
|
||||
Object value = map.get(key);
|
||||
return value != null ? value.toString() : null;
|
||||
}
|
||||
|
||||
private Long getLong(Map<Object, Object> map, String key) {
|
||||
String value = getString(map, key);
|
||||
return value != null && !value.isEmpty() ? Long.parseLong(value) : null;
|
||||
}
|
||||
|
||||
private Integer getInteger(Map<Object, Object> map, String key) {
|
||||
String value = getString(map, key);
|
||||
return value != null && !value.isEmpty() ? Integer.parseInt(value) : null;
|
||||
}
|
||||
|
||||
private LocalDateTime getLocalDateTime(Map<Object, Object> map, String key) {
|
||||
String value = getString(map, key);
|
||||
return value != null && !value.isEmpty() ? LocalDateTime.parse(value) : null;
|
||||
}
|
||||
}
|
||||
|
||||
+259
-1
@@ -1,16 +1,29 @@
|
||||
package com.kt.event.content.infra.gateway.mock;
|
||||
|
||||
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.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.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 (테스트용)
|
||||
@@ -19,10 +32,14 @@ import java.util.Optional;
|
||||
@Slf4j
|
||||
@Component
|
||||
@Profile({"local", "test"})
|
||||
public class MockRedisGateway implements RedisAIDataReader, RedisImageWriter {
|
||||
public class MockRedisGateway implements RedisAIDataReader, RedisImageWriter, ImageWriter, ImageReader, JobWriter, JobReader {
|
||||
|
||||
private final Map<Long, Map<String, Object>> aiDataCache = new HashMap<>();
|
||||
|
||||
// In-memory storage for images and jobs
|
||||
private final Map<String, RedisImageData> imageStorage = new ConcurrentHashMap<>();
|
||||
private final Map<String, RedisJobData> jobStorage = new ConcurrentHashMap<>();
|
||||
|
||||
// ========================================
|
||||
// RedisAIDataReader 구현
|
||||
// ========================================
|
||||
@@ -49,4 +66,245 @@ public class MockRedisGateway implements RedisAIDataReader, RedisImageWriter {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 호환성을 위한 메서드 (Phase 3에서 삭제 예정)
|
||||
* Job 도메인 객체를 RedisJobData로 변환하여 저장
|
||||
*
|
||||
* @param job Job 도메인 객체
|
||||
* @return 저장된 Job
|
||||
*/
|
||||
@Override
|
||||
public Job save(Job job) {
|
||||
log.debug("[MOCK] Job 저장 (호환성): jobId={}, status={}", job.getId(), job.getStatus());
|
||||
|
||||
RedisJobData jobData = RedisJobData.builder()
|
||||
.id(job.getId())
|
||||
.eventDraftId(job.getEventDraftId())
|
||||
.jobType(job.getJobType())
|
||||
.status(job.getStatus().name())
|
||||
.progress(job.getProgress())
|
||||
.resultMessage(job.getResultMessage())
|
||||
.errorMessage(job.getErrorMessage())
|
||||
.createdAt(job.getCreatedAt())
|
||||
.updatedAt(job.getUpdatedAt())
|
||||
.build();
|
||||
|
||||
saveJob(jobData, 86400); // 24시간 TTL
|
||||
return job;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 호환성을 위한 메서드 (Phase 3에서 삭제 예정)
|
||||
*
|
||||
* @param jobId Job ID
|
||||
* @return Job 도메인 객체
|
||||
*/
|
||||
@Override
|
||||
public Optional<Job> findById(String jobId) {
|
||||
log.debug("[MOCK] Job 조회 (호환성): jobId={}", jobId);
|
||||
return getJob(jobId).map(data -> Job.builder()
|
||||
.id(data.getId())
|
||||
.eventDraftId(data.getEventDraftId())
|
||||
.jobType(data.getJobType())
|
||||
.status(Job.Status.valueOf(data.getStatus()))
|
||||
.progress(data.getProgress())
|
||||
.resultMessage(data.getResultMessage())
|
||||
.errorMessage(data.getErrorMessage())
|
||||
.createdAt(data.getCreatedAt())
|
||||
.updatedAt(data.getUpdatedAt())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user