diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index b7b3d1b..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-# 디폴트 무시된 파일
-/shelf/
-/workspace.xml
-# 환경에 따라 달라지는 Maven 홈 디렉터리
-/mavenHomeManager.xml
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 9018a0d..0000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 6ed36dd..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..fc7acb6
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "customColor": "",
+ "associatedIndex": 4
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+ false
+
+
+
+
+
+
+ 1749618504890
+
+
+ 1749618504890
+
+
+
+
\ No newline at end of file
diff --git a/smarketing-ai/app.py b/smarketing-ai/app.py
index 35cf56c..ebe2175 100644
--- a/smarketing-ai/app.py
+++ b/smarketing-ai/app.py
@@ -9,10 +9,8 @@ import os
from datetime import datetime
import traceback
from config.config import Config
-# from services.content_service import ContentService
from services.poster_service import PosterService
from services.sns_content_service import SnsContentService
-# from services.poster_generation_service import PosterGenerationService
from models.request_models import ContentRequest, PosterRequest, SnsContentGetRequest, PosterContentGetRequest
@@ -75,6 +73,7 @@ def create_app():
requirement=data.get('requirement'),
toneAndManner=data.get('toneAndManner'),
emotionIntensity=data.get('emotionIntensity'),
+ menuName=data.get('menuName'),
eventName=data.get('eventName'),
startDate=data.get('startDate'),
endDate=data.get('endDate')
@@ -124,6 +123,7 @@ def create_app():
requirement=data.get('requirement'),
toneAndManner=data.get('toneAndManner'),
emotionIntensity=data.get('emotionIntensity'),
+ menuName=data.get('menuName'),
eventName=data.get('eventName'),
startDate=data.get('startDate'),
endDate=data.get('endDate')
diff --git a/smarketing-ai/models/request_models.py b/smarketing-ai/models/request_models.py
index 8816533..b47b257 100644
--- a/smarketing-ai/models/request_models.py
+++ b/smarketing-ai/models/request_models.py
@@ -17,6 +17,7 @@ class SnsContentGetRequest:
requirement: Optional[str] = None
toneAndManner: Optional[str] = None
emotionIntensity: Optional[str] = None
+ menuName: Optional[str] = None
eventName: Optional[str] = None
startDate: Optional[str] = None
endDate: Optional[str] = None
@@ -33,6 +34,7 @@ class PosterContentGetRequest:
requirement: Optional[str] = None
toneAndManner: Optional[str] = None
emotionIntensity: Optional[str] = None
+ menuName: Optional[str] = None
eventName: Optional[str] = None
startDate: Optional[str] = None
endDate: Optional[str] = None
diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py
index e5090e0..fc80913 100644
--- a/smarketing-ai/services/sns_content_service.py
+++ b/smarketing-ai/services/sns_content_service.py
@@ -205,6 +205,9 @@ class SnsContentService:
"""
metadata_html = '
'
+ if request.menuName:
+ metadata_html += f'
메뉴: {request.menuName}
'
+
if request.eventName:
metadata_html += f'
이벤트: {request.eventName}
'
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/MarketingContentServiceApplication.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/MarketingContentServiceApplication.java
index 50d69a1..537a189 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/MarketingContentServiceApplication.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/MarketingContentServiceApplication.java
@@ -3,6 +3,7 @@ package com.won.smarketing.content;
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;
/**
@@ -17,8 +18,9 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
"com.won.smarketing.content.infrastructure.repository"
})
@EntityScan(basePackages = {
- "com.won.smarketing.content.domain.model"
+ "com.won.smarketing.content.infrastructure.entity"
})
+@EnableJpaAuditing
public class MarketingContentServiceApplication {
public static void main(String[] args) {
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java
index fec5d4e..dd8e603 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java
@@ -42,7 +42,7 @@ public class SnsContentService implements SnsContentUseCase {
public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request) {
// AI를 사용하여 SNS 콘텐츠 생성
String generatedContent = aiContentGenerator.generateSnsContent(request);
-
+
// 플랫폼에 맞는 해시태그 생성
Platform platform = Platform.fromString(request.getPlatform());
List
hashtags = aiContentGenerator.generateHashtags(generatedContent, platform);
@@ -60,7 +60,7 @@ public class SnsContentService implements SnsContentUseCase {
// 임시 콘텐츠 생성 (저장하지 않음)
Content content = Content.builder()
- .contentType(ContentType.SNS_POST)
+// .contentType(ContentType.SNS_POST)
.platform(platform)
.title(request.getTitle())
.content(generatedContent)
@@ -88,7 +88,7 @@ public class SnsContentService implements SnsContentUseCase {
/**
* SNS 콘텐츠 저장
- *
+ *
* @param request SNS 콘텐츠 저장 요청
*/
@Override
@@ -107,7 +107,7 @@ public class SnsContentService implements SnsContentUseCase {
// 콘텐츠 엔티티 생성 및 저장
Content content = Content.builder()
- .contentType(ContentType.SNS_POST)
+// .contentType(ContentType.SNS_POST)
.platform(Platform.fromString(request.getPlatform()))
.title(request.getTitle())
.content(request.getContent())
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
index 973b234..6bf2960 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
@@ -1,3 +1,4 @@
+// marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
package com.won.smarketing.content.application.usecase;
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
@@ -5,23 +6,21 @@ import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse;
import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
/**
- * 포스터 콘텐츠 관련 Use Case 인터페이스
- * 홍보 포스터 생성 및 저장 기능 정의
+ * 포스터 콘텐츠 관련 UseCase 인터페이스
+ * Clean Architecture의 Application Layer에서 비즈니스 로직 정의
*/
public interface PosterContentUseCase {
-
+
/**
* 포스터 콘텐츠 생성
- *
* @param request 포스터 콘텐츠 생성 요청
- * @return 생성된 포스터 콘텐츠 정보
+ * @return 포스터 콘텐츠 생성 응답
*/
PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request);
-
+
/**
* 포스터 콘텐츠 저장
- *
* @param request 포스터 콘텐츠 저장 요청
*/
void savePosterContent(PosterContentSaveRequest request);
-}
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java
index e62902d..d2c6751 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java
@@ -1,3 +1,4 @@
+// marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java
package com.won.smarketing.content.application.usecase;
import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest;
@@ -5,23 +6,21 @@ import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse;
import com.won.smarketing.content.presentation.dto.SnsContentSaveRequest;
/**
- * SNS 콘텐츠 관련 Use Case 인터페이스
- * SNS 게시물 생성 및 저장 기능 정의
+ * SNS 콘텐츠 관련 UseCase 인터페이스
+ * Clean Architecture의 Application Layer에서 비즈니스 로직 정의
*/
public interface SnsContentUseCase {
-
+
/**
* SNS 콘텐츠 생성
- *
* @param request SNS 콘텐츠 생성 요청
- * @return 생성된 SNS 콘텐츠 정보
+ * @return SNS 콘텐츠 생성 응답
*/
SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request);
-
+
/**
* SNS 콘텐츠 저장
- *
* @param request SNS 콘텐츠 저장 요청
*/
void saveSnsContent(SnsContentSaveRequest request);
-}
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/ContentConfig.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/ContentConfig.java
new file mode 100644
index 0000000..3931d19
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/ContentConfig.java
@@ -0,0 +1,9 @@
+package com.won.smarketing.content.config;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ComponentScan(basePackages = "com.won.smarketing.content")
+public class ContentConfig {
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/JpaConfig.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/JpaConfig.java
deleted file mode 100644
index e95312d..0000000
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/JpaConfig.java
+++ /dev/null
@@ -1,18 +0,0 @@
-// marketing-content/src/main/java/com/won/smarketing/content/config/JpaConfig.java
-package com.won.smarketing.content.config;
-
-import org.springframework.boot.autoconfigure.domain.EntityScan;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
-
-/**
- * JPA 설정 클래스
- *
- * @author smarketing-team
- * @version 1.0
- */
-@Configuration
-@EntityScan(basePackages = "com.won.smarketing.content.infrastructure.entity")
-@EnableJpaRepositories(basePackages = "com.won.smarketing.content.infrastructure.repository")
-public class JpaConfig {
-}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java
index 4e95d02..9a19b77 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java
@@ -1,14 +1,10 @@
// marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java
package com.won.smarketing.content.domain.model;
-import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
-import org.springframework.data.annotation.CreatedDate;
-import org.springframework.data.annotation.LastModifiedDate;
-import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -17,616 +13,151 @@ import java.util.List;
/**
* 콘텐츠 도메인 모델
*
- * 이 클래스는 마케팅 콘텐츠의 핵심 정보와 비즈니스 로직을 포함하는
- * DDD(Domain-Driven Design) 엔티티입니다.
- *
- * Clean Architecture의 Domain Layer에 위치하며,
- * 비즈니스 규칙과 도메인 로직을 캡슐화합니다.
+ * Clean Architecture의 Domain Layer에 위치하는 핵심 엔티티
+ * JPA 애노테이션을 제거하여 순수 도메인 모델로 유지
+ * Infrastructure Layer에서 별도의 JPA 엔티티로 매핑
*/
-@Entity
-@Table(
- name = "contents",
- indexes = {
- @Index(name = "idx_store_id", columnList = "store_id"),
- @Index(name = "idx_content_type", columnList = "content_type"),
- @Index(name = "idx_platform", columnList = "platform"),
- @Index(name = "idx_status", columnList = "status"),
- @Index(name = "idx_promotion_dates", columnList = "promotion_start_date, promotion_end_date"),
- @Index(name = "idx_created_at", columnList = "created_at")
- }
-)
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
-@EntityListeners(AuditingEntityListener.class)
public class Content {
// ==================== 기본키 및 식별자 ====================
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- @Column(name = "content_id")
private Long id;
// ==================== 콘텐츠 분류 ====================
-
- @Enumerated(EnumType.STRING)
- @Column(name = "content_type", nullable = false, length = 20)
private ContentType contentType;
-
- @Enumerated(EnumType.STRING)
- @Column(name = "platform", nullable = false, length = 20)
private Platform platform;
// ==================== 콘텐츠 내용 ====================
-
- @Column(name = "title", nullable = false, length = 200)
private String title;
-
- @Column(name = "content", nullable = false, columnDefinition = "TEXT")
private String content;
// ==================== 멀티미디어 및 메타데이터 ====================
-
- @ElementCollection(fetch = FetchType.LAZY)
- @CollectionTable(
- name = "content_hashtags",
- joinColumns = @JoinColumn(name = "content_id"),
- indexes = @Index(name = "idx_content_hashtags", columnList = "content_id")
- )
- @Column(name = "hashtag", length = 100)
@Builder.Default
private List hashtags = new ArrayList<>();
- @ElementCollection(fetch = FetchType.LAZY)
- @CollectionTable(
- name = "content_images",
- joinColumns = @JoinColumn(name = "content_id"),
- indexes = @Index(name = "idx_content_images", columnList = "content_id")
- )
- @Column(name = "image_url", length = 500)
@Builder.Default
private List images = new ArrayList<>();
// ==================== 상태 관리 ====================
+ private ContentStatus status;
- @Enumerated(EnumType.STRING)
- @Column(name = "status", nullable = false, length = 20)
- @Builder.Default
- private ContentStatus status = ContentStatus.DRAFT;
-
- // ==================== AI 생성 조건 (Embedded) ====================
-
- @Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "toneAndManner", column = @Column(name = "tone_and_manner", length = 50)),
- @AttributeOverride(name = "promotionType", column = @Column(name = "promotion_type", length = 50)),
- @AttributeOverride(name = "emotionIntensity", column = @Column(name = "emotion_intensity", length = 50)),
- @AttributeOverride(name = "targetAudience", column = @Column(name = "target_audience", length = 50)),
- @AttributeOverride(name = "eventName", column = @Column(name = "event_name", length = 100))
- })
+ // ==================== 생성 조건 ====================
private CreationConditions creationConditions;
- // ==================== 비즈니스 관계 ====================
-
- @Column(name = "store_id", nullable = false)
+ // ==================== 매장 정보 ====================
private Long storeId;
- // ==================== 홍보 기간 ====================
-
- @Column(name = "promotion_start_date")
+ // ==================== 프로모션 기간 ====================
private LocalDateTime promotionStartDate;
-
- @Column(name = "promotion_end_date")
private LocalDateTime promotionEndDate;
- // ==================== 감사(Audit) 정보 ====================
-
- @CreatedDate
- @Column(name = "created_at", nullable = false, updatable = false)
+ // ==================== 메타데이터 ====================
private LocalDateTime createdAt;
-
- @LastModifiedDate
- @Column(name = "updated_at")
private LocalDateTime updatedAt;
public Content(ContentId of, ContentType contentType, Platform platform, String title, String content, List strings, List strings1, ContentStatus contentStatus, CreationConditions conditions, Long storeId, LocalDateTime createdAt, LocalDateTime updatedAt) {
}
- // ==================== 비즈니스 로직 메서드 ====================
+ // ==================== 비즈니스 메서드 ====================
/**
* 콘텐츠 제목 수정
- *
- * 비즈니스 규칙:
- * - 제목은 null이거나 빈 값일 수 없음
- * - 200자를 초과할 수 없음
- * - 발행된 콘텐츠는 제목 변경 시 상태가 DRAFT로 변경됨
- *
- * @param title 새로운 제목
- * @throws IllegalArgumentException 제목이 유효하지 않은 경우
+ * @param newTitle 새로운 제목
*/
- public void updateTitle(String title) {
- validateTitle(title);
-
- boolean wasPublished = isPublished();
- this.title = title.trim();
-
- // 발행된 콘텐츠의 제목이 변경되면 재검토 필요
- if (wasPublished) {
- this.status = ContentStatus.DRAFT;
+ public void updateTitle(String newTitle) {
+ if (newTitle == null || newTitle.trim().isEmpty()) {
+ throw new IllegalArgumentException("제목은 필수입니다.");
}
+ this.title = newTitle.trim();
+ this.updatedAt = LocalDateTime.now();
}
/**
* 콘텐츠 내용 수정
- *
- * 비즈니스 규칙:
- * - 내용은 null이거나 빈 값일 수 없음
- * - 발행된 콘텐츠는 내용 변경 시 상태가 DRAFT로 변경됨
- *
- * @param content 새로운 콘텐츠 내용
- * @throws IllegalArgumentException 내용이 유효하지 않은 경우
+ * @param newContent 새로운 내용
*/
- public void updateContent(String content) {
- validateContent(content);
+ public void updateContent(String newContent) {
+ this.content = newContent;
+ this.updatedAt = LocalDateTime.now();
+ }
- boolean wasPublished = isPublished();
- this.content = content.trim();
-
- // 발행된 콘텐츠의 내용이 변경되면 재검토 필요
- if (wasPublished) {
- this.status = ContentStatus.DRAFT;
+ /**
+ * 프로모션 기간 설정
+ * @param startDate 시작일
+ * @param endDate 종료일
+ */
+ public void updatePeriod(LocalDateTime startDate, LocalDateTime endDate) {
+ if (startDate != null && endDate != null && startDate.isAfter(endDate)) {
+ throw new IllegalArgumentException("시작일은 종료일보다 이후일 수 없습니다.");
}
+ this.promotionStartDate = startDate;
+ this.promotionEndDate = endDate;
+ this.updatedAt = LocalDateTime.now();
}
/**
* 콘텐츠 상태 변경
- *
- * 비즈니스 규칙:
- * - PUBLISHED 상태로 변경시 유효성 검증 수행
- * - ARCHIVED 상태에서는 PUBLISHED로만 변경 가능
- *
- * @param status 새로운 상태
- * @throws IllegalStateException 잘못된 상태 전환인 경우
+ * @param newStatus 새로운 상태
*/
- public void changeStatus(ContentStatus status) {
- validateStatusTransition(this.status, status);
-
- if (status == ContentStatus.PUBLISHED) {
- validateForPublication();
+ public void updateStatus(ContentStatus newStatus) {
+ if (newStatus == null) {
+ throw new IllegalArgumentException("상태는 필수입니다.");
}
-
- this.status = status;
- }
-
- /**
- * 홍보 기간 설정
- *
- * 비즈니스 규칙:
- * - 시작일은 종료일보다 이전이어야 함
- * - 과거 날짜로 설정 불가 (현재 시간 기준)
- *
- * @param startDate 홍보 시작일
- * @param endDate 홍보 종료일
- * @throws IllegalArgumentException 날짜가 유효하지 않은 경우
- */
- public void setPromotionPeriod(LocalDateTime startDate, LocalDateTime endDate) {
- validatePromotionPeriod(startDate, endDate);
-
- this.promotionStartDate = startDate;
- this.promotionEndDate = endDate;
- }
-
- /**
- * 홍보 기간 설정
- *
- * 비즈니스 규칙:
- * - 시작일은 종료일보다 이전이어야 함
- * - 과거 날짜로 설정 불가 (현재 시간 기준)
- *
- * @param startDate 홍보 시작일
- * @param endDate 홍보 종료일
- * @throws IllegalArgumentException 날짜가 유효하지 않은 경우
- */
- public void updatePeriod(LocalDateTime startDate, LocalDateTime endDate) {
- validatePromotionPeriod(startDate, endDate);
-
- this.promotionStartDate = startDate;
- this.promotionEndDate = endDate;
+ this.status = newStatus;
+ this.updatedAt = LocalDateTime.now();
}
/**
* 해시태그 추가
- *
- * @param hashtag 추가할 해시태그 (# 없이)
+ * @param hashtag 추가할 해시태그
*/
public void addHashtag(String hashtag) {
if (hashtag != null && !hashtag.trim().isEmpty()) {
- String cleanHashtag = hashtag.trim().replace("#", "");
- if (!this.hashtags.contains(cleanHashtag)) {
- this.hashtags.add(cleanHashtag);
+ if (this.hashtags == null) {
+ this.hashtags = new ArrayList<>();
}
- }
- }
-
- /**
- * 해시태그 제거
- *
- * @param hashtag 제거할 해시태그
- */
- public void removeHashtag(String hashtag) {
- if (hashtag != null) {
- String cleanHashtag = hashtag.trim().replace("#", "");
- this.hashtags.remove(cleanHashtag);
+ this.hashtags.add(hashtag.trim());
+ this.updatedAt = LocalDateTime.now();
}
}
/**
* 이미지 추가
- *
- * @param imageUrl 이미지 URL
+ * @param imageUrl 추가할 이미지 URL
*/
public void addImage(String imageUrl) {
if (imageUrl != null && !imageUrl.trim().isEmpty()) {
- if (!this.images.contains(imageUrl.trim())) {
- this.images.add(imageUrl.trim());
+ if (this.images == null) {
+ this.images = new ArrayList<>();
}
+ this.images.add(imageUrl.trim());
+ this.updatedAt = LocalDateTime.now();
}
}
/**
- * 이미지 제거
- *
- * @param imageUrl 제거할 이미지 URL
+ * 프로모션 진행 중 여부 확인
+ * @return 현재 시간이 프로모션 기간 내에 있으면 true
*/
- public void removeImage(String imageUrl) {
- if (imageUrl != null) {
- this.images.remove(imageUrl.trim());
- }
- }
-
- // ==================== 도메인 조회 메서드 ====================
-
- /**
- * 발행 상태 확인
- *
- * @return 발행된 상태이면 true
- */
- public boolean isPublished() {
- return this.status == ContentStatus.PUBLISHED;
- }
-
- /**
- * 수정 가능 상태 확인
- *
- * @return 임시저장 또는 예약발행 상태이면 true
- */
- public boolean isEditable() {
- return this.status == ContentStatus.DRAFT || this.status == ContentStatus.PUBLISHED;
- }
-
- /**
- * 현재 홍보 진행 중인지 확인
- *
- * @return 홍보 기간 내이고 발행 상태이면 true
- */
- public boolean isOngoingPromotion() {
- if (!isPublished() || promotionStartDate == null || promotionEndDate == null) {
- return false;
- }
-
- LocalDateTime now = LocalDateTime.now();
- return now.isAfter(promotionStartDate) && now.isBefore(promotionEndDate);
- }
-
- /**
- * 홍보 예정 상태 확인
- *
- * @return 홍보 시작 전이면 true
- */
- public boolean isUpcomingPromotion() {
- if (promotionStartDate == null) {
- return false;
- }
-
- return LocalDateTime.now().isBefore(promotionStartDate);
- }
-
- /**
- * 홍보 완료 상태 확인
- *
- * @return 홍보 종료 후이면 true
- */
- public boolean isCompletedPromotion() {
- if (promotionEndDate == null) {
- return false;
- }
-
- return LocalDateTime.now().isAfter(promotionEndDate);
- }
-
- /**
- * SNS 콘텐츠 여부 확인
- *
- * @return SNS 게시물이면 true
- */
- public boolean isSnsContent() {
- return this.contentType == ContentType.SNS_POST;
- }
-
- /**
- * 포스터 콘텐츠 여부 확인
- *
- * @return 포스터이면 true
- */
- public boolean isPosterContent() {
- return this.contentType == ContentType.POSTER;
- }
-
- /**
- * 이미지가 있는 콘텐츠인지 확인
- *
- * @return 이미지가 1개 이상 있으면 true
- */
- public boolean hasImages() {
- return this.images != null && !this.images.isEmpty();
- }
-
- /**
- * 해시태그가 있는 콘텐츠인지 확인
- *
- * @return 해시태그가 1개 이상 있으면 true
- */
- public boolean hasHashtags() {
- return this.hashtags != null && !this.hashtags.isEmpty();
- }
-
- // ==================== 유효성 검증 메서드 ====================
-
- /**
- * 제목 유효성 검증
- */
- private void validateTitle(String title) {
- if (title == null || title.trim().isEmpty()) {
- throw new IllegalArgumentException("제목은 필수 입력 사항입니다.");
- }
- if (title.trim().length() > 200) {
- throw new IllegalArgumentException("제목은 200자를 초과할 수 없습니다.");
- }
- }
-
- /**
- * 내용 유효성 검증
- */
- private void validateContent(String content) {
- if (content == null || content.trim().isEmpty()) {
- throw new IllegalArgumentException("콘텐츠 내용은 필수 입력 사항입니다.");
- }
- }
-
- /**
- * 홍보 기간 유효성 검증
- */
- private void validatePromotionPeriod(LocalDateTime startDate, LocalDateTime endDate) {
- if (startDate == null || endDate == null) {
- throw new IllegalArgumentException("홍보 시작일과 종료일은 필수 입력 사항입니다.");
- }
- if (startDate.isAfter(endDate)) {
- throw new IllegalArgumentException("홍보 시작일은 종료일보다 이전이어야 합니다.");
- }
- if (endDate.isBefore(LocalDateTime.now())) {
- throw new IllegalArgumentException("홍보 종료일은 현재 시간 이후여야 합니다.");
- }
- }
-
- /**
- * 상태 전환 유효성 검증
- */
- private void validateStatusTransition(ContentStatus from, ContentStatus to) {
- if (from == ContentStatus.ARCHIVED && to != ContentStatus.PUBLISHED) {
- throw new IllegalStateException("보관된 콘텐츠는 발행 상태로만 변경할 수 있습니다.");
- }
- }
-
- /**
- * 발행을 위한 유효성 검증
- */
- private void validateForPublication() {
- validateTitle(this.title);
- validateContent(this.content);
-
- if (this.promotionStartDate == null || this.promotionEndDate == null) {
- throw new IllegalStateException("발행하려면 홍보 기간을 설정해야 합니다.");
- }
-
- if (this.contentType == ContentType.POSTER && !hasImages()) {
- throw new IllegalStateException("포스터 콘텐츠는 이미지가 필수입니다.");
- }
- }
-
- // ==================== 비즈니스 계산 메서드 ====================
-
- /**
- * 홍보 진행률 계산 (0-100%)
- *
- * @return 진행률
- */
- public double calculateProgress() {
+ public boolean isPromotionActive() {
if (promotionStartDate == null || promotionEndDate == null) {
- return 0.0;
+ return false;
}
-
LocalDateTime now = LocalDateTime.now();
-
- if (now.isBefore(promotionStartDate)) {
- return 0.0;
- } else if (now.isAfter(promotionEndDate)) {
- return 100.0;
- }
-
- long totalDuration = java.time.Duration.between(promotionStartDate, promotionEndDate).toHours();
- long elapsedDuration = java.time.Duration.between(promotionStartDate, now).toHours();
-
- if (totalDuration == 0) {
- return 100.0;
- }
-
- return (double) elapsedDuration / totalDuration * 100.0;
+ return !now.isBefore(promotionStartDate) && !now.isAfter(promotionEndDate);
}
/**
- * 남은 홍보 일수 계산
- *
- * @return 남은 일수 (음수면 0)
+ * 콘텐츠 게시 가능 여부 확인
+ * @return 필수 정보가 모두 입력되어 있으면 true
*/
- public long calculateRemainingDays() {
- if (promotionEndDate == null) {
- return 0L;
- }
-
- LocalDateTime now = LocalDateTime.now();
- if (now.isAfter(promotionEndDate)) {
- return 0L;
- }
-
- return java.time.Duration.between(now, promotionEndDate).toDays();
+ public boolean canBePublished() {
+ return title != null && !title.trim().isEmpty()
+ && contentType != null
+ && platform != null
+ && storeId != null;
}
-
- // ==================== 팩토리 메서드 ====================
-
- /**
- * SNS 콘텐츠 생성 팩토리 메서드
- */
- public static Content createSnsContent(String title, String content, Platform platform,
- Long storeId, CreationConditions conditions) {
- Content snsContent = Content.builder()
- .contentType(ContentType.SNS_POST)
- .platform(platform)
- .title(title)
- .content(content)
- .storeId(storeId)
- .creationConditions(conditions)
- .status(ContentStatus.DRAFT)
- .hashtags(new ArrayList<>())
- .images(new ArrayList<>())
- .build();
-
- // 유효성 검증
- snsContent.validateTitle(title);
- snsContent.validateContent(content);
-
- return snsContent;
- }
-
- /**
- * 포스터 콘텐츠 생성 팩토리 메서드
- */
- public static Content createPosterContent(String title, String content, List images,
- Long storeId, CreationConditions conditions) {
- if (images == null || images.isEmpty()) {
- throw new IllegalArgumentException("포스터 콘텐츠는 이미지가 필수입니다.");
- }
-
- Content posterContent = Content.builder()
- .contentType(ContentType.POSTER)
- .platform(Platform.INSTAGRAM) // 기본값
- .title(title)
- .content(content)
- .storeId(storeId)
- .creationConditions(conditions)
- .status(ContentStatus.DRAFT)
- .hashtags(new ArrayList<>())
- .images(new ArrayList<>(images))
- .build();
-
- // 유효성 검증
- posterContent.validateTitle(title);
- posterContent.validateContent(content);
-
- return posterContent;
- }
-
- // ==================== Object 메서드 오버라이드 ====================
-
- /**
- * 비즈니스 키 기반 동등성 비교
- * JPA 엔티티에서는 ID가 아닌 비즈니스 키 사용 권장
- */
- @Override
- public boolean equals(Object obj) {
- if (this == obj) return true;
- if (obj == null || getClass() != obj.getClass()) return false;
-
- Content content = (Content) obj;
- return id != null && id.equals(content.id);
- }
-
- @Override
- public int hashCode() {
- return getClass().hashCode();
- }
-
- /**
- * 디버깅용 toString (민감한 정보 제외)
- */
- @Override
- public String toString() {
- return "Content{" +
- "id=" + id +
- ", contentType=" + contentType +
- ", platform=" + platform +
- ", title='" + title + '\'' +
- ", status=" + status +
- ", storeId=" + storeId +
- ", promotionStartDate=" + promotionStartDate +
- ", promotionEndDate=" + promotionEndDate +
- ", createdAt=" + createdAt +
- '}';
- }
-}
-
-/*
-==================== 데이터베이스 스키마 (참고용) ====================
-
-CREATE TABLE contents (
- content_id BIGINT NOT NULL AUTO_INCREMENT,
- content_type VARCHAR(20) NOT NULL,
- platform VARCHAR(20) NOT NULL,
- title VARCHAR(200) NOT NULL,
- content TEXT NOT NULL,
- status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
- tone_and_manner VARCHAR(50),
- promotion_type VARCHAR(50),
- emotion_intensity VARCHAR(50),
- target_audience VARCHAR(50),
- event_name VARCHAR(100),
- store_id BIGINT NOT NULL,
- promotion_start_date DATETIME,
- promotion_end_date DATETIME,
- created_at DATETIME NOT NULL,
- updated_at DATETIME,
- PRIMARY KEY (content_id),
- INDEX idx_store_id (store_id),
- INDEX idx_content_type (content_type),
- INDEX idx_platform (platform),
- INDEX idx_status (status),
- INDEX idx_promotion_dates (promotion_start_date, promotion_end_date),
- INDEX idx_created_at (created_at)
-);
-
-CREATE TABLE content_hashtags (
- content_id BIGINT NOT NULL,
- hashtag VARCHAR(100) NOT NULL,
- INDEX idx_content_hashtags (content_id),
- FOREIGN KEY (content_id) REFERENCES contents(content_id) ON DELETE CASCADE
-);
-
-CREATE TABLE content_images (
- content_id BIGINT NOT NULL,
- image_url VARCHAR(500) NOT NULL,
- INDEX idx_content_images (content_id),
- FOREIGN KEY (content_id) REFERENCES contents(content_id) ON DELETE CASCADE
-);
-*/
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentId.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentId.java
index 13bb3b0..2f07e2c 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentId.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentId.java
@@ -1,24 +1,24 @@
+// marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentId.java
package com.won.smarketing.content.domain.model;
-import lombok.*;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
/**
- * 콘텐츠 식별자 값 객체
- * 콘텐츠의 고유 식별자를 나타내는 도메인 객체
+ * 콘텐츠 ID 값 객체
+ * Clean Architecture의 Domain Layer에서 식별자를 타입 안전하게 관리
*/
@Getter
-@Setter
-@NoArgsConstructor(access = AccessLevel.PROTECTED)
-@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@RequiredArgsConstructor
@EqualsAndHashCode
public class ContentId {
- private Long value;
+ private final Long value;
/**
- * ContentId 생성 팩토리 메서드
- *
- * @param value 식별자 값
+ * Long 값으로부터 ContentId 생성
+ * @param value ID 값
* @return ContentId 인스턴스
*/
public static ContentId of(Long value) {
@@ -27,4 +27,25 @@ public class ContentId {
}
return new ContentId(value);
}
-}
+
+ /**
+ * 새로운 ContentId 생성 (ID가 없는 경우)
+ * @return null 값을 가진 ContentId
+ */
+ public static ContentId newId() {
+ return new ContentId(null);
+ }
+
+ /**
+ * ID 값 존재 여부 확인
+ * @return ID가 null이 아니면 true
+ */
+ public boolean hasValue() {
+ return value != null;
+ }
+
+ @Override
+ public String toString() {
+ return "ContentId{" + "value=" + value + '}';
+ }
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentStatus.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentStatus.java
index c40ec47..b235310 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentStatus.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentStatus.java
@@ -1,3 +1,4 @@
+// marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentStatus.java
package com.won.smarketing.content.domain.model;
import lombok.Getter;
@@ -5,35 +6,35 @@ import lombok.RequiredArgsConstructor;
/**
* 콘텐츠 상태 열거형
- * 콘텐츠의 생명주기 상태 정의
+ * Clean Architecture의 Domain Layer에 위치하는 비즈니스 규칙
*/
@Getter
@RequiredArgsConstructor
public enum ContentStatus {
-
+
DRAFT("임시저장"),
- PUBLISHED("발행됨"),
- ARCHIVED("보관됨");
+ PUBLISHED("게시됨"),
+ SCHEDULED("예약됨"),
+ DELETED("삭제됨"),
+ PROCESSING("처리중");
private final String displayName;
/**
* 문자열로부터 ContentStatus 변환
- *
- * @param status 상태 문자열
- * @return ContentStatus
+ * @param value 문자열 값
+ * @return ContentStatus enum
+ * @throws IllegalArgumentException 유효하지 않은 값인 경우
*/
- public static ContentStatus fromString(String status) {
- if (status == null) {
- return DRAFT;
+ public static ContentStatus fromString(String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("ContentStatus 값은 null일 수 없습니다.");
}
-
- for (ContentStatus s : ContentStatus.values()) {
- if (s.name().equalsIgnoreCase(status)) {
- return s;
- }
+
+ try {
+ return ContentStatus.valueOf(value.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("유효하지 않은 ContentStatus 값입니다: " + value);
}
-
- throw new IllegalArgumentException("알 수 없는 콘텐츠 상태: " + status);
}
-}
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentType.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentType.java
index dd91b91..f70228b 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentType.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentType.java
@@ -1,3 +1,4 @@
+// marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentType.java
package com.won.smarketing.content.domain.model;
import lombok.Getter;
@@ -5,34 +6,34 @@ import lombok.RequiredArgsConstructor;
/**
* 콘텐츠 타입 열거형
- * 지원되는 마케팅 콘텐츠 유형 정의
+ * Clean Architecture의 Domain Layer에 위치하는 비즈니스 규칙
*/
@Getter
@RequiredArgsConstructor
public enum ContentType {
-
- SNS_POST("SNS 게시물"),
- POSTER("홍보 포스터");
+
+ SNS("SNS 게시물"),
+ POSTER("홍보 포스터"),
+ VIDEO("동영상"),
+ BLOG("블로그 포스트");
private final String displayName;
/**
* 문자열로부터 ContentType 변환
- *
- * @param type 타입 문자열
- * @return ContentType
+ * @param value 문자열 값
+ * @return ContentType enum
+ * @throws IllegalArgumentException 유효하지 않은 값인 경우
*/
- public static ContentType fromString(String type) {
- if (type == null) {
- return null;
+ public static ContentType fromString(String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("ContentType 값은 null일 수 없습니다.");
}
-
- for (ContentType contentType : ContentType.values()) {
- if (contentType.name().equalsIgnoreCase(type)) {
- return contentType;
- }
+
+ try {
+ return ContentType.valueOf(value.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("유효하지 않은 ContentType 값입니다: " + value);
}
-
- throw new IllegalArgumentException("알 수 없는 콘텐츠 타입: " + type);
}
-}
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java
index cf3c04e..d7a9543 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java
@@ -1,66 +1,58 @@
+// marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java
package com.won.smarketing.content.domain.model;
-import lombok.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
import java.time.LocalDate;
/**
* 콘텐츠 생성 조건 도메인 모델
- * AI 콘텐츠 생성 시 사용되는 조건 정보
+ * Clean Architecture의 Domain Layer에 위치하는 값 객체
+ *
+ * JPA 애노테이션을 제거하여 순수 도메인 모델로 유지
+ * Infrastructure Layer의 JPA 엔티티는 별도로 관리
*/
@Getter
-@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@NoArgsConstructor
@AllArgsConstructor
-@Builder(toBuilder = true)
+@Builder
public class CreationConditions {
- /**
- * 홍보 대상 카테고리
- */
+ private String id;
private String category;
-
- /**
- * 특별 요구사항
- */
private String requirement;
-
- /**
- * 톤앤매너
- */
private String toneAndManner;
-
- /**
- * 감정 강도
- */
private String emotionIntensity;
-
- /**
- * 이벤트명
- */
private String eventName;
-
- /**
- * 홍보 시작일
- */
private LocalDate startDate;
-
- /**
- * 홍보 종료일
- */
private LocalDate endDate;
-
- /**
- * 사진 스타일 (포스터용)
- */
private String photoStyle;
-
- /**
- * 타겟 고객
- */
- private String targetAudience;
-
- /**
- * 프로모션 타입
- */
private String promotionType;
-}
+
+ public CreationConditions(String category, String requirement, String toneAndManner, String emotionIntensity, String eventName, LocalDate startDate, LocalDate endDate, String photoStyle, String promotionType) {
+ }
+
+ /**
+ * 이벤트 기간 유효성 검증
+ * @return 시작일이 종료일보다 이전이거나 같으면 true
+ */
+ public boolean isValidEventPeriod() {
+ if (startDate == null || endDate == null) {
+ return true;
+ }
+ return !startDate.isAfter(endDate);
+ }
+
+ /**
+ * 이벤트 조건 유무 확인
+ * @return 이벤트명이나 날짜가 설정되어 있으면 true
+ */
+ public boolean hasEventInfo() {
+ return eventName != null && !eventName.trim().isEmpty()
+ || startDate != null
+ || endDate != null;
+ }
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java
index acd6b33..66e266c 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java
@@ -1,3 +1,4 @@
+// marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java
package com.won.smarketing.content.domain.model;
import lombok.Getter;
@@ -5,35 +6,36 @@ import lombok.RequiredArgsConstructor;
/**
* 플랫폼 열거형
- * 콘텐츠가 게시될 플랫폼 정의
+ * Clean Architecture의 Domain Layer에 위치하는 비즈니스 규칙
*/
@Getter
@RequiredArgsConstructor
public enum Platform {
-
+
INSTAGRAM("인스타그램"),
NAVER_BLOG("네이버 블로그"),
- GENERAL("범용");
+ FACEBOOK("페이스북"),
+ KAKAO_STORY("카카오스토리"),
+ YOUTUBE("유튜브"),
+ GENERAL("일반");
private final String displayName;
/**
* 문자열로부터 Platform 변환
- *
- * @param platform 플랫폼 문자열
- * @return Platform
+ * @param value 문자열 값
+ * @return Platform enum
+ * @throws IllegalArgumentException 유효하지 않은 값인 경우
*/
- public static Platform fromString(String platform) {
- if (platform == null) {
- return GENERAL;
+ public static Platform fromString(String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("Platform 값은 null일 수 없습니다.");
}
-
- for (Platform p : Platform.values()) {
- if (p.name().equalsIgnoreCase(platform)) {
- return p;
- }
+
+ try {
+ return Platform.valueOf(value.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("유효하지 않은 Platform 값입니다: " + value);
}
-
- throw new IllegalArgumentException("알 수 없는 플랫폼: " + platform);
}
-}
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/ContentRepository.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/ContentRepository.java
index 818506f..a2bfc43 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/ContentRepository.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/ContentRepository.java
@@ -1,40 +1,36 @@
+// marketing-content/src/main/java/com/won/smarketing/content/domain/repository/ContentRepository.java
package com.won.smarketing.content.domain.repository;
import com.won.smarketing.content.domain.model.Content;
import com.won.smarketing.content.domain.model.ContentId;
import com.won.smarketing.content.domain.model.ContentType;
import com.won.smarketing.content.domain.model.Platform;
-import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
- * 콘텐츠 저장소 인터페이스
- * 콘텐츠 도메인의 데이터 접근 추상화
+ * 콘텐츠 리포지토리 인터페이스
+ * Clean Architecture의 Domain Layer에서 데이터 접근 정의
*/
-@Repository
public interface ContentRepository {
-
+
/**
* 콘텐츠 저장
- *
* @param content 저장할 콘텐츠
* @return 저장된 콘텐츠
*/
Content save(Content content);
-
+
/**
- * 콘텐츠 ID로 조회
- *
+ * ID로 콘텐츠 조회
* @param id 콘텐츠 ID
- * @return 콘텐츠 (Optional)
+ * @return 조회된 콘텐츠
*/
Optional findById(ContentId id);
-
+
/**
* 필터 조건으로 콘텐츠 목록 조회
- *
* @param contentType 콘텐츠 타입
* @param platform 플랫폼
* @param period 기간
@@ -42,19 +38,17 @@ public interface ContentRepository {
* @return 콘텐츠 목록
*/
List findByFilters(ContentType contentType, Platform platform, String period, String sortBy);
-
+
/**
* 진행 중인 콘텐츠 목록 조회
- *
* @param period 기간
* @return 진행 중인 콘텐츠 목록
*/
List findOngoingContents(String period);
-
+
/**
- * 콘텐츠 삭제
- *
+ * ID로 콘텐츠 삭제
* @param id 삭제할 콘텐츠 ID
*/
void deleteById(ContentId id);
-}
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/SpringDataContentRepository.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/SpringDataContentRepository.java
new file mode 100644
index 0000000..d3a6e42
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/SpringDataContentRepository.java
@@ -0,0 +1,38 @@
+package com.won.smarketing.content.domain.repository;
+import com.won.smarketing.content.infrastructure.entity.ContentEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * Spring Data JPA ContentRepository
+ * JPA 기반 콘텐츠 데이터 접근
+ */
+@Repository
+public interface SpringDataContentRepository extends JpaRepository {
+
+ /**
+ * 매장별 콘텐츠 조회
+ *
+ * @param storeId 매장 ID
+ * @return 콘텐츠 목록
+ */
+ List findByStoreId(Long storeId);
+
+ /**
+ * 콘텐츠 타입별 조회
+ *
+ * @param contentType 콘텐츠 타입
+ * @return 콘텐츠 목록
+ */
+ List findByContentType(String contentType);
+
+ /**
+ * 플랫폼별 조회
+ *
+ * @param platform 플랫폼
+ * @return 콘텐츠 목록
+ */
+ List findByPlatform(String platform);
+}
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentConditionsJpaEntity.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentConditionsJpaEntity.java
index 17f49f8..b549b05 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentConditionsJpaEntity.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentConditionsJpaEntity.java
@@ -1,6 +1,8 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentConditionsJpaEntity.java
package com.won.smarketing.content.infrastructure.entity;
import jakarta.persistence.*;
+import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@@ -8,24 +10,21 @@ import lombok.Setter;
import java.time.LocalDate;
/**
- * 콘텐츠 조건 JPA 엔티티
- *
- * @author smarketing-team
- * @version 1.0
+ * 콘텐츠 생성 조건 JPA 엔티티
+ * Infrastructure Layer에서 데이터베이스 매핑을 담당
*/
@Entity
-@Table(name = "contents_conditions")
+@Table(name = "content_conditions")
@Getter
@Setter
-@NoArgsConstructor
public class ContentConditionsJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- @OneToOne
- @JoinColumn(name = "content_id")
+ @OneToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "content_id", nullable = false)
private ContentJpaEntity content;
@Column(name = "category", length = 100)
@@ -37,7 +36,7 @@ public class ContentConditionsJpaEntity {
@Column(name = "tone_and_manner", length = 100)
private String toneAndManner;
- @Column(name = "emotion_intensity", length = 100)
+ @Column(name = "emotion_intensity", length = 50)
private String emotionIntensity;
@Column(name = "event_name", length = 200)
@@ -52,9 +51,34 @@ public class ContentConditionsJpaEntity {
@Column(name = "photo_style", length = 100)
private String photoStyle;
- @Column(name = "TargetAudience", length = 100)
- private String targetAudience;
+ @Column(name = "promotion_type", length = 100)
+ private String promotionType;
- @Column(name = "PromotionType", length = 100)
- private String PromotionType;
-}
+ // 생성자
+ public ContentConditionsJpaEntity(ContentJpaEntity content, String category, String requirement,
+ String toneAndManner, String emotionIntensity, String eventName,
+ LocalDate startDate, LocalDate endDate, String photoStyle, String promotionType) {
+ this.content = content;
+ this.category = category;
+ this.requirement = requirement;
+ this.toneAndManner = toneAndManner;
+ this.emotionIntensity = emotionIntensity;
+ this.eventName = eventName;
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.photoStyle = photoStyle;
+ this.promotionType = promotionType;
+ }
+
+ public ContentConditionsJpaEntity() {
+
+ }
+
+ // 팩토리 메서드
+ public static ContentConditionsJpaEntity create(ContentJpaEntity content, String category, String requirement,
+ String toneAndManner, String emotionIntensity, String eventName,
+ LocalDate startDate, LocalDate endDate, String photoStyle, String promotionType) {
+ return new ContentConditionsJpaEntity(content, category, requirement, toneAndManner, emotionIntensity,
+ eventName, startDate, endDate, photoStyle, promotionType);
+ }
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentEntity.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentEntity.java
new file mode 100644
index 0000000..ba941d4
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentEntity.java
@@ -0,0 +1,60 @@
+package com.won.smarketing.content.infrastructure.entity;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+
+/**
+ * 콘텐츠 엔티티
+ * 콘텐츠 정보를 데이터베이스에 저장하기 위한 JPA 엔티티
+ */
+@Entity
+@Table(name = "contents")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@EntityListeners(AuditingEntityListener.class)
+public class ContentEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "content_type", nullable = false)
+ private String contentType;
+
+ @Column(name = "platform", nullable = false)
+ private String platform;
+
+ @Column(name = "title", nullable = false)
+ private String title;
+
+ @Column(name = "content", columnDefinition = "TEXT")
+ private String content;
+
+ @Column(name = "hashtags")
+ private String hashtags;
+
+ @Column(name = "images", columnDefinition = "TEXT")
+ private String images;
+
+ @Column(name = "status", nullable = false)
+ private String status;
+
+ @Column(name = "store_id", nullable = false)
+ private Long storeId;
+
+ @CreatedDate
+ @Column(name = "created_at", updatable = false)
+ private LocalDateTime createdAt;
+
+ @LastModifiedDate
+ @Column(name = "updated_at")
+ private LocalDateTime updatedAt;
+}
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentJpaEntity.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentJpaEntity.java
index 7f87560..bcc8499 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentJpaEntity.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentJpaEntity.java
@@ -1,27 +1,25 @@
-// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentJpaEntity.java
package com.won.smarketing.content.infrastructure.entity;
import jakarta.persistence.*;
+import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
-import org.hibernate.annotations.CreationTimestamp;
-import org.hibernate.annotations.UpdateTimestamp;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
-import java.util.List;
+import java.util.Date;
/**
* 콘텐츠 JPA 엔티티
- *
- * @author smarketing-team
- * @version 1.0
*/
@Entity
@Table(name = "contents")
@Getter
@Setter
-@NoArgsConstructor
+@EntityListeners(AuditingEntityListener.class)
public class ContentJpaEntity {
@Id
@@ -40,27 +38,33 @@ public class ContentJpaEntity {
@Column(name = "title", length = 500)
private String title;
+ @Column(name = "PromotionStartDate")
+ private LocalDateTime PromotionStartDate;
+
+ @Column(name = "PromotionEndDate")
+ private LocalDateTime PromotionEndDate;
+
@Column(name = "content", columnDefinition = "TEXT")
private String content;
- @Column(name = "hashtags", columnDefinition = "JSON")
+ @Column(name = "hashtags", columnDefinition = "TEXT")
private String hashtags;
- @Column(name = "images", columnDefinition = "JSON")
+ @Column(name = "images", columnDefinition = "TEXT")
private String images;
- @Column(name = "status", length = 50)
+ @Column(name = "status", nullable = false, length = 20)
private String status;
- @CreationTimestamp
- @Column(name = "created_at")
+ @CreatedDate
+ @Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
- @UpdateTimestamp
+ @LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
- // 연관 엔티티
+ // CreationConditions와의 관계 - OneToOne으로 별도 엔티티로 관리
@OneToOne(mappedBy = "content", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private ContentConditionsJpaEntity conditions;
}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiContentGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiContentGenerator.java
new file mode 100644
index 0000000..b1d0e6d
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiContentGenerator.java
@@ -0,0 +1,32 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiContentGenerator.java
+package com.won.smarketing.content.infrastructure.external;
+
+import com.won.smarketing.content.domain.model.Platform;
+import com.won.smarketing.content.domain.model.CreationConditions;
+
+import java.util.List;
+
+/**
+ * AI 콘텐츠 생성 인터페이스
+ * Clean Architecture의 Infrastructure Layer에서 외부 AI 서비스와의 연동 정의
+ */
+public interface AiContentGenerator {
+
+ /**
+ * SNS 콘텐츠 생성
+ * @param title 제목
+ * @param category 카테고리
+ * @param platform 플랫폼
+ * @param conditions 생성 조건
+ * @return 생성된 콘텐츠 텍스트
+ */
+ String generateSnsContent(String title, String category, Platform platform, CreationConditions conditions);
+
+ /**
+ * 해시태그 생성
+ * @param content 콘텐츠 내용
+ * @param platform 플랫폼
+ * @return 생성된 해시태그 목록
+ */
+ List generateHashtags(String content, Platform platform);
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiPosterGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiPosterGenerator.java
new file mode 100644
index 0000000..8bbe931
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiPosterGenerator.java
@@ -0,0 +1,29 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiPosterGenerator.java
+package com.won.smarketing.content.infrastructure.external;
+
+import com.won.smarketing.content.domain.model.CreationConditions;
+
+import java.util.Map;
+
+/**
+ * AI 포스터 생성 인터페이스
+ * Clean Architecture의 Infrastructure Layer에서 외부 AI 서비스와의 연동 정의
+ */
+public interface AiPosterGenerator {
+
+ /**
+ * 포스터 이미지 생성
+ * @param title 제목
+ * @param category 카테고리
+ * @param conditions 생성 조건
+ * @return 생성된 포스터 이미지 URL
+ */
+ String generatePoster(String title, String category, CreationConditions conditions);
+
+ /**
+ * 포스터 다양한 사이즈 생성
+ * @param originalImage 원본 이미지 URL
+ * @return 사이즈별 이미지 URL 맵
+ */
+ Map generatePosterSizes(String originalImage);
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java
new file mode 100644
index 0000000..9d72f1f
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java
@@ -0,0 +1,95 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java
+package com.won.smarketing.content.infrastructure.external;
+
+// 수정: domain 패키지의 인터페이스를 import
+import com.won.smarketing.content.domain.service.AiContentGenerator;
+import com.won.smarketing.content.domain.model.Platform;
+import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Claude AI를 활용한 콘텐츠 생성 구현체
+ * Clean Architecture의 Infrastructure Layer에 위치
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class ClaudeAiContentGenerator implements AiContentGenerator {
+
+ /**
+ * SNS 콘텐츠 생성
+ */
+ @Override
+ public String generateSnsContent(SnsContentCreateRequest request) {
+ try {
+ String prompt = buildContentPrompt(request);
+ return generateDummySnsContent(request.getTitle(), Platform.fromString(request.getPlatform()));
+ } catch (Exception e) {
+ log.error("AI 콘텐츠 생성 실패: {}", e.getMessage(), e);
+ return generateFallbackContent(request.getTitle(), Platform.fromString(request.getPlatform()));
+ }
+ }
+
+ /**
+ * 플랫폼별 해시태그 생성
+ */
+ @Override
+ public List generateHashtags(String content, Platform platform) {
+ try {
+ return generateDummyHashtags(platform);
+ } catch (Exception e) {
+ log.error("해시태그 생성 실패: {}", e.getMessage(), e);
+ return generateFallbackHashtags();
+ }
+ }
+
+ private String buildContentPrompt(SnsContentCreateRequest request) {
+ StringBuilder prompt = new StringBuilder();
+ prompt.append("제목: ").append(request.getTitle()).append("\n");
+ prompt.append("카테고리: ").append(request.getCategory()).append("\n");
+ prompt.append("플랫폼: ").append(request.getPlatform()).append("\n");
+
+ if (request.getRequirement() != null) {
+ prompt.append("요구사항: ").append(request.getRequirement()).append("\n");
+ }
+
+ if (request.getToneAndManner() != null) {
+ prompt.append("톤앤매너: ").append(request.getToneAndManner()).append("\n");
+ }
+
+ return prompt.toString();
+ }
+
+ private String generateDummySnsContent(String title, Platform platform) {
+ String baseContent = "🌟 " + title + "를 소개합니다! 🌟\n\n" +
+ "저희 매장에서 특별한 경험을 만나보세요.\n" +
+ "고객 여러분의 소중한 시간을 더욱 특별하게 만들어드리겠습니다.\n\n";
+
+ if (platform == Platform.INSTAGRAM) {
+ return baseContent + "더 많은 정보는 프로필 링크에서 확인하세요! 📸";
+ } else {
+ return baseContent + "자세한 내용은 저희 블로그를 방문해 주세요! ✨";
+ }
+ }
+
+ private String generateFallbackContent(String title, Platform platform) {
+ return title + "에 대한 멋진 콘텐츠입니다. 많은 관심 부탁드립니다!";
+ }
+
+ private List generateDummyHashtags(Platform platform) {
+ if (platform == Platform.INSTAGRAM) {
+ return Arrays.asList("#맛집", "#데일리", "#소상공인", "#추천", "#인스타그램");
+ } else {
+ return Arrays.asList("#맛집추천", "#블로그", "#리뷰", "#맛있는곳", "#소상공인응원");
+ }
+ }
+
+ private List generateFallbackHashtags() {
+ return Arrays.asList("#소상공인", "#마케팅", "#홍보");
+ }
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiPosterGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiPosterGenerator.java
new file mode 100644
index 0000000..7495966
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiPosterGenerator.java
@@ -0,0 +1,86 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiPosterGenerator.java
+package com.won.smarketing.content.infrastructure.external;
+
+import com.won.smarketing.content.domain.service.AiPosterGenerator; // 도메인 인터페이스 import
+import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Claude AI를 활용한 포스터 생성 구현체
+ * Clean Architecture의 Infrastructure Layer에 위치
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class ClaudeAiPosterGenerator implements AiPosterGenerator {
+
+ /**
+ * 포스터 생성
+ *
+ * @param request 포스터 생성 요청
+ * @return 생성된 포스터 이미지 URL
+ */
+ @Override
+ public String generatePoster(PosterContentCreateRequest request) {
+ try {
+ // Claude AI API 호출 로직
+ String prompt = buildPosterPrompt(request);
+
+ // TODO: 실제 Claude AI API 호출
+ // 현재는 더미 데이터 반환
+ return generateDummyPosterUrl(request.getTitle());
+
+ } catch (Exception e) {
+ log.error("AI 포스터 생성 실패: {}", e.getMessage(), e);
+ return generateFallbackPosterUrl();
+ }
+ }
+
+ /**
+ * 다양한 사이즈의 포스터 생성
+ *
+ * @param baseImage 기본 이미지
+ * @return 사이즈별 포스터 URL 맵
+ */
+ @Override
+ public Map generatePosterSizes(String baseImage) {
+ Map sizes = new HashMap<>();
+
+ // 다양한 사이즈 생성 (더미 구현)
+ sizes.put("instagram_square", baseImage + "_1080x1080.jpg");
+ sizes.put("instagram_story", baseImage + "_1080x1920.jpg");
+ sizes.put("facebook_post", baseImage + "_1200x630.jpg");
+ sizes.put("a4_poster", baseImage + "_2480x3508.jpg");
+
+ return sizes;
+ }
+
+ private String buildPosterPrompt(PosterContentCreateRequest request) {
+ StringBuilder prompt = new StringBuilder();
+ prompt.append("포스터 제목: ").append(request.getTitle()).append("\n");
+ prompt.append("카테고리: ").append(request.getCategory()).append("\n");
+
+ if (request.getRequirement() != null) {
+ prompt.append("요구사항: ").append(request.getRequirement()).append("\n");
+ }
+
+ if (request.getToneAndManner() != null) {
+ prompt.append("톤앤매너: ").append(request.getToneAndManner()).append("\n");
+ }
+
+ return prompt.toString();
+ }
+
+ private String generateDummyPosterUrl(String title) {
+ return "https://dummy-ai-service.com/posters/" + title.hashCode() + ".jpg";
+ }
+
+ private String generateFallbackPosterUrl() {
+ return "https://dummy-ai-service.com/posters/fallback.jpg";
+ }
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/mapper/ContentMapper.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/mapper/ContentMapper.java
index 49cc6b4..44fdb68 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/mapper/ContentMapper.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/mapper/ContentMapper.java
@@ -1,3 +1,4 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/mapper/ContentMapper.java
package com.won.smarketing.content.infrastructure.mapper;
import com.won.smarketing.content.domain.model.*;
@@ -14,6 +15,7 @@ import java.util.List;
/**
* 콘텐츠 도메인-엔티티 매퍼
+ * Clean Architecture에서 Infrastructure Layer와 Domain Layer 간 변환 담당
*
* @author smarketing-team
* @version 1.0
@@ -26,7 +28,7 @@ public class ContentMapper {
private final ObjectMapper objectMapper;
/**
- * 도메인 모델을 JPA 엔티티로 변환합니다.
+ * 도메인 모델을 JPA 엔티티로 변환
*
* @param content 도메인 콘텐츠
* @return JPA 엔티티
@@ -37,32 +39,30 @@ public class ContentMapper {
}
ContentJpaEntity entity = new ContentJpaEntity();
+
+ // 기본 필드 매핑
if (content.getId() != null) {
entity.setId(content.getId());
}
entity.setStoreId(content.getStoreId());
- entity.setContentType(content.getContentType().name());
+ entity.setContentType(content.getContentType() != null ? content.getContentType().name() : null);
entity.setPlatform(content.getPlatform() != null ? content.getPlatform().name() : null);
entity.setTitle(content.getTitle());
entity.setContent(content.getContent());
- entity.setHashtags(convertListToJson(content.getHashtags()));
- entity.setImages(convertListToJson(content.getImages()));
- entity.setStatus(content.getStatus().name());
+ entity.setStatus(content.getStatus() != null ? content.getStatus().name() : "DRAFT");
+ entity.setPromotionStartDate(content.getPromotionStartDate());
+ entity.setPromotionEndDate(content.getPromotionEndDate());
entity.setCreatedAt(content.getCreatedAt());
entity.setUpdatedAt(content.getUpdatedAt());
- // 조건 정보 매핑
+ // 컬렉션 필드를 JSON으로 변환
+ entity.setHashtags(convertListToJson(content.getHashtags()));
+ entity.setImages(convertListToJson(content.getImages()));
+
+ // 생성 조건 정보 매핑
if (content.getCreationConditions() != null) {
- ContentConditionsJpaEntity conditionsEntity = new ContentConditionsJpaEntity();
+ ContentConditionsJpaEntity conditionsEntity = mapToConditionsEntity(content.getCreationConditions());
conditionsEntity.setContent(entity);
- conditionsEntity.setCategory(content.getCreationConditions().getCategory());
- conditionsEntity.setRequirement(content.getCreationConditions().getRequirement());
- conditionsEntity.setToneAndManner(content.getCreationConditions().getToneAndManner());
- conditionsEntity.setEmotionIntensity(content.getCreationConditions().getEmotionIntensity());
- conditionsEntity.setEventName(content.getCreationConditions().getEventName());
- conditionsEntity.setStartDate(content.getCreationConditions().getStartDate());
- conditionsEntity.setEndDate(content.getCreationConditions().getEndDate());
- conditionsEntity.setPhotoStyle(content.getCreationConditions().getPhotoStyle());
entity.setConditions(conditionsEntity);
}
@@ -70,50 +70,74 @@ public class ContentMapper {
}
/**
- * JPA 엔티티를 도메인 모델로 변환합니다.
+ * JPA 엔티티를 도메인 모델로 변환
*
* @param entity JPA 엔티티
- * @return 도메인 콘텐츠
+ * @return 도메인 모델
*/
public Content toDomain(ContentJpaEntity entity) {
if (entity == null) {
return null;
}
- CreationConditions conditions = null;
- if (entity.getConditions() != null) {
- conditions = new CreationConditions(
- entity.getConditions().getCategory(),
- entity.getConditions().getRequirement(),
- entity.getConditions().getToneAndManner(),
- entity.getConditions().getEmotionIntensity(),
- entity.getConditions().getEventName(),
- entity.getConditions().getStartDate(),
- entity.getConditions().getEndDate(),
- entity.getConditions().getPhotoStyle(),
- entity.getConditions().getTargetAudience(),
- entity.getConditions().getPromotionType()
- );
- }
-
- return new Content(
- ContentId.of(entity.getId()),
- ContentType.valueOf(entity.getContentType()),
- entity.getPlatform() != null ? Platform.valueOf(entity.getPlatform()) : null,
- entity.getTitle(),
- entity.getContent(),
- convertJsonToList(entity.getHashtags()),
- convertJsonToList(entity.getImages()),
- ContentStatus.valueOf(entity.getStatus()),
- conditions,
- entity.getStoreId(),
- entity.getCreatedAt(),
- entity.getUpdatedAt()
- );
+ return Content.builder()
+ .id(entity.getId())
+ .storeId(entity.getStoreId())
+ .contentType(parseContentType(entity.getContentType()))
+ .platform(parsePlatform(entity.getPlatform()))
+ .title(entity.getTitle())
+ .content(entity.getContent())
+ .hashtags(convertJsonToList(entity.getHashtags()))
+ .images(convertJsonToList(entity.getImages()))
+ .status(parseContentStatus(entity.getStatus()))
+ .promotionStartDate(entity.getPromotionStartDate())
+ .promotionEndDate(entity.getPromotionEndDate())
+ .creationConditions(mapToConditionsDomain(entity.getConditions()))
+ .createdAt(entity.getCreatedAt())
+ .updatedAt(entity.getUpdatedAt())
+ .build();
}
/**
- * List를 JSON 문자열로 변환합니다.
+ * CreationConditions 도메인을 JPA 엔티티로 변환
+ */
+ private ContentConditionsJpaEntity mapToConditionsEntity(CreationConditions conditions) {
+ ContentConditionsJpaEntity entity = new ContentConditionsJpaEntity();
+ entity.setCategory(conditions.getCategory());
+ entity.setRequirement(conditions.getRequirement());
+ entity.setToneAndManner(conditions.getToneAndManner());
+ entity.setEmotionIntensity(conditions.getEmotionIntensity());
+ entity.setEventName(conditions.getEventName());
+ entity.setStartDate(conditions.getStartDate());
+ entity.setEndDate(conditions.getEndDate());
+ entity.setPhotoStyle(conditions.getPhotoStyle());
+ entity.setPromotionType(conditions.getPromotionType());
+ return entity;
+ }
+
+ /**
+ * CreationConditions JPA 엔티티를 도메인으로 변환
+ */
+ private CreationConditions mapToConditionsDomain(ContentConditionsJpaEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+
+ return CreationConditions.builder()
+ .category(entity.getCategory())
+ .requirement(entity.getRequirement())
+ .toneAndManner(entity.getToneAndManner())
+ .emotionIntensity(entity.getEmotionIntensity())
+ .eventName(entity.getEventName())
+ .startDate(entity.getStartDate())
+ .endDate(entity.getEndDate())
+ .photoStyle(entity.getPhotoStyle())
+ .promotionType(entity.getPromotionType())
+ .build();
+ }
+
+ /**
+ * List를 JSON 문자열로 변환
*/
private String convertListToJson(List list) {
if (list == null || list.isEmpty()) {
@@ -128,7 +152,7 @@ public class ContentMapper {
}
/**
- * JSON 문자열을 List로 변환합니다.
+ * JSON 문자열을 List로 변환
*/
private List convertJsonToList(String json) {
if (json == null || json.trim().isEmpty()) {
@@ -141,4 +165,49 @@ public class ContentMapper {
return Collections.emptyList();
}
}
-}
+
+ /**
+ * 문자열을 ContentType 열거형으로 변환
+ */
+ private ContentType parseContentType(String contentType) {
+ if (contentType == null) {
+ return null;
+ }
+ try {
+ return ContentType.valueOf(contentType);
+ } catch (IllegalArgumentException e) {
+ log.warn("Unknown content type: {}", contentType);
+ return null;
+ }
+ }
+
+ /**
+ * 문자열을 Platform 열거형으로 변환
+ */
+ private Platform parsePlatform(String platform) {
+ if (platform == null) {
+ return null;
+ }
+ try {
+ return Platform.valueOf(platform);
+ } catch (IllegalArgumentException e) {
+ log.warn("Unknown platform: {}", platform);
+ return null;
+ }
+ }
+
+ /**
+ * 문자열을 ContentStatus 열거형으로 변환
+ */
+ private ContentStatus parseContentStatus(String status) {
+ if (status == null) {
+ return ContentStatus.DRAFT;
+ }
+ try {
+ return ContentStatus.valueOf(status);
+ } catch (IllegalArgumentException e) {
+ log.warn("Unknown content status: {}", status);
+ return ContentStatus.DRAFT;
+ }
+ }
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepository.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepository.java
index 9396d4d..f3f38ed 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepository.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepository.java
@@ -1,3 +1,4 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepository.java
package com.won.smarketing.content.infrastructure.repository;
import com.won.smarketing.content.domain.model.Content;
@@ -16,66 +17,69 @@ import java.util.Optional;
import java.util.stream.Collectors;
/**
- * JPA 기반 콘텐츠 Repository 구현체
- *
- * @author smarketing-team
- * @version 1.0
+ * JPA를 활용한 콘텐츠 리포지토리 구현체
+ * Clean Architecture의 Infrastructure Layer에 위치
+ * JPA 엔티티와 도메인 모델 간 변환을 위해 ContentMapper 사용
*/
@Repository
@RequiredArgsConstructor
@Slf4j
public class JpaContentRepository implements ContentRepository {
- private final SpringDataContentRepository springDataContentRepository;
+ private final JpaContentRepositoryInterface jpaRepository;
private final ContentMapper contentMapper;
/**
- * 콘텐츠를 저장합니다.
- *
- * @param content 저장할 콘텐츠
- * @return 저장된 콘텐츠
+ * 콘텐츠 저장
+ * @param content 저장할 도메인 콘텐츠
+ * @return 저장된 도메인 콘텐츠
*/
@Override
public Content save(Content content) {
- log.debug("Saving content: {}", content.getId());
+ log.debug("Saving content: {}", content.getTitle());
+
+ // 도메인 모델을 JPA 엔티티로 변환
ContentJpaEntity entity = contentMapper.toEntity(content);
- ContentJpaEntity savedEntity = springDataContentRepository.save(entity);
- return contentMapper.toDomain(savedEntity);
+
+ // JPA로 저장
+ ContentJpaEntity savedEntity = jpaRepository.save(entity);
+
+ // JPA 엔티티를 도메인 모델로 변환하여 반환
+ Content savedContent = contentMapper.toDomain(savedEntity);
+
+ log.debug("Content saved with ID: {}", savedContent.getId());
+ return savedContent;
}
/**
- * ID로 콘텐츠를 조회합니다.
- *
+ * ID로 콘텐츠 조회
* @param id 콘텐츠 ID
- * @return 조회된 콘텐츠
+ * @return 조회된 도메인 콘텐츠
*/
@Override
public Optional findById(ContentId id) {
- log.debug("Finding content by id: {}", id.getValue());
- return springDataContentRepository.findById(id.getValue())
+ log.debug("Finding content by ID: {}", id.getValue());
+
+ return jpaRepository.findById(id.getValue())
.map(contentMapper::toDomain);
}
/**
- * 필터 조건으로 콘텐츠 목록을 조회합니다.
- *
+ * 필터 조건으로 콘텐츠 목록 조회
* @param contentType 콘텐츠 타입
* @param platform 플랫폼
- * @param period 기간
- * @param sortBy 정렬 기준
- * @return 콘텐츠 목록
+ * @param period 기간 (현재는 사용하지 않음)
+ * @param sortBy 정렬 기준 (현재는 사용하지 않음)
+ * @return 도메인 콘텐츠 목록
*/
@Override
public List findByFilters(ContentType contentType, Platform platform, String period, String sortBy) {
- log.debug("Finding contents by filters - type: {}, platform: {}, period: {}, sortBy: {}",
- contentType, platform, period, sortBy);
+ log.debug("Finding contents with filters - contentType: {}, platform: {}", contentType, platform);
- List entities = springDataContentRepository.findByFilters(
- contentType != null ? contentType.name() : null,
- platform != null ? platform.name() : null,
- period,
- sortBy
- );
+ String contentTypeStr = contentType != null ? contentType.name() : null;
+ String platformStr = platform != null ? platform.name() : null;
+
+ List entities = jpaRepository.findByFilters(contentTypeStr, platformStr, null);
return entities.stream()
.map(contentMapper::toDomain)
@@ -83,15 +87,15 @@ public class JpaContentRepository implements ContentRepository {
}
/**
- * 진행 중인 콘텐츠 목록을 조회합니다.
- *
- * @param period 기간
- * @return 진행 중인 콘텐츠 목록
+ * 진행 중인 콘텐츠 목록 조회
+ * @param period 기간 (현재는 사용하지 않음)
+ * @return 진행 중인 도메인 콘텐츠 목록
*/
@Override
public List findOngoingContents(String period) {
- log.debug("Finding ongoing contents for period: {}", period);
- List entities = springDataContentRepository.findOngoingContents(period);
+ log.debug("Finding ongoing contents");
+
+ List entities = jpaRepository.findOngoingContents();
return entities.stream()
.map(contentMapper::toDomain)
@@ -99,13 +103,45 @@ public class JpaContentRepository implements ContentRepository {
}
/**
- * ID로 콘텐츠를 삭제합니다.
- *
- * @param id 콘텐츠 ID
+ * ID로 콘텐츠 삭제
+ * @param id 삭제할 콘텐츠 ID
*/
@Override
public void deleteById(ContentId id) {
- log.debug("Deleting content by id: {}", id.getValue());
- springDataContentRepository.deleteById(id.getValue());
+ log.debug("Deleting content by ID: {}", id.getValue());
+
+ jpaRepository.deleteById(id.getValue());
+
+ log.debug("Content deleted successfully");
+ }
+
+ /**
+ * 매장 ID로 콘텐츠 목록 조회 (추가 메서드)
+ * @param storeId 매장 ID
+ * @return 도메인 콘텐츠 목록
+ */
+ public List findByStoreId(Long storeId) {
+ log.debug("Finding contents by store ID: {}", storeId);
+
+ List entities = jpaRepository.findByStoreId(storeId);
+
+ return entities.stream()
+ .map(contentMapper::toDomain)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 콘텐츠 타입으로 조회 (추가 메서드)
+ * @param contentType 콘텐츠 타입
+ * @return 도메인 콘텐츠 목록
+ */
+ public List findByContentType(ContentType contentType) {
+ log.debug("Finding contents by type: {}", contentType);
+
+ List entities = jpaRepository.findByContentType(contentType.name());
+
+ return entities.stream()
+ .map(contentMapper::toDomain)
+ .collect(Collectors.toList());
}
}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepositoryInterface.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepositoryInterface.java
new file mode 100644
index 0000000..37c4e74
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepositoryInterface.java
@@ -0,0 +1,87 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepositoryInterface.java
+package com.won.smarketing.content.infrastructure.repository;
+
+import com.won.smarketing.content.infrastructure.entity.ContentJpaEntity;
+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;
+
+/**
+ * Spring Data JPA 콘텐츠 리포지토리 인터페이스
+ * Clean Architecture의 Infrastructure Layer에 위치
+ * JPA 엔티티(ContentJpaEntity)를 사용하여 데이터베이스 접근
+ */
+public interface JpaContentRepositoryInterface extends JpaRepository {
+
+ /**
+ * 매장 ID로 콘텐츠 목록 조회
+ * @param storeId 매장 ID
+ * @return 콘텐츠 엔티티 목록
+ */
+ List findByStoreId(Long storeId);
+
+ /**
+ * 콘텐츠 타입으로 조회
+ * @param contentType 콘텐츠 타입
+ * @return 콘텐츠 엔티티 목록
+ */
+ List findByContentType(String contentType);
+
+ /**
+ * 플랫폼으로 조회
+ * @param platform 플랫폼
+ * @return 콘텐츠 엔티티 목록
+ */
+ List findByPlatform(String platform);
+
+ /**
+ * 상태로 조회
+ * @param status 상태
+ * @return 콘텐츠 엔티티 목록
+ */
+ List findByStatus(String status);
+
+ /**
+ * 필터 조건으로 콘텐츠 목록 조회
+ * @param contentType 콘텐츠 타입 (null 가능)
+ * @param platform 플랫폼 (null 가능)
+ * @param status 상태 (null 가능)
+ * @return 콘텐츠 엔티티 목록
+ */
+ @Query("SELECT c FROM ContentJpaEntity c WHERE " +
+ "(:contentType IS NULL OR c.contentType = :contentType) AND " +
+ "(:platform IS NULL OR c.platform = :platform) AND " +
+ "(:status IS NULL OR c.status = :status) " +
+ "ORDER BY c.createdAt DESC")
+ List findByFilters(@Param("contentType") String contentType,
+ @Param("platform") String platform,
+ @Param("status") String status);
+
+ /**
+ * 진행 중인 콘텐츠 목록 조회 (발행된 상태의 콘텐츠)
+ * @return 진행 중인 콘텐츠 엔티티 목록
+ */
+ @Query("SELECT c FROM ContentJpaEntity c WHERE " +
+ "c.status IN ('PUBLISHED', 'SCHEDULED') " +
+ "ORDER BY c.createdAt DESC")
+ List findOngoingContents();
+
+ /**
+ * 매장 ID와 콘텐츠 타입으로 조회
+ * @param storeId 매장 ID
+ * @param contentType 콘텐츠 타입
+ * @return 콘텐츠 엔티티 목록
+ */
+ List findByStoreIdAndContentType(Long storeId, String contentType);
+
+ /**
+ * 최근 생성된 콘텐츠 조회 (limit 적용)
+ * @param storeId 매장 ID
+ * @return 최근 콘텐츠 엔티티 목록
+ */
+ @Query("SELECT c FROM ContentJpaEntity c WHERE c.storeId = :storeId " +
+ "ORDER BY c.createdAt DESC")
+ List findRecentContentsByStoreId(@Param("storeId") Long storeId);
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/SpringDataContentRepository.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/SpringDataContentRepository.java
deleted file mode 100644
index feba6b4..0000000
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/SpringDataContentRepository.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.won.smarketing.content.infrastructure.repository;
-
-import com.won.smarketing.content.infrastructure.entity.ContentJpaEntity;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
-import org.springframework.stereotype.Repository;
-
-import java.util.List;
-
-/**
- * Spring Data JPA 콘텐츠 Repository
- *
- * @author smarketing-team
- * @version 1.0
- */
-@Repository
-public interface SpringDataContentRepository extends JpaRepository {
-
- /**
- * 필터 조건으로 콘텐츠를 조회합니다.
- *
- * @param contentType 콘텐츠 타입
- * @param platform 플랫폼
- * @param period 기간
- * @param sortBy 정렬 기준
- * @return 콘텐츠 목록
- */
- @Query("SELECT c FROM ContentJpaEntity c WHERE " +
- "(:contentType IS NULL OR c.contentType = :contentType) AND " +
- "(:platform IS NULL OR c.platform = :platform) AND " +
- "(:period IS NULL OR DATE(c.createdAt) >= CURRENT_DATE - INTERVAL :period DAY) " +
- "ORDER BY " +
- "CASE WHEN :sortBy = 'latest' THEN c.createdAt END DESC, " +
- "CASE WHEN :sortBy = 'oldest' THEN c.createdAt END ASC")
- List findByFilters(@Param("contentType") String contentType,
- @Param("platform") String platform,
- @Param("period") String period,
- @Param("sortBy") String sortBy);
-
- /**
- * 진행 중인 콘텐츠를 조회합니다.
- *
- * @param period 기간
- * @return 진행 중인 콘텐츠 목록
- */
- @Query("SELECT c FROM ContentJpaEntity c " +
- "WHERE c.status = 'PUBLISHED' AND " +
- "(:period IS NULL OR DATE(c.createdAt) >= CURRENT_DATE - INTERVAL :period DAY)")
- List findOngoingContents(@Param("period") String period);
-}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/CreationConditionsDto.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/CreationConditionsDto.java
new file mode 100644
index 0000000..403cdfa
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/CreationConditionsDto.java
@@ -0,0 +1,45 @@
+// marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/CreationConditionsDto.java
+package com.won.smarketing.content.presentation.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDate;
+
+/**
+ * 콘텐츠 생성 조건 DTO
+ */
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@Schema(description = "콘텐츠 생성 조건")
+public class CreationConditionsDto {
+
+ @Schema(description = "카테고리", example = "음식")
+ private String category;
+
+ @Schema(description = "생성 요구사항", example = "젊은 고객층을 타겟으로 한 재미있는 콘텐츠")
+ private String requirement;
+
+ @Schema(description = "톤앤매너", example = "친근하고 활발한")
+ private String toneAndManner;
+
+ @Schema(description = "감정 강도", example = "보통")
+ private String emotionIntensity;
+
+ @Schema(description = "이벤트명", example = "신메뉴 출시 이벤트")
+ private String eventName;
+
+ @Schema(description = "시작일")
+ private LocalDate startDate;
+
+ @Schema(description = "종료일")
+ private LocalDate endDate;
+
+ @Schema(description = "사진 스타일", example = "모던하고 깔끔한")
+ private String photoStyle;
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateResponse.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateResponse.java
index ce5ee97..0acf9ec 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateResponse.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateResponse.java
@@ -306,7 +306,7 @@ public class SnsContentCreateResponse {
// 생성 조건 정보 설정
if (content.getCreationConditions() != null) {
builder.generationConditions(GenerationConditionsDto.builder()
- .targetAudience(content.getCreationConditions().getTargetAudience())
+ //.targetAudience(content.getCreationConditions().getTargetAudience())
.eventName(content.getCreationConditions().getEventName())
.toneAndManner(content.getCreationConditions().getToneAndManner())
.promotionType(content.getCreationConditions().getPromotionType())
diff --git a/smarketing-java/marketing-content/src/main/resources/application.yml b/smarketing-java/marketing-content/src/main/resources/application.yml
index 9f7259f..10dc73d 100644
--- a/smarketing-java/marketing-content/src/main/resources/application.yml
+++ b/smarketing-java/marketing-content/src/main/resources/application.yml
@@ -11,27 +11,23 @@ spring:
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
- ddl-auto: ${JPA_DDL_AUTO:update}
- show-sql: ${JPA_SHOW_SQL:true}
+ ddl-auto: ${DDL_AUTO:update}
+ show-sql: ${SHOW_SQL:true}
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
-ai:
- service:
- url: ${AI_SERVICE_URL:http://localhost:8080/ai}
- timeout: ${AI_SERVICE_TIMEOUT:30000}
+ data:
+ redis:
+ host: ${REDIS_HOST:localhost}
+ port: ${REDIS_PORT:6379}
+ password: ${REDIS_PASSWORD:}
-external:
- claude-ai:
- api-key: ${CLAUDE_AI_API_KEY:your-claude-api-key}
- base-url: ${CLAUDE_AI_BASE_URL:https://api.anthropic.com}
- model: ${CLAUDE_AI_MODEL:claude-3-sonnet-20240229}
- max-tokens: ${CLAUDE_AI_MAX_TOKENS:4000}
jwt:
secret: ${JWT_SECRET:mySecretKeyForJWTTokenGenerationAndValidation123456789}
access-token-validity: ${JWT_ACCESS_VALIDITY:3600000}
refresh-token-validity: ${JWT_REFRESH_VALIDITY:604800000}
+
logging:
level:
- com.won.smarketing.content: ${LOG_LEVEL:DEBUG}
+ com.won.smarketing: ${LOG_LEVEL:DEBUG}