diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesController.kt new file mode 100644 index 0000000..13cdc3a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesController.kt @@ -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() + ) + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesRepository.kt new file mode 100644 index 0000000..345e4fb --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesRepository.kt @@ -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, ContentSeriesQueryRepository + +interface ContentSeriesQueryRepository { + fun getSeriesTotalCount(creatorId: Long, isAuth: Boolean): Int + fun getSeriesList( + imageHost: String, + creatorId: Long, + isAuth: Boolean, + offset: Long, + limit: Long + ): List + + fun getSeriesDetail(seriesId: Long, isAuth: Boolean): Series? + fun getKeywordList(seriesId: Long): List + 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 { + 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 { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt new file mode 100644 index 0000000..3e05ff2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt @@ -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): 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" + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesDetailResponse.kt new file mode 100644 index 0000000..508c441 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesDetailResponse.kt @@ -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, + val publishedDaysOfWeek: String, + val contentList: List, + val contentCount: Int +) { + data class GetSeriesDetailCreator( + val creatorId: Long, + val nickname: String, + val profileImage: String, + val isFollow: Boolean + ) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesListResponse.kt new file mode 100644 index 0000000..f83b40c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesListResponse.kt @@ -0,0 +1,24 @@ +package kr.co.vividnext.sodalive.content.series + +data class GetSeriesListResponse( + val totalCount: Int, + val items: List +) { + 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 + ) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/content/ContentSeriesContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/content/ContentSeriesContentRepository.kt new file mode 100644 index 0000000..469dbc2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/content/ContentSeriesContentRepository.kt @@ -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, ContentSeriesContentQueryRepository + +interface ContentSeriesContentQueryRepository { + fun getContentCount(seriesId: Long, isAdult: Boolean): Int + fun getContentList( + seriesId: Long, + isAdult: Boolean, + imageHost: String, + offset: Long, + limit: Long + ): List + + 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 { + 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 + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/content/GetSeriesContentListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/content/GetSeriesContentListResponse.kt new file mode 100644 index 0000000..00f774c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/content/GetSeriesContentListResponse.kt @@ -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 +) + +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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/content/GetSeriesContentMinMaxPriceResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/content/GetSeriesContentMinMaxPriceResponse.kt new file mode 100644 index 0000000..ff19274 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/content/GetSeriesContentMinMaxPriceResponse.kt @@ -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 +) 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 index 658c1cc..c7e1a91 100644 --- 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 @@ -5,7 +5,7 @@ import kr.co.vividnext.sodalive.common.SodaException data class CreateSeriesRequest( val title: String, val introduction: String, - val publishedDaysOfWeek: Set, + val publishedDaysOfWeek: MutableSet, val keyword: String, val genreId: Long = 0, val isAdult: Boolean = false, 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 index ce1adfd..0785176 100644 --- 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 @@ -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!! + ) + ) + } } 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 index 14fd766..f391285 100644 --- 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 @@ -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, CreatorAdminContentSeriesQueryRepository @@ -14,17 +16,24 @@ interface CreatorAdminContentSeriesQueryRepository { fun getSeriesList( offset: Long, limit: Long, - creatorId: Long, - imageHost: String - ): List + creatorId: Long + ): List fun getSeriesContentCount(creatorId: Long): Int fun getSeriesContentList( offset: Long, limit: Long, + seriesId: Long, creatorId: Long, imageHost: String ): List + + fun searchContentNotInSeries( + seriesId: Long, + searchWord: String, + memberId: Long, + imageHost: String + ): List } class CreatorAdminContentSeriesQueryRepositoryImpl( @@ -55,18 +64,10 @@ class CreatorAdminContentSeriesQueryRepositoryImpl( override fun getSeriesList( offset: Long, limit: Long, - creatorId: Long, - imageHost: String - ): List { + creatorId: Long + ): List { 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 { @@ -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 { + 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() + } } 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 index d000fe4..b0317bc 100644 --- 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 @@ -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 { + return repository.searchContentNotInSeries( + seriesId, + searchWord, + memberId, + imageHost = coverImageHost + ) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesDetailResponse.kt index 9cc2271..436b7fe 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesDetailResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesDetailResponse.kt @@ -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, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesListResponse.kt index da32444..f7c6484 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesListResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesListResponse.kt @@ -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 ) -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, + val genreId: Long, + val isAdult: Boolean, + val state: SeriesState, + val isActive: Boolean, + val writer: String?, + val studio: String? ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/ModifySeriesRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/ModifySeriesRequest.kt index 4057c55..f51b4cb 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/ModifySeriesRequest.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/ModifySeriesRequest.kt @@ -7,6 +7,7 @@ data class ModifySeriesRequest( val publishedDaysOfWeek: Set?, val genreId: Long?, val isAdult: Boolean?, + val state: SeriesState?, val isActive: Boolean?, val writer: String?, val studio: String? 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 c801f13..82a63f4 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 @@ -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 = mutableSetOf(), + val publishedDaysOfWeek: MutableSet = 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 = mutableListOf() - @OneToMany(mappedBy = "series", cascade = [CascadeType.ALL]) + @OneToMany(mappedBy = "series", cascade = [CascadeType.ALL], orphanRemoval = true) var keywordList: MutableList = 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 }, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/content/SearchContentNotInSeriesResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/content/SearchContentNotInSeriesResponse.kt new file mode 100644 index 0000000..79c9f8e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/content/SearchContentNotInSeriesResponse.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt index 6186c1b..2a1515d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt @@ -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 ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt index da86441..bfdd016 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt @@ -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, val cheers: GetCheersResponse, val activitySummary: GetCreatorActivitySummary, + val seriesList: List, val isBlock: Boolean )