mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2025-12-06 13:26:23 +00:00
content-service 핵심 비즈니스 로직 및 영속성 계층 구현 완료
- Domain 모델 구현 (ImageStyle, Platform, GeneratedImage, Content, Job) - JPA Entity 및 Repository 구현 (3개 엔티티, 3개 리포지토리) - UseCase 인터페이스 정의 (Inbound 6개, Outbound 8개) - Service 구현 (JobManagement, GetEventContent, GetImageList, GetImageDetail) - DTO 구현 (ContentCommand, ContentInfo, ImageInfo, JobInfo) - Application 설정 (ContentApplication, application.yml) - 컴파일 오류 수정 및 검증 완료 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ea82ff4748
commit
3d1dbda74b
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,99 @@
|
|||||||
|
package com.kt.event.content.biz.domain;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 도메인 모델
|
||||||
|
* 이벤트에 대한 전체 콘텐츠 정보 (이미지 목록 포함)
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Content {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 ID
|
||||||
|
*/
|
||||||
|
private final Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 ID (이벤트 초안 ID)
|
||||||
|
*/
|
||||||
|
private final Long eventDraftId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 제목
|
||||||
|
*/
|
||||||
|
private final String eventTitle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 설명
|
||||||
|
*/
|
||||||
|
private final String eventDescription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성된 이미지 목록
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private final List<GeneratedImage> images = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성일시
|
||||||
|
*/
|
||||||
|
private final LocalDateTime createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수정일시
|
||||||
|
*/
|
||||||
|
private final LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 추가
|
||||||
|
*
|
||||||
|
* @param image 생성된 이미지
|
||||||
|
*/
|
||||||
|
public void addImage(GeneratedImage image) {
|
||||||
|
this.images.add(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 선택된 이미지 조회
|
||||||
|
*
|
||||||
|
* @return 선택된 이미지 목록
|
||||||
|
*/
|
||||||
|
public List<GeneratedImage> getSelectedImages() {
|
||||||
|
return images.stream()
|
||||||
|
.filter(GeneratedImage::isSelected)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 스타일의 이미지 조회
|
||||||
|
*
|
||||||
|
* @param style 이미지 스타일
|
||||||
|
* @return 해당 스타일의 이미지 목록
|
||||||
|
*/
|
||||||
|
public List<GeneratedImage> getImagesByStyle(ImageStyle style) {
|
||||||
|
return images.stream()
|
||||||
|
.filter(image -> image.getStyle() == style)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 플랫폼의 이미지 조회
|
||||||
|
*
|
||||||
|
* @param platform 플랫폼
|
||||||
|
* @return 해당 플랫폼의 이미지 목록
|
||||||
|
*/
|
||||||
|
public List<GeneratedImage> getImagesByPlatform(Platform platform) {
|
||||||
|
return images.stream()
|
||||||
|
.filter(image -> image.getPlatform() == platform)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
package com.kt.event.content.biz.domain;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성된 이미지 도메인 모델
|
||||||
|
* AI가 생성한 이미지의 비즈니스 정보
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class GeneratedImage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 ID
|
||||||
|
*/
|
||||||
|
private final Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 ID (이벤트 초안 ID)
|
||||||
|
*/
|
||||||
|
private final Long eventDraftId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 스타일
|
||||||
|
*/
|
||||||
|
private final ImageStyle style;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 플랫폼
|
||||||
|
*/
|
||||||
|
private final Platform platform;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CDN URL (Azure Blob Storage)
|
||||||
|
*/
|
||||||
|
private final String cdnUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 프롬프트
|
||||||
|
*/
|
||||||
|
private final String prompt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 선택 여부
|
||||||
|
*/
|
||||||
|
private boolean selected;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성일시
|
||||||
|
*/
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수정일시
|
||||||
|
*/
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 선택
|
||||||
|
*/
|
||||||
|
public void select() {
|
||||||
|
this.selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 선택 해제
|
||||||
|
*/
|
||||||
|
public void deselect() {
|
||||||
|
this.selected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.kt.event.content.biz.domain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 스타일 enum
|
||||||
|
* AI가 생성하는 이미지의 스타일 유형
|
||||||
|
*/
|
||||||
|
public enum ImageStyle {
|
||||||
|
/**
|
||||||
|
* 심플 스타일 - 깔끔하고 미니멀한 디자인
|
||||||
|
*/
|
||||||
|
SIMPLE("심플"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 화려한 스타일 - 화려하고 풍부한 디자인
|
||||||
|
*/
|
||||||
|
FANCY("화려한"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 트렌디 스타일 - 최신 트렌드를 반영한 디자인
|
||||||
|
*/
|
||||||
|
TRENDY("트렌디");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
ImageStyle(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
package com.kt.event.content.biz.domain;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 도메인 모델
|
||||||
|
* 이미지 생성 작업의 비즈니스 정보
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Job {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 상태 enum
|
||||||
|
*/
|
||||||
|
public enum Status {
|
||||||
|
PENDING, // 대기 중
|
||||||
|
PROCESSING, // 처리 중
|
||||||
|
COMPLETED, // 완료
|
||||||
|
FAILED // 실패
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job ID
|
||||||
|
*/
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 ID (이벤트 초안 ID)
|
||||||
|
*/
|
||||||
|
private final Long eventDraftId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 타입 (image-generation)
|
||||||
|
*/
|
||||||
|
private final String jobType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 상태
|
||||||
|
*/
|
||||||
|
private Status status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 진행률 (0-100)
|
||||||
|
*/
|
||||||
|
private int progress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 결과 메시지
|
||||||
|
*/
|
||||||
|
private String resultMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 메시지
|
||||||
|
*/
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성일시
|
||||||
|
*/
|
||||||
|
private final LocalDateTime createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수정일시
|
||||||
|
*/
|
||||||
|
private final LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 시작
|
||||||
|
*/
|
||||||
|
public void start() {
|
||||||
|
this.status = Status.PROCESSING;
|
||||||
|
this.progress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 진행률 업데이트
|
||||||
|
*
|
||||||
|
* @param progress 진행률 (0-100)
|
||||||
|
*/
|
||||||
|
public void updateProgress(int progress) {
|
||||||
|
if (progress < 0 || progress > 100) {
|
||||||
|
throw new IllegalArgumentException("진행률은 0-100 사이여야 합니다");
|
||||||
|
}
|
||||||
|
this.progress = progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 완료 처리
|
||||||
|
*
|
||||||
|
* @param resultMessage 결과 메시지
|
||||||
|
*/
|
||||||
|
public void complete(String resultMessage) {
|
||||||
|
this.status = Status.COMPLETED;
|
||||||
|
this.progress = 100;
|
||||||
|
this.resultMessage = resultMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 실패 처리
|
||||||
|
*
|
||||||
|
* @param errorMessage 에러 메시지
|
||||||
|
*/
|
||||||
|
public void fail(String errorMessage) {
|
||||||
|
this.status = Status.FAILED;
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 진행 중 여부
|
||||||
|
*
|
||||||
|
* @return 진행 중이면 true
|
||||||
|
*/
|
||||||
|
public boolean isProcessing() {
|
||||||
|
return status == Status.PROCESSING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 완료 여부
|
||||||
|
*
|
||||||
|
* @return 완료되었으면 true
|
||||||
|
*/
|
||||||
|
public boolean isCompleted() {
|
||||||
|
return status == Status.COMPLETED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 실패 여부
|
||||||
|
*
|
||||||
|
* @return 실패했으면 true
|
||||||
|
*/
|
||||||
|
public boolean isFailed() {
|
||||||
|
return status == Status.FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package com.kt.event.content.biz.domain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 플랫폼 enum
|
||||||
|
* 이미지가 배포될 SNS 플랫폼 유형
|
||||||
|
*/
|
||||||
|
public enum Platform {
|
||||||
|
/**
|
||||||
|
* Instagram - 1080x1080 정사각형
|
||||||
|
*/
|
||||||
|
INSTAGRAM("Instagram", 1080, 1080),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 네이버 블로그 - 800x600
|
||||||
|
*/
|
||||||
|
NAVER("네이버 블로그", 800, 600),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카카오 채널 - 800x800 정사각형
|
||||||
|
*/
|
||||||
|
KAKAO("카카오 채널", 800, 800);
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
private final int width;
|
||||||
|
private final int height;
|
||||||
|
|
||||||
|
Platform(String displayName, int width, int height) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 크기 문자열 반환
|
||||||
|
*
|
||||||
|
* @return 가로x세로 형식 (예: 1080x1080)
|
||||||
|
*/
|
||||||
|
public String getSizeString() {
|
||||||
|
return width + "x" + height;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
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.Getter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 관련 커맨드 DTO
|
||||||
|
*/
|
||||||
|
public class ContentCommand {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 생성 요청 커맨드
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class GenerateImages {
|
||||||
|
private Long eventDraftId;
|
||||||
|
private String eventTitle;
|
||||||
|
private String eventDescription;
|
||||||
|
private List<ImageStyle> styles;
|
||||||
|
private List<Platform> platforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 재생성 요청 커맨드
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class RegenerateImage {
|
||||||
|
private Long imageId;
|
||||||
|
private String newPrompt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package com.kt.event.content.biz.dto;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.domain.Content;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 정보 DTO
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ContentInfo {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private Long eventDraftId;
|
||||||
|
private String eventTitle;
|
||||||
|
private String eventDescription;
|
||||||
|
private List<ImageInfo> images;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도메인 모델로부터 생성
|
||||||
|
*
|
||||||
|
* @param content 콘텐츠 도메인 모델
|
||||||
|
* @return ContentInfo
|
||||||
|
*/
|
||||||
|
public static ContentInfo from(Content content) {
|
||||||
|
return ContentInfo.builder()
|
||||||
|
.id(content.getId())
|
||||||
|
.eventDraftId(content.getEventDraftId())
|
||||||
|
.eventTitle(content.getEventTitle())
|
||||||
|
.eventDescription(content.getEventDescription())
|
||||||
|
.images(content.getImages().stream()
|
||||||
|
.map(ImageInfo::from)
|
||||||
|
.collect(Collectors.toList()))
|
||||||
|
.createdAt(content.getCreatedAt())
|
||||||
|
.updatedAt(content.getUpdatedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package com.kt.event.content.biz.dto;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.domain.GeneratedImage;
|
||||||
|
import com.kt.event.content.biz.domain.ImageStyle;
|
||||||
|
import com.kt.event.content.biz.domain.Platform;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 정보 DTO
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ImageInfo {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private Long eventDraftId;
|
||||||
|
private ImageStyle style;
|
||||||
|
private Platform platform;
|
||||||
|
private String cdnUrl;
|
||||||
|
private String prompt;
|
||||||
|
private boolean selected;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도메인 모델로부터 생성
|
||||||
|
*
|
||||||
|
* @param image 이미지 도메인 모델
|
||||||
|
* @return ImageInfo
|
||||||
|
*/
|
||||||
|
public static ImageInfo from(GeneratedImage image) {
|
||||||
|
return ImageInfo.builder()
|
||||||
|
.id(image.getId())
|
||||||
|
.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package com.kt.event.content.biz.dto;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.domain.Job;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 정보 DTO
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class JobInfo {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private Long eventDraftId;
|
||||||
|
private String jobType;
|
||||||
|
private Job.Status status;
|
||||||
|
private int progress;
|
||||||
|
private String resultMessage;
|
||||||
|
private String errorMessage;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도메인 모델로부터 생성
|
||||||
|
*
|
||||||
|
* @param job Job 도메인 모델
|
||||||
|
* @return JobInfo
|
||||||
|
*/
|
||||||
|
public static JobInfo from(Job job) {
|
||||||
|
return JobInfo.builder()
|
||||||
|
.id(job.getId())
|
||||||
|
.eventDraftId(job.getEventDraftId())
|
||||||
|
.jobType(job.getJobType())
|
||||||
|
.status(job.getStatus())
|
||||||
|
.progress(job.getProgress())
|
||||||
|
.resultMessage(job.getResultMessage())
|
||||||
|
.errorMessage(job.getErrorMessage())
|
||||||
|
.createdAt(job.getCreatedAt())
|
||||||
|
.updatedAt(job.getUpdatedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.kt.event.content.biz.service;
|
||||||
|
|
||||||
|
import com.kt.event.common.exception.BusinessException;
|
||||||
|
import com.kt.event.common.exception.ErrorCode;
|
||||||
|
import com.kt.event.content.biz.domain.Content;
|
||||||
|
import com.kt.event.content.biz.dto.ContentInfo;
|
||||||
|
import com.kt.event.content.biz.usecase.in.GetEventContentUseCase;
|
||||||
|
import com.kt.event.content.biz.usecase.out.ContentReader;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 콘텐츠 조회 서비스
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class GetEventContentService implements GetEventContentUseCase {
|
||||||
|
|
||||||
|
private final ContentReader contentReader;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ContentInfo execute(Long eventDraftId) {
|
||||||
|
Content content = contentReader.findByEventDraftIdWithImages(eventDraftId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.COMMON_001, "콘텐츠를 찾을 수 없습니다"));
|
||||||
|
|
||||||
|
return ContentInfo.from(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.kt.event.content.biz.service;
|
||||||
|
|
||||||
|
import com.kt.event.common.exception.BusinessException;
|
||||||
|
import com.kt.event.common.exception.ErrorCode;
|
||||||
|
import com.kt.event.content.biz.domain.GeneratedImage;
|
||||||
|
import com.kt.event.content.biz.dto.ImageInfo;
|
||||||
|
import com.kt.event.content.biz.usecase.in.GetImageDetailUseCase;
|
||||||
|
import com.kt.event.content.biz.usecase.out.ContentReader;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 상세 조회 서비스
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class GetImageDetailService implements GetImageDetailUseCase {
|
||||||
|
|
||||||
|
private final ContentReader contentReader;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ImageInfo execute(Long imageId) {
|
||||||
|
GeneratedImage image = contentReader.findImageById(imageId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.COMMON_001, "이미지를 찾을 수 없습니다"));
|
||||||
|
|
||||||
|
return ImageInfo.from(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.kt.event.content.biz.service;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.domain.GeneratedImage;
|
||||||
|
import com.kt.event.content.biz.dto.ImageInfo;
|
||||||
|
import com.kt.event.content.biz.usecase.in.GetImageListUseCase;
|
||||||
|
import com.kt.event.content.biz.usecase.out.ContentReader;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 목록 조회 서비스
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class GetImageListService implements GetImageListUseCase {
|
||||||
|
|
||||||
|
private final ContentReader contentReader;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ImageInfo> execute(Long eventDraftId) {
|
||||||
|
List<GeneratedImage> images = contentReader.findImagesByEventDraftId(eventDraftId);
|
||||||
|
return images.stream()
|
||||||
|
.map(ImageInfo::from)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.kt.event.content.biz.service;
|
||||||
|
|
||||||
|
import com.kt.event.common.exception.BusinessException;
|
||||||
|
import com.kt.event.common.exception.ErrorCode;
|
||||||
|
import com.kt.event.content.biz.domain.Job;
|
||||||
|
import com.kt.event.content.biz.dto.JobInfo;
|
||||||
|
import com.kt.event.content.biz.usecase.in.GetJobStatusUseCase;
|
||||||
|
import com.kt.event.content.biz.usecase.out.JobReader;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 관리 서비스
|
||||||
|
* Job 상태 조회 기능 제공
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class JobManagementService implements GetJobStatusUseCase {
|
||||||
|
|
||||||
|
private final JobReader jobReader;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JobInfo execute(String jobId) {
|
||||||
|
Job job = jobReader.findById(jobId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.COMMON_001, "Job을 찾을 수 없습니다"));
|
||||||
|
|
||||||
|
return JobInfo.from(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.in;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.dto.ContentCommand;
|
||||||
|
import com.kt.event.content.biz.dto.JobInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 생성 UseCase
|
||||||
|
* 비동기로 이미지 생성 작업을 시작
|
||||||
|
*/
|
||||||
|
public interface GenerateImagesUseCase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 생성 요청
|
||||||
|
*
|
||||||
|
* @param command 이미지 생성 커맨드
|
||||||
|
* @return Job 정보
|
||||||
|
*/
|
||||||
|
JobInfo execute(ContentCommand.GenerateImages command);
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.in;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.dto.ContentInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 콘텐츠 조회 UseCase
|
||||||
|
*/
|
||||||
|
public interface GetEventContentUseCase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 전체 콘텐츠 조회 (이미지 목록 포함)
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @return 콘텐츠 정보
|
||||||
|
*/
|
||||||
|
ContentInfo execute(Long eventDraftId);
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.in;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.dto.ImageInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 상세 조회 UseCase
|
||||||
|
*/
|
||||||
|
public interface GetImageDetailUseCase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 상세 정보 조회
|
||||||
|
*
|
||||||
|
* @param imageId 이미지 ID
|
||||||
|
* @return 이미지 정보
|
||||||
|
*/
|
||||||
|
ImageInfo execute(Long imageId);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.in;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.dto.ImageInfo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 목록 조회 UseCase
|
||||||
|
*/
|
||||||
|
public interface GetImageListUseCase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트의 이미지 목록 조회
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @return 이미지 정보 목록
|
||||||
|
*/
|
||||||
|
List<ImageInfo> execute(Long eventDraftId);
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.in;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.dto.JobInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 상태 조회 UseCase
|
||||||
|
*/
|
||||||
|
public interface GetJobStatusUseCase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 상태 조회
|
||||||
|
*
|
||||||
|
* @param jobId Job ID
|
||||||
|
* @return Job 정보
|
||||||
|
*/
|
||||||
|
JobInfo execute(String jobId);
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.in;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.dto.ContentCommand;
|
||||||
|
import com.kt.event.content.biz.dto.JobInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 재생성 UseCase
|
||||||
|
*/
|
||||||
|
public interface RegenerateImageUseCase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 재생성 요청
|
||||||
|
*
|
||||||
|
* @param command 이미지 재생성 커맨드
|
||||||
|
* @return Job 정보
|
||||||
|
*/
|
||||||
|
JobInfo execute(ContentCommand.RegenerateImage command);
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.out;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CDN 업로드 포트
|
||||||
|
* Azure Blob Storage에 이미지 업로드
|
||||||
|
*/
|
||||||
|
public interface CDNUploader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 업로드
|
||||||
|
*
|
||||||
|
* @param imageData 이미지 바이트 데이터
|
||||||
|
* @param fileName 파일명
|
||||||
|
* @return CDN URL
|
||||||
|
*/
|
||||||
|
String upload(byte[] imageData, String fileName);
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.out;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.domain.Content;
|
||||||
|
import com.kt.event.content.biz.domain.GeneratedImage;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 조회 포트
|
||||||
|
*/
|
||||||
|
public interface ContentReader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID로 콘텐츠 조회 (이미지 목록 포함)
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @return 콘텐츠 도메인 모델
|
||||||
|
*/
|
||||||
|
Optional<Content> findByEventDraftIdWithImages(Long eventDraftId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 ID로 이미지 조회
|
||||||
|
*
|
||||||
|
* @param imageId 이미지 ID
|
||||||
|
* @return 이미지 도메인 모델
|
||||||
|
*/
|
||||||
|
Optional<GeneratedImage> findImageById(Long imageId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID로 이미지 목록 조회
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @return 이미지 도메인 모델 목록
|
||||||
|
*/
|
||||||
|
List<GeneratedImage> findImagesByEventDraftId(Long eventDraftId);
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.out;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.domain.Content;
|
||||||
|
import com.kt.event.content.biz.domain.GeneratedImage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 저장 포트
|
||||||
|
*/
|
||||||
|
public interface ContentWriter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 저장
|
||||||
|
*
|
||||||
|
* @param content 콘텐츠 도메인 모델
|
||||||
|
* @return 저장된 콘텐츠
|
||||||
|
*/
|
||||||
|
Content save(Content content);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 저장
|
||||||
|
*
|
||||||
|
* @param image 이미지 도메인 모델
|
||||||
|
* @return 저장된 이미지
|
||||||
|
*/
|
||||||
|
GeneratedImage saveImage(GeneratedImage image);
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.out;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.domain.ImageStyle;
|
||||||
|
import com.kt.event.content.biz.domain.Platform;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 생성 API 호출 포트
|
||||||
|
* Stable Diffusion, DALL-E 등 외부 이미지 생성 API 호출
|
||||||
|
*/
|
||||||
|
public interface ImageGeneratorCaller {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 생성
|
||||||
|
*
|
||||||
|
* @param prompt 프롬프트
|
||||||
|
* @param style 이미지 스타일
|
||||||
|
* @param platform 플랫폼 (이미지 크기 결정)
|
||||||
|
* @return 생성된 이미지 바이트 데이터
|
||||||
|
*/
|
||||||
|
byte[] generateImage(String prompt, ImageStyle style, Platform platform);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.out;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.domain.Job;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 조회 포트
|
||||||
|
*/
|
||||||
|
public interface JobReader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job ID로 조회
|
||||||
|
*
|
||||||
|
* @param jobId Job ID
|
||||||
|
* @return Job 도메인 모델
|
||||||
|
*/
|
||||||
|
Optional<Job> findById(String jobId);
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.out;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.domain.Job;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 저장 포트
|
||||||
|
*/
|
||||||
|
public interface JobWriter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 저장
|
||||||
|
*
|
||||||
|
* @param job Job 도메인 모델
|
||||||
|
* @return 저장된 Job
|
||||||
|
*/
|
||||||
|
Job save(Job job);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.out;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis AI 데이터 조회 포트
|
||||||
|
* Event Service가 저장한 AI 추천 데이터를 읽음
|
||||||
|
*/
|
||||||
|
public interface RedisAIDataReader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 추천 데이터 조회
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @return AI 추천 데이터 (JSON 형태의 Map)
|
||||||
|
*/
|
||||||
|
Optional<Map<String, Object>> getAIRecommendation(Long eventDraftId);
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.kt.event.content.biz.usecase.out;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.domain.GeneratedImage;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis 이미지 데이터 저장 포트
|
||||||
|
* 생성된 이미지 정보를 Redis에 캐싱
|
||||||
|
*/
|
||||||
|
public interface RedisImageWriter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 목록 캐싱
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @param images 이미지 목록
|
||||||
|
* @param ttlSeconds TTL (초)
|
||||||
|
*/
|
||||||
|
void cacheImages(Long eventDraftId, List<GeneratedImage> images, long ttlSeconds);
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.kt.event.content.infra;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content Service Application
|
||||||
|
*/
|
||||||
|
@SpringBootApplication(scanBasePackages = {
|
||||||
|
"com.kt.event.content",
|
||||||
|
"com.kt.event.common"
|
||||||
|
})
|
||||||
|
@EntityScan(basePackages = {
|
||||||
|
"com.kt.event.content.infra.gateway.entity",
|
||||||
|
"com.kt.event.common.entity"
|
||||||
|
})
|
||||||
|
@EnableJpaRepositories(basePackages = "com.kt.event.content.infra.gateway.repository")
|
||||||
|
@EnableJpaAuditing
|
||||||
|
public class ContentApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(ContentApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
package com.kt.event.content.infra.gateway.entity;
|
||||||
|
|
||||||
|
import com.kt.event.common.entity.BaseTimeEntity;
|
||||||
|
import com.kt.event.content.biz.domain.Content;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 엔티티
|
||||||
|
* 이벤트에 대한 전체 콘텐츠 정보를 저장
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "contents")
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ContentEntity extends BaseTimeEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 ID (이벤트 초안 ID)
|
||||||
|
*/
|
||||||
|
@Column(name = "event_draft_id", nullable = false)
|
||||||
|
private Long eventDraftId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 제목
|
||||||
|
*/
|
||||||
|
@Column(name = "event_title", nullable = false, length = 200)
|
||||||
|
private String eventTitle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 설명
|
||||||
|
*/
|
||||||
|
@Column(name = "event_description", columnDefinition = "TEXT")
|
||||||
|
private String eventDescription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성된 이미지 목록
|
||||||
|
*/
|
||||||
|
@OneToMany(mappedBy = "content", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
private List<GeneratedImageEntity> images = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 정적 팩토리 메서드: 새 콘텐츠 생성
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @param eventTitle 이벤트 제목
|
||||||
|
* @param eventDescription 이벤트 설명
|
||||||
|
* @return ContentEntity
|
||||||
|
*/
|
||||||
|
public static ContentEntity create(Long eventDraftId, String eventTitle, String eventDescription) {
|
||||||
|
ContentEntity entity = new ContentEntity();
|
||||||
|
entity.eventDraftId = eventDraftId;
|
||||||
|
entity.eventTitle = eventTitle;
|
||||||
|
entity.eventDescription = eventDescription;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도메인 모델로 변환
|
||||||
|
*
|
||||||
|
* @return Content 도메인 모델
|
||||||
|
*/
|
||||||
|
public Content toDomain() {
|
||||||
|
return Content.builder()
|
||||||
|
.id(id)
|
||||||
|
.eventDraftId(eventDraftId)
|
||||||
|
.eventTitle(eventTitle)
|
||||||
|
.eventDescription(eventDescription)
|
||||||
|
.images(images.stream()
|
||||||
|
.map(GeneratedImageEntity::toDomain)
|
||||||
|
.collect(Collectors.toList()))
|
||||||
|
.createdAt(getCreatedAt())
|
||||||
|
.updatedAt(getUpdatedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 추가
|
||||||
|
*
|
||||||
|
* @param image 생성된 이미지 엔티티
|
||||||
|
*/
|
||||||
|
public void addImage(GeneratedImageEntity image) {
|
||||||
|
images.add(image);
|
||||||
|
image.assignContent(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
package com.kt.event.content.infra.gateway.entity;
|
||||||
|
|
||||||
|
import com.kt.event.common.entity.BaseTimeEntity;
|
||||||
|
import com.kt.event.content.biz.domain.GeneratedImage;
|
||||||
|
import com.kt.event.content.biz.domain.ImageStyle;
|
||||||
|
import com.kt.event.content.biz.domain.Platform;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성된 이미지 엔티티
|
||||||
|
* AI가 생성한 이미지 정보를 저장
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "generated_images", indexes = {
|
||||||
|
@Index(name = "idx_event_draft_id", columnList = "event_draft_id"),
|
||||||
|
@Index(name = "idx_style_platform", columnList = "style,platform")
|
||||||
|
})
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class GeneratedImageEntity extends BaseTimeEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 (양방향 관계)
|
||||||
|
*/
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "content_id")
|
||||||
|
private ContentEntity content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 ID (이벤트 초안 ID)
|
||||||
|
*/
|
||||||
|
@Column(name = "event_draft_id", nullable = false)
|
||||||
|
private Long eventDraftId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 스타일
|
||||||
|
*/
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "style", nullable = false, length = 20)
|
||||||
|
private ImageStyle style;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 플랫폼
|
||||||
|
*/
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "platform", nullable = false, length = 20)
|
||||||
|
private Platform platform;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CDN URL (Azure Blob Storage)
|
||||||
|
*/
|
||||||
|
@Column(name = "cdn_url", nullable = false, length = 500)
|
||||||
|
private String cdnUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 프롬프트
|
||||||
|
*/
|
||||||
|
@Column(name = "prompt", columnDefinition = "TEXT")
|
||||||
|
private String prompt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 선택 여부
|
||||||
|
*/
|
||||||
|
@Column(name = "selected", nullable = false)
|
||||||
|
private boolean selected;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 정적 팩토리 메서드: 새 이미지 생성
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @param style 이미지 스타일
|
||||||
|
* @param platform 플랫폼
|
||||||
|
* @param cdnUrl CDN URL
|
||||||
|
* @param prompt 프롬프트
|
||||||
|
* @return GeneratedImageEntity
|
||||||
|
*/
|
||||||
|
public static GeneratedImageEntity create(Long eventDraftId, ImageStyle style, Platform platform,
|
||||||
|
String cdnUrl, String prompt) {
|
||||||
|
GeneratedImageEntity entity = new GeneratedImageEntity();
|
||||||
|
entity.eventDraftId = eventDraftId;
|
||||||
|
entity.style = style;
|
||||||
|
entity.platform = platform;
|
||||||
|
entity.cdnUrl = cdnUrl;
|
||||||
|
entity.prompt = prompt;
|
||||||
|
entity.selected = false;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도메인 모델로 변환
|
||||||
|
*
|
||||||
|
* @return GeneratedImage 도메인 모델
|
||||||
|
*/
|
||||||
|
public GeneratedImage toDomain() {
|
||||||
|
return GeneratedImage.builder()
|
||||||
|
.id(id)
|
||||||
|
.eventDraftId(eventDraftId)
|
||||||
|
.style(style)
|
||||||
|
.platform(platform)
|
||||||
|
.cdnUrl(cdnUrl)
|
||||||
|
.prompt(prompt)
|
||||||
|
.selected(selected)
|
||||||
|
.createdAt(getCreatedAt())
|
||||||
|
.updatedAt(getUpdatedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 할당 (양방향 관계 설정용)
|
||||||
|
*
|
||||||
|
* @param content 콘텐츠 엔티티
|
||||||
|
*/
|
||||||
|
protected void assignContent(ContentEntity content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 선택
|
||||||
|
*/
|
||||||
|
public void select() {
|
||||||
|
this.selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 선택 해제
|
||||||
|
*/
|
||||||
|
public void deselect() {
|
||||||
|
this.selected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,143 @@
|
|||||||
|
package com.kt.event.content.infra.gateway.entity;
|
||||||
|
|
||||||
|
import com.kt.event.common.entity.BaseTimeEntity;
|
||||||
|
import com.kt.event.content.biz.domain.Job;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 엔티티
|
||||||
|
* 이미지 생성 작업 정보를 저장
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "jobs", indexes = {
|
||||||
|
@Index(name = "idx_event_draft_id", columnList = "event_draft_id"),
|
||||||
|
@Index(name = "idx_status", columnList = "status")
|
||||||
|
})
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class JobEntity extends BaseTimeEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "id", length = 36)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 ID (이벤트 초안 ID)
|
||||||
|
*/
|
||||||
|
@Column(name = "event_draft_id", nullable = false)
|
||||||
|
private Long eventDraftId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 타입
|
||||||
|
*/
|
||||||
|
@Column(name = "job_type", nullable = false, length = 50)
|
||||||
|
private String jobType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 상태
|
||||||
|
*/
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
|
private Job.Status status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 진행률 (0-100)
|
||||||
|
*/
|
||||||
|
@Column(name = "progress", nullable = false)
|
||||||
|
private int progress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 결과 메시지
|
||||||
|
*/
|
||||||
|
@Column(name = "result_message", columnDefinition = "TEXT")
|
||||||
|
private String resultMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 메시지
|
||||||
|
*/
|
||||||
|
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 정적 팩토리 메서드: 새 Job 생성
|
||||||
|
*
|
||||||
|
* @param id Job ID (UUID)
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @param jobType Job 타입
|
||||||
|
* @return JobEntity
|
||||||
|
*/
|
||||||
|
public static JobEntity create(String id, Long eventDraftId, String jobType) {
|
||||||
|
JobEntity entity = new JobEntity();
|
||||||
|
entity.id = id;
|
||||||
|
entity.eventDraftId = eventDraftId;
|
||||||
|
entity.jobType = jobType;
|
||||||
|
entity.status = Job.Status.PENDING;
|
||||||
|
entity.progress = 0;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도메인 모델로 변환
|
||||||
|
*
|
||||||
|
* @return Job 도메인 모델
|
||||||
|
*/
|
||||||
|
public Job toDomain() {
|
||||||
|
return Job.builder()
|
||||||
|
.id(id)
|
||||||
|
.eventDraftId(eventDraftId)
|
||||||
|
.jobType(jobType)
|
||||||
|
.status(status)
|
||||||
|
.progress(progress)
|
||||||
|
.resultMessage(resultMessage)
|
||||||
|
.errorMessage(errorMessage)
|
||||||
|
.createdAt(getCreatedAt())
|
||||||
|
.updatedAt(getUpdatedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 시작
|
||||||
|
*/
|
||||||
|
public void start() {
|
||||||
|
this.status = Job.Status.PROCESSING;
|
||||||
|
this.progress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 진행률 업데이트
|
||||||
|
*
|
||||||
|
* @param progress 진행률 (0-100)
|
||||||
|
*/
|
||||||
|
public void updateProgress(int progress) {
|
||||||
|
if (progress < 0 || progress > 100) {
|
||||||
|
throw new IllegalArgumentException("진행률은 0-100 사이여야 합니다");
|
||||||
|
}
|
||||||
|
this.progress = progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 완료 처리
|
||||||
|
*
|
||||||
|
* @param resultMessage 결과 메시지
|
||||||
|
*/
|
||||||
|
public void complete(String resultMessage) {
|
||||||
|
this.status = Job.Status.COMPLETED;
|
||||||
|
this.progress = 100;
|
||||||
|
this.resultMessage = resultMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job 실패 처리
|
||||||
|
*
|
||||||
|
* @param errorMessage 에러 메시지
|
||||||
|
*/
|
||||||
|
public void fail(String errorMessage) {
|
||||||
|
this.status = Job.Status.FAILED;
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.kt.event.content.infra.gateway.repository;
|
||||||
|
|
||||||
|
import com.kt.event.content.infra.gateway.entity.ContentEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 JPA 리포지토리
|
||||||
|
*/
|
||||||
|
public interface ContentJpaRepository extends JpaRepository<ContentEntity, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID로 콘텐츠 조회 (이미지 목록 포함)
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @return 콘텐츠 엔티티
|
||||||
|
*/
|
||||||
|
@Query("SELECT DISTINCT c FROM ContentEntity c " +
|
||||||
|
"LEFT JOIN FETCH c.images " +
|
||||||
|
"WHERE c.eventDraftId = :eventDraftId")
|
||||||
|
Optional<ContentEntity> findByEventDraftIdWithImages(@Param("eventDraftId") Long eventDraftId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID로 콘텐츠 조회
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @return 콘텐츠 엔티티
|
||||||
|
*/
|
||||||
|
Optional<ContentEntity> findByEventDraftId(Long eventDraftId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID로 콘텐츠 존재 여부 확인
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @return 존재 여부
|
||||||
|
*/
|
||||||
|
boolean existsByEventDraftId(Long eventDraftId);
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
package com.kt.event.content.infra.gateway.repository;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.domain.ImageStyle;
|
||||||
|
import com.kt.event.content.biz.domain.Platform;
|
||||||
|
import com.kt.event.content.infra.gateway.entity.GeneratedImageEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성된 이미지 JPA 리포지토리
|
||||||
|
*/
|
||||||
|
public interface GeneratedImageJpaRepository extends JpaRepository<GeneratedImageEntity, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID로 이미지 목록 조회
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @return 이미지 엔티티 목록
|
||||||
|
*/
|
||||||
|
List<GeneratedImageEntity> findByEventDraftId(Long eventDraftId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID와 스타일로 이미지 목록 조회
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @param style 이미지 스타일
|
||||||
|
* @return 이미지 엔티티 목록
|
||||||
|
*/
|
||||||
|
List<GeneratedImageEntity> findByEventDraftIdAndStyle(Long eventDraftId, ImageStyle style);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID와 플랫폼으로 이미지 목록 조회
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @param platform 플랫폼
|
||||||
|
* @return 이미지 엔티티 목록
|
||||||
|
*/
|
||||||
|
List<GeneratedImageEntity> findByEventDraftIdAndPlatform(Long eventDraftId, Platform platform);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID와 선택 여부로 이미지 목록 조회
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @param selected 선택 여부
|
||||||
|
* @return 이미지 엔티티 목록
|
||||||
|
*/
|
||||||
|
List<GeneratedImageEntity> findByEventDraftIdAndSelected(Long eventDraftId, boolean selected);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID로 선택된 이미지 목록 조회
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @return 선택된 이미지 엔티티 목록
|
||||||
|
*/
|
||||||
|
@Query("SELECT i FROM GeneratedImageEntity i WHERE i.eventDraftId = :eventDraftId AND i.selected = true")
|
||||||
|
List<GeneratedImageEntity> findSelectedImages(@Param("eventDraftId") Long eventDraftId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID로 모든 이미지 선택 해제
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
*/
|
||||||
|
@Query("UPDATE GeneratedImageEntity i SET i.selected = false WHERE i.eventDraftId = :eventDraftId")
|
||||||
|
void deselectAllByEventDraftId(@Param("eventDraftId") Long eventDraftId);
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package com.kt.event.content.infra.gateway.repository;
|
||||||
|
|
||||||
|
import com.kt.event.content.biz.domain.Job;
|
||||||
|
import com.kt.event.content.infra.gateway.entity.JobEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job JPA 리포지토리
|
||||||
|
*/
|
||||||
|
public interface JobJpaRepository extends JpaRepository<JobEntity, String> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID로 Job 목록 조회
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @return Job 엔티티 목록
|
||||||
|
*/
|
||||||
|
List<JobEntity> findByEventDraftId(Long eventDraftId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID와 상태로 Job 목록 조회
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @param status Job 상태
|
||||||
|
* @return Job 엔티티 목록
|
||||||
|
*/
|
||||||
|
List<JobEntity> findByEventDraftIdAndStatus(Long eventDraftId, Job.Status status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 초안 ID와 Job 타입으로 최신 Job 조회
|
||||||
|
*
|
||||||
|
* @param eventDraftId 이벤트 초안 ID
|
||||||
|
* @param jobType Job 타입
|
||||||
|
* @return Job 엔티티
|
||||||
|
*/
|
||||||
|
Optional<JobEntity> findFirstByEventDraftIdAndJobTypeOrderByCreatedAtDesc(Long eventDraftId, String jobType);
|
||||||
|
}
|
||||||
50
content-service/src/main/resources/application.yml
Normal file
50
content-service/src/main/resources/application.yml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: content-service
|
||||||
|
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://4.217.131.139:5432/contentdb
|
||||||
|
username: eventuser
|
||||||
|
password: Hi5Jessica!
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
|
jpa:
|
||||||
|
database-platform: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: update
|
||||||
|
show-sql: true
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
format_sql: true
|
||||||
|
use_sql_comments: true
|
||||||
|
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: 4.217.131.139
|
||||||
|
port: 6379
|
||||||
|
|
||||||
|
kafka:
|
||||||
|
bootstrap-servers: 20.249.125.115:9092
|
||||||
|
consumer:
|
||||||
|
group-id: content-service-consumers
|
||||||
|
auto-offset-reset: earliest
|
||||||
|
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||||
|
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 8084
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: kt-event-marketing-jwt-secret-key-for-authentication-and-authorization-2025
|
||||||
|
access-token-validity: 3600000
|
||||||
|
refresh-token-validity: 604800000
|
||||||
|
|
||||||
|
azure:
|
||||||
|
storage:
|
||||||
|
connection-string: ${AZURE_STORAGE_CONNECTION_STRING:}
|
||||||
|
container-name: event-images
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
com.kt.event: DEBUG
|
||||||
|
org.hibernate.SQL: DEBUG
|
||||||
Loading…
x
Reference in New Issue
Block a user