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.aws.s3.S3Uploader
|
||||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
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 kr.co.vividnext.sodalive.utils.generateFileName
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.data.domain.PageRequest
|
import org.springframework.data.domain.PageRequest
|
||||||
@@ -30,7 +31,7 @@ import org.springframework.web.multipart.MultipartFile
|
|||||||
@RequestMapping("/admin/audio-content/series/banner")
|
@RequestMapping("/admin/audio-content/series/banner")
|
||||||
@PreAuthorize("hasRole('ADMIN')")
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
class AdminContentSeriesBannerController(
|
class AdminContentSeriesBannerController(
|
||||||
private val bannerService: AdminContentSeriesBannerService,
|
private val bannerService: ContentSeriesBannerService,
|
||||||
private val s3Uploader: S3Uploader,
|
private val s3Uploader: S3Uploader,
|
||||||
|
|
||||||
@Value("\${cloud.aws.s3.bucket}")
|
@Value("\${cloud.aws.s3.bucket}")
|
||||||
|
|||||||
@@ -43,6 +43,21 @@ interface ContentSeriesQueryRepository {
|
|||||||
limit: Long
|
limit: Long
|
||||||
): List<Series>
|
): 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 getSeriesDetail(seriesId: Long, isAuth: Boolean, contentType: ContentType): Series?
|
||||||
fun getKeywordList(seriesId: Long): List<String>
|
fun getKeywordList(seriesId: Long): List<String>
|
||||||
fun getSeriesContentMinMaxPrice(seriesId: Long): GetSeriesContentMinMaxPriceResponse
|
fun getSeriesContentMinMaxPrice(seriesId: Long): GetSeriesContentMinMaxPriceResponse
|
||||||
@@ -168,6 +183,85 @@ class ContentSeriesQueryRepositoryImpl(
|
|||||||
.fetch()
|
.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? {
|
override fun getSeriesDetail(seriesId: Long, isAuth: Boolean, contentType: ContentType): Series? {
|
||||||
var where = series.id.eq(seriesId)
|
var where = series.id.eq(seriesId)
|
||||||
.and(series.isActive.isTrue)
|
.and(series.isActive.isTrue)
|
||||||
|
|||||||
@@ -83,6 +83,35 @@ class ContentSeriesService(
|
|||||||
return GetSeriesListResponse(totalCount, items)
|
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(
|
fun getSeriesDetail(
|
||||||
seriesId: Long,
|
seriesId: Long,
|
||||||
isAdultContentVisible: Boolean,
|
isAdultContentVisible: Boolean,
|
||||||
@@ -208,7 +237,7 @@ class ContentSeriesService(
|
|||||||
val seriesList = repository.getRecommendSeriesList(
|
val seriesList = repository.getRecommendSeriesList(
|
||||||
isAuth = isAuth,
|
isAuth = isAuth,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
limit = 10
|
limit = 20
|
||||||
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) }
|
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) }
|
||||||
|
|
||||||
return seriesToSeriesListItem(seriesList = seriesList, isAdult = isAuth, contentType = contentType)
|
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.admin.content.series.AdminContentSeriesRepository
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
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.Page
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class AdminContentSeriesBannerService(
|
class ContentSeriesBannerService(
|
||||||
private val bannerRepository: SeriesBannerRepository,
|
private val bannerRepository: SeriesBannerRepository,
|
||||||
private val seriesRepository: AdminContentSeriesRepository
|
private val seriesRepository: AdminContentSeriesRepository
|
||||||
) {
|
) {
|
||||||
@@ -22,7 +21,7 @@ class AdminContentSeriesBannerService(
|
|||||||
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") }
|
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@org.springframework.transaction.annotation.Transactional
|
@Transactional
|
||||||
fun registerBanner(seriesId: Long, imagePath: String): SeriesBanner {
|
fun registerBanner(seriesId: Long, imagePath: String): SeriesBanner {
|
||||||
val series = seriesRepository.findByIdAndActiveTrue(seriesId)
|
val series = seriesRepository.findByIdAndActiveTrue(seriesId)
|
||||||
?: throw SodaException("시리즈를 찾을 수 없습니다: $seriesId")
|
?: throw SodaException("시리즈를 찾을 수 없습니다: $seriesId")
|
||||||
@@ -37,7 +36,7 @@ class AdminContentSeriesBannerService(
|
|||||||
return bannerRepository.save(banner)
|
return bannerRepository.save(banner)
|
||||||
}
|
}
|
||||||
|
|
||||||
@org.springframework.transaction.annotation.Transactional
|
@Transactional
|
||||||
fun updateBanner(
|
fun updateBanner(
|
||||||
bannerId: Long,
|
bannerId: Long,
|
||||||
imagePath: String? = null,
|
imagePath: String? = null,
|
||||||
@@ -58,7 +57,7 @@ class AdminContentSeriesBannerService(
|
|||||||
return bannerRepository.save(banner)
|
return bannerRepository.save(banner)
|
||||||
}
|
}
|
||||||
|
|
||||||
@org.springframework.transaction.annotation.Transactional
|
@Transactional
|
||||||
fun deleteBanner(bannerId: Long) {
|
fun deleteBanner(bannerId: Long) {
|
||||||
val banner = bannerRepository.findById(bannerId)
|
val banner = bannerRepository.findById(bannerId)
|
||||||
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") }
|
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") }
|
||||||
@@ -66,7 +65,7 @@ class AdminContentSeriesBannerService(
|
|||||||
bannerRepository.save(banner)
|
bannerRepository.save(banner)
|
||||||
}
|
}
|
||||||
|
|
||||||
@org.springframework.transaction.annotation.Transactional
|
@Transactional
|
||||||
fun updateBannerOrders(ids: List<Long>): List<SeriesBanner> {
|
fun updateBannerOrders(ids: List<Long>): List<SeriesBanner> {
|
||||||
val updated = mutableListOf<SeriesBanner>()
|
val updated = mutableListOf<SeriesBanner>()
|
||||||
for (index in ids.indices) {
|
for (index in ids.indices) {
|
||||||
Reference in New Issue
Block a user