commit
c45c97e29d
|
@ -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
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue