@startuml !theme mono title Content Service - 클래스 다이어그램 (Clean Architecture) ' ============================================ ' Domain Layer (엔티티 및 비즈니스 로직) ' ============================================ package "com.kt.event.content.biz.domain" <> { class Content { - id: Long - eventId: String - eventTitle: String - eventDescription: String - images: List - createdAt: LocalDateTime - updatedAt: LocalDateTime + addImage(image: GeneratedImage): void + getSelectedImages(): List + getImagesByStyle(style: ImageStyle): List + getImagesByPlatform(platform: Platform): List } class GeneratedImage { - id: Long - eventId: String - style: ImageStyle - platform: Platform - cdnUrl: String - prompt: String - selected: boolean - createdAt: LocalDateTime - updatedAt: LocalDateTime + select(): void + deselect(): void } class Job { - id: String - eventId: String - jobType: String - status: Status - progress: int - resultMessage: String - errorMessage: String - createdAt: LocalDateTime - updatedAt: LocalDateTime + start(): void + updateProgress(progress: int): void + complete(resultMessage: String): void + fail(errorMessage: String): void + isProcessing(): boolean + isCompleted(): boolean + isFailed(): boolean } enum JobStatus { PENDING PROCESSING COMPLETED FAILED } enum ImageStyle { FANCY SIMPLE TRENDY + getDescription(): String } enum Platform { INSTAGRAM FACEBOOK KAKAO BLOG + getWidth(): int + getHeight(): int + getAspectRatio(): String } } ' ============================================ ' Application Layer (Use Cases) ' ============================================ package "com.kt.event.content.biz.usecase" <> { package "in" { interface GenerateImagesUseCase { + execute(command: GenerateImagesCommand): JobInfo } interface GetJobStatusUseCase { + execute(jobId: String): JobInfo } interface GetEventContentUseCase { + execute(eventId: String): ContentInfo } interface GetImageListUseCase { + execute(eventId: String, style: ImageStyle, platform: Platform): List } interface GetImageDetailUseCase { + execute(imageId: Long): ImageInfo } interface RegenerateImageUseCase { + execute(command: RegenerateImageCommand): JobInfo } interface DeleteImageUseCase { + execute(imageId: Long): void } } package "out" { interface ContentReader { + findByEventDraftIdWithImages(eventId: String): Optional + findImageById(imageId: Long): Optional + findImagesByEventDraftId(eventId: String): List } interface ContentWriter { + save(content: Content): Content + saveImage(image: GeneratedImage): GeneratedImage + getImageById(imageId: Long): GeneratedImage + deleteImageById(imageId: Long): void } interface ImageReader { + getImage(eventId: String, style: ImageStyle, platform: Platform): Optional + getImagesByEventId(eventId: String): List } interface ImageWriter { + saveImage(image: GeneratedImage): GeneratedImage + getImageById(imageId: Long): GeneratedImage + deleteImageById(imageId: Long): void } interface JobReader { + getJob(jobId: String): Optional } interface JobWriter { + saveJob(jobData: RedisJobData, ttlSeconds: long): void + updateJobStatus(jobId: String, status: String, progress: Integer): void + updateJobResult(jobId: String, resultMessage: String): void + updateJobError(jobId: String, errorMessage: String): void } interface RedisAIDataReader { + getAIRecommendation(eventId: String): Optional> } interface RedisImageWriter { + cacheImages(eventId: String, images: List, ttlSeconds: long): void } interface CDNUploader { + upload(imageData: byte[], fileName: String): String } interface ImageGeneratorCaller { + generateImage(prompt: String, width: int, height: int): String } } } ' ============================================ ' Application Service Layer ' ============================================ package "com.kt.event.content.biz.service" <> { class StableDiffusionImageGenerator implements GenerateImagesUseCase { - replicateClient: ReplicateApiClient - cdnUploader: CDNUploader - jobWriter: JobWriter - contentWriter: ContentWriter - circuitBreaker: CircuitBreaker - modelVersion: String + execute(command: GenerateImagesCommand): JobInfo - processImageGeneration(jobId: String, command: GenerateImagesCommand): void - generateImage(prompt: String, platform: Platform): String - waitForCompletion(predictionId: String): String - buildPrompt(command: GenerateImagesCommand, style: ImageStyle, platform: Platform): String - downloadImage(imageUrl: String): byte[] - createPredictionWithCircuitBreaker(request: ReplicateRequest): ReplicateResponse - getPredictionWithCircuitBreaker(predictionId: String): ReplicateResponse } class JobManagementService implements GetJobStatusUseCase { - jobReader: JobReader + execute(jobId: String): JobInfo } class GetEventContentService implements GetEventContentUseCase { - contentReader: ContentReader - redisAIDataReader: RedisAIDataReader + execute(eventId: String): ContentInfo } class GetImageListService implements GetImageListUseCase { - imageReader: ImageReader + execute(eventId: String, style: ImageStyle, platform: Platform): List } class GetImageDetailService implements GetImageDetailUseCase { - imageReader: ImageReader + execute(imageId: Long): ImageInfo } class RegenerateImageService implements RegenerateImageUseCase { - imageReader: ImageReader - imageWriter: ImageWriter - jobWriter: JobWriter - cdnUploader: CDNUploader - replicateClient: ReplicateApiClient + execute(command: RegenerateImageCommand): JobInfo } class DeleteImageService implements DeleteImageUseCase { - imageWriter: ImageWriter + execute(imageId: Long): void } } ' ============================================ ' DTO Layer ' ============================================ package "com.kt.event.content.biz.dto" <> { class ContentCommand class GenerateImagesCommand { - eventId: String - eventTitle: String - eventDescription: String - industry: String - location: String - trends: List - styles: List - platforms: List } class RegenerateImageCommand { - imageId: Long - newPrompt: String } class ContentInfo { - id: Long - eventId: String - eventTitle: String - eventDescription: String - images: List - createdAt: LocalDateTime - updatedAt: LocalDateTime + {static} from(content: Content): ContentInfo } class ImageInfo { - id: Long - eventId: String - style: ImageStyle - platform: Platform - cdnUrl: String - prompt: String - selected: boolean - createdAt: LocalDateTime - updatedAt: LocalDateTime + {static} from(image: GeneratedImage): ImageInfo } class JobInfo { - id: String - eventId: String - jobType: String - status: String - progress: int - resultMessage: String - errorMessage: String - createdAt: LocalDateTime - updatedAt: LocalDateTime + {static} from(job: Job): JobInfo } class RedisJobData { - id: String - eventId: String - jobType: String - status: String - progress: int - resultMessage: String - errorMessage: String - createdAt: LocalDateTime - updatedAt: LocalDateTime } class RedisImageData { - eventId: String - style: ImageStyle - platform: Platform - imageUrl: String - prompt: String - createdAt: LocalDateTime } class RedisAIEventData { - eventId: String - recommendedStyles: List - recommendedKeywords: List - cachedAt: LocalDateTime } } ' ============================================ ' Infrastructure Layer (Gateway & Adapter) ' ============================================ package "com.kt.event.content.infra.gateway" <> { class RedisGateway implements ContentReader, ContentWriter, ImageReader, ImageWriter, JobReader, JobWriter, RedisAIDataReader, RedisImageWriter { - redisTemplate: RedisTemplate - objectMapper: ObjectMapper - nextContentId: Long - nextImageId: Long + getAIRecommendation(eventId: String): Optional> + cacheImages(eventId: String, images: List, ttlSeconds: long): void + saveImage(imageData: RedisImageData, ttlSeconds: long): void + getImage(eventId: String, style: ImageStyle, platform: Platform): Optional + getImagesByEventId(eventId: String): List + deleteImage(eventId: String, style: ImageStyle, platform: Platform): void + saveImages(eventId: String, images: List, ttlSeconds: long): void + saveJob(jobData: RedisJobData, ttlSeconds: long): void + getJob(jobId: String): Optional + updateJobStatus(jobId: String, status: String, progress: Integer): void + updateJobResult(jobId: String, resultMessage: String): void + updateJobError(jobId: String, errorMessage: String): void + findByEventDraftIdWithImages(eventId: String): Optional + findImageById(imageId: Long): Optional + findImagesByEventDraftId(eventId: String): List + save(content: Content): Content + saveImage(image: GeneratedImage): GeneratedImage + getImageById(imageId: Long): GeneratedImage + deleteImageById(imageId: Long): void - buildImageKey(eventId: String, style: ImageStyle, platform: Platform): String - getString(map: Map, key: String): String - getLong(map: Map, key: String): Long - getInteger(map: Map, key: String): Integer - getLocalDateTime(map: Map, key: String): LocalDateTime } package "client" { class ReplicateApiClient { - apiToken: String - baseUrl: String - restClient: RestClient + createPrediction(request: ReplicateRequest): ReplicateResponse + getPrediction(predictionId: String): ReplicateResponse } class AzureBlobStorageUploader implements CDNUploader { - connectionString: String - containerName: String - circuitBreaker: CircuitBreaker - blobServiceClient: BlobServiceClient - containerClient: BlobContainerClient + init(): void + upload(imageData: byte[], fileName: String): String - doUpload(imageData: byte[], fileName: String): String - generateBlobName(fileName: String): String } class ReplicateRequest { - version: String - input: Input } class ReplicateInputRequest { - prompt: String - negativePrompt: String - width: int - height: int - numOutputs: int - guidanceScale: double - numInferenceSteps: int - seed: long } class ReplicateResponse { - id: String - status: String - output: List - error: String } class ReplicateApiConfig { - apiToken: String - baseUrl: String + restClient(): RestClient } } } ' ============================================ ' Presentation Layer (REST Controller) ' ============================================ package "com.kt.event.content.infra.web.controller" <> { class ContentController { - generateImagesUseCase: GenerateImagesUseCase - getJobStatusUseCase: GetJobStatusUseCase - getEventContentUseCase: GetEventContentUseCase - getImageListUseCase: GetImageListUseCase - getImageDetailUseCase: GetImageDetailUseCase - regenerateImageUseCase: RegenerateImageUseCase - deleteImageUseCase: DeleteImageUseCase + generateImages(command: GenerateImagesCommand): ResponseEntity + getJobStatus(jobId: String): ResponseEntity + getContentByEventId(eventId: String): ResponseEntity + getImages(eventId: String, style: String, platform: String): ResponseEntity> + getImageById(imageId: Long): ResponseEntity + deleteImage(imageId: Long): ResponseEntity + regenerateImage(imageId: Long, requestBody: RegenerateImageCommand): ResponseEntity } } ' ============================================ ' Configuration Layer ' ============================================ package "com.kt.event.content.infra.config" <> { class RedisConfig { - host: String - port: int + redisConnectionFactory(): RedisConnectionFactory + redisTemplate(): RedisTemplate + objectMapper(): ObjectMapper } class Resilience4jConfig { + replicateCircuitBreaker(): CircuitBreaker + azureCircuitBreaker(): CircuitBreaker } class SecurityConfig { + securityFilterChain(http: HttpSecurity): SecurityFilterChain } class SwaggerConfig { + openAPI(): OpenAPI } } ' ============================================ ' 관계 정의 (Domain) ' ============================================ Content "1" o-- "0..*" GeneratedImage : contains GeneratedImage --> ImageStyle : uses GeneratedImage --> Platform : uses Job --> JobStatus : has ' ============================================ ' 관계 정의 (Service → Port) ' ============================================ StableDiffusionImageGenerator ..> CDNUploader StableDiffusionImageGenerator ..> JobWriter StableDiffusionImageGenerator ..> ContentWriter StableDiffusionImageGenerator --> ReplicateApiClient JobManagementService ..> JobReader GetEventContentService ..> ContentReader GetEventContentService ..> RedisAIDataReader GetImageListService ..> ImageReader GetImageDetailService ..> ImageReader RegenerateImageService ..> ImageReader RegenerateImageService ..> ImageWriter RegenerateImageService ..> JobWriter RegenerateImageService ..> CDNUploader DeleteImageService ..> ImageWriter ' ============================================ ' 관계 정의 (Gateway → Port Implementation) ' ============================================ RedisGateway ..|> ContentReader RedisGateway ..|> ContentWriter RedisGateway ..|> ImageReader RedisGateway ..|> ImageWriter RedisGateway ..|> JobReader RedisGateway ..|> JobWriter RedisGateway ..|> RedisAIDataReader RedisGateway ..|> RedisImageWriter AzureBlobStorageUploader ..|> CDNUploader ' ============================================ ' 관계 정의 (Controller → UseCase) ' ============================================ ContentController ..> GenerateImagesUseCase ContentController ..> GetJobStatusUseCase ContentController ..> GetEventContentUseCase ContentController ..> GetImageListUseCase ContentController ..> GetImageDetailUseCase ContentController ..> RegenerateImageUseCase ContentController ..> DeleteImageUseCase ' ============================================ ' 관계 정의 (DTO) ' ============================================ ContentInfo ..> ImageInfo ContentCommand ..> GenerateImagesCommand : uses ContentCommand ..> RegenerateImageCommand : uses ReplicateRequest ..> ReplicateInputRequest : contains ' ============================================ ' 레이어 노트 ' ============================================ note top of Content : Domain Layer\n순수 비즈니스 로직\n외부 의존성 없음 note top of GenerateImagesUseCase : Application Layer (Input Port)\nUse Case 인터페이스 note top of ContentReader : Application Layer (Output Port)\n외부 시스템 의존성 추상화 note top of StableDiffusionImageGenerator : Application Service Layer\nUse Case 구현체\n비즈니스 로직 오케스트레이션 note top of RedisGateway : Infrastructure Layer\nOutput Port 구현체\nRedis 연동 note top of ContentController : Presentation Layer\nREST API 엔드포인트 note top of RedisConfig : Configuration Layer\n인프라 설정 @enduml