diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreateSeriesRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreateSeriesRequest.kt new file mode 100644 index 0000000..658c1cc --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreateSeriesRequest.kt @@ -0,0 +1,38 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +import kr.co.vividnext.sodalive.common.SodaException + +data class CreateSeriesRequest( + val title: String, + val introduction: String, + val publishedDaysOfWeek: Set, + val keyword: String, + val genreId: Long = 0, + val isAdult: Boolean = false, + val writer: String? = null, + val studio: String? = null +) { + fun toSeries(): Series { + validate() + + return Series( + title = title, + introduction = introduction, + writer = writer, + studio = studio, + publishedDaysOfWeek = publishedDaysOfWeek, + isAdult = isAdult + ) + } + + private fun validate() { + if (title.isBlank()) throw SodaException("시리즈 제목을 입력하세요") + if (introduction.isBlank()) throw SodaException("시리즈 소개를 입력하세요") + if (keyword.isBlank()) throw SodaException("시리즈를 설명할 수 있는 키워드를 입력하세요") + if (genreId <= 0) throw SodaException("올바른 장르를 선택하세요") + if (publishedDaysOfWeek.isEmpty()) throw SodaException("시리즈 연재요일을 선택하세요") + if (publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM) && publishedDaysOfWeek.size > 1) { + throw SodaException("랜덤과 연재요일 동시에 선택할 수 없습니다.") + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesController.kt new file mode 100644 index 0000000..c288fdb --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesController.kt @@ -0,0 +1,28 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +@RestController +@PreAuthorize("hasRole('CREATOR')") +@RequestMapping("/creator-admin/audio-content/series") +class CreatorAdminContentSeriesController(private val service: CreatorAdminContentSeriesService) { + @PostMapping + fun createSeries( + @RequestPart("image") image: MultipartFile?, + @RequestPart("request") requestString: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.createSeries(image, requestString, member), "시리즈가 생성되었습니다.") + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesRepository.kt new file mode 100644 index 0000000..262ef25 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesRepository.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +import com.querydsl.jpa.impl.JPAQueryFactory +import org.springframework.data.jpa.repository.JpaRepository + +interface CreatorAdminContentSeriesRepository : JpaRepository, CreatorAdminContentSeriesQueryRepository + +interface CreatorAdminContentSeriesQueryRepository + +class CreatorAdminContentSeriesQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : CreatorAdminContentSeriesQueryRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesService.kt new file mode 100644 index 0000000..d7f5bcd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesService.kt @@ -0,0 +1,79 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +import com.amazonaws.services.s3.model.ObjectMetadata +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.hashtag.HashTag +import kr.co.vividnext.sodalive.content.hashtag.HashTagRepository +import kr.co.vividnext.sodalive.creator.admin.content.series.genre.CreatorAdminContentSeriesGenreRepository +import kr.co.vividnext.sodalive.creator.admin.content.series.keyword.SeriesKeyword +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.utils.generateFileName +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile + +@Service +class CreatorAdminContentSeriesService( + private val repository: CreatorAdminContentSeriesRepository, + private val genreRepository: CreatorAdminContentSeriesGenreRepository, + private val hashTagRepository: HashTagRepository, + + private val s3Uploader: S3Uploader, + private val objectMapper: ObjectMapper, + + @Value("\${cloud.aws.s3.bucket}") + private val coverImageBucket: String +) { + @Transactional + fun createSeries(coverImage: MultipartFile?, requestString: String, member: Member) { + if (coverImage == null) throw SodaException("커버이미지를 선택해 주세요.") + + val request = objectMapper.readValue(requestString, CreateSeriesRequest::class.java) + val series = request.toSeries() + val genre = genreRepository.findActiveSeriesGenreById(request.genreId) + series.genre = genre + repository.save(series) + + val keywords = request.keyword + .replace("#", " #") + .split(" ") + .map { it.trim() } + .filter { it.isNotBlank() } + .map { + val tag = if (!it.startsWith("#")) { + "#$it" + } else { + it + } + + val hashTag = hashTagRepository.findByTag(tag) + ?: hashTagRepository.save(HashTag(tag)) + + val seriesKeyword = SeriesKeyword() + seriesKeyword.series = series + seriesKeyword.keyword = hashTag + + seriesKeyword + } + + series.keywordList.addAll(keywords) + + val metadata = ObjectMetadata() + metadata.contentLength = coverImage.size + + // 커버 이미지 파일명 생성 + val coverImageFileName = generateFileName(prefix = "${series.id}-cover") + // 커버 이미지 업로드 + val coverImagePath = s3Uploader.upload( + inputStream = coverImage.inputStream, + bucket = coverImageBucket, + filePath = "series_cover/${series.id}/$coverImageFileName", + metadata = metadata + ) + + series.coverImage = coverImagePath + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/Series.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/Series.kt index b8c7521..3590f5a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/Series.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/Series.kt @@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.creator.admin.content.series import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.creator.admin.content.series.keyword.SeriesKeyword +import javax.persistence.CascadeType import javax.persistence.CollectionTable import javax.persistence.Column import javax.persistence.ElementCollection @@ -10,6 +12,7 @@ import javax.persistence.EnumType import javax.persistence.Enumerated import javax.persistence.FetchType import javax.persistence.JoinColumn +import javax.persistence.OneToMany import javax.persistence.OneToOne enum class SeriesPublishedDaysOfWeek { @@ -27,6 +30,8 @@ data class Series( var introduction: String, @Enumerated(value = EnumType.STRING) var state: SeriesState = SeriesState.PROCEEDING, + var writer: String? = null, + var studio: String? = null, @ElementCollection(targetClass = SeriesPublishedDaysOfWeek::class, fetch = FetchType.EAGER) @Enumerated(value = EnumType.STRING) @CollectionTable(name = "series_published_days_of_week", joinColumns = [JoinColumn(name = "series_id")]) @@ -39,4 +44,10 @@ data class Series( var genre: SeriesGenre? = null var coverImage: String? = null + + @OneToMany(mappedBy = "series", cascade = [CascadeType.ALL]) + var contentList: MutableList = mutableListOf() + + @OneToMany(mappedBy = "series", cascade = [CascadeType.ALL]) + var keywordList: MutableList = mutableListOf() } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/SeriesContent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/SeriesContent.kt new file mode 100644 index 0000000..eabc927 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/SeriesContent.kt @@ -0,0 +1,34 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +import kr.co.vividnext.sodalive.content.AudioContent +import java.time.LocalDateTime +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.PrePersist + +@Entity +class SeriesContent { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null + + var createdAt: LocalDateTime? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "series_id", nullable = false) + var series: Series? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "content_id", nullable = false) + var content: AudioContent? = null + + @PrePersist + fun prePersist() { + createdAt = LocalDateTime.now() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/genre/CreatorAdminContentSeriesGenreRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/genre/CreatorAdminContentSeriesGenreRepository.kt index e35112d..78fcc34 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/genre/CreatorAdminContentSeriesGenreRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/genre/CreatorAdminContentSeriesGenreRepository.kt @@ -9,12 +9,23 @@ interface CreatorAdminContentSeriesGenreRepository : JpaRepository, CreatorAdminContentSeriesGenreQueryRepository interface CreatorAdminContentSeriesGenreQueryRepository { + fun findActiveSeriesGenreById(id: Long): SeriesGenre fun getGenreList(isAdult: Boolean): List } class CreatorAdminContentSeriesGenreQueryRepositoryImpl( private val queryFactory: JPAQueryFactory ) : CreatorAdminContentSeriesGenreQueryRepository { + override fun findActiveSeriesGenreById(id: Long): SeriesGenre { + return queryFactory + .selectFrom(seriesGenre) + .where( + seriesGenre.id.eq(id) + .and(seriesGenre.isActive.isTrue) + ) + .fetchFirst() + } + override fun getGenreList(isAdult: Boolean): List { var where = seriesGenre.isActive.isTrue diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/keyword/SeriesKeyword.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/keyword/SeriesKeyword.kt index 32d90dd..d36e591 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/keyword/SeriesKeyword.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/keyword/SeriesKeyword.kt @@ -1,15 +1,25 @@ package kr.co.vividnext.sodalive.creator.admin.content.series.keyword -import kr.co.vividnext.sodalive.common.BaseEntity import kr.co.vividnext.sodalive.content.hashtag.HashTag import kr.co.vividnext.sodalive.creator.admin.content.series.Series +import java.time.LocalDateTime import javax.persistence.Entity import javax.persistence.FetchType +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id import javax.persistence.JoinColumn import javax.persistence.ManyToOne +import javax.persistence.PrePersist @Entity -class SeriesKeyword : BaseEntity() { +class SeriesKeyword { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null + + var createdAt: LocalDateTime? = null + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "series_id", nullable = false) var series: Series? = null @@ -18,5 +28,8 @@ class SeriesKeyword : BaseEntity() { @JoinColumn(name = "keyword_id", nullable = false) var keyword: HashTag? = null - var isActive: Boolean = true + @PrePersist + fun prePersist() { + createdAt = LocalDateTime.now() + } }