fix marketing-contents errors

This commit is contained in:
박서은 2025-06-11 17:32:36 +09:00
parent 7b486d853d
commit 032e8d0b0c
11 changed files with 493 additions and 8 deletions

View File

@ -9,9 +9,16 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
* 마케팅 콘텐츠 서비스 메인 애플리케이션 클래스 * 마케팅 콘텐츠 서비스 메인 애플리케이션 클래스
* Clean Architecture 패턴을 적용한 마케팅 콘텐츠 관리 서비스 * Clean Architecture 패턴을 적용한 마케팅 콘텐츠 관리 서비스
*/ */
@SpringBootApplication(scanBasePackages = {"com.won.smarketing.content", "com.won.smarketing.common"}) @SpringBootApplication(scanBasePackages = {
@EntityScan(basePackages = {"com.won.smarketing.content.infrastructure.entity"}) "com.won.smarketing.content",
@EnableJpaRepositories(basePackages = {"com.won.smarketing.content.infrastructure.repository"}) "com.won.smarketing.common"
})
@EnableJpaRepositories(basePackages = {
"com.won.smarketing.content.infrastructure.repository"
})
@EntityScan(basePackages = {
"com.won.smarketing.content.domain.model"
})
public class MarketingContentServiceApplication { public class MarketingContentServiceApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -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 {
}

View File

@ -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;
}
}

View File

@ -131,6 +131,9 @@ public class Content {
@Column(name = "updated_at") @Column(name = "updated_at")
private LocalDateTime updatedAt; 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) {
}
// ==================== 비즈니스 로직 메서드 ==================== // ==================== 비즈니스 로직 메서드 ====================
/** /**

View File

@ -1,16 +1,13 @@
package com.won.smarketing.content.domain.model; package com.won.smarketing.content.domain.model;
import lombok.AccessLevel; import lombok.*;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
/** /**
* 콘텐츠 식별자 객체 * 콘텐츠 식별자 객체
* 콘텐츠의 고유 식별자를 나타내는 도메인 객체 * 콘텐츠의 고유 식별자를 나타내는 도메인 객체
*/ */
@Getter @Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode @EqualsAndHashCode

View File

@ -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.ContentId;
import com.won.smarketing.content.domain.model.ContentType; import com.won.smarketing.content.domain.model.ContentType;
import com.won.smarketing.content.domain.model.Platform; import com.won.smarketing.content.domain.model.Platform;
import org.springframework.stereotype.Repository;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -12,6 +13,7 @@ import java.util.Optional;
* 콘텐츠 저장소 인터페이스 * 콘텐츠 저장소 인터페이스
* 콘텐츠 도메인의 데이터 접근 추상화 * 콘텐츠 도메인의 데이터 접근 추상화
*/ */
@Repository
public interface ContentRepository { public interface ContentRepository {
/** /**

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
}
}
}

View File

@ -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());
}
}

View File

@ -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);
}