feat(series-main): 시리즈 홈, 요일별 시리즈, 장르별 시리즈 API 추가
This commit is contained in:
@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.admin.content.series.banner.dto.SeriesBannerUpda
|
||||
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.content.series.main.banner.ContentSeriesBannerService
|
||||
import kr.co.vividnext.sodalive.utils.generateFileName
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.data.domain.PageRequest
|
||||
@@ -30,7 +31,7 @@ import org.springframework.web.multipart.MultipartFile
|
||||
@RequestMapping("/admin/audio-content/series/banner")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
class AdminContentSeriesBannerController(
|
||||
private val bannerService: AdminContentSeriesBannerService,
|
||||
private val bannerService: ContentSeriesBannerService,
|
||||
private val s3Uploader: S3Uploader,
|
||||
|
||||
@Value("\${cloud.aws.s3.bucket}")
|
||||
|
||||
@@ -43,6 +43,21 @@ interface ContentSeriesQueryRepository {
|
||||
limit: Long
|
||||
): List<Series>
|
||||
|
||||
fun getSeriesByGenreTotalCount(
|
||||
genreId: Long,
|
||||
isAuth: Boolean,
|
||||
contentType: ContentType
|
||||
): Int
|
||||
|
||||
fun getSeriesByGenreList(
|
||||
imageHost: String,
|
||||
genreId: Long,
|
||||
isAuth: Boolean,
|
||||
contentType: ContentType,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<Series>
|
||||
|
||||
fun getSeriesDetail(seriesId: Long, isAuth: Boolean, contentType: ContentType): Series?
|
||||
fun getKeywordList(seriesId: Long): List<String>
|
||||
fun getSeriesContentMinMaxPrice(seriesId: Long): GetSeriesContentMinMaxPriceResponse
|
||||
@@ -168,6 +183,85 @@ class ContentSeriesQueryRepositoryImpl(
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun getSeriesByGenreTotalCount(
|
||||
genreId: Long,
|
||||
isAuth: Boolean,
|
||||
contentType: ContentType
|
||||
): Int {
|
||||
var where = series.isActive.isTrue
|
||||
.and(series.genre.id.eq(genreId))
|
||||
|
||||
if (!isAuth) {
|
||||
where = where.and(series.isAdult.isFalse)
|
||||
} else {
|
||||
if (contentType != ContentType.ALL) {
|
||||
where = where.and(
|
||||
series.member.isNull.or(
|
||||
series.member.auth.gender.eq(
|
||||
if (contentType == ContentType.MALE) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return queryFactory
|
||||
.select(series.id)
|
||||
.from(series)
|
||||
.innerJoin(series.member, member)
|
||||
.innerJoin(series.genre, seriesGenre)
|
||||
.where(where)
|
||||
.fetch()
|
||||
.size
|
||||
}
|
||||
|
||||
override fun getSeriesByGenreList(
|
||||
imageHost: String,
|
||||
genreId: Long,
|
||||
isAuth: Boolean,
|
||||
contentType: ContentType,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<Series> {
|
||||
var where = series.isActive.isTrue
|
||||
.and(series.genre.id.eq(genreId))
|
||||
|
||||
if (!isAuth) {
|
||||
where = where.and(series.isAdult.isFalse)
|
||||
} else {
|
||||
if (contentType != ContentType.ALL) {
|
||||
where = where.and(
|
||||
series.member.isNull.or(
|
||||
series.member.auth.gender.eq(
|
||||
if (contentType == ContentType.MALE) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return queryFactory
|
||||
.select(series)
|
||||
.from(seriesContent)
|
||||
.innerJoin(seriesContent.series, series)
|
||||
.innerJoin(seriesContent.content, audioContent)
|
||||
.innerJoin(series.member, member)
|
||||
.innerJoin(series.genre, seriesGenre)
|
||||
.where(where)
|
||||
.orderBy(audioContent.releaseDate.desc(), series.createdAt.asc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun getSeriesDetail(seriesId: Long, isAuth: Boolean, contentType: ContentType): Series? {
|
||||
var where = series.id.eq(seriesId)
|
||||
.and(series.isActive.isTrue)
|
||||
|
||||
@@ -83,6 +83,35 @@ class ContentSeriesService(
|
||||
return GetSeriesListResponse(totalCount, items)
|
||||
}
|
||||
|
||||
fun getSeriesListByGenre(
|
||||
genreId: Long,
|
||||
isAdultContentVisible: Boolean,
|
||||
contentType: ContentType,
|
||||
member: Member,
|
||||
offset: Long = 0,
|
||||
limit: Long = 20
|
||||
): GetSeriesListResponse {
|
||||
val isAuth = member.auth != null && isAdultContentVisible
|
||||
|
||||
val totalCount = repository.getSeriesByGenreTotalCount(
|
||||
genreId = genreId,
|
||||
isAuth = isAuth,
|
||||
contentType = contentType
|
||||
)
|
||||
|
||||
val rawItems = repository.getSeriesByGenreList(
|
||||
imageHost = coverImageHost,
|
||||
genreId = genreId,
|
||||
isAuth = isAuth,
|
||||
contentType = contentType,
|
||||
offset = offset,
|
||||
limit = limit
|
||||
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) }
|
||||
|
||||
val items = seriesToSeriesListItem(seriesList = rawItems, isAdult = isAuth, contentType = contentType)
|
||||
return GetSeriesListResponse(totalCount, items)
|
||||
}
|
||||
|
||||
fun getSeriesDetail(
|
||||
seriesId: Long,
|
||||
isAdultContentVisible: Boolean,
|
||||
@@ -208,7 +237,7 @@ class ContentSeriesService(
|
||||
val seriesList = repository.getRecommendSeriesList(
|
||||
isAuth = isAuth,
|
||||
contentType = contentType,
|
||||
limit = 10
|
||||
limit = 20
|
||||
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) }
|
||||
|
||||
return seriesToSeriesListItem(seriesList = seriesList, isAdult = isAuth, contentType = contentType)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.content.series.main
|
||||
|
||||
import kr.co.vividnext.sodalive.admin.content.series.banner.dto.SeriesBannerResponse
|
||||
import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse
|
||||
|
||||
data class SeriesHomeResponse(
|
||||
val banners: List<SeriesBannerResponse>,
|
||||
val completedSeriesList: List<GetSeriesListResponse.SeriesListItem>,
|
||||
val recommendSeriesList: List<GetSeriesListResponse.SeriesListItem>
|
||||
)
|
||||
@@ -0,0 +1,150 @@
|
||||
package kr.co.vividnext.sodalive.content.series.main
|
||||
|
||||
import kr.co.vividnext.sodalive.admin.content.series.banner.dto.SeriesBannerResponse
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.content.ContentType
|
||||
import kr.co.vividnext.sodalive.content.series.ContentSeriesService
|
||||
import kr.co.vividnext.sodalive.content.series.main.banner.ContentSeriesBannerService
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
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/main")
|
||||
class SeriesMainController(
|
||||
private val contentSeriesService: ContentSeriesService,
|
||||
private val bannerService: ContentSeriesBannerService,
|
||||
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val imageHost: String
|
||||
) {
|
||||
@GetMapping
|
||||
fun fetchData(
|
||||
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
|
||||
@RequestParam("contentType", required = false) contentType: ContentType? = null,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
val banners = bannerService.getActiveBanners(PageRequest.of(0, 10))
|
||||
.content
|
||||
.map {
|
||||
SeriesBannerResponse.from(it, imageHost)
|
||||
}
|
||||
|
||||
val completedSeriesList = contentSeriesService.getSeriesList(
|
||||
creatorId = null,
|
||||
isCompleted = true,
|
||||
isAdultContentVisible = isAdultContentVisible ?: true,
|
||||
contentType = contentType ?: ContentType.ALL,
|
||||
member = member
|
||||
).items
|
||||
|
||||
val recommendSeriesList = contentSeriesService.getRecommendSeriesList(
|
||||
isAdultContentVisible = isAdultContentVisible ?: true,
|
||||
contentType = contentType ?: ContentType.ALL,
|
||||
member = member
|
||||
)
|
||||
|
||||
ApiResponse.ok(
|
||||
SeriesHomeResponse(
|
||||
banners = banners,
|
||||
completedSeriesList = completedSeriesList,
|
||||
recommendSeriesList = recommendSeriesList
|
||||
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@GetMapping("/recommend")
|
||||
fun getRecommendSeriesList(
|
||||
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
|
||||
@RequestParam("contentType", required = false) contentType: ContentType? = null,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(
|
||||
contentSeriesService.getRecommendSeriesList(
|
||||
isAdultContentVisible = isAdultContentVisible ?: true,
|
||||
contentType = contentType ?: ContentType.ALL,
|
||||
member = member
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@GetMapping("/day-of-week")
|
||||
fun getDayOfWeekSeriesList(
|
||||
@RequestParam("dayOfWeek") dayOfWeek: SeriesPublishedDaysOfWeek,
|
||||
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
|
||||
@RequestParam("contentType", required = false) contentType: ContentType? = null,
|
||||
@RequestParam(defaultValue = "0") page: Int,
|
||||
@RequestParam(defaultValue = "20") size: Int,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
val pageable = PageRequest.of(page, size)
|
||||
|
||||
ApiResponse.ok(
|
||||
contentSeriesService.getDayOfWeekSeriesList(
|
||||
memberId = member.id,
|
||||
isAdult = member.auth != null && (isAdultContentVisible ?: true),
|
||||
contentType = contentType ?: ContentType.ALL,
|
||||
dayOfWeek = dayOfWeek,
|
||||
offset = pageable.offset,
|
||||
limit = pageable.pageSize.toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@GetMapping("/genre-list")
|
||||
fun getGenreList(
|
||||
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
|
||||
@RequestParam("contentType", required = false) contentType: ContentType? = null,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
val memberId = member.id!!
|
||||
val isAdult = member.auth != null && (isAdultContentVisible ?: true)
|
||||
|
||||
ApiResponse.ok(
|
||||
contentSeriesService.getGenreList(
|
||||
memberId = memberId,
|
||||
isAdult = isAdult,
|
||||
contentType = contentType ?: ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@GetMapping("/list-by-genre")
|
||||
fun getSeriesListByGenre(
|
||||
@RequestParam("genreId") genreId: Long,
|
||||
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
|
||||
@RequestParam("contentType", required = false) contentType: ContentType? = null,
|
||||
@RequestParam(defaultValue = "0") page: Int,
|
||||
@RequestParam(defaultValue = "20") size: Int,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
val pageable = PageRequest.of(page, size)
|
||||
|
||||
ApiResponse.ok(
|
||||
contentSeriesService.getSeriesListByGenre(
|
||||
genreId = genreId,
|
||||
isAdultContentVisible = isAdultContentVisible ?: true,
|
||||
contentType = contentType ?: ContentType.ALL,
|
||||
member = member,
|
||||
offset = pageable.offset,
|
||||
limit = pageable.pageSize.toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
package kr.co.vividnext.sodalive.admin.content.series.banner
|
||||
package kr.co.vividnext.sodalive.content.series.main.banner
|
||||
|
||||
import kr.co.vividnext.sodalive.admin.content.series.AdminContentSeriesRepository
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.content.series.main.banner.SeriesBanner
|
||||
import kr.co.vividnext.sodalive.content.series.main.banner.SeriesBannerRepository
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Service
|
||||
class AdminContentSeriesBannerService(
|
||||
class ContentSeriesBannerService(
|
||||
private val bannerRepository: SeriesBannerRepository,
|
||||
private val seriesRepository: AdminContentSeriesRepository
|
||||
) {
|
||||
@@ -22,7 +21,7 @@ class AdminContentSeriesBannerService(
|
||||
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") }
|
||||
}
|
||||
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
@Transactional
|
||||
fun registerBanner(seriesId: Long, imagePath: String): SeriesBanner {
|
||||
val series = seriesRepository.findByIdAndActiveTrue(seriesId)
|
||||
?: throw SodaException("시리즈를 찾을 수 없습니다: $seriesId")
|
||||
@@ -37,7 +36,7 @@ class AdminContentSeriesBannerService(
|
||||
return bannerRepository.save(banner)
|
||||
}
|
||||
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
@Transactional
|
||||
fun updateBanner(
|
||||
bannerId: Long,
|
||||
imagePath: String? = null,
|
||||
@@ -58,7 +57,7 @@ class AdminContentSeriesBannerService(
|
||||
return bannerRepository.save(banner)
|
||||
}
|
||||
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
@Transactional
|
||||
fun deleteBanner(bannerId: Long) {
|
||||
val banner = bannerRepository.findById(bannerId)
|
||||
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") }
|
||||
@@ -66,7 +65,7 @@ class AdminContentSeriesBannerService(
|
||||
bannerRepository.save(banner)
|
||||
}
|
||||
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
@Transactional
|
||||
fun updateBannerOrders(ids: List<Long>): List<SeriesBanner> {
|
||||
val updated = mutableListOf<SeriesBanner>()
|
||||
for (index in ids.indices) {
|
||||
Reference in New Issue
Block a user