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
71cbbf6822
@ -9,9 +9,16 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
* 마케팅 콘텐츠 서비스 메인 애플리케이션 클래스
|
||||
* Clean Architecture 패턴을 적용한 마케팅 콘텐츠 관리 서비스
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = {"com.won.smarketing.content", "com.won.smarketing.common"})
|
||||
@EntityScan(basePackages = {"com.won.smarketing.content.infrastructure.entity"})
|
||||
@EnableJpaRepositories(basePackages = {"com.won.smarketing.content.infrastructure.repository"})
|
||||
@SpringBootApplication(scanBasePackages = {
|
||||
"com.won.smarketing.content",
|
||||
"com.won.smarketing.common"
|
||||
})
|
||||
@EnableJpaRepositories(basePackages = {
|
||||
"com.won.smarketing.content.infrastructure.repository"
|
||||
})
|
||||
@EntityScan(basePackages = {
|
||||
"com.won.smarketing.content.domain.model"
|
||||
})
|
||||
public class MarketingContentServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
// 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 {
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
|
||||
|
||||
// marketing-content/src/main/java/com/won/smarketing/content/config/ObjectMapperConfig.java
|
||||
package com.won.smarketing.content.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* ObjectMapper 설정 클래스
|
||||
*
|
||||
* @author smarketing-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Configuration
|
||||
public class ObjectMapperConfig {
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
return objectMapper;
|
||||
}
|
||||
}
|
||||
@ -131,6 +131,9 @@ public class Content {
|
||||
@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) {
|
||||
}
|
||||
|
||||
// ==================== 비즈니스 로직 메서드 ====================
|
||||
|
||||
/**
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
package com.won.smarketing.content.domain.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* 콘텐츠 식별자 값 객체
|
||||
* 콘텐츠의 고유 식별자를 나타내는 도메인 객체
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode
|
||||
|
||||
@ -4,6 +4,7 @@ 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;
|
||||
@ -12,6 +13,7 @@ import java.util.Optional;
|
||||
* 콘텐츠 저장소 인터페이스
|
||||
* 콘텐츠 도메인의 데이터 접근 추상화
|
||||
*/
|
||||
@Repository
|
||||
public interface ContentRepository {
|
||||
|
||||
/**
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
package com.won.smarketing.content.infrastructure.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 콘텐츠 조건 JPA 엔티티
|
||||
*
|
||||
* @author smarketing-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "contents_conditions")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class ContentConditionsJpaEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@OneToOne
|
||||
@JoinColumn(name = "content_id")
|
||||
private ContentJpaEntity content;
|
||||
|
||||
@Column(name = "category", length = 100)
|
||||
private String category;
|
||||
|
||||
@Column(name = "requirement", columnDefinition = "TEXT")
|
||||
private String requirement;
|
||||
|
||||
@Column(name = "tone_and_manner", length = 100)
|
||||
private String toneAndManner;
|
||||
|
||||
@Column(name = "emotion_intensity", length = 100)
|
||||
private String emotionIntensity;
|
||||
|
||||
@Column(name = "event_name", length = 200)
|
||||
private String eventName;
|
||||
|
||||
@Column(name = "start_date")
|
||||
private LocalDate startDate;
|
||||
|
||||
@Column(name = "end_date")
|
||||
private LocalDate endDate;
|
||||
|
||||
@Column(name = "photo_style", length = 100)
|
||||
private String photoStyle;
|
||||
|
||||
@Column(name = "TargetAudience", length = 100)
|
||||
private String targetAudience;
|
||||
|
||||
@Column(name = "PromotionType", length = 100)
|
||||
private String PromotionType;
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
// 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.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 콘텐츠 JPA 엔티티
|
||||
*
|
||||
* @author smarketing-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "contents")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class ContentJpaEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "store_id", nullable = false)
|
||||
private Long storeId;
|
||||
|
||||
@Column(name = "content_type", nullable = false, length = 50)
|
||||
private String contentType;
|
||||
|
||||
@Column(name = "platform", length = 50)
|
||||
private String platform;
|
||||
|
||||
@Column(name = "title", length = 500)
|
||||
private String title;
|
||||
|
||||
@Column(name = "content", columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@Column(name = "hashtags", columnDefinition = "JSON")
|
||||
private String hashtags;
|
||||
|
||||
@Column(name = "images", columnDefinition = "JSON")
|
||||
private String images;
|
||||
|
||||
@Column(name = "status", length = 50)
|
||||
private String status;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
// 연관 엔티티
|
||||
@OneToOne(mappedBy = "content", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private ContentConditionsJpaEntity conditions;
|
||||
}
|
||||
@ -0,0 +1,144 @@
|
||||
package com.won.smarketing.content.infrastructure.mapper;
|
||||
|
||||
import com.won.smarketing.content.domain.model.*;
|
||||
import com.won.smarketing.content.infrastructure.entity.ContentConditionsJpaEntity;
|
||||
import com.won.smarketing.content.infrastructure.entity.ContentJpaEntity;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 콘텐츠 도메인-엔티티 매퍼
|
||||
*
|
||||
* @author smarketing-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ContentMapper {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 도메인 모델을 JPA 엔티티로 변환합니다.
|
||||
*
|
||||
* @param content 도메인 콘텐츠
|
||||
* @return JPA 엔티티
|
||||
*/
|
||||
public ContentJpaEntity toEntity(Content content) {
|
||||
if (content == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ContentJpaEntity entity = new ContentJpaEntity();
|
||||
if (content.getId() != null) {
|
||||
entity.setId(content.getId());
|
||||
}
|
||||
entity.setStoreId(content.getStoreId());
|
||||
entity.setContentType(content.getContentType().name());
|
||||
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.setCreatedAt(content.getCreatedAt());
|
||||
entity.setUpdatedAt(content.getUpdatedAt());
|
||||
|
||||
// 조건 정보 매핑
|
||||
if (content.getCreationConditions() != null) {
|
||||
ContentConditionsJpaEntity conditionsEntity = new ContentConditionsJpaEntity();
|
||||
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);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* JPA 엔티티를 도메인 모델로 변환합니다.
|
||||
*
|
||||
* @param entity JPA 엔티티
|
||||
* @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()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* List를 JSON 문자열로 변환합니다.
|
||||
*/
|
||||
private String convertListToJson(List<String> list) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.writeValueAsString(list);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to convert list to JSON: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 문자열을 List로 변환합니다.
|
||||
*/
|
||||
private List<String> convertJsonToList(String json) {
|
||||
if (json == null || json.trim().isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json, new TypeReference<List<String>>() {});
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to convert JSON to list: {}", e.getMessage());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
package com.won.smarketing.content.infrastructure.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 com.won.smarketing.content.domain.repository.ContentRepository;
|
||||
import com.won.smarketing.content.infrastructure.entity.ContentJpaEntity;
|
||||
import com.won.smarketing.content.infrastructure.mapper.ContentMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* JPA 기반 콘텐츠 Repository 구현체
|
||||
*
|
||||
* @author smarketing-team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class JpaContentRepository implements ContentRepository {
|
||||
|
||||
private final SpringDataContentRepository springDataContentRepository;
|
||||
private final ContentMapper contentMapper;
|
||||
|
||||
/**
|
||||
* 콘텐츠를 저장합니다.
|
||||
*
|
||||
* @param content 저장할 콘텐츠
|
||||
* @return 저장된 콘텐츠
|
||||
*/
|
||||
@Override
|
||||
public Content save(Content content) {
|
||||
log.debug("Saving content: {}", content.getId());
|
||||
ContentJpaEntity entity = contentMapper.toEntity(content);
|
||||
ContentJpaEntity savedEntity = springDataContentRepository.save(entity);
|
||||
return contentMapper.toDomain(savedEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* ID로 콘텐츠를 조회합니다.
|
||||
*
|
||||
* @param id 콘텐츠 ID
|
||||
* @return 조회된 콘텐츠
|
||||
*/
|
||||
@Override
|
||||
public Optional<Content> findById(ContentId id) {
|
||||
log.debug("Finding content by id: {}", id.getValue());
|
||||
return springDataContentRepository.findById(id.getValue())
|
||||
.map(contentMapper::toDomain);
|
||||
}
|
||||
|
||||
/**
|
||||
* 필터 조건으로 콘텐츠 목록을 조회합니다.
|
||||
*
|
||||
* @param contentType 콘텐츠 타입
|
||||
* @param platform 플랫폼
|
||||
* @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);
|
||||
|
||||
List<ContentJpaEntity> entities = springDataContentRepository.findByFilters(
|
||||
contentType != null ? contentType.name() : null,
|
||||
platform != null ? platform.name() : null,
|
||||
period,
|
||||
sortBy
|
||||
);
|
||||
|
||||
return entities.stream()
|
||||
.map(contentMapper::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 진행 중인 콘텐츠 목록을 조회합니다.
|
||||
*
|
||||
* @param period 기간
|
||||
* @return 진행 중인 콘텐츠 목록
|
||||
*/
|
||||
@Override
|
||||
public List<Content> findOngoingContents(String period) {
|
||||
log.debug("Finding ongoing contents for period: {}", period);
|
||||
List<ContentJpaEntity> entities = springDataContentRepository.findOngoingContents(period);
|
||||
|
||||
return entities.stream()
|
||||
.map(contentMapper::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* ID로 콘텐츠를 삭제합니다.
|
||||
*
|
||||
* @param id 콘텐츠 ID
|
||||
*/
|
||||
@Override
|
||||
public void deleteById(ContentId id) {
|
||||
log.debug("Deleting content by id: {}", id.getValue());
|
||||
springDataContentRepository.deleteById(id.getValue());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
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);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user