| @@ -0,0 +1,67 @@ | ||||
| package kr.co.vividnext.sodalive.content.series | ||||
|  | ||||
| import kr.co.vividnext.sodalive.common.ApiResponse | ||||
| import kr.co.vividnext.sodalive.common.SodaException | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesSortType | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import org.springframework.data.domain.Pageable | ||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal | ||||
| import org.springframework.web.bind.annotation.GetMapping | ||||
| import org.springframework.web.bind.annotation.PathVariable | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RequestParam | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
|  | ||||
| @RestController | ||||
| @RequestMapping("/audio-content/series") | ||||
| class ContentSeriesController(private val service: ContentSeriesService) { | ||||
|     @GetMapping | ||||
|     fun getSeriesList( | ||||
|         @RequestParam creatorId: Long, | ||||
|         @RequestParam("sortType", required = false) sortType: SeriesSortType = SeriesSortType.NEWEST, | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, | ||||
|         pageable: Pageable | ||||
|     ) = run { | ||||
|         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") | ||||
|  | ||||
|         ApiResponse.ok( | ||||
|             service.getSeriesList( | ||||
|                 creatorId = creatorId, | ||||
|                 sortType = sortType, | ||||
|                 member = member, | ||||
|                 offset = pageable.offset, | ||||
|                 limit = pageable.pageSize.toLong() | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     @GetMapping("/{id}") | ||||
|     fun getSeriesDetail( | ||||
|         @PathVariable id: Long, | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||
|     ) = run { | ||||
|         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") | ||||
|  | ||||
|         ApiResponse.ok( | ||||
|             service.getSeriesDetail(seriesId = id, member = member) | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     @GetMapping("/{id}/content") | ||||
|     fun getSeriesContentList( | ||||
|         @PathVariable id: Long, | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, | ||||
|         pageable: Pageable | ||||
|     ) = run { | ||||
|         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") | ||||
|  | ||||
|         ApiResponse.ok( | ||||
|             service.getSeriesContentList( | ||||
|                 seriesId = id, | ||||
|                 member = member, | ||||
|                 offset = pageable.offset, | ||||
|                 limit = pageable.pageSize.toLong() | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,110 @@ | ||||
| package kr.co.vividnext.sodalive.content.series | ||||
|  | ||||
| import com.querydsl.jpa.impl.JPAQueryFactory | ||||
| import kr.co.vividnext.sodalive.content.QAudioContent.audioContent | ||||
| import kr.co.vividnext.sodalive.content.hashtag.QHashTag.hashTag | ||||
| import kr.co.vividnext.sodalive.content.series.content.GetSeriesContentMinMaxPriceResponse | ||||
| import kr.co.vividnext.sodalive.content.series.content.QGetSeriesContentMinMaxPriceResponse | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.Series | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.keyword.QSeriesKeyword.seriesKeyword | ||||
| import org.springframework.data.jpa.repository.JpaRepository | ||||
|  | ||||
| interface ContentSeriesRepository : JpaRepository<Series, Long>, ContentSeriesQueryRepository | ||||
|  | ||||
| interface ContentSeriesQueryRepository { | ||||
|     fun getSeriesTotalCount(creatorId: Long, isAuth: Boolean): Int | ||||
|     fun getSeriesList( | ||||
|         imageHost: String, | ||||
|         creatorId: Long, | ||||
|         isAuth: Boolean, | ||||
|         offset: Long, | ||||
|         limit: Long | ||||
|     ): List<Series> | ||||
|  | ||||
|     fun getSeriesDetail(seriesId: Long, isAuth: Boolean): Series? | ||||
|     fun getKeywordList(seriesId: Long): List<String> | ||||
|     fun getSeriesContentMinMaxPrice(seriesId: Long): GetSeriesContentMinMaxPriceResponse | ||||
| } | ||||
|  | ||||
| class ContentSeriesQueryRepositoryImpl( | ||||
|     private val queryFactory: JPAQueryFactory | ||||
| ) : ContentSeriesQueryRepository { | ||||
|     override fun getSeriesTotalCount(creatorId: Long, isAuth: Boolean): Int { | ||||
|         var where = series.member.id.eq(creatorId) | ||||
|             .and(series.isActive.isTrue) | ||||
|  | ||||
|         if (!isAuth) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .select(series.id) | ||||
|             .from(series) | ||||
|             .where(where) | ||||
|             .fetch() | ||||
|             .size | ||||
|     } | ||||
|  | ||||
|     override fun getSeriesList( | ||||
|         imageHost: String, | ||||
|         creatorId: Long, | ||||
|         isAuth: Boolean, | ||||
|         offset: Long, | ||||
|         limit: Long | ||||
|     ): List<Series> { | ||||
|         var where = series.member.id.eq(creatorId) | ||||
|             .and(series.isActive.isTrue) | ||||
|  | ||||
|         if (!isAuth) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .selectFrom(series) | ||||
|             .where(where) | ||||
|             .offset(offset) | ||||
|             .limit(limit) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     override fun getSeriesDetail(seriesId: Long, isAuth: Boolean): Series? { | ||||
|         var where = series.id.eq(seriesId) | ||||
|             .and(series.isActive.isTrue) | ||||
|  | ||||
|         if (!isAuth) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .selectFrom(series) | ||||
|             .where(where) | ||||
|             .fetchFirst() | ||||
|     } | ||||
|  | ||||
|     override fun getKeywordList(seriesId: Long): List<String> { | ||||
|         return queryFactory | ||||
|             .select(hashTag.tag) | ||||
|             .from(series) | ||||
|             .innerJoin(series.keywordList, seriesKeyword) | ||||
|             .innerJoin(seriesKeyword.keyword, hashTag) | ||||
|             .where(series.id.eq(seriesId)) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     override fun getSeriesContentMinMaxPrice(seriesId: Long): GetSeriesContentMinMaxPriceResponse { | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QGetSeriesContentMinMaxPriceResponse( | ||||
|                     audioContent.price.min(), | ||||
|                     audioContent.price.max() | ||||
|                 ) | ||||
|             ) | ||||
|             .from(series) | ||||
|             .innerJoin(series.contentList, seriesContent) | ||||
|             .innerJoin(seriesContent.content, audioContent) | ||||
|             .where(series.id.eq(seriesId)) | ||||
|             .fetchFirst() | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,192 @@ | ||||
| package kr.co.vividnext.sodalive.content.series | ||||
|  | ||||
| import kr.co.vividnext.sodalive.common.SodaException | ||||
| import kr.co.vividnext.sodalive.content.order.OrderRepository | ||||
| import kr.co.vividnext.sodalive.content.order.OrderType | ||||
| import kr.co.vividnext.sodalive.content.series.content.ContentSeriesContentRepository | ||||
| import kr.co.vividnext.sodalive.content.series.content.GetSeriesContentListResponse | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesSortType | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState | ||||
| import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import org.springframework.beans.factory.annotation.Value | ||||
| import org.springframework.stereotype.Service | ||||
| import java.time.LocalDateTime | ||||
| import java.time.ZoneId | ||||
| import java.time.format.DateTimeFormatter | ||||
|  | ||||
| @Service | ||||
| class ContentSeriesService( | ||||
|     private val repository: ContentSeriesRepository, | ||||
|     private val orderRepository: OrderRepository, | ||||
|     private val explorerQueryRepository: ExplorerQueryRepository, | ||||
|     private val seriesContentRepository: ContentSeriesContentRepository, | ||||
|  | ||||
|     @Value("\${cloud.aws.cloud-front.host}") | ||||
|     private val coverImageHost: String | ||||
| ) { | ||||
|     fun getSeriesList( | ||||
|         creatorId: Long, | ||||
|         member: Member, | ||||
|         sortType: SeriesSortType = SeriesSortType.NEWEST, | ||||
|         offset: Long = 0, | ||||
|         limit: Long = 10 | ||||
|     ): GetSeriesListResponse { | ||||
|         val totalCount = repository.getSeriesTotalCount(creatorId = creatorId, isAuth = member.auth != null) | ||||
|         val rawItems = repository.getSeriesList( | ||||
|             imageHost = coverImageHost, | ||||
|             creatorId = creatorId, | ||||
|             isAuth = member.auth != null, | ||||
|             offset = offset, | ||||
|             limit = limit | ||||
|         ) | ||||
|  | ||||
|         val items = rawItems | ||||
|             .map { | ||||
|                 GetSeriesListResponse.SeriesListItem( | ||||
|                     seriesId = it.id!!, | ||||
|                     title = it.title, | ||||
|                     coverImage = "$coverImageHost/${it.coverImage!!}", | ||||
|                     publishedDaysOfWeek = publishedDaysOfWeekText(it.publishedDaysOfWeek), | ||||
|                     isComplete = it.state == SeriesState.COMPLETE, | ||||
|                     creator = GetSeriesListResponse.SeriesListItemCreator( | ||||
|                         creatorId = it.member!!.id!!, | ||||
|                         nickname = it.member!!.nickname, | ||||
|                         profileImage = "$coverImageHost/${it.member!!.profileImage!!}" | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|             .map { | ||||
|                 it.numberOfContent = seriesContentRepository.getContentCount( | ||||
|                     seriesId = it.seriesId, | ||||
|                     isAdult = member.auth != null | ||||
|                 ) | ||||
|  | ||||
|                 it | ||||
|             } | ||||
|             .map { | ||||
|                 val nowDateTime = LocalDateTime.now() | ||||
|  | ||||
|                 it.isNew = seriesContentRepository.isNewContent( | ||||
|                     seriesId = it.seriesId, | ||||
|                     isAdult = member.auth == null, | ||||
|                     fromDate = nowDateTime.minusDays(7), | ||||
|                     nowDate = nowDateTime | ||||
|                 ) | ||||
|  | ||||
|                 it | ||||
|             } | ||||
|  | ||||
|         return GetSeriesListResponse(totalCount, items) | ||||
|     } | ||||
|  | ||||
|     fun getSeriesDetail(seriesId: Long, member: Member): GetSeriesDetailResponse { | ||||
|         val series = repository.getSeriesDetail( | ||||
|             seriesId = seriesId, | ||||
|             isAuth = member.auth != null | ||||
|         ) ?: throw SodaException("잘못된 시리즈 입니다.\n다시 시도해 주세요") | ||||
|  | ||||
|         val isFollow = explorerQueryRepository.isFollow( | ||||
|             creatorId = series.member!!.id!!, | ||||
|             memberId = member.id!! | ||||
|         ) | ||||
|  | ||||
|         val keywordList = repository.getKeywordList(seriesId = seriesId) | ||||
|             .filter { it.isNotBlank() } | ||||
|  | ||||
|         val minMaxPrice = repository.getSeriesContentMinMaxPrice(seriesId = seriesId) | ||||
|         val minPrice = minMaxPrice.minPrice | ||||
|         val maxPrice = minMaxPrice.maxPrice | ||||
|         val rentalMinPrice = (minMaxPrice.minPrice * 0.7).toInt() | ||||
|         val rentalMaxPrice = (minMaxPrice.maxPrice * 0.7).toInt() | ||||
|  | ||||
|         val seriesContentList = getSeriesContentList(seriesId = seriesId, member = member, offset = 0, limit = 5) | ||||
|  | ||||
|         val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") | ||||
|         return GetSeriesDetailResponse( | ||||
|             seriesId = seriesId, | ||||
|             title = series.title, | ||||
|             coverImage = "$coverImageHost/${series.coverImage}", | ||||
|             introduction = series.introduction, | ||||
|             genre = series.genre!!.genre, | ||||
|             isAdult = series.isAdult, | ||||
|             writer = series.writer, | ||||
|             studio = series.studio, | ||||
|             publishedDate = series.createdAt!! | ||||
|                 .atZone(ZoneId.of("UTC")) | ||||
|                 .withZoneSameInstant(ZoneId.of("Asia/Seoul")) | ||||
|                 .toLocalDateTime() | ||||
|                 .format(dateTimeFormatter), | ||||
|             creator = GetSeriesDetailResponse.GetSeriesDetailCreator( | ||||
|                 creatorId = series.member!!.id!!, | ||||
|                 nickname = series.member!!.nickname, | ||||
|                 profileImage = "$coverImageHost/${series.member!!.profileImage}", | ||||
|                 isFollow = isFollow | ||||
|             ), | ||||
|             rentalMinPrice = rentalMinPrice, | ||||
|             rentalMaxPrice = rentalMaxPrice, | ||||
|             rentalPeriod = 15, | ||||
|             minPrice = minPrice, | ||||
|             maxPrice = maxPrice, | ||||
|             keywordList = keywordList, | ||||
|             publishedDaysOfWeek = publishedDaysOfWeekText(series.publishedDaysOfWeek), | ||||
|             contentList = seriesContentList.items, | ||||
|             contentCount = seriesContentList.totalCount | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     fun getSeriesContentList(seriesId: Long, member: Member, offset: Long, limit: Long): GetSeriesContentListResponse { | ||||
|         val isAdult = member.auth != null | ||||
|  | ||||
|         val totalCount = seriesContentRepository.getContentCount(seriesId, isAdult = isAdult) | ||||
|         val contentList = seriesContentRepository.getContentList( | ||||
|             seriesId = seriesId, | ||||
|             isAdult = isAdult, | ||||
|             imageHost = coverImageHost, | ||||
|             offset = offset, | ||||
|             limit = limit | ||||
|         ) | ||||
|             .map { | ||||
|                 val (isExistsAudioContent, orderType) = orderRepository.isExistOrderedAndOrderType( | ||||
|                     memberId = member.id!!, | ||||
|                     contentId = it.contentId | ||||
|                 ) | ||||
|  | ||||
|                 if (isExistsAudioContent) { | ||||
|                     if (orderType == OrderType.RENTAL) { | ||||
|                         it.isRented = true | ||||
|                     } else { | ||||
|                         it.isOwned = true | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 it | ||||
|             } | ||||
|  | ||||
|         return GetSeriesContentListResponse(totalCount, contentList) | ||||
|     } | ||||
|  | ||||
|     private fun publishedDaysOfWeekText(publishedDaysOfWeek: Set<SeriesPublishedDaysOfWeek>): String { | ||||
|         val dayOfWeekText = publishedDaysOfWeek.toList().sortedBy { it.ordinal } | ||||
|             .map { | ||||
|                 when (it) { | ||||
|                     SeriesPublishedDaysOfWeek.SUN -> "일" | ||||
|                     SeriesPublishedDaysOfWeek.MON -> "월" | ||||
|                     SeriesPublishedDaysOfWeek.TUE -> "화" | ||||
|                     SeriesPublishedDaysOfWeek.WED -> "수" | ||||
|                     SeriesPublishedDaysOfWeek.THU -> "목" | ||||
|                     SeriesPublishedDaysOfWeek.FRI -> "금" | ||||
|                     SeriesPublishedDaysOfWeek.SAT -> "토" | ||||
|                     SeriesPublishedDaysOfWeek.RANDOM -> "랜덤" | ||||
|                 } | ||||
|             } | ||||
|             .joinToString(", ") { it } | ||||
|  | ||||
|         return if (publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM)) { | ||||
|             dayOfWeekText | ||||
|         } else { | ||||
|             "매주 $dayOfWeekText" | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,32 @@ | ||||
| package kr.co.vividnext.sodalive.content.series | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.series.content.GetSeriesContentListItem | ||||
|  | ||||
| data class GetSeriesDetailResponse( | ||||
|     val seriesId: Long, | ||||
|     val title: String, | ||||
|     val coverImage: String, | ||||
|     val introduction: String, | ||||
|     val genre: String, | ||||
|     val isAdult: Boolean, | ||||
|     val writer: String?, | ||||
|     val studio: String?, | ||||
|     val publishedDate: String, | ||||
|     val creator: GetSeriesDetailCreator, | ||||
|     var rentalMinPrice: Int, | ||||
|     var rentalMaxPrice: Int, | ||||
|     val rentalPeriod: Int, | ||||
|     val minPrice: Int, | ||||
|     val maxPrice: Int, | ||||
|     val keywordList: List<String>, | ||||
|     val publishedDaysOfWeek: String, | ||||
|     val contentList: List<GetSeriesContentListItem>, | ||||
|     val contentCount: Int | ||||
| ) { | ||||
|     data class GetSeriesDetailCreator( | ||||
|         val creatorId: Long, | ||||
|         val nickname: String, | ||||
|         val profileImage: String, | ||||
|         val isFollow: Boolean | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,24 @@ | ||||
| package kr.co.vividnext.sodalive.content.series | ||||
|  | ||||
| data class GetSeriesListResponse( | ||||
|     val totalCount: Int, | ||||
|     val items: List<SeriesListItem> | ||||
| ) { | ||||
|     data class SeriesListItem( | ||||
|         val seriesId: Long, | ||||
|         val title: String, | ||||
|         val coverImage: String, | ||||
|         val publishedDaysOfWeek: String, | ||||
|         val isComplete: Boolean = false, | ||||
|         val creator: SeriesListItemCreator, | ||||
|         var numberOfContent: Int = 0, | ||||
|         var isNew: Boolean = false, | ||||
|         var isPopular: Boolean = false | ||||
|     ) | ||||
|  | ||||
|     data class SeriesListItemCreator( | ||||
|         val creatorId: Long, | ||||
|         val nickname: String, | ||||
|         val profileImage: String | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,123 @@ | ||||
| package kr.co.vividnext.sodalive.content.series.content | ||||
|  | ||||
| import com.querydsl.core.types.dsl.Expressions | ||||
| import com.querydsl.jpa.impl.JPAQueryFactory | ||||
| import kr.co.vividnext.sodalive.content.QAudioContent.audioContent | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent | ||||
| import org.springframework.data.jpa.repository.JpaRepository | ||||
| import java.time.LocalDateTime | ||||
|  | ||||
| interface ContentSeriesContentRepository : JpaRepository<SeriesContent, Long>, ContentSeriesContentQueryRepository | ||||
|  | ||||
| interface ContentSeriesContentQueryRepository { | ||||
|     fun getContentCount(seriesId: Long, isAdult: Boolean): Int | ||||
|     fun getContentList( | ||||
|         seriesId: Long, | ||||
|         isAdult: Boolean, | ||||
|         imageHost: String, | ||||
|         offset: Long, | ||||
|         limit: Long | ||||
|     ): List<GetSeriesContentListItem> | ||||
|  | ||||
|     fun isNewContent(seriesId: Long, isAdult: Boolean, fromDate: LocalDateTime, nowDate: LocalDateTime): Boolean | ||||
| } | ||||
|  | ||||
| class ContentSeriesContentQueryRepositoryImpl( | ||||
|     private val queryFactory: JPAQueryFactory | ||||
| ) : ContentSeriesContentQueryRepository { | ||||
|     override fun getContentCount(seriesId: Long, isAdult: Boolean): Int { | ||||
|         var where = seriesContent.series.id.eq(seriesId) | ||||
|             .and(seriesContent.content.isActive.isTrue) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(seriesContent.content.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .select(seriesContent.id) | ||||
|             .from(seriesContent) | ||||
|             .innerJoin(seriesContent.series, series) | ||||
|             .innerJoin(seriesContent.content, audioContent) | ||||
|             .where(where) | ||||
|             .fetch() | ||||
|             .size | ||||
|     } | ||||
|  | ||||
|     override fun getContentList( | ||||
|         seriesId: Long, | ||||
|         isAdult: Boolean, | ||||
|         imageHost: String, | ||||
|         offset: Long, | ||||
|         limit: Long | ||||
|     ): List<GetSeriesContentListItem> { | ||||
|         var where = series.id.eq(seriesId) | ||||
|             .and(audioContent.isActive.isTrue) | ||||
|             .and(audioContent.duration.isNotNull) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(audioContent.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         val formattedDate = Expressions.stringTemplate( | ||||
|             "DATE_FORMAT({0}, {1})", | ||||
|             Expressions.dateTimeTemplate( | ||||
|                 LocalDateTime::class.java, | ||||
|                 "CONVERT_TZ({0},{1},{2})", | ||||
|                 audioContent.releaseDate, | ||||
|                 "UTC", | ||||
|                 "Asia/Seoul" | ||||
|             ), | ||||
|             "%y.%m.%d" | ||||
|         ) | ||||
|  | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QGetSeriesContentListItem( | ||||
|                     audioContent.id, | ||||
|                     audioContent.title, | ||||
|                     audioContent.coverImage.prepend("/").prepend(imageHost), | ||||
|                     formattedDate, | ||||
|                     audioContent.duration, | ||||
|                     audioContent.price, | ||||
|                     Expressions.asBoolean(false), | ||||
|                     Expressions.asBoolean(false) | ||||
|                 ) | ||||
|             ) | ||||
|             .from(seriesContent) | ||||
|             .innerJoin(seriesContent.series, series) | ||||
|             .innerJoin(seriesContent.content, audioContent) | ||||
|             .where(where) | ||||
|             .offset(offset) | ||||
|             .limit(limit) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     override fun isNewContent( | ||||
|         seriesId: Long, | ||||
|         isAdult: Boolean, | ||||
|         fromDate: LocalDateTime, | ||||
|         nowDate: LocalDateTime | ||||
|     ): Boolean { | ||||
|         var where = seriesContent.series.id.eq(seriesId) | ||||
|             .and(seriesContent.content.isActive.isTrue) | ||||
|             .and(seriesContent.content.releaseDate.after(fromDate)) | ||||
|             .and(seriesContent.content.releaseDate.before(nowDate)) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(seriesContent.content.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         val itemCount = queryFactory | ||||
|             .select(seriesContent.id) | ||||
|             .from(seriesContent) | ||||
|             .innerJoin(seriesContent.series, series) | ||||
|             .innerJoin(seriesContent.content, audioContent) | ||||
|             .where(where) | ||||
|             .fetch() | ||||
|             .size | ||||
|  | ||||
|         return itemCount > 0 | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,19 @@ | ||||
| package kr.co.vividnext.sodalive.content.series.content | ||||
|  | ||||
| import com.querydsl.core.annotations.QueryProjection | ||||
|  | ||||
| data class GetSeriesContentListResponse( | ||||
|     val totalCount: Int, | ||||
|     val items: List<GetSeriesContentListItem> | ||||
| ) | ||||
|  | ||||
| data class GetSeriesContentListItem @QueryProjection constructor( | ||||
|     val contentId: Long, | ||||
|     val title: String, | ||||
|     val coverImage: String, | ||||
|     val releaseDate: String, | ||||
|     val duration: String, | ||||
|     val price: Int, | ||||
|     var isRented: Boolean, | ||||
|     var isOwned: Boolean | ||||
| ) | ||||
| @@ -0,0 +1,8 @@ | ||||
| package kr.co.vividnext.sodalive.content.series.content | ||||
|  | ||||
| import com.querydsl.core.annotations.QueryProjection | ||||
|  | ||||
| data class GetSeriesContentMinMaxPriceResponse @QueryProjection constructor( | ||||
|     val minPrice: Int, | ||||
|     val maxPrice: Int | ||||
| ) | ||||
| @@ -5,7 +5,7 @@ import kr.co.vividnext.sodalive.common.SodaException | ||||
| data class CreateSeriesRequest( | ||||
|     val title: String, | ||||
|     val introduction: String, | ||||
|     val publishedDaysOfWeek: Set<SeriesPublishedDaysOfWeek>, | ||||
|     val publishedDaysOfWeek: MutableSet<SeriesPublishedDaysOfWeek>, | ||||
|     val keyword: String, | ||||
|     val genreId: Long = 0, | ||||
|     val isAdult: Boolean = false, | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.PostMapping | ||||
| import org.springframework.web.bind.annotation.PutMapping | ||||
| import org.springframework.web.bind.annotation.RequestBody | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RequestParam | ||||
| import org.springframework.web.bind.annotation.RequestPart | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
| import org.springframework.web.multipart.MultipartFile | ||||
| @@ -116,4 +117,21 @@ class CreatorAdminContentSeriesController(private val service: CreatorAdminConte | ||||
|             "콘텐츠를 삭제하였습니다." | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     @GetMapping("/content/search") | ||||
|     fun searchContentNotInSeries( | ||||
|         @RequestParam(value = "series_id") seriesId: Long, | ||||
|         @RequestParam(value = "search_word") searchWord: String, | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||
|     ) = run { | ||||
|         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") | ||||
|  | ||||
|         ApiResponse.ok( | ||||
|             service.searchContentNotInSeries( | ||||
|                 seriesId = seriesId, | ||||
|                 searchWord = searchWord, | ||||
|                 memberId = member.id!! | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,8 @@ import com.querydsl.jpa.impl.JPAQueryFactory | ||||
| import kr.co.vividnext.sodalive.content.QAudioContent.audioContent | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.content.QSearchContentNotInSeriesResponse | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.content.SearchContentNotInSeriesResponse | ||||
| import org.springframework.data.jpa.repository.JpaRepository | ||||
|  | ||||
| interface CreatorAdminContentSeriesRepository : JpaRepository<Series, Long>, CreatorAdminContentSeriesQueryRepository | ||||
| @@ -14,17 +16,24 @@ interface CreatorAdminContentSeriesQueryRepository { | ||||
|     fun getSeriesList( | ||||
|         offset: Long, | ||||
|         limit: Long, | ||||
|         creatorId: Long, | ||||
|         imageHost: String | ||||
|     ): List<GetCreatorAdminContentSeriesListItem> | ||||
|         creatorId: Long | ||||
|     ): List<Series> | ||||
|  | ||||
|     fun getSeriesContentCount(creatorId: Long): Int | ||||
|     fun getSeriesContentList( | ||||
|         offset: Long, | ||||
|         limit: Long, | ||||
|         seriesId: Long, | ||||
|         creatorId: Long, | ||||
|         imageHost: String | ||||
|     ): List<GetCreatorAdminContentSeriesContentItem> | ||||
|  | ||||
|     fun searchContentNotInSeries( | ||||
|         seriesId: Long, | ||||
|         searchWord: String, | ||||
|         memberId: Long, | ||||
|         imageHost: String | ||||
|     ): List<SearchContentNotInSeriesResponse> | ||||
| } | ||||
|  | ||||
| class CreatorAdminContentSeriesQueryRepositoryImpl( | ||||
| @@ -55,18 +64,10 @@ class CreatorAdminContentSeriesQueryRepositoryImpl( | ||||
|     override fun getSeriesList( | ||||
|         offset: Long, | ||||
|         limit: Long, | ||||
|         creatorId: Long, | ||||
|         imageHost: String | ||||
|     ): List<GetCreatorAdminContentSeriesListItem> { | ||||
|         creatorId: Long | ||||
|     ): List<Series> { | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QGetCreatorAdminContentSeriesListItem( | ||||
|                     series.id, | ||||
|                     series.title, | ||||
|                     series.coverImage.prepend("/").prepend(imageHost) | ||||
|                 ) | ||||
|             ) | ||||
|             .from(series) | ||||
|             .selectFrom(series) | ||||
|             .where( | ||||
|                 series.member.id.eq(creatorId) | ||||
|                     .and(series.isActive.isTrue) | ||||
| @@ -95,6 +96,7 @@ class CreatorAdminContentSeriesQueryRepositoryImpl( | ||||
|     override fun getSeriesContentList( | ||||
|         offset: Long, | ||||
|         limit: Long, | ||||
|         seriesId: Long, | ||||
|         creatorId: Long, | ||||
|         imageHost: String | ||||
|     ): List<GetCreatorAdminContentSeriesContentItem> { | ||||
| @@ -112,6 +114,7 @@ class CreatorAdminContentSeriesQueryRepositoryImpl( | ||||
|             .innerJoin(seriesContent.content, audioContent) | ||||
|             .where( | ||||
|                 series.member.id.eq(creatorId) | ||||
|                     .and(series.id.eq(seriesId)) | ||||
|                     .and(audioContent.member.id.eq(creatorId)) | ||||
|                     .and(series.isActive.isTrue) | ||||
|             ) | ||||
| @@ -119,4 +122,35 @@ class CreatorAdminContentSeriesQueryRepositoryImpl( | ||||
|             .limit(limit) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     override fun searchContentNotInSeries( | ||||
|         seriesId: Long, | ||||
|         searchWord: String, | ||||
|         memberId: Long, | ||||
|         imageHost: String | ||||
|     ): List<SearchContentNotInSeriesResponse> { | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QSearchContentNotInSeriesResponse( | ||||
|                     audioContent.id, | ||||
|                     audioContent.title, | ||||
|                     audioContent.coverImage.prepend("/").prepend(imageHost) | ||||
|                 ) | ||||
|             ) | ||||
|             .from(audioContent) | ||||
|             .leftJoin(seriesContent) | ||||
|             .on( | ||||
|                 audioContent.id.eq(seriesContent.content.id) | ||||
|                     .and(seriesContent.series.id.eq(seriesId)) | ||||
|             ) | ||||
|             .where( | ||||
|                 audioContent.duration.isNotNull | ||||
|                     .and(audioContent.member.isNotNull) | ||||
|                     .and(audioContent.member.id.eq(memberId)) | ||||
|                     .and(audioContent.isActive.isTrue.or(audioContent.releaseDate.isNotNull)) | ||||
|                     .and(audioContent.title.contains(searchWord)) | ||||
|                     .and(seriesContent.id.isNull) | ||||
|             ) | ||||
|             .fetch() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -9,6 +9,7 @@ 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.content.AddingContentToTheSeriesRequest | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.content.RemoveContentToTheSeriesRequest | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.content.SearchContentNotInSeriesResponse | ||||
| 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 | ||||
| @@ -48,17 +49,20 @@ class CreatorAdminContentSeriesService( | ||||
|         val keywords = request.keyword | ||||
|             .replace("#", " #") | ||||
|             .split(" ") | ||||
|             .asSequence() | ||||
|             .map { it.trim() } | ||||
|             .filter { it.isNotBlank() } | ||||
|             .map { | ||||
|                 val tag = if (!it.startsWith("#")) { | ||||
|                 if (!it.startsWith("#")) { | ||||
|                     "#$it" | ||||
|                 } else { | ||||
|                     it | ||||
|                 } | ||||
|  | ||||
|                 val hashTag = hashTagRepository.findByTag(tag) | ||||
|                     ?: hashTagRepository.save(HashTag(tag)) | ||||
|             } | ||||
|             .toSet() | ||||
|             .map { | ||||
|                 val hashTag = hashTagRepository.findByTag(it) | ||||
|                     ?: hashTagRepository.save(HashTag(it)) | ||||
|  | ||||
|                 val seriesKeyword = SeriesKeyword() | ||||
|                 seriesKeyword.series = series | ||||
| @@ -66,6 +70,7 @@ class CreatorAdminContentSeriesService( | ||||
|  | ||||
|                 seriesKeyword | ||||
|             } | ||||
|             .toList() | ||||
|  | ||||
|         series.keywordList.addAll(keywords) | ||||
|  | ||||
| @@ -96,8 +101,10 @@ class CreatorAdminContentSeriesService( | ||||
|             request.publishedDaysOfWeek == null && | ||||
|             request.genreId == null && | ||||
|             request.isAdult == null && | ||||
|             request.state == null && | ||||
|             request.writer == null && | ||||
|             request.studio == null | ||||
|             request.studio == null && | ||||
|             request.isActive == null | ||||
|         ) { | ||||
|             throw SodaException("변경사항이 없습니다.") | ||||
|         } | ||||
| @@ -138,8 +145,8 @@ class CreatorAdminContentSeriesService( | ||||
|                 throw SodaException("랜덤과 연재요일 동시에 선택할 수 없습니다.") | ||||
|             } | ||||
|  | ||||
|             series.publishedDaysOfWeek.toMutableSet().clear() | ||||
|             series.publishedDaysOfWeek.toMutableSet().addAll(request.publishedDaysOfWeek) | ||||
|             series.publishedDaysOfWeek.clear() | ||||
|             series.publishedDaysOfWeek.addAll(request.publishedDaysOfWeek) | ||||
|         } | ||||
|  | ||||
|         if (request.genreId != null) { | ||||
| @@ -151,6 +158,10 @@ class CreatorAdminContentSeriesService( | ||||
|             series.isAdult = request.isAdult | ||||
|         } | ||||
|  | ||||
|         if (request.state != null) { | ||||
|             series.state = request.state | ||||
|         } | ||||
|  | ||||
|         if (request.isActive != null) { | ||||
|             series.isActive = request.isActive | ||||
|         } | ||||
| @@ -169,9 +180,23 @@ class CreatorAdminContentSeriesService( | ||||
|         val seriesList = repository.getSeriesList( | ||||
|             offset = offset, | ||||
|             limit = limit, | ||||
|             creatorId = creatorId, | ||||
|             imageHost = coverImageHost | ||||
|             creatorId = creatorId | ||||
|         ) | ||||
|             .map { | ||||
|                 GetCreatorAdminContentSeriesListItem( | ||||
|                     seriesId = it.id!!, | ||||
|                     title = it.title, | ||||
|                     introduction = it.introduction, | ||||
|                     coverImageUrl = "$coverImageHost/${it.coverImage!!}", | ||||
|                     publishedDaysOfWeek = it.publishedDaysOfWeek.toList(), | ||||
|                     genreId = it.genre!!.id!!, | ||||
|                     isAdult = it.isAdult, | ||||
|                     state = it.state, | ||||
|                     isActive = it.isActive, | ||||
|                     writer = it.writer, | ||||
|                     studio = it.studio | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|         return GetCreatorAdminContentSeriesListResponse(totalCount, seriesList) | ||||
|     } | ||||
| @@ -193,6 +218,7 @@ class CreatorAdminContentSeriesService( | ||||
|         val seriesContentList = repository.getSeriesContentList( | ||||
|             offset = offset, | ||||
|             limit = limit, | ||||
|             seriesId = seriesId, | ||||
|             creatorId = creatorId, | ||||
|             imageHost = coverImageHost | ||||
|         ) | ||||
| @@ -218,7 +244,7 @@ class CreatorAdminContentSeriesService( | ||||
|         } | ||||
|  | ||||
|         if (seriesContentList.size > 0) { | ||||
|             series.contentList.addAll(seriesContentList) | ||||
|             series.contentList.addAll(seriesContentList.toSet()) | ||||
|         } else { | ||||
|             throw SodaException("추가된 콘텐츠가 없습니다.") | ||||
|         } | ||||
| @@ -231,4 +257,17 @@ class CreatorAdminContentSeriesService( | ||||
|  | ||||
|         series.contentList.removeIf { it.content!!.id == request.contentId } | ||||
|     } | ||||
|  | ||||
|     fun searchContentNotInSeries( | ||||
|         seriesId: Long, | ||||
|         searchWord: String, | ||||
|         memberId: Long | ||||
|     ): List<SearchContentNotInSeriesResponse> { | ||||
|         return repository.searchContentNotInSeries( | ||||
|             seriesId, | ||||
|             searchWord, | ||||
|             memberId, | ||||
|             imageHost = coverImageHost | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ data class GetCreatorAdminContentSeriesDetailResponse( | ||||
|     val seriesId: Long, | ||||
|     val title: String, | ||||
|     val introduction: String, | ||||
|     val coverImage: String, | ||||
|     val coverImageUrl: String, | ||||
|     val publishedDaysOfWeek: String, | ||||
|     val genre: String, | ||||
|     val keywords: String, | ||||
|   | ||||
| @@ -1,14 +1,20 @@ | ||||
| package kr.co.vividnext.sodalive.creator.admin.content.series | ||||
|  | ||||
| import com.querydsl.core.annotations.QueryProjection | ||||
|  | ||||
| data class GetCreatorAdminContentSeriesListResponse( | ||||
|     val totalCount: Int, | ||||
|     val items: List<GetCreatorAdminContentSeriesListItem> | ||||
| ) | ||||
|  | ||||
| data class GetCreatorAdminContentSeriesListItem @QueryProjection constructor( | ||||
| data class GetCreatorAdminContentSeriesListItem( | ||||
|     val seriesId: Long, | ||||
|     val title: String, | ||||
|     val coverImageUrl: String | ||||
|     val introduction: String, | ||||
|     val coverImageUrl: String, | ||||
|     val publishedDaysOfWeek: List<SeriesPublishedDaysOfWeek>, | ||||
|     val genreId: Long, | ||||
|     val isAdult: Boolean, | ||||
|     val state: SeriesState, | ||||
|     val isActive: Boolean, | ||||
|     val writer: String?, | ||||
|     val studio: String? | ||||
| ) | ||||
|   | ||||
| @@ -7,6 +7,7 @@ data class ModifySeriesRequest( | ||||
|     val publishedDaysOfWeek: Set<SeriesPublishedDaysOfWeek>?, | ||||
|     val genreId: Long?, | ||||
|     val isAdult: Boolean?, | ||||
|     val state: SeriesState?, | ||||
|     val isActive: Boolean?, | ||||
|     val writer: String?, | ||||
|     val studio: String? | ||||
|   | ||||
| @@ -25,6 +25,10 @@ enum class SeriesState { | ||||
|     PROCEEDING, SUSPEND, COMPLETE | ||||
| } | ||||
|  | ||||
| enum class SeriesSortType { | ||||
|     NEWEST, POPULAR | ||||
| } | ||||
|  | ||||
| @Entity | ||||
| data class Series( | ||||
|     var title: String, | ||||
| @@ -37,7 +41,7 @@ data class Series( | ||||
|     @ElementCollection(targetClass = SeriesPublishedDaysOfWeek::class, fetch = FetchType.EAGER) | ||||
|     @Enumerated(value = EnumType.STRING) | ||||
|     @CollectionTable(name = "series_published_days_of_week", joinColumns = [JoinColumn(name = "series_id")]) | ||||
|     val publishedDaysOfWeek: Set<SeriesPublishedDaysOfWeek> = mutableSetOf(), | ||||
|     val publishedDaysOfWeek: MutableSet<SeriesPublishedDaysOfWeek> = mutableSetOf(), | ||||
|     var isAdult: Boolean = false, | ||||
|     var isActive: Boolean = true | ||||
| ) : BaseEntity() { | ||||
| @@ -51,10 +55,10 @@ data class Series( | ||||
|  | ||||
|     var coverImage: String? = null | ||||
|  | ||||
|     @OneToMany(mappedBy = "series", cascade = [CascadeType.ALL]) | ||||
|     @OneToMany(mappedBy = "series", cascade = [CascadeType.ALL], orphanRemoval = true) | ||||
|     var contentList: MutableList<SeriesContent> = mutableListOf() | ||||
|  | ||||
|     @OneToMany(mappedBy = "series", cascade = [CascadeType.ALL]) | ||||
|     @OneToMany(mappedBy = "series", cascade = [CascadeType.ALL], orphanRemoval = true) | ||||
|     var keywordList: MutableList<SeriesKeyword> = mutableListOf() | ||||
|  | ||||
|     fun toDetailResponse(imageHost: String): GetCreatorAdminContentSeriesDetailResponse { | ||||
| @@ -62,7 +66,7 @@ data class Series( | ||||
|             seriesId = id!!, | ||||
|             title = title, | ||||
|             introduction = introduction, | ||||
|             coverImage = "$imageHost/$coverImage!!", | ||||
|             coverImageUrl = "$imageHost/${coverImage!!}", | ||||
|             publishedDaysOfWeek = publishedDaysOfWeekText(), | ||||
|             genre = genre!!.genre, | ||||
|             keywords = keywordList.map { it.keyword!!.tag }.joinToString(" ") { it }, | ||||
|   | ||||
| @@ -0,0 +1,9 @@ | ||||
| package kr.co.vividnext.sodalive.creator.admin.content.series.content | ||||
|  | ||||
| import com.querydsl.core.annotations.QueryProjection | ||||
|  | ||||
| data class SearchContentNotInSeriesResponse @QueryProjection constructor( | ||||
|     val contentId: Long, | ||||
|     val title: String, | ||||
|     val coverImage: String | ||||
| ) | ||||
| @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.explorer | ||||
| import kr.co.vividnext.sodalive.common.SodaException | ||||
| import kr.co.vividnext.sodalive.content.AudioContentService | ||||
| import kr.co.vividnext.sodalive.content.SortType | ||||
| import kr.co.vividnext.sodalive.content.series.ContentSeriesService | ||||
| import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListResponse | ||||
| import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListResponseItem | ||||
| import kr.co.vividnext.sodalive.explorer.profile.ChannelNotice | ||||
| @@ -38,6 +39,7 @@ class ExplorerService( | ||||
|     private val cheersRepository: CreatorCheersRepository, | ||||
|     private val noticeRepository: ChannelNoticeRepository, | ||||
|     private val communityService: CreatorCommunityService, | ||||
|     private val seriesService: ContentSeriesService, | ||||
|  | ||||
|     private val applicationEventPublisher: ApplicationEventPublisher, | ||||
|  | ||||
| @@ -252,6 +254,10 @@ class ExplorerService( | ||||
|         val liveContributorCount = queryRepository.getLiveContributorCount(creatorId) ?: 0 | ||||
|         val contentCount = queryRepository.getContentCount(creatorId) ?: 0 | ||||
|  | ||||
|         val seriesList = seriesService | ||||
|             .getSeriesList(creatorId = creatorId, member = member) | ||||
|             .items | ||||
|  | ||||
|         return GetCreatorProfileResponse( | ||||
|             creator = CreatorResponse( | ||||
|                 creatorId = creatorAccount.id!!, | ||||
| @@ -283,6 +289,7 @@ class ExplorerService( | ||||
|                 liveContributorCount = liveContributorCount, | ||||
|                 contentCount = contentCount | ||||
|             ), | ||||
|             seriesList = seriesList, | ||||
|             isBlock = isBlock | ||||
|         ) | ||||
|     } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package kr.co.vividnext.sodalive.explorer | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.GetAudioContentListItem | ||||
| import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse | ||||
| import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.GetCommunityPostListResponse | ||||
|  | ||||
| data class GetCreatorProfileResponse( | ||||
| @@ -13,6 +14,7 @@ data class GetCreatorProfileResponse( | ||||
|     val communityPostList: List<GetCommunityPostListResponse>, | ||||
|     val cheers: GetCheersResponse, | ||||
|     val activitySummary: GetCreatorActivitySummary, | ||||
|     val seriesList: List<GetSeriesListResponse.SeriesListItem>, | ||||
|     val isBlock: Boolean | ||||
| ) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user