feat(home): 크리에이터 채널 홈 정책을 보강한다

This commit is contained in:
2026-07-31 01:46:39 +09:00
parent f1c2e6c50b
commit 13e0b42375
20 changed files with 1811 additions and 51 deletions

View File

@@ -36,6 +36,7 @@ import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
import kr.co.vividnext.sodalive.i18n.translation.ResourceTranslationJobScheduler
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.member.contentpreference.isAdultVisibleByPolicy
import kr.co.vividnext.sodalive.utils.generateFileName
@@ -57,6 +58,8 @@ import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
private const val MAX_PIN_CONTENT_COUNT = 9
@Service
@Transactional(readOnly = true)
class AudioContentService(
@@ -72,6 +75,7 @@ class AudioContentService(
private val commentRepository: AudioContentCommentRepository,
private val audioContentLikeRepository: AudioContentLikeRepository,
private val pinContentRepository: PinContentRepository,
private val memberRepository: MemberRepository,
private val resourceTranslationJobScheduler: ResourceTranslationJobScheduler,
private val contentTranslationRepository: ContentTranslationRepository,
@@ -795,7 +799,7 @@ class AudioContentService(
val pinContentListCount = pinContentRepository.getPinContentList(memberId = member.id!!, active = true).size
val isAvailablePin = if (member.id!! == audioContent.member!!.id!!) {
pinContentListCount < 3
pinContentListCount < MAX_PIN_CONTENT_COUNT
} else {
false
}
@@ -1227,7 +1231,9 @@ class AudioContentService(
@Transactional
fun pinToTheTop(contentId: Long, member: Member) {
val audioContent = repository.findByIdAndCreatorId(contentId = contentId, creatorId = member.id!!)
val creator = memberRepository.findByIdForUpdate(member.id!!)
?: throw SodaException(messageKey = "content.error.invalid_content_retry")
val audioContent = repository.findByIdAndCreatorId(contentId = contentId, creatorId = creator.id!!)
?: throw SodaException(messageKey = "content.error.invalid_content_retry")
if (audioContent.releaseDate != null && audioContent.releaseDate!! >= LocalDateTime.now()) {
@@ -1236,21 +1242,24 @@ class AudioContentService(
var pinContent = pinContentRepository.findByContentIdAndMemberId(
contentId = contentId,
memberId = member.id!!
memberId = creator.id!!
)
val activePinContentList = pinContentRepository.getPinContentList(memberId = creator.id!!, active = true)
if (pinContent != null) {
if (!pinContent.isActive && activePinContentList.size >= MAX_PIN_CONTENT_COUNT) {
activePinContentList[0].isActive = false
}
pinContent.isActive = true
} else {
val pinContentList = pinContentRepository.getPinContentList(memberId = member.id!!)
pinContent = if (pinContentList.size >= 3) {
pinContentList[0]
pinContent = if (activePinContentList.size >= MAX_PIN_CONTENT_COUNT) {
activePinContentList[0]
} else {
PinContent()
}
pinContent.isActive = true
pinContent.member = member
pinContent.member = creator
pinContent.content = audioContent
pinContentRepository.save(pinContent)
}
@@ -1258,9 +1267,11 @@ class AudioContentService(
@Transactional
fun unpinAtTheTop(contentId: Long, member: Member) {
val creator = memberRepository.findByIdForUpdate(member.id!!)
?: throw SodaException(messageKey = "content.error.invalid_content_retry")
val pinContent = pinContentRepository.findByContentIdAndMemberId(
contentId = contentId,
memberId = member.id!!
memberId = creator.id!!
) ?: throw SodaException(messageKey = "content.error.invalid_content_retry")
pinContent.isActive = false

View File

@@ -6,6 +6,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.home.application.CreatorChann
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.time.ZoneOffset
@Service
@Transactional(readOnly = true)
@@ -15,7 +16,7 @@ class CreatorChannelHomeFacade(
fun getHome(
creatorId: Long,
viewer: Member,
now: LocalDateTime = LocalDateTime.now()
now: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
): CreatorChannelHomeResponse {
return CreatorChannelHomeResponse.from(
creatorChannelHomeQueryService.getHome(

View File

@@ -9,6 +9,7 @@ import kr.co.vividnext.sodalive.content.ContentType
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.order.QOrder.order
import kr.co.vividnext.sodalive.content.pin.QPinContent.pinContent
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.explorer.profile.QCreatorCheers.creatorCheers
@@ -269,7 +270,7 @@ class DefaultCreatorChannelHomeQueryRepository(
viewerId: Long?,
limit: Int
): List<CreatorChannelAudioContentRecord> {
val rows = findAudioContentRows(creatorId, now, latestAudioContentId, canViewAdultContent, limit)
val rows = findAudioContentRows(creatorId, now, latestAudioContentId, canViewAdultContent, limit, pinFirst = true)
val contentIds = rows.map { itAudioId(it) }
val firstContentId = firstAudioContentId(creatorId, now, canViewAdultContent)
val seriesByContentId = audioSeriesByContentIds(contentIds)
@@ -435,21 +436,33 @@ class DefaultCreatorChannelHomeQueryRepository(
now: LocalDateTime,
excludedContentId: Long?,
canViewAdultContent: Boolean,
limit: Int
) = queryFactory
.select(
audioContent.id,
audioContent.title,
audioContent.duration,
audioContent.coverImage,
audioContent.price,
audioContent.isAdult,
audioContent.isPointAvailable,
audioContent.releaseDate,
audioContent.createdAt
)
.from(audioContent)
.where(
limit: Int,
pinFirst: Boolean = false
): List<com.querydsl.core.Tuple> {
val query = queryFactory
.select(
audioContent.id,
audioContent.title,
audioContent.duration,
audioContent.coverImage,
audioContent.price,
audioContent.isAdult,
audioContent.isPointAvailable,
audioContent.releaseDate,
audioContent.createdAt
)
.from(audioContent)
if (pinFirst) {
query.leftJoin(pinContent)
.on(
pinContent.member.id.eq(creatorId),
pinContent.content.id.eq(audioContent.id),
pinContent.isActive.isTrue
)
}
query.where(
audioContent.member.id.eq(creatorId),
audioContent.member.isActive.isTrue,
audioContent.isActive.isTrue,
@@ -459,9 +472,22 @@ class DefaultCreatorChannelHomeQueryRepository(
excludedContentId?.let { audioContent.id.ne(it) },
adultAudioCondition(canViewAdultContent)
)
.orderBy(audioContent.releaseDate.desc(), audioContent.id.desc())
.limit(limit.toLong())
.fetch()
if (pinFirst) {
query.orderBy(
pinContent.isActive.desc(),
pinContent.updatedAt.desc(),
audioContent.releaseDate.desc(),
audioContent.id.desc()
)
} else {
query.orderBy(audioContent.releaseDate.desc(), audioContent.id.desc())
}
return query
.limit(limit.toLong())
.fetch()
}
private fun itAudioId(row: com.querydsl.core.Tuple): Long = row.get(audioContent.id)!!

View File

@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.creator.channel.home.application
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Gender
@@ -36,6 +37,7 @@ import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.time.ZoneOffset
@Service
@Transactional(readOnly = true)
@@ -52,7 +54,7 @@ class CreatorChannelHomeQueryService(
fun getHome(
creatorId: Long,
viewer: Member,
now: LocalDateTime = LocalDateTime.now()
now: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
): CreatorChannelHome {
val viewerId = viewer.id!!
val creator = queryPort.findCreator(creatorId, viewerId)
@@ -68,8 +70,8 @@ class CreatorChannelHomeQueryService(
validateCreatorRole(creator)
val preference = memberContentPreferenceService.getStoredPreference(viewer)
val canViewAdultContent = memberContentPreferenceService.canViewAdultContent(viewer)
val isViewerCreator = viewerId == creatorId
val canViewAdultContent = preference.isAdult
val isViewerCreator = viewer.role == MemberRole.CREATOR
val effectiveViewerGender = viewer.effectiveGender()
val latestAudioContent = queryPort
.findLatestAudioContent(creatorId, now, canViewAdultContent, viewerId)
@@ -187,7 +189,7 @@ class CreatorChannelHomeQueryService(
)
private fun CreatorChannelDonationRecord.toDomain() = CreatorChannelDonation(
nickname = nickname,
nickname = nickname.removeDeletedNicknamePrefix(),
profileImageUrl = profileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(),
can = can,
message = message,