mirror of
https://github.com/won-ktds/smarketing-backend.git
synced 2025-12-06 07:06:24 +00:00
Merge branch 'main' of https://github.com/won-ktds/smarketing-backend
This commit is contained in:
commit
16ce90c394
5
.idea/.gitignore
generated
vendored
5
.idea/.gitignore
generated
vendored
@ -1,5 +0,0 @@
|
||||
# 디폴트 무시된 파일
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 환경에 따라 달라지는 Maven 홈 디렉터리
|
||||
/mavenHomeManager.xml
|
||||
11
.idea/gradle.xml
generated
11
.idea/gradle.xml
generated
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="ms-21" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
4
.idea/misc.xml
generated
4
.idea/misc.xml
generated
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
109
.idea/workspace.xml
generated
Normal file
109
.idea/workspace.xml
generated
Normal file
@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="7d9c48b3-e5c8-4a1c-af9a-469e24fa5715" name="변경" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.gitignore" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/gradle.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="ExternalProjectsData">
|
||||
<projectState path="$PROJECT_DIR$">
|
||||
<ProjectState />
|
||||
</projectState>
|
||||
</component>
|
||||
<component name="ExternalProjectsManager">
|
||||
<system id="GRADLE">
|
||||
<state>
|
||||
<task path="$PROJECT_DIR$">
|
||||
<activation />
|
||||
</task>
|
||||
<projects_view />
|
||||
</state>
|
||||
</system>
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"customColor": "",
|
||||
"associatedIndex": 4
|
||||
}</component>
|
||||
<component name="ProjectId" id="2yLeuaqHXgKgtNCa4XzAZzifagS" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"Gradle.member.executor": "Run",
|
||||
"Gradle.소스 다운로드.executor": "Run",
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "main",
|
||||
"last_opened_file_path": "C:/home/workspace/smarketing/smarketing-backend",
|
||||
"project.structure.last.edited": "SDK",
|
||||
"project.structure.proportion": "0.15",
|
||||
"project.structure.side.proportion": "0.2",
|
||||
"settings.editor.selected.configurable": "reference.settingsdialog.project.gradle"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RunDashboard">
|
||||
<option name="configurationTypes">
|
||||
<set>
|
||||
<option value="GradleRunConfiguration" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
<component name="RunManager">
|
||||
<configuration name="member" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="env">
|
||||
<map>
|
||||
<entry key="POSTGRES_HOST" value="psql-digitalgarage-02.postgres.database.azure.com" />
|
||||
<entry key="POSTGRES_PASSWORD" value="DG_Won!" />
|
||||
<entry key="POSTGRES_USER" value="pgadmin" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="passParentEnvs" value="false" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value=":member:bootRun" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="디폴트 작업">
|
||||
<changelist id="7d9c48b3-e5c8-4a1c-af9a-469e24fa5715" name="변경" comment="" />
|
||||
<created>1749618504890</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1749618504890</updated>
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
</project>
|
||||
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -205,6 +205,9 @@ class SnsContentService:
|
||||
"""
|
||||
metadata_html = '<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #e1e8ed; font-size: 12px; color: #666;">'
|
||||
|
||||
if request.menuName:
|
||||
metadata_html += f'<div><strong>메뉴:</strong> {request.menuName}</div>'
|
||||
|
||||
if request.eventName:
|
||||
metadata_html += f'<div><strong>이벤트:</strong> {request.eventName}</div>'
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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<String> 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())
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
@ -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<String> 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<String> 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<String> strings, List<String> 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<String> 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
|
||||
);
|
||||
*/
|
||||
}
|
||||
@ -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 + '}';
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<Content> findById(ContentId id);
|
||||
|
||||
|
||||
/**
|
||||
* 필터 조건으로 콘텐츠 목록 조회
|
||||
*
|
||||
* @param contentType 콘텐츠 타입
|
||||
* @param platform 플랫폼
|
||||
* @param period 기간
|
||||
@ -42,19 +38,17 @@ public interface ContentRepository {
|
||||
* @return 콘텐츠 목록
|
||||
*/
|
||||
List<Content> findByFilters(ContentType contentType, Platform platform, String period, String sortBy);
|
||||
|
||||
|
||||
/**
|
||||
* 진행 중인 콘텐츠 목록 조회
|
||||
*
|
||||
* @param period 기간
|
||||
* @return 진행 중인 콘텐츠 목록
|
||||
*/
|
||||
List<Content> findOngoingContents(String period);
|
||||
|
||||
|
||||
/**
|
||||
* 콘텐츠 삭제
|
||||
*
|
||||
* ID로 콘텐츠 삭제
|
||||
* @param id 삭제할 콘텐츠 ID
|
||||
*/
|
||||
void deleteById(ContentId id);
|
||||
}
|
||||
}
|
||||
@ -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<ContentEntity, Long> {
|
||||
|
||||
/**
|
||||
* 매장별 콘텐츠 조회
|
||||
*
|
||||
* @param storeId 매장 ID
|
||||
* @return 콘텐츠 목록
|
||||
*/
|
||||
List<ContentEntity> findByStoreId(Long storeId);
|
||||
|
||||
/**
|
||||
* 콘텐츠 타입별 조회
|
||||
*
|
||||
* @param contentType 콘텐츠 타입
|
||||
* @return 콘텐츠 목록
|
||||
*/
|
||||
List<ContentEntity> findByContentType(String contentType);
|
||||
|
||||
/**
|
||||
* 플랫폼별 조회
|
||||
*
|
||||
* @param platform 플랫폼
|
||||
* @return 콘텐츠 목록
|
||||
*/
|
||||
List<ContentEntity> findByPlatform(String platform);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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<String> generateHashtags(String content, Platform platform);
|
||||
}
|
||||
@ -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<String, String> generatePosterSizes(String originalImage);
|
||||
}
|
||||
@ -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<String> 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<String> generateDummyHashtags(Platform platform) {
|
||||
if (platform == Platform.INSTAGRAM) {
|
||||
return Arrays.asList("#맛집", "#데일리", "#소상공인", "#추천", "#인스타그램");
|
||||
} else {
|
||||
return Arrays.asList("#맛집추천", "#블로그", "#리뷰", "#맛있는곳", "#소상공인응원");
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> generateFallbackHashtags() {
|
||||
return Arrays.asList("#소상공인", "#마케팅", "#홍보");
|
||||
}
|
||||
}
|
||||
@ -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<String, String> generatePosterSizes(String baseImage) {
|
||||
Map<String, String> 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";
|
||||
}
|
||||
}
|
||||
@ -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<String> list) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
@ -128,7 +152,7 @@ public class ContentMapper {
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 문자열을 List로 변환합니다.
|
||||
* JSON 문자열을 List로 변환
|
||||
*/
|
||||
private List<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<Content> 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<Content> 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<ContentJpaEntity> 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<ContentJpaEntity> 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<Content> findOngoingContents(String period) {
|
||||
log.debug("Finding ongoing contents for period: {}", period);
|
||||
List<ContentJpaEntity> entities = springDataContentRepository.findOngoingContents(period);
|
||||
log.debug("Finding ongoing contents");
|
||||
|
||||
List<ContentJpaEntity> 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<Content> findByStoreId(Long storeId) {
|
||||
log.debug("Finding contents by store ID: {}", storeId);
|
||||
|
||||
List<ContentJpaEntity> entities = jpaRepository.findByStoreId(storeId);
|
||||
|
||||
return entities.stream()
|
||||
.map(contentMapper::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 콘텐츠 타입으로 조회 (추가 메서드)
|
||||
* @param contentType 콘텐츠 타입
|
||||
* @return 도메인 콘텐츠 목록
|
||||
*/
|
||||
public List<Content> findByContentType(ContentType contentType) {
|
||||
log.debug("Finding contents by type: {}", contentType);
|
||||
|
||||
List<ContentJpaEntity> entities = jpaRepository.findByContentType(contentType.name());
|
||||
|
||||
return entities.stream()
|
||||
.map(contentMapper::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@ -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<ContentJpaEntity, Long> {
|
||||
|
||||
/**
|
||||
* 매장 ID로 콘텐츠 목록 조회
|
||||
* @param storeId 매장 ID
|
||||
* @return 콘텐츠 엔티티 목록
|
||||
*/
|
||||
List<ContentJpaEntity> findByStoreId(Long storeId);
|
||||
|
||||
/**
|
||||
* 콘텐츠 타입으로 조회
|
||||
* @param contentType 콘텐츠 타입
|
||||
* @return 콘텐츠 엔티티 목록
|
||||
*/
|
||||
List<ContentJpaEntity> findByContentType(String contentType);
|
||||
|
||||
/**
|
||||
* 플랫폼으로 조회
|
||||
* @param platform 플랫폼
|
||||
* @return 콘텐츠 엔티티 목록
|
||||
*/
|
||||
List<ContentJpaEntity> findByPlatform(String platform);
|
||||
|
||||
/**
|
||||
* 상태로 조회
|
||||
* @param status 상태
|
||||
* @return 콘텐츠 엔티티 목록
|
||||
*/
|
||||
List<ContentJpaEntity> 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<ContentJpaEntity> 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<ContentJpaEntity> findOngoingContents();
|
||||
|
||||
/**
|
||||
* 매장 ID와 콘텐츠 타입으로 조회
|
||||
* @param storeId 매장 ID
|
||||
* @param contentType 콘텐츠 타입
|
||||
* @return 콘텐츠 엔티티 목록
|
||||
*/
|
||||
List<ContentJpaEntity> 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<ContentJpaEntity> findRecentContentsByStoreId(@Param("storeId") Long storeId);
|
||||
}
|
||||
@ -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<ContentJpaEntity, Long> {
|
||||
|
||||
/**
|
||||
* 필터 조건으로 콘텐츠를 조회합니다.
|
||||
*
|
||||
* @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<ContentJpaEntity> 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<ContentJpaEntity> findOngoingContents(@Param("period") String period);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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())
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user