mirror of
https://github.com/won-ktds/smarketing-backend.git
synced 2025-12-06 07:06:24 +00:00
modify: folder - java/python
This commit is contained in:
parent
a86d2e47ce
commit
7813f934b9
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# 디폴트 무시된 파일
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 환경에 따라 달라지는 Maven 홈 디렉터리
|
||||||
|
/mavenHomeManager.xml
|
||||||
11
.idea/gradle.xml
generated
Normal file
11
.idea/gradle.xml
generated
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?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
Normal file
4
.idea/misc.xml
generated
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -1,114 +0,0 @@
|
|||||||
package com.won.smarketing.content.domain.model;
|
|
||||||
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 마케팅 콘텐츠 도메인 모델
|
|
||||||
* 콘텐츠의 핵심 비즈니스 로직과 상태를 관리
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Builder
|
|
||||||
public class Content {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 콘텐츠 고유 식별자
|
|
||||||
*/
|
|
||||||
private ContentId id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 콘텐츠 타입 (SNS 게시물, 포스터 등)
|
|
||||||
*/
|
|
||||||
private ContentType contentType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 플랫폼 (인스타그램, 네이버 블로그 등)
|
|
||||||
*/
|
|
||||||
private Platform platform;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 콘텐츠 제목
|
|
||||||
*/
|
|
||||||
private String title;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 콘텐츠 내용
|
|
||||||
*/
|
|
||||||
private String content;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 해시태그 목록
|
|
||||||
*/
|
|
||||||
private List<String> hashtags;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 이미지 URL 목록
|
|
||||||
*/
|
|
||||||
private List<String> images;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 콘텐츠 상태
|
|
||||||
*/
|
|
||||||
private ContentStatus status;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 콘텐츠 생성 조건
|
|
||||||
*/
|
|
||||||
private CreationConditions creationConditions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 매장 ID
|
|
||||||
*/
|
|
||||||
private Long storeId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 생성 시각
|
|
||||||
*/
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 수정 시각
|
|
||||||
*/
|
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 콘텐츠 제목 업데이트
|
|
||||||
*
|
|
||||||
* @param title 새로운 제목
|
|
||||||
*/
|
|
||||||
public void updateTitle(String title) {
|
|
||||||
this.title = title;
|
|
||||||
this.updatedAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 콘텐츠 기간 업데이트
|
|
||||||
*
|
|
||||||
* @param startDate 시작일
|
|
||||||
* @param endDate 종료일
|
|
||||||
*/
|
|
||||||
public void updatePeriod(LocalDate startDate, LocalDate endDate) {
|
|
||||||
if (this.creationConditions != null) {
|
|
||||||
this.creationConditions = this.creationConditions.toBuilder()
|
|
||||||
.startDate(startDate)
|
|
||||||
.endDate(endDate)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
this.updatedAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 콘텐츠 상태 변경
|
|
||||||
*
|
|
||||||
* @param status 새로운 상태
|
|
||||||
*/
|
|
||||||
public void changeStatus(ContentStatus status) {
|
|
||||||
this.status = status;
|
|
||||||
this.updatedAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0
.gitignore → smarketing-java/.gitignore
vendored
0
.gitignore → smarketing-java/.gitignore
vendored
0
gradlew → smarketing-java/gradlew
vendored
0
gradlew → smarketing-java/gradlew
vendored
@ -3,11 +3,7 @@ package com.won.smarketing.content.application.service;
|
|||||||
import com.won.smarketing.common.exception.BusinessException;
|
import com.won.smarketing.common.exception.BusinessException;
|
||||||
import com.won.smarketing.common.exception.ErrorCode;
|
import com.won.smarketing.common.exception.ErrorCode;
|
||||||
import com.won.smarketing.content.application.usecase.ContentQueryUseCase;
|
import com.won.smarketing.content.application.usecase.ContentQueryUseCase;
|
||||||
import com.won.smarketing.content.domain.model.Content;
|
import com.won.smarketing.content.domain.model.*;
|
||||||
import com.won.smarketing.content.domain.model.ContentId;
|
|
||||||
import com.won.smarketing.content.domain.model.ContentStatus;
|
|
||||||
import com.won.smarketing.content.domain.model.ContentType;
|
|
||||||
import com.won.smarketing.content.domain.model.Platform;
|
|
||||||
import com.won.smarketing.content.domain.repository.ContentRepository;
|
import com.won.smarketing.content.domain.repository.ContentRepository;
|
||||||
import com.won.smarketing.content.presentation.dto.*;
|
import com.won.smarketing.content.presentation.dto.*;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -181,20 +177,15 @@ public class ContentQueryService implements ContentQueryUseCase {
|
|||||||
* @param conditions CreationConditions 도메인 객체
|
* @param conditions CreationConditions 도메인 객체
|
||||||
* @return CreationConditionsDto
|
* @return CreationConditionsDto
|
||||||
*/
|
*/
|
||||||
private CreationConditionsDto toCreationConditionsDto(CreationConditions conditions) {
|
private ContentDetailResponse.CreationConditionsDto toCreationConditionsDto(CreationConditions conditions) {
|
||||||
if (conditions == null) {
|
if (conditions == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CreationConditionsDto.builder()
|
return ContentDetailResponse.CreationConditionsDto.builder()
|
||||||
.category(conditions.getCategory())
|
|
||||||
.requirement(conditions.getRequirement())
|
|
||||||
.toneAndManner(conditions.getToneAndManner())
|
.toneAndManner(conditions.getToneAndManner())
|
||||||
.emotionIntensity(conditions.getEmotionIntensity())
|
.emotionIntensity(conditions.getEmotionIntensity())
|
||||||
.eventName(conditions.getEventName())
|
.eventName(conditions.getEventName())
|
||||||
.startDate(conditions.getStartDate())
|
|
||||||
.endDate(conditions.getEndDate())
|
|
||||||
.photoStyle(conditions.getPhotoStyle())
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,7 +39,7 @@ public class SnsContentService implements SnsContentUseCase {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request) {
|
/* public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request) {
|
||||||
// AI를 사용하여 SNS 콘텐츠 생성
|
// AI를 사용하여 SNS 콘텐츠 생성
|
||||||
String generatedContent = aiContentGenerator.generateSnsContent(request);
|
String generatedContent = aiContentGenerator.generateSnsContent(request);
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ public class SnsContentService implements SnsContentUseCase {
|
|||||||
.status(content.getStatus().name())
|
.status(content.getStatus().name())
|
||||||
.createdAt(content.getCreatedAt())
|
.createdAt(content.getCreatedAt())
|
||||||
.build();
|
.build();
|
||||||
}
|
}*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SNS 콘텐츠 저장
|
* SNS 콘텐츠 저장
|
||||||
@ -0,0 +1,611 @@
|
|||||||
|
// 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;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 도메인 모델
|
||||||
|
*
|
||||||
|
* 이 클래스는 마케팅 콘텐츠의 핵심 정보와 비즈니스 로직을 포함하는
|
||||||
|
* DDD(Domain-Driven Design) 엔티티입니다.
|
||||||
|
*
|
||||||
|
* Clean Architecture의 Domain Layer에 위치하며,
|
||||||
|
* 비즈니스 규칙과 도메인 로직을 캡슐화합니다.
|
||||||
|
*/
|
||||||
|
@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<>();
|
||||||
|
|
||||||
|
// ==================== 상태 관리 ====================
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
// ==================== 비즈니스 로직 메서드 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 제목 수정
|
||||||
|
*
|
||||||
|
* 비즈니스 규칙:
|
||||||
|
* - 제목은 null이거나 빈 값일 수 없음
|
||||||
|
* - 200자를 초과할 수 없음
|
||||||
|
* - 발행된 콘텐츠는 제목 변경 시 상태가 DRAFT로 변경됨
|
||||||
|
*
|
||||||
|
* @param title 새로운 제목
|
||||||
|
* @throws IllegalArgumentException 제목이 유효하지 않은 경우
|
||||||
|
*/
|
||||||
|
public void updateTitle(String title) {
|
||||||
|
validateTitle(title);
|
||||||
|
|
||||||
|
boolean wasPublished = isPublished();
|
||||||
|
this.title = title.trim();
|
||||||
|
|
||||||
|
// 발행된 콘텐츠의 제목이 변경되면 재검토 필요
|
||||||
|
if (wasPublished) {
|
||||||
|
this.status = ContentStatus.DRAFT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 내용 수정
|
||||||
|
*
|
||||||
|
* 비즈니스 규칙:
|
||||||
|
* - 내용은 null이거나 빈 값일 수 없음
|
||||||
|
* - 발행된 콘텐츠는 내용 변경 시 상태가 DRAFT로 변경됨
|
||||||
|
*
|
||||||
|
* @param content 새로운 콘텐츠 내용
|
||||||
|
* @throws IllegalArgumentException 내용이 유효하지 않은 경우
|
||||||
|
*/
|
||||||
|
public void updateContent(String content) {
|
||||||
|
validateContent(content);
|
||||||
|
|
||||||
|
boolean wasPublished = isPublished();
|
||||||
|
this.content = content.trim();
|
||||||
|
|
||||||
|
// 발행된 콘텐츠의 내용이 변경되면 재검토 필요
|
||||||
|
if (wasPublished) {
|
||||||
|
this.status = ContentStatus.DRAFT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 상태 변경
|
||||||
|
*
|
||||||
|
* 비즈니스 규칙:
|
||||||
|
* - PUBLISHED 상태로 변경시 유효성 검증 수행
|
||||||
|
* - ARCHIVED 상태에서는 PUBLISHED로만 변경 가능
|
||||||
|
*
|
||||||
|
* @param status 새로운 상태
|
||||||
|
* @throws IllegalStateException 잘못된 상태 전환인 경우
|
||||||
|
*/
|
||||||
|
public void changeStatus(ContentStatus status) {
|
||||||
|
validateStatusTransition(this.status, status);
|
||||||
|
|
||||||
|
if (status == ContentStatus.PUBLISHED) {
|
||||||
|
validateForPublication();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 해시태그 제거
|
||||||
|
*
|
||||||
|
* @param hashtag 제거할 해시태그
|
||||||
|
*/
|
||||||
|
public void removeHashtag(String hashtag) {
|
||||||
|
if (hashtag != null) {
|
||||||
|
String cleanHashtag = hashtag.trim().replace("#", "");
|
||||||
|
this.hashtags.remove(cleanHashtag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 추가
|
||||||
|
*
|
||||||
|
* @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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 제거
|
||||||
|
*
|
||||||
|
* @param imageUrl 제거할 이미지 URL
|
||||||
|
*/
|
||||||
|
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() {
|
||||||
|
if (promotionStartDate == null || promotionEndDate == null) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 남은 일수 (음수면 0)
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 팩토리 메서드 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
);
|
||||||
|
*/
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 상세 응답 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "콘텐츠 상세 응답")
|
||||||
|
public class ContentDetailResponse {
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 ID", example = "1")
|
||||||
|
private Long contentId;
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 타입", example = "SNS_POST")
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
|
@Schema(description = "플랫폼", example = "INSTAGRAM")
|
||||||
|
private String platform;
|
||||||
|
|
||||||
|
@Schema(description = "제목", example = "맛있는 신메뉴를 소개합니다!")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 내용")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Schema(description = "해시태그 목록")
|
||||||
|
private List<String> hashtags;
|
||||||
|
|
||||||
|
@Schema(description = "이미지 URL 목록")
|
||||||
|
private List<String> images;
|
||||||
|
|
||||||
|
@Schema(description = "상태", example = "PUBLISHED")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "홍보 시작일")
|
||||||
|
private LocalDateTime promotionStartDate;
|
||||||
|
|
||||||
|
@Schema(description = "홍보 종료일")
|
||||||
|
private LocalDateTime promotionEndDate;
|
||||||
|
|
||||||
|
@Schema(description = "생성 조건")
|
||||||
|
private CreationConditionsDto creationConditions;
|
||||||
|
|
||||||
|
@Schema(description = "생성일시")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "수정일시")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성 조건 내부 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "콘텐츠 생성 조건")
|
||||||
|
public static class CreationConditionsDto {
|
||||||
|
|
||||||
|
@Schema(description = "톤앤매너", example = "친근함")
|
||||||
|
private String toneAndManner;
|
||||||
|
|
||||||
|
@Schema(description = "프로모션 유형", example = "할인 정보")
|
||||||
|
private String promotionType;
|
||||||
|
|
||||||
|
@Schema(description = "감정 강도", example = "보통")
|
||||||
|
private String emotionIntensity;
|
||||||
|
|
||||||
|
@Schema(description = "홍보 대상", example = "메뉴")
|
||||||
|
private String targetAudience;
|
||||||
|
|
||||||
|
@Schema(description = "이벤트명", example = "신메뉴 출시 이벤트")
|
||||||
|
private String eventName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 목록 조회 요청 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "콘텐츠 목록 조회 요청")
|
||||||
|
public class ContentListRequest {
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 타입", example = "SNS_POST")
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
|
@Schema(description = "플랫폼", example = "INSTAGRAM")
|
||||||
|
private String platform;
|
||||||
|
|
||||||
|
@Schema(description = "조회 기간", example = "7days")
|
||||||
|
private String period;
|
||||||
|
|
||||||
|
@Schema(description = "정렬 기준", example = "createdAt")
|
||||||
|
private String sortBy;
|
||||||
|
|
||||||
|
@Schema(description = "정렬 방향", example = "DESC")
|
||||||
|
private String sortDirection;
|
||||||
|
|
||||||
|
@Schema(description = "페이지 번호", example = "0")
|
||||||
|
private Integer page;
|
||||||
|
|
||||||
|
@Schema(description = "페이지 크기", example = "20")
|
||||||
|
private Integer size;
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 재생성 요청 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "콘텐츠 재생성 요청")
|
||||||
|
public class ContentRegenerateRequest {
|
||||||
|
|
||||||
|
@Schema(description = "원본 콘텐츠 ID", example = "1", required = true)
|
||||||
|
@NotNull(message = "원본 콘텐츠 ID는 필수입니다")
|
||||||
|
private Long originalContentId;
|
||||||
|
|
||||||
|
@Schema(description = "수정된 톤앤매너", example = "전문적")
|
||||||
|
private String toneAndManner;
|
||||||
|
|
||||||
|
@Schema(description = "수정된 프로모션 유형", example = "신메뉴 알림")
|
||||||
|
private String promotionType;
|
||||||
|
|
||||||
|
@Schema(description = "수정된 감정 강도", example = "열정적")
|
||||||
|
private String emotionIntensity;
|
||||||
|
|
||||||
|
@Schema(description = "추가 요구사항", example = "더 감성적으로 작성해주세요")
|
||||||
|
private String additionalRequirements;
|
||||||
|
}
|
||||||
@ -0,0 +1,361 @@
|
|||||||
|
// marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/ContentResponse.java
|
||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 응답 DTO
|
||||||
|
* 콘텐츠 목록 조회 시 사용되는 기본 응답 DTO
|
||||||
|
*
|
||||||
|
* 이 클래스는 콘텐츠의 핵심 정보만을 포함하여 목록 조회 시 성능을 최적화합니다.
|
||||||
|
* 상세 정보가 필요한 경우 ContentDetailResponse를 사용합니다.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "콘텐츠 응답")
|
||||||
|
public class ContentResponse {
|
||||||
|
|
||||||
|
// ==================== 기본 식별 정보 ====================
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 ID", example = "1")
|
||||||
|
private Long contentId;
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 타입", example = "SNS_POST",
|
||||||
|
allowableValues = {"SNS_POST", "POSTER"})
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
|
@Schema(description = "플랫폼", example = "INSTAGRAM",
|
||||||
|
allowableValues = {"INSTAGRAM", "NAVER_BLOG", "FACEBOOK", "KAKAO_STORY"})
|
||||||
|
private String platform;
|
||||||
|
|
||||||
|
// ==================== 콘텐츠 정보 ====================
|
||||||
|
|
||||||
|
@Schema(description = "제목", example = "맛있는 신메뉴를 소개합니다!")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 내용", example = "특별한 신메뉴가 출시되었습니다! 🍽️\n지금 바로 맛보세요!")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Schema(description = "해시태그 목록", example = "[\"#맛집\", \"#신메뉴\", \"#추천\", \"#인스타그램\"]")
|
||||||
|
private List<String> hashtags;
|
||||||
|
|
||||||
|
@Schema(description = "이미지 URL 목록",
|
||||||
|
example = "[\"https://example.com/image1.jpg\", \"https://example.com/image2.jpg\"]")
|
||||||
|
private List<String> images;
|
||||||
|
|
||||||
|
// ==================== 상태 관리 ====================
|
||||||
|
|
||||||
|
@Schema(description = "상태", example = "PUBLISHED",
|
||||||
|
allowableValues = {"DRAFT", "PUBLISHED", "SCHEDULED", "ARCHIVED"})
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "상태 표시명", example = "발행완료")
|
||||||
|
private String statusDisplay;
|
||||||
|
|
||||||
|
// ==================== 홍보 기간 ====================
|
||||||
|
|
||||||
|
@Schema(description = "홍보 시작일", example = "2024-01-15T09:00:00")
|
||||||
|
private LocalDateTime promotionStartDate;
|
||||||
|
|
||||||
|
@Schema(description = "홍보 종료일", example = "2024-01-22T23:59:59")
|
||||||
|
private LocalDateTime promotionEndDate;
|
||||||
|
|
||||||
|
// ==================== 시간 정보 ====================
|
||||||
|
|
||||||
|
@Schema(description = "생성일시", example = "2024-01-15T10:30:00")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "수정일시", example = "2024-01-15T14:20:00")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
// ==================== 계산된 필드들 ====================
|
||||||
|
|
||||||
|
@Schema(description = "홍보 진행 상태", example = "ONGOING",
|
||||||
|
allowableValues = {"UPCOMING", "ONGOING", "COMPLETED"})
|
||||||
|
private String promotionStatus;
|
||||||
|
|
||||||
|
@Schema(description = "남은 홍보 일수", example = "5")
|
||||||
|
private Long remainingDays;
|
||||||
|
|
||||||
|
@Schema(description = "홍보 진행률 (%)", example = "60.5")
|
||||||
|
private Double progressPercentage;
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 요약 (첫 50자)", example = "특별한 신메뉴가 출시되었습니다! 지금 바로 맛보세요...")
|
||||||
|
private String contentSummary;
|
||||||
|
|
||||||
|
@Schema(description = "이미지 개수", example = "3")
|
||||||
|
private Integer imageCount;
|
||||||
|
|
||||||
|
@Schema(description = "해시태그 개수", example = "8")
|
||||||
|
private Integer hashtagCount;
|
||||||
|
|
||||||
|
// ==================== 비즈니스 메서드 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 요약 생성
|
||||||
|
* 콘텐츠가 길 경우 첫 50자만 표시하고 "..." 추가
|
||||||
|
*
|
||||||
|
* @param content 원본 콘텐츠
|
||||||
|
* @param maxLength 최대 길이
|
||||||
|
* @return 요약된 콘텐츠
|
||||||
|
*/
|
||||||
|
public static String createContentSummary(String content, int maxLength) {
|
||||||
|
if (content == null || content.length() <= maxLength) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return content.substring(0, maxLength) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 홍보 상태 계산
|
||||||
|
* 현재 시간과 홍보 기간을 비교하여 상태 결정
|
||||||
|
*
|
||||||
|
* @param startDate 홍보 시작일
|
||||||
|
* @param endDate 홍보 종료일
|
||||||
|
* @return 홍보 상태
|
||||||
|
*/
|
||||||
|
public static String calculatePromotionStatus(LocalDateTime startDate, LocalDateTime endDate) {
|
||||||
|
if (startDate == null || endDate == null) {
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
if (now.isBefore(startDate)) {
|
||||||
|
return "UPCOMING"; // 홍보 예정
|
||||||
|
} else if (now.isAfter(endDate)) {
|
||||||
|
return "COMPLETED"; // 홍보 완료
|
||||||
|
} else {
|
||||||
|
return "ONGOING"; // 홍보 진행중
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 남은 일수 계산
|
||||||
|
* 홍보 종료일까지 남은 일수 계산
|
||||||
|
*
|
||||||
|
* @param endDate 홍보 종료일
|
||||||
|
* @return 남은 일수 (음수면 0 반환)
|
||||||
|
*/
|
||||||
|
public static Long calculateRemainingDays(LocalDateTime endDate) {
|
||||||
|
if (endDate == null) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
if (now.isAfter(endDate)) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
return java.time.Duration.between(now, endDate).toDays();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 진행률 계산
|
||||||
|
* 홍보 기간 대비 진행률 계산 (0-100%)
|
||||||
|
*
|
||||||
|
* @param startDate 홍보 시작일
|
||||||
|
* @param endDate 홍보 종료일
|
||||||
|
* @return 진행률 (0-100%)
|
||||||
|
*/
|
||||||
|
public static Double calculateProgressPercentage(LocalDateTime startDate, LocalDateTime endDate) {
|
||||||
|
if (startDate == null || endDate == null) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
if (now.isBefore(startDate)) {
|
||||||
|
return 0.0; // 아직 시작 안함
|
||||||
|
} else if (now.isAfter(endDate)) {
|
||||||
|
return 100.0; // 완료
|
||||||
|
}
|
||||||
|
|
||||||
|
long totalDuration = java.time.Duration.between(startDate, endDate).toHours();
|
||||||
|
long elapsedDuration = java.time.Duration.between(startDate, now).toHours();
|
||||||
|
|
||||||
|
if (totalDuration == 0) {
|
||||||
|
return 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (double) elapsedDuration / totalDuration * 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상태 표시명 변환
|
||||||
|
* 영문 상태를 한글로 변환
|
||||||
|
*
|
||||||
|
* @param status 영문 상태
|
||||||
|
* @return 한글 상태명
|
||||||
|
*/
|
||||||
|
public static String getStatusDisplay(String status) {
|
||||||
|
if (status == null) {
|
||||||
|
return "알 수 없음";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "DRAFT":
|
||||||
|
return "임시저장";
|
||||||
|
case "PUBLISHED":
|
||||||
|
return "발행완료";
|
||||||
|
case "SCHEDULED":
|
||||||
|
return "예약발행";
|
||||||
|
case "ARCHIVED":
|
||||||
|
return "보관됨";
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Builder 확장 메서드 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도메인 엔티티에서 ContentResponse 생성
|
||||||
|
* 계산된 필드들을 자동으로 설정
|
||||||
|
*
|
||||||
|
* @param content 콘텐츠 도메인 엔티티
|
||||||
|
* @return ContentResponse
|
||||||
|
*/
|
||||||
|
public static ContentResponse fromDomain(com.won.smarketing.content.domain.model.Content content) {
|
||||||
|
ContentResponseBuilder builder = ContentResponse.builder()
|
||||||
|
.contentId(content.getId().getValue())
|
||||||
|
.contentType(content.getContentType().name())
|
||||||
|
.platform(content.getPlatform().name())
|
||||||
|
.title(content.getTitle())
|
||||||
|
.content(content.getContent())
|
||||||
|
.hashtags(content.getHashtags())
|
||||||
|
.images(content.getImages())
|
||||||
|
.status(content.getStatus().name())
|
||||||
|
.statusDisplay(getStatusDisplay(content.getStatus().name()))
|
||||||
|
.promotionStartDate(content.getPromotionStartDate())
|
||||||
|
.promotionEndDate(content.getPromotionEndDate())
|
||||||
|
.createdAt(content.getCreatedAt())
|
||||||
|
.updatedAt(content.getUpdatedAt());
|
||||||
|
|
||||||
|
// 계산된 필드들 설정
|
||||||
|
builder.contentSummary(createContentSummary(content.getContent(), 50));
|
||||||
|
builder.imageCount(content.getImages() != null ? content.getImages().size() : 0);
|
||||||
|
builder.hashtagCount(content.getHashtags() != null ? content.getHashtags().size() : 0);
|
||||||
|
|
||||||
|
// 홍보 관련 계산 필드들
|
||||||
|
builder.promotionStatus(calculatePromotionStatus(
|
||||||
|
content.getPromotionStartDate(),
|
||||||
|
content.getPromotionEndDate()));
|
||||||
|
builder.remainingDays(calculateRemainingDays(content.getPromotionEndDate()));
|
||||||
|
builder.progressPercentage(calculateProgressPercentage(
|
||||||
|
content.getPromotionStartDate(),
|
||||||
|
content.getPromotionEndDate()));
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 유틸리티 메서드 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠가 현재 활성 상태인지 확인
|
||||||
|
*
|
||||||
|
* @return 홍보 기간 내이고 발행 상태면 true
|
||||||
|
*/
|
||||||
|
public boolean isActive() {
|
||||||
|
return "PUBLISHED".equals(status) && "ONGOING".equals(promotionStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 수정 가능 여부 확인
|
||||||
|
*
|
||||||
|
* @return 임시저장 상태이거나 예약발행 상태면 true
|
||||||
|
*/
|
||||||
|
public boolean isEditable() {
|
||||||
|
return "DRAFT".equals(status) || "SCHEDULED".equals(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지가 있는 콘텐츠인지 확인
|
||||||
|
*
|
||||||
|
* @return 이미지가 1개 이상 있으면 true
|
||||||
|
*/
|
||||||
|
public boolean hasImages() {
|
||||||
|
return images != null && !images.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 해시태그가 있는 콘텐츠인지 확인
|
||||||
|
*
|
||||||
|
* @return 해시태그가 1개 이상 있으면 true
|
||||||
|
*/
|
||||||
|
public boolean hasHashtags() {
|
||||||
|
return hashtags != null && !hashtags.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 디버깅용 toString (간소화된 정보만)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ContentResponse{" +
|
||||||
|
"contentId=" + contentId +
|
||||||
|
", contentType='" + contentType + '\'' +
|
||||||
|
", platform='" + platform + '\'' +
|
||||||
|
", title='" + title + '\'' +
|
||||||
|
", status='" + status + '\'' +
|
||||||
|
", promotionStatus='" + promotionStatus + '\'' +
|
||||||
|
", createdAt=" + createdAt +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
==================== 사용 예시 ====================
|
||||||
|
|
||||||
|
// 1. 도메인 엔티티에서 DTO 생성
|
||||||
|
Content domainContent = contentRepository.findById(contentId);
|
||||||
|
ContentResponse response = ContentResponse.fromDomain(domainContent);
|
||||||
|
|
||||||
|
// 2. 수동으로 빌더 사용
|
||||||
|
ContentResponse response = ContentResponse.builder()
|
||||||
|
.contentId(1L)
|
||||||
|
.contentType("SNS_POST")
|
||||||
|
.platform("INSTAGRAM")
|
||||||
|
.title("맛있는 신메뉴")
|
||||||
|
.content("특별한 신메뉴가 출시되었습니다!")
|
||||||
|
.status("PUBLISHED")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 3. 비즈니스 로직 활용
|
||||||
|
boolean canEdit = response.isEditable();
|
||||||
|
boolean isLive = response.isActive();
|
||||||
|
String summary = response.getContentSummary();
|
||||||
|
|
||||||
|
==================== JSON 응답 예시 ====================
|
||||||
|
|
||||||
|
{
|
||||||
|
"contentId": 1,
|
||||||
|
"contentType": "SNS_POST",
|
||||||
|
"platform": "INSTAGRAM",
|
||||||
|
"title": "맛있는 신메뉴를 소개합니다!",
|
||||||
|
"content": "특별한 신메뉴가 출시되었습니다! 🍽️\n지금 바로 맛보세요!",
|
||||||
|
"hashtags": ["#맛집", "#신메뉴", "#추천", "#인스타그램"],
|
||||||
|
"images": ["https://example.com/image1.jpg"],
|
||||||
|
"status": "PUBLISHED",
|
||||||
|
"statusDisplay": "발행완료",
|
||||||
|
"promotionStartDate": "2024-01-15T09:00:00",
|
||||||
|
"promotionEndDate": "2024-01-22T23:59:59",
|
||||||
|
"createdAt": "2024-01-15T10:30:00",
|
||||||
|
"updatedAt": "2024-01-15T14:20:00",
|
||||||
|
"promotionStatus": "ONGOING",
|
||||||
|
"remainingDays": 5,
|
||||||
|
"progressPercentage": 60.5,
|
||||||
|
"contentSummary": "특별한 신메뉴가 출시되었습니다! 지금 바로 맛보세요...",
|
||||||
|
"imageCount": 1,
|
||||||
|
"hashtagCount": 4
|
||||||
|
}
|
||||||
|
*/
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 통계 응답 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "콘텐츠 통계 응답")
|
||||||
|
public class ContentStatisticsResponse {
|
||||||
|
|
||||||
|
@Schema(description = "총 콘텐츠 수", example = "150")
|
||||||
|
private Long totalContents;
|
||||||
|
|
||||||
|
@Schema(description = "이번 달 생성된 콘텐츠 수", example = "25")
|
||||||
|
private Long thisMonthContents;
|
||||||
|
|
||||||
|
@Schema(description = "발행된 콘텐츠 수", example = "120")
|
||||||
|
private Long publishedContents;
|
||||||
|
|
||||||
|
@Schema(description = "임시저장된 콘텐츠 수", example = "30")
|
||||||
|
private Long draftContents;
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 타입별 통계")
|
||||||
|
private Map<String, Long> contentTypeStats;
|
||||||
|
|
||||||
|
@Schema(description = "플랫폼별 통계")
|
||||||
|
private Map<String, Long> platformStats;
|
||||||
|
|
||||||
|
@Schema(description = "월별 생성 통계 (최근 6개월)")
|
||||||
|
private Map<String, Long> monthlyStats;
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 수정 요청 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "콘텐츠 수정 요청")
|
||||||
|
public class ContentUpdateRequest {
|
||||||
|
|
||||||
|
@Schema(description = "제목", example = "수정된 제목")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 내용")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Schema(description = "홍보 시작일")
|
||||||
|
private LocalDateTime promotionStartDate;
|
||||||
|
|
||||||
|
@Schema(description = "홍보 종료일")
|
||||||
|
private LocalDateTime promotionEndDate;
|
||||||
|
|
||||||
|
@Schema(description = "상태", example = "PUBLISHED")
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 수정 응답 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "콘텐츠 수정 응답")
|
||||||
|
public class ContentUpdateResponse {
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 ID", example = "1")
|
||||||
|
private Long contentId;
|
||||||
|
|
||||||
|
@Schema(description = "수정된 제목", example = "수정된 제목")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "수정된 콘텐츠 내용")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Schema(description = "상태", example = "PUBLISHED")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "수정일시")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 진행 중인 콘텐츠 응답 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "진행 중인 콘텐츠 응답")
|
||||||
|
public class OngoingContentResponse {
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 ID", example = "1")
|
||||||
|
private Long contentId;
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 타입", example = "SNS_POST")
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
|
@Schema(description = "플랫폼", example = "INSTAGRAM")
|
||||||
|
private String platform;
|
||||||
|
|
||||||
|
@Schema(description = "제목", example = "진행 중인 이벤트")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "상태", example = "PUBLISHED")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "홍보 시작일")
|
||||||
|
private LocalDateTime promotionStartDate;
|
||||||
|
|
||||||
|
@Schema(description = "홍보 종료일")
|
||||||
|
private LocalDateTime promotionEndDate;
|
||||||
|
|
||||||
|
@Schema(description = "남은 일수", example = "5")
|
||||||
|
private Long remainingDays;
|
||||||
|
|
||||||
|
@Schema(description = "진행률 (%)", example = "60.5")
|
||||||
|
private Double progressPercentage;
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 콘텐츠 생성 요청 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "포스터 콘텐츠 생성 요청")
|
||||||
|
public class PosterContentCreateRequest {
|
||||||
|
|
||||||
|
@Schema(description = "홍보 대상", example = "메뉴", required = true)
|
||||||
|
@NotBlank(message = "홍보 대상은 필수입니다")
|
||||||
|
private String targetAudience;
|
||||||
|
|
||||||
|
@Schema(description = "홍보 시작일", required = true)
|
||||||
|
@NotNull(message = "홍보 시작일은 필수입니다")
|
||||||
|
private LocalDateTime promotionStartDate;
|
||||||
|
|
||||||
|
@Schema(description = "홍보 종료일", required = true)
|
||||||
|
@NotNull(message = "홍보 종료일은 필수입니다")
|
||||||
|
private LocalDateTime promotionEndDate;
|
||||||
|
|
||||||
|
@Schema(description = "이벤트명 (이벤트 홍보시)", example = "신메뉴 출시 이벤트")
|
||||||
|
private String eventName;
|
||||||
|
|
||||||
|
@Schema(description = "이미지 스타일", example = "모던")
|
||||||
|
private String imageStyle;
|
||||||
|
|
||||||
|
@Schema(description = "프로모션 유형", example = "할인 정보")
|
||||||
|
private String promotionType;
|
||||||
|
|
||||||
|
@Schema(description = "감정 강도", example = "보통")
|
||||||
|
private String emotionIntensity;
|
||||||
|
|
||||||
|
@Schema(description = "업로드된 이미지 URL 목록", required = true)
|
||||||
|
@NotNull(message = "이미지는 1개 이상 필수입니다")
|
||||||
|
@Size(min = 1, message = "이미지는 1개 이상 업로드해야 합니다")
|
||||||
|
private List<String> images;
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 콘텐츠 생성 응답 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "포스터 콘텐츠 생성 응답")
|
||||||
|
public class PosterContentCreateResponse {
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 ID", example = "1")
|
||||||
|
private Long contentId;
|
||||||
|
|
||||||
|
@Schema(description = "생성된 포스터 제목", example = "특별 이벤트 안내")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "생성된 포스터 텍스트 내용")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Schema(description = "포스터 이미지 URL 목록")
|
||||||
|
private List<String> posterImages;
|
||||||
|
|
||||||
|
@Schema(description = "원본 이미지 URL 목록")
|
||||||
|
private List<String> originalImages;
|
||||||
|
|
||||||
|
@Schema(description = "이미지 스타일", example = "모던")
|
||||||
|
private String imageStyle;
|
||||||
|
|
||||||
|
@Schema(description = "생성 상태", example = "DRAFT")
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 콘텐츠 저장 요청 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "포스터 콘텐츠 저장 요청")
|
||||||
|
public class PosterContentSaveRequest {
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 ID", example = "1", required = true)
|
||||||
|
@NotNull(message = "콘텐츠 ID는 필수입니다")
|
||||||
|
private Long contentId;
|
||||||
|
|
||||||
|
@Schema(description = "최종 제목", example = "특별 이벤트 안내")
|
||||||
|
private String finalTitle;
|
||||||
|
|
||||||
|
@Schema(description = "최종 콘텐츠 내용")
|
||||||
|
private String finalContent;
|
||||||
|
|
||||||
|
@Schema(description = "선택된 포스터 이미지 URL")
|
||||||
|
private String selectedPosterImage;
|
||||||
|
|
||||||
|
@Schema(description = "발행 상태", example = "PUBLISHED")
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@ -0,0 +1,380 @@
|
|||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SNS 콘텐츠 생성 응답 DTO
|
||||||
|
*
|
||||||
|
* AI를 통해 SNS 콘텐츠를 생성한 후 클라이언트에게 반환되는 응답 정보입니다.
|
||||||
|
* 생성된 콘텐츠의 기본 정보와 함께 사용자가 추가 편집할 수 있는 정보를 포함합니다.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "SNS 콘텐츠 생성 응답")
|
||||||
|
public class SnsContentCreateResponse {
|
||||||
|
|
||||||
|
// ==================== 기본 식별 정보 ====================
|
||||||
|
|
||||||
|
@Schema(description = "생성된 콘텐츠 ID", example = "1")
|
||||||
|
private Long contentId;
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 타입", example = "SNS_POST")
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
|
@Schema(description = "대상 플랫폼", example = "INSTAGRAM",
|
||||||
|
allowableValues = {"INSTAGRAM", "NAVER_BLOG", "FACEBOOK", "KAKAO_STORY"})
|
||||||
|
private String platform;
|
||||||
|
|
||||||
|
// ==================== AI 생성 콘텐츠 ====================
|
||||||
|
|
||||||
|
@Schema(description = "AI가 생성한 콘텐츠 제목",
|
||||||
|
example = "맛있는 신메뉴를 소개합니다! ✨")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "AI가 생성한 콘텐츠 내용",
|
||||||
|
example = "안녕하세요! 😊\n\n특별한 신메뉴가 출시되었습니다!\n진짜 맛있어서 꼭 한번 드셔보세요 🍽️\n\n매장에서 기다리고 있을게요! 💫")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Schema(description = "AI가 생성한 해시태그 목록",
|
||||||
|
example = "[\"맛집\", \"신메뉴\", \"추천\", \"인스타그램\", \"일상\", \"좋아요\", \"팔로우\", \"맛있어요\"]")
|
||||||
|
private List<String> hashtags;
|
||||||
|
|
||||||
|
// ==================== 플랫폼별 최적화 정보 ====================
|
||||||
|
|
||||||
|
@Schema(description = "플랫폼별 최적화된 콘텐츠 길이", example = "280")
|
||||||
|
private Integer contentLength;
|
||||||
|
|
||||||
|
@Schema(description = "플랫폼별 권장 해시태그 개수", example = "8")
|
||||||
|
private Integer recommendedHashtagCount;
|
||||||
|
|
||||||
|
@Schema(description = "플랫폼별 최대 해시태그 개수", example = "15")
|
||||||
|
private Integer maxHashtagCount;
|
||||||
|
|
||||||
|
// ==================== 생성 조건 정보 ====================
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 생성에 사용된 조건들")
|
||||||
|
private GenerationConditionsDto generationConditions;
|
||||||
|
|
||||||
|
// ==================== 상태 및 메타데이터 ====================
|
||||||
|
|
||||||
|
@Schema(description = "생성 상태", example = "DRAFT",
|
||||||
|
allowableValues = {"DRAFT", "PUBLISHED", "SCHEDULED"})
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "생성 일시", example = "2024-01-15T10:30:00")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "AI 모델 버전", example = "gpt-4-turbo")
|
||||||
|
private String aiModelVersion;
|
||||||
|
|
||||||
|
@Schema(description = "생성 시간 (초)", example = "3.5")
|
||||||
|
private Double generationTimeSeconds;
|
||||||
|
|
||||||
|
// ==================== 추가 정보 ====================
|
||||||
|
|
||||||
|
@Schema(description = "업로드된 원본 이미지 URL 목록")
|
||||||
|
private List<String> originalImages;
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 품질 점수 (1-100)", example = "85")
|
||||||
|
private Integer qualityScore;
|
||||||
|
|
||||||
|
@Schema(description = "예상 참여율 (%)", example = "12.5")
|
||||||
|
private Double expectedEngagementRate;
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 카테고리", example = "음식/메뉴소개")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
// ==================== 편집 가능 여부 ====================
|
||||||
|
|
||||||
|
@Schema(description = "제목 편집 가능 여부", example = "true")
|
||||||
|
@Builder.Default
|
||||||
|
private Boolean titleEditable = true;
|
||||||
|
|
||||||
|
@Schema(description = "내용 편집 가능 여부", example = "true")
|
||||||
|
@Builder.Default
|
||||||
|
private Boolean contentEditable = true;
|
||||||
|
|
||||||
|
@Schema(description = "해시태그 편집 가능 여부", example = "true")
|
||||||
|
@Builder.Default
|
||||||
|
private Boolean hashtagsEditable = true;
|
||||||
|
|
||||||
|
// ==================== 대안 콘텐츠 ====================
|
||||||
|
|
||||||
|
@Schema(description = "대안 제목 목록 (사용자 선택용)")
|
||||||
|
private List<String> alternativeTitles;
|
||||||
|
|
||||||
|
@Schema(description = "대안 해시태그 세트 목록")
|
||||||
|
private List<List<String>> alternativeHashtagSets;
|
||||||
|
|
||||||
|
// ==================== 내부 DTO 클래스 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 생성 조건 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "콘텐츠 생성 조건")
|
||||||
|
public static class GenerationConditionsDto {
|
||||||
|
|
||||||
|
@Schema(description = "홍보 대상", example = "메뉴")
|
||||||
|
private String targetAudience;
|
||||||
|
|
||||||
|
@Schema(description = "이벤트명", example = "신메뉴 출시 이벤트")
|
||||||
|
private String eventName;
|
||||||
|
|
||||||
|
@Schema(description = "톤앤매너", example = "친근함")
|
||||||
|
private String toneAndManner;
|
||||||
|
|
||||||
|
@Schema(description = "프로모션 유형", example = "할인 정보")
|
||||||
|
private String promotionType;
|
||||||
|
|
||||||
|
@Schema(description = "감정 강도", example = "보통")
|
||||||
|
private String emotionIntensity;
|
||||||
|
|
||||||
|
@Schema(description = "홍보 시작일", example = "2024-01-15T09:00:00")
|
||||||
|
private LocalDateTime promotionStartDate;
|
||||||
|
|
||||||
|
@Schema(description = "홍보 종료일", example = "2024-01-22T23:59:59")
|
||||||
|
private LocalDateTime promotionEndDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 비즈니스 메서드 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 플랫폼별 콘텐츠 최적화 여부 확인
|
||||||
|
*
|
||||||
|
* @return 콘텐츠가 플랫폼 권장 사항을 만족하면 true
|
||||||
|
*/
|
||||||
|
public boolean isOptimizedForPlatform() {
|
||||||
|
if (content == null || hashtags == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 플랫폼별 최적화 기준
|
||||||
|
switch (platform.toUpperCase()) {
|
||||||
|
case "INSTAGRAM":
|
||||||
|
return content.length() <= 2200 &&
|
||||||
|
hashtags.size() <= 15 &&
|
||||||
|
hashtags.size() >= 5;
|
||||||
|
case "NAVER_BLOG":
|
||||||
|
return content.length() >= 300 &&
|
||||||
|
hashtags.size() <= 10 &&
|
||||||
|
hashtags.size() >= 3;
|
||||||
|
case "FACEBOOK":
|
||||||
|
return content.length() <= 500 &&
|
||||||
|
hashtags.size() <= 5;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 고품질 콘텐츠 여부 확인
|
||||||
|
*
|
||||||
|
* @return 품질 점수가 80점 이상이면 true
|
||||||
|
*/
|
||||||
|
public boolean isHighQuality() {
|
||||||
|
return qualityScore != null && qualityScore >= 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참여율 예상 등급 반환
|
||||||
|
*
|
||||||
|
* @return 예상 참여율 등급 (HIGH, MEDIUM, LOW)
|
||||||
|
*/
|
||||||
|
public String getExpectedEngagementLevel() {
|
||||||
|
if (expectedEngagementRate == null) {
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedEngagementRate >= 15.0) {
|
||||||
|
return "HIGH";
|
||||||
|
} else if (expectedEngagementRate >= 8.0) {
|
||||||
|
return "MEDIUM";
|
||||||
|
} else {
|
||||||
|
return "LOW";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 해시태그를 문자열로 변환 (# 포함)
|
||||||
|
*
|
||||||
|
* @return #으로 시작하는 해시태그 문자열
|
||||||
|
*/
|
||||||
|
public String getHashtagsAsString() {
|
||||||
|
if (hashtags == null || hashtags.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashtags.stream()
|
||||||
|
.map(tag -> "#" + tag)
|
||||||
|
.reduce((a, b) -> a + " " + b)
|
||||||
|
.orElse("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 요약 생성
|
||||||
|
*
|
||||||
|
* @param maxLength 최대 길이
|
||||||
|
* @return 요약된 콘텐츠
|
||||||
|
*/
|
||||||
|
public String getContentSummary(int maxLength) {
|
||||||
|
if (content == null || content.length() <= maxLength) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return content.substring(0, maxLength) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 플랫폼별 최적화 제안사항 반환
|
||||||
|
*
|
||||||
|
* @return 최적화 제안사항 목록
|
||||||
|
*/
|
||||||
|
public List<String> getOptimizationSuggestions() {
|
||||||
|
List<String> suggestions = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
if (!isOptimizedForPlatform()) {
|
||||||
|
switch (platform.toUpperCase()) {
|
||||||
|
case "INSTAGRAM":
|
||||||
|
if (content != null && content.length() > 2200) {
|
||||||
|
suggestions.add("콘텐츠 길이를 2200자 이하로 줄여주세요.");
|
||||||
|
}
|
||||||
|
if (hashtags != null && hashtags.size() > 15) {
|
||||||
|
suggestions.add("해시태그를 15개 이하로 줄여주세요.");
|
||||||
|
}
|
||||||
|
if (hashtags != null && hashtags.size() < 5) {
|
||||||
|
suggestions.add("해시태그를 5개 이상 추가해주세요.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "NAVER_BLOG":
|
||||||
|
if (content != null && content.length() < 300) {
|
||||||
|
suggestions.add("블로그 포스팅을 위해 내용을 300자 이상으로 늘려주세요.");
|
||||||
|
}
|
||||||
|
if (hashtags != null && hashtags.size() > 10) {
|
||||||
|
suggestions.add("네이버 블로그는 해시태그를 10개 이하로 사용하는 것이 좋습니다.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "FACEBOOK":
|
||||||
|
if (content != null && content.length() > 500) {
|
||||||
|
suggestions.add("페이스북에서는 500자 이하의 짧은 글이 더 효과적입니다.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 팩토리 메서드 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도메인 엔티티에서 SnsContentCreateResponse 생성
|
||||||
|
*
|
||||||
|
* @param content 콘텐츠 도메인 엔티티
|
||||||
|
* @param aiMetadata AI 생성 메타데이터
|
||||||
|
* @return SnsContentCreateResponse
|
||||||
|
*/
|
||||||
|
public static SnsContentCreateResponse fromDomain(
|
||||||
|
com.won.smarketing.content.domain.model.Content content,
|
||||||
|
AiGenerationMetadata aiMetadata) {
|
||||||
|
|
||||||
|
SnsContentCreateResponseBuilder builder = SnsContentCreateResponse.builder()
|
||||||
|
.contentId(content.getId())
|
||||||
|
.contentType(content.getContentType().name())
|
||||||
|
.platform(content.getPlatform().name())
|
||||||
|
.title(content.getTitle())
|
||||||
|
.content(content.getContent())
|
||||||
|
.hashtags(content.getHashtags())
|
||||||
|
.status(content.getStatus().name())
|
||||||
|
.createdAt(content.getCreatedAt())
|
||||||
|
.originalImages(content.getImages());
|
||||||
|
|
||||||
|
// 생성 조건 정보 설정
|
||||||
|
if (content.getCreationConditions() != null) {
|
||||||
|
builder.generationConditions(GenerationConditionsDto.builder()
|
||||||
|
.targetAudience(content.getCreationConditions().getTargetAudience())
|
||||||
|
.eventName(content.getCreationConditions().getEventName())
|
||||||
|
.toneAndManner(content.getCreationConditions().getToneAndManner())
|
||||||
|
.promotionType(content.getCreationConditions().getPromotionType())
|
||||||
|
.emotionIntensity(content.getCreationConditions().getEmotionIntensity())
|
||||||
|
.promotionStartDate(content.getPromotionStartDate())
|
||||||
|
.promotionEndDate(content.getPromotionEndDate())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 메타데이터 설정
|
||||||
|
if (aiMetadata != null) {
|
||||||
|
builder.aiModelVersion(aiMetadata.getModelVersion())
|
||||||
|
.generationTimeSeconds(aiMetadata.getGenerationTime())
|
||||||
|
.qualityScore(aiMetadata.getQualityScore())
|
||||||
|
.expectedEngagementRate(aiMetadata.getExpectedEngagementRate())
|
||||||
|
.alternativeTitles(aiMetadata.getAlternativeTitles())
|
||||||
|
.alternativeHashtagSets(aiMetadata.getAlternativeHashtagSets());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 플랫폼별 최적화 정보 설정
|
||||||
|
SnsContentCreateResponse response = builder.build();
|
||||||
|
response.setContentLength(response.getContent() != null ? response.getContent().length() : 0);
|
||||||
|
response.setRecommendedHashtagCount(getRecommendedHashtagCount(content.getPlatform().name()));
|
||||||
|
response.setMaxHashtagCount(getMaxHashtagCount(content.getPlatform().name()));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 플랫폼별 권장 해시태그 개수 반환
|
||||||
|
*/
|
||||||
|
private static Integer getRecommendedHashtagCount(String platform) {
|
||||||
|
switch (platform.toUpperCase()) {
|
||||||
|
case "INSTAGRAM": return 8;
|
||||||
|
case "NAVER_BLOG": return 5;
|
||||||
|
case "FACEBOOK": return 3;
|
||||||
|
case "KAKAO_STORY": return 5;
|
||||||
|
default: return 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 플랫폼별 최대 해시태그 개수 반환
|
||||||
|
*/
|
||||||
|
private static Integer getMaxHashtagCount(String platform) {
|
||||||
|
switch (platform.toUpperCase()) {
|
||||||
|
case "INSTAGRAM": return 15;
|
||||||
|
case "NAVER_BLOG": return 10;
|
||||||
|
case "FACEBOOK": return 5;
|
||||||
|
case "KAKAO_STORY": return 8;
|
||||||
|
default: return 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== AI 생성 메타데이터 DTO ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 생성 메타데이터
|
||||||
|
* AI 생성 과정에서 나온 부가 정보들
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public static class AiGenerationMetadata {
|
||||||
|
private String modelVersion;
|
||||||
|
private Double generationTime;
|
||||||
|
private Integer qualityScore;
|
||||||
|
private Double expectedEngagementRate;
|
||||||
|
private List<String> alternativeTitles;
|
||||||
|
private List<List<String>> alternativeHashtagSets;
|
||||||
|
private String category;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SNS 콘텐츠 저장 요청 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "SNS 콘텐츠 저장 요청")
|
||||||
|
public class SnsContentSaveRequest {
|
||||||
|
|
||||||
|
@Schema(description = "콘텐츠 ID", example = "1", required = true)
|
||||||
|
@NotNull(message = "콘텐츠 ID는 필수입니다")
|
||||||
|
private Long contentId;
|
||||||
|
|
||||||
|
@Schema(description = "최종 제목", example = "맛있는 신메뉴를 소개합니다!")
|
||||||
|
private String finalTitle;
|
||||||
|
|
||||||
|
@Schema(description = "최종 콘텐츠 내용")
|
||||||
|
private String finalContent;
|
||||||
|
|
||||||
|
@Schema(description = "발행 상태", example = "PUBLISHED")
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user