크리에이터 관리자 API
- 시리즈 생성 API 추가
This commit is contained in:
		| @@ -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<SeriesPublishedDaysOfWeek>, | ||||
|     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("랜덤과 연재요일 동시에 선택할 수 없습니다.") | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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), "시리즈가 생성되었습니다.") | ||||
|     } | ||||
| } | ||||
| @@ -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<Series, Long>, CreatorAdminContentSeriesQueryRepository | ||||
|  | ||||
| interface CreatorAdminContentSeriesQueryRepository | ||||
|  | ||||
| class CreatorAdminContentSeriesQueryRepositoryImpl( | ||||
|     private val queryFactory: JPAQueryFactory | ||||
| ) : CreatorAdminContentSeriesQueryRepository | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
| @@ -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<SeriesContent> = mutableListOf() | ||||
|  | ||||
|     @OneToMany(mappedBy = "series", cascade = [CascadeType.ALL]) | ||||
|     var keywordList: MutableList<SeriesKeyword> = mutableListOf() | ||||
| } | ||||
|   | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
| @@ -9,12 +9,23 @@ interface CreatorAdminContentSeriesGenreRepository : | ||||
|     JpaRepository<SeriesGenre, Long>, CreatorAdminContentSeriesGenreQueryRepository | ||||
|  | ||||
| interface CreatorAdminContentSeriesGenreQueryRepository { | ||||
|     fun findActiveSeriesGenreById(id: Long): SeriesGenre | ||||
|     fun getGenreList(isAdult: Boolean): List<GetGenreListResponse> | ||||
| } | ||||
|  | ||||
| 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<GetGenreListResponse> { | ||||
|         var where = seriesGenre.isActive.isTrue | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user