feat(series-main): 시리즈 홈, 요일별 시리즈, 장르별 시리즈 API 추가

This commit is contained in:
2025-11-13 16:02:11 +09:00
parent 27be9a4fc2
commit 4f89b0189e
6 changed files with 293 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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