Merge pull request '시리즈' (#167) from test into main

Reviewed-on: #167
This commit is contained in:
klaus 2024-04-26 18:51:10 +00:00
commit c45c97e29d
19 changed files with 729 additions and 34 deletions

View File

@ -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()
)
)
}
}

View File

@ -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()
}
}

View File

@ -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"
}
}
}

View File

@ -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
)
}

View File

@ -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
)
}

View File

@ -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
}
}

View File

@ -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
)

View File

@ -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
)

View File

@ -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,

View File

@ -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!!
)
)
}
}

View File

@ -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()
}
}

View File

@ -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
)
}
}

View File

@ -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,

View File

@ -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?
)

View File

@ -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?

View File

@ -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 },

View File

@ -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
)

View File

@ -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
)
}

View File

@ -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
)