Test #457

Merged
klaus merged 5 commits from test into main 2026-10-16 07:50:59 +00:00
66 changed files with 3022 additions and 373 deletions
Showing only changes of commit eef2e2bf55 - Show all commits
@@ -342,6 +342,7 @@ class AdminLiveService(
type = FcmEventType.CANCEL_LIVE,
category = PushNotificationCategory.LIVE,
title = room.member!!.nickname,
titleMemberId = room.member!!.id,
messageKey = "live.room.fcm.message.canceled",
senderMemberId = room.member!!.id,
args = listOf(room.title),
@@ -24,6 +24,7 @@ import kr.co.vividnext.sodalive.query.recommend.RecommendChannelQueryService
import kr.co.vividnext.sodalive.rank.ContentRankingSortType
import kr.co.vividnext.sodalive.rank.RankingRepository
import kr.co.vividnext.sodalive.rank.RankingService
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
@@ -49,6 +50,7 @@ class HomeService(
private val explorerQueryRepository: ExplorerQueryRepository,
private val langContext: LangContext,
private val localizedNameService: CreatorLocalizedNameService,
private val memberContentPreferenceService: MemberContentPreferenceService,
@Value("\${cloud.aws.cloud-front.host}")
@@ -87,7 +89,7 @@ class HomeService(
timezone = timezone
)
val creatorRankingMembers = rankingRepository.getCreatorRankings(memberId = memberId)
val creatorRankingMembers = rankingRepository.getCreatorRankings(memberId = memberId, lang = langContext.lang)
val creatorRankingIds = creatorRankingMembers.mapNotNull { it.id }
val followedCreatorIds = if (memberId != null) {
explorerQueryRepository.getFollowedCreatorIds(creatorRankingIds, memberId)
@@ -99,7 +101,7 @@ class HomeService(
val creatorId = creator.id!!
val follow = memberId != null && followedCreatorIds.contains(creatorId)
creator.toExplorerSectionCreator(imageHost, follow)
creator.copy(follow = follow)
}
val latestContentThemeList = contentThemeService.getActiveThemeOfContent(
@@ -171,6 +173,10 @@ class HomeService(
isAdult = isAdult,
contentType = resolvedContentType
)
val recommendedNames = localizedNameService.findCurrentNames(
recommendChannelList.map { it.channelId },
langContext.lang
)
val freeContentList = getRandomizedContentList(
memberId = memberId,
@@ -212,7 +218,9 @@ class HomeService(
dayOfWeekSeriesList = translatedDayOfWeekSeriesList,
popularCharacters = translatedPopularCharacters,
contentRanking = contentRanking,
recommendChannelList = recommendChannelList,
recommendChannelList = recommendChannelList.map {
it.copy(creatorNickname = recommendedNames[it.channelId] ?: it.creatorNickname)
},
freeContentList = freeContentList,
pointAvailableContentList = pointAvailableContentList,
recommendContentList = getRecommendContentListByPreference(
@@ -5,7 +5,9 @@ import kr.co.vividnext.sodalive.audition.AuditionApplicant
import kr.co.vividnext.sodalive.audition.QAuditionApplicant.auditionApplicant
import kr.co.vividnext.sodalive.audition.QAuditionRole.auditionRole
import kr.co.vividnext.sodalive.audition.QAuditionVote.auditionVote
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
@@ -18,7 +20,8 @@ interface AuditionApplicantQueryRepository {
auditionRoleId: Long,
sortType: AuditionApplicantSortType,
offset: Long,
limit: Long
limit: Long,
lang: Lang = Lang.KO
): List<GetAuditionRoleApplicantItem>
fun findActiveApplicantByMemberIdAndRoleId(memberId: Long, roleId: Long): AuditionApplicant?
@@ -64,8 +67,10 @@ class AuditionApplicantQueryRepositoryImpl(
auditionRoleId: Long,
sortType: AuditionApplicantSortType,
offset: Long,
limit: Long
limit: Long,
lang: Lang
): List<GetAuditionRoleApplicantItem> {
val creatorName = CreatorNameJoin(member, lang, "applicantMemberName")
val orderBy = if (sortType == AuditionApplicantSortType.LIKES) {
auditionVote.id.count().desc()
} else {
@@ -77,7 +82,7 @@ class AuditionApplicantQueryRepositoryImpl(
QGetAuditionRoleApplicantItem(
auditionApplicant.id,
member.id,
member.nickname,
creatorName.displayName,
member.profileImage.prepend("/").prepend(cloudFrontHost),
auditionApplicant.voicePath.prepend("/").prepend(cloudFrontHost),
auditionVote.id.count()
@@ -85,6 +90,7 @@ class AuditionApplicantQueryRepositoryImpl(
)
.from(auditionApplicant)
.innerJoin(auditionApplicant.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.innerJoin(auditionApplicant.role, auditionRole)
.leftJoin(auditionVote).on(auditionApplicant.id.eq(auditionVote.applicant.id))
.where(
@@ -92,7 +98,7 @@ class AuditionApplicantQueryRepositoryImpl(
auditionApplicant.isActive.isTrue,
member.isActive.isTrue
)
.groupBy(auditionApplicant.id)
.groupBy(auditionApplicant.id, member.id, creatorName.name.displayName)
.orderBy(orderBy)
.offset(offset)
.limit(limit)
@@ -6,6 +6,7 @@ import kr.co.vividnext.sodalive.audition.AuditionApplicant
import kr.co.vividnext.sodalive.audition.role.AuditionRoleRepository
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
@@ -21,6 +22,7 @@ class AuditionApplicantService(
private val s3Uploader: S3Uploader,
private val objectMapper: ObjectMapper,
private val langContext: LangContext,
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String
@@ -36,7 +38,8 @@ class AuditionApplicantService(
auditionRoleId = auditionRoleId,
sortType = sortType,
offset = offset,
limit = limit
limit = limit,
lang = langContext.lang
)
return GetAuditionApplicantListResponse(
@@ -19,10 +19,12 @@ import kr.co.vividnext.sodalive.chat.character.QChatCharacter.chatCharacter
import kr.co.vividnext.sodalive.chat.character.image.QCharacterImage.characterImage
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.QCreatorCommunity.creatorCommunity
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.QMember
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@@ -32,7 +34,7 @@ interface CanRepository : JpaRepository<Can, Long>, CanQueryRepository
interface CanQueryRepository {
fun findAllByStatusAndCurrency(status: CanStatus, currency: String?): List<CanResponse>
fun getCanUseStatus(member: Member, pageable: Pageable, container: String): List<UseCanQueryDto>
fun getCanUseStatus(member: Member, pageable: Pageable, container: String, lang: Lang = Lang.KO): List<UseCanQueryDto>
fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List<Charge>
fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan?
fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long, canUsage: CanUsage = CanUsage.LIVE): UseCan?
@@ -66,11 +68,15 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue
.fetch()
}
override fun getCanUseStatus(member: Member, pageable: Pageable, container: String): List<UseCanQueryDto> {
override fun getCanUseStatus(member: Member, pageable: Pageable, container: String, lang: Lang): List<UseCanQueryDto> {
val qRoomMember = QMember("roomMember")
val qAudioContentMember = QMember("audioContentMember")
val qCommunityPostMember = QMember("communityPostMember")
val qRecipientMember = QMember("recipientMember")
val roomName = CreatorNameJoin(qRoomMember, lang, "receiptRoomCreatorName")
val audioName = CreatorNameJoin(qAudioContentMember, lang, "receiptAudioCreatorName")
val communityName = CreatorNameJoin(qCommunityPostMember, lang, "receiptCommunityCreatorName")
val recipientName = CreatorNameJoin(qRecipientMember, lang, "receiptRecipientCreatorName")
val gatewayCondition = when (container) {
"aos" -> useCanCalculate.paymentGateway.`in`(
@@ -96,23 +102,26 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue
useCan.can,
useCan.rewardCan,
useCan.createdAt,
qRoomMember.nickname,
roomName.displayName,
liveRoom.title,
qAudioContentMember.nickname,
audioName.displayName,
audioContent.title,
qCommunityPostMember.nickname,
communityName.displayName,
audition.title,
chatCharacter.name,
qRecipientMember.nickname
recipientName.displayName
)
)
.from(useCan)
.leftJoin(useCan.room, liveRoom)
.leftJoin(liveRoom.member, qRoomMember)
.leftJoin(roomName.name).on(roomName.condition)
.leftJoin(useCan.audioContent, audioContent)
.leftJoin(audioContent.member, qAudioContentMember)
.leftJoin(audioName.name).on(audioName.condition)
.leftJoin(useCan.communityPost, creatorCommunity)
.leftJoin(creatorCommunity.member, qCommunityPostMember)
.leftJoin(communityName.name).on(communityName.condition)
.leftJoin(useCan.auditionApplicant, auditionApplicant)
.leftJoin(auditionApplicant.role, auditionRole)
.leftJoin(auditionRole.audition, audition)
@@ -120,6 +129,7 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue
.leftJoin(characterImage.chatCharacter, chatCharacter)
.innerJoin(useCan.useCanCalculates, useCanCalculate)
.leftJoin(qRecipientMember).on(useCanCalculate.recipientCreatorId.eq(qRecipientMember.id))
.leftJoin(recipientName.name).on(recipientName.condition)
.where(
useCan.member.id.eq(member.id)
.and(useCan.isRefund.isFalse)
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.can
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
@@ -14,7 +15,8 @@ import java.time.format.DateTimeFormatter
@Transactional(readOnly = true)
class CanService(
private val repository: CanRepository,
private val countryContext: CountryContext
private val countryContext: CountryContext,
private val langContext: LangContext
) {
fun getCans(forcedCurrency: String? = null): List<CanResponse> {
val currency = forcedCurrency ?: when (countryContext.countryCode) {
@@ -43,7 +45,7 @@ class CanService(
ZoneId.of("UTC")
}
return repository.getCanUseStatus(member, pageable, container)
return repository.getCanUseStatus(member, pageable, container, langContext.lang)
.map {
val title: String = when (it.canUsage) {
CanUsage.HEART, CanUsage.DONATION, CanUsage.SPIN_ROULETTE -> {
@@ -3,9 +3,11 @@ package kr.co.vividnext.sodalive.content
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.like.PutAudioContentLikeRequest
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.member.contentpreference.ViewerContentPreference
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.springframework.data.domain.Pageable
import org.springframework.lang.Nullable
import org.springframework.security.access.prepost.PreAuthorize
@@ -29,7 +31,9 @@ import java.time.temporal.TemporalAdjusters
@RequestMapping("/audio-content")
class AudioContentController(
private val service: AudioContentService,
private val memberContentPreferenceService: MemberContentPreferenceService
private val memberContentPreferenceService: MemberContentPreferenceService,
private val localizedNameService: CreatorLocalizedNameService,
private val langContext: LangContext
) {
@PostMapping
@PreAuthorize("hasRole('CREATOR')")
@@ -214,7 +218,10 @@ class AudioContentController(
offset = pageable.offset,
limit = pageable.pageSize.toLong(),
sortType = sortType ?: "매출"
)
).let { response ->
val names = localizedNameService.findCurrentNames(response.items.map { it.creatorId }, langContext.lang)
response.copy(items = response.items.map { it.copy(creatorNickname = names[it.creatorId] ?: it.creatorNickname) })
}
)
}
@@ -29,9 +29,11 @@ import kr.co.vividnext.sodalive.content.translation.QContentTranslation.contentT
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.event.QEvent.event
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@@ -85,7 +87,8 @@ interface AudioContentQueryRepository {
contentType: ContentType,
offset: Long = 0,
limit: Long = 20,
isFree: Boolean = false
isFree: Boolean = false,
lang: Lang = Lang.KO
): List<GetAudioContentMainItem>
fun findAlarmContentByTheme(
@@ -167,7 +170,7 @@ interface AudioContentQueryRepository {
fun isContentCreator(contentId: Long, memberId: Long): Boolean
fun fetchContentForPlaylist(contentIdList: List<Long>): List<AudioContentPlaylistContent>
fun fetchContentForPlaylist(contentIdList: List<Long>, lang: Lang = Lang.KO): List<AudioContentPlaylistContent>
fun getCoverImageById(id: Long): String?
fun findPreviousContent(seriesId: Long, title: String, isAdult: Boolean): OtherContentResponse?
fun findNextContent(seriesId: Long, title: String, isAdult: Boolean): OtherContentResponse?
@@ -467,8 +470,10 @@ class AudioContentQueryRepositoryImpl(
contentType: ContentType,
offset: Long,
limit: Long,
isFree: Boolean
isFree: Boolean,
lang: Lang
): List<GetAudioContentMainItem> {
val creatorName = CreatorNameJoin(member, lang, "themeCreatorName")
val blockMemberCondition = blockMember.isActive.isTrue
.and(
blockMember.member.id.eq(member.id)
@@ -535,7 +540,7 @@ class AudioContentQueryRepositoryImpl(
where = where.and(audioContent.price.loe(0))
}
return queryFactory
val query = queryFactory
.select(
QGetAudioContentMainItem(
audioContent.id,
@@ -543,7 +548,7 @@ class AudioContentQueryRepositoryImpl(
audioContent.title,
member.id,
member.profileImage.prepend("/").prepend(imageHost),
member.nickname,
creatorName.displayName,
audioContent.price,
audioContent.duration,
audioContent.isPointAvailable
@@ -557,7 +562,8 @@ class AudioContentQueryRepositoryImpl(
.offset(offset)
.limit(limit)
.orderBy(*orderBy.toTypedArray())
.fetch()
query.leftJoin(creatorName.name).on(creatorName.condition)
return query.fetch()
}
override fun findAlarmContentByTheme(
@@ -1210,7 +1216,8 @@ class AudioContentQueryRepositoryImpl(
return foundedContentId != null && foundedContentId == contentId
}
override fun fetchContentForPlaylist(contentIdList: List<Long>): List<AudioContentPlaylistContent> {
override fun fetchContentForPlaylist(contentIdList: List<Long>, lang: Lang): List<AudioContentPlaylistContent> {
val creatorName = CreatorNameJoin(member, lang, "playlistCreatorName")
return queryFactory
.select(
QAudioContentPlaylistContent(
@@ -1219,12 +1226,13 @@ class AudioContentQueryRepositoryImpl(
audioContentTheme.theme,
audioContent.coverImage.prepend("/").prepend(imageHost),
audioContent.duration,
member.nickname,
creatorName.displayName,
member.profileImage.prepend("/").prepend(imageHost)
)
)
.from(audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.innerJoin(audioContent.theme, audioContentTheme)
.where(audioContent.id.`in`(contentIdList))
.fetch()
@@ -1466,6 +1474,7 @@ class AudioContentQueryRepositoryImpl(
}
val coverImageUrl = audioContent.coverImage.prepend("/").prepend(imageHost)
val creatorName = CreatorNameJoin(member, Lang.fromCode(locale), "latestCreatorName")
var select = queryFactory
.select(
@@ -1474,11 +1483,12 @@ class AudioContentQueryRepositoryImpl(
audioContent.title,
contentTranslation.renderedPayload,
coverImageUrl,
member.nickname,
creatorName.displayName,
audioContent.isPointAvailable
)
.from(audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.innerJoin(audioContent.theme, audioContentTheme)
.leftJoin(contentTranslation)
.on(contentTranslation.contentId.eq(audioContent.id).and(contentTranslation.locale.eq(locale)))
@@ -1523,7 +1533,7 @@ class AudioContentQueryRepositoryImpl(
val payload = row.get(contentTranslation.renderedPayload)
val translatedTitle = payload?.title
val imageUrl = row.get(coverImageUrl)!!
val creatorNickname = row.get(member.nickname)!!
val creatorNickname = row.get(creatorName.displayName)!!
val isPointAvailableValue = row.get(audioContent.isPointAvailable) ?: false
AudioContentMainItem(
@@ -40,6 +40,7 @@ import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.utils.generateFileName
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
import kr.co.vividnext.sodalive.v2.recommendation.application.CreatorContentViewHistoryService
import org.slf4j.LoggerFactory
@@ -87,6 +88,7 @@ class AudioContentService(
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
private val localizedNameService: CreatorLocalizedNameService,
private val contentThemeTranslationRepository: ContentThemeTranslationRepository,
private val homeFollowingNewsPublishService: HomeFollowingNewsPublishService,
@@ -512,6 +514,7 @@ class AudioContentService(
type = FcmEventType.UPLOAD_CONTENT,
category = PushNotificationCategory.CONTENT,
title = audioContent.member!!.nickname,
titleMemberId = audioContent.member!!.id,
messageKey = "content.notification.uploaded_message",
senderMemberId = audioContent.member!!.id,
args = listOf(audioContent.title),
@@ -542,6 +545,7 @@ class AudioContentService(
type = FcmEventType.UPLOAD_CONTENT,
category = PushNotificationCategory.CONTENT,
title = audioContent.member!!.nickname,
titleMemberId = audioContent.member!!.id,
messageKey = "content.notification.uploaded_message",
senderMemberId = audioContent.member!!.id,
args = listOf(audioContent.title),
@@ -702,7 +706,8 @@ class AudioContentService(
isContentCreator = creatorId == member.id!!,
timezone = timezone,
offset = 0,
limit = 1
limit = 1,
lang = langContext.lang
)
} else {
listOf()
@@ -844,7 +849,7 @@ class AudioContentService(
audioContent.limited != null &&
audioContent.remaining != null
) {
orderRepository.findBuyerListByContentId(id)
orderRepository.findBuyerListByContentId(id, langContext.lang)
} else {
listOf()
}
@@ -920,6 +925,10 @@ class AudioContentService(
)
}
val names = localizedNameService.findLocalizedNames(
listOf(creatorId),
langContext.lang
)
return GetAudioContentDetailResponse(
contentId = audioContent.id!!,
title = audioContent.title,
@@ -955,7 +964,7 @@ class AudioContentService(
isAvailablePin = isAvailablePin,
creator = AudioContentCreator(
creatorId = creatorId,
nickname = creator.nickname,
nickname = names[creatorId] ?: creator.nickname,
profileImageUrl = if (creator.profileImage != null) {
"$coverImageHost/${creator.profileImage}"
} else {
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.content.comment
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.useraction.ActionType
import kr.co.vividnext.sodalive.useraction.UserActionService
@@ -18,7 +19,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController
class AudioContentCommentController(
private val service: AudioContentCommentService,
private val userActionService: UserActionService
private val userActionService: UserActionService,
private val langContext: LangContext
) {
@PostMapping("/audio-content/comment")
fun registerComment(
@@ -79,7 +81,8 @@ class AudioContentCommentController(
audioContentId = audioContentId,
memberId = member.id!!,
timezone = timezone,
pageable = pageable
pageable = pageable,
lang = langContext.lang
)
)
}
@@ -98,7 +101,8 @@ class AudioContentCommentController(
commentId = commentId,
memberId = member.id!!,
timezone = timezone,
pageable = pageable
pageable = pageable,
lang = langContext.lang
)
)
}
@@ -1,16 +1,18 @@
package kr.co.vividnext.sodalive.content.comment
import com.querydsl.core.types.dsl.CaseBuilder
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.JPAExpressions
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.content.comment.QAudioContentComment.audioContentComment
import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix
import kr.co.vividnext.sodalive.fcm.PushTokenInfo
import kr.co.vividnext.sodalive.fcm.QPushToken.pushToken
import kr.co.vividnext.sodalive.fcm.QPushTokenInfo
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
@@ -26,7 +28,8 @@ interface AudioContentCommentQueryRepository {
isContentCreator: Boolean,
timezone: String,
offset: Long,
limit: Int
limit: Int,
lang: Lang? = null
): List<GetAudioContentCommentListItem>
fun totalCountCommentByContentId(contentId: Long, memberId: Long, isContentCreator: Boolean): Int
@@ -37,7 +40,8 @@ interface AudioContentCommentQueryRepository {
memberId: Long,
timezone: String,
offset: Long,
limit: Int
limit: Int,
lang: Lang? = null
): List<GetAudioContentCommentListItem>
fun findPushTokenByContentIdAndCommentParentIdMyMemberId(
@@ -58,8 +62,13 @@ class AudioContentCommentQueryRepositoryImpl(
isContentCreator: Boolean,
timezone: String,
offset: Long,
limit: Int
limit: Int,
lang: Lang?
): List<GetAudioContentCommentListItem> {
val creatorName = lang?.let { CreatorNameJoin(member, it, "audioCommentWriterName") }
val originalName = CaseBuilder().`when`(member.nickname.startsWith("deleted_"))
.then(member.nickname.substring(8)).otherwise(member.nickname)
val displayName = creatorName?.name?.displayName?.coalesce(originalName) ?: originalName
var where = audioContentComment.audioContent.id.eq(contentId)
.and(audioContentComment.isActive.isTrue)
.and(audioContentComment.parent.isNull)
@@ -85,12 +94,12 @@ class AudioContentCommentQueryRepositoryImpl(
"%Y.%m.%d %W %h:%i %p"
)
return queryFactory
val query = queryFactory
.select(
QGetAudioContentCommentListItem(
audioContentComment.id,
audioContentComment.member.id,
audioContentComment.member.nickname,
displayName,
audioContentComment.member.profileImage.prepend("/").prepend(cloudFrontHost),
audioContentComment.comment,
audioContentComment.languageCode,
@@ -107,10 +116,10 @@ class AudioContentCommentQueryRepositoryImpl(
.offset(offset)
.limit(limit.toLong())
.orderBy(audioContentComment.createdAt.desc())
.fetch()
if (creatorName != null) query.leftJoin(creatorName.name).on(creatorName.condition)
return query.fetch()
.map {
it.copy(
nickname = it.nickname.removeDeletedNicknamePrefix(),
replyCount = commentReplyCountByAudioContentCommentId(it.id, memberId)
)
}
@@ -160,8 +169,13 @@ class AudioContentCommentQueryRepositoryImpl(
memberId: Long,
timezone: String,
offset: Long,
limit: Int
limit: Int,
lang: Lang?
): List<GetAudioContentCommentListItem> {
val creatorName = lang?.let { CreatorNameJoin(member, it, "audioReplyWriterName") }
val originalName = CaseBuilder().`when`(member.nickname.startsWith("deleted_"))
.then(member.nickname.substring(8)).otherwise(member.nickname)
val displayName = creatorName?.name?.displayName?.coalesce(originalName) ?: originalName
val formattedDate = Expressions.stringTemplate(
"DATE_FORMAT({0}, {1})",
Expressions.dateTimeTemplate(
@@ -174,12 +188,12 @@ class AudioContentCommentQueryRepositoryImpl(
"%Y.%m.%d %W %h:%i %p"
)
return queryFactory
val query = queryFactory
.select(
QGetAudioContentCommentListItem(
audioContentComment.id,
audioContentComment.member.id,
audioContentComment.member.nickname,
displayName,
audioContentComment.member.profileImage.prepend("/").prepend(cloudFrontHost),
audioContentComment.comment,
audioContentComment.languageCode,
@@ -201,10 +215,8 @@ class AudioContentCommentQueryRepositoryImpl(
.offset(offset)
.limit(limit.toLong())
.orderBy(audioContentComment.createdAt.desc())
.fetch()
.map {
it.copy(nickname = it.nickname.removeDeletedNicknamePrefix())
}
if (creatorName != null) query.leftJoin(creatorName.name).on(creatorName.condition)
return query.fetch()
}
override fun findPushTokenByContentIdAndCommentParentIdMyMemberId(
@@ -9,10 +9,12 @@ import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue
import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType
import kr.co.vividnext.sodalive.fcm.notification.PushNotificationCategory
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.domain.Pageable
@@ -30,6 +32,7 @@ class AudioContentCommentService(
private val orderRepository: OrderRepository,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
private val localizedNameService: CreatorLocalizedNameService,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
@@ -49,7 +52,9 @@ class AudioContentCommentService(
val creator = audioContent.member!!
val isBlocked = isBlockedBetweenMembers(memberId = member.id!!, creatorId = creator.id!!)
if (isBlocked) {
throw SodaException(formatMessage("content.comment.error.blocked_by_creator", creator.nickname))
val name = localizedNameService.findLocalizedNames(listOf(creator.id!!), langContext.lang)[creator.id]
?: creator.nickname
throw SodaException(formatMessage("content.comment.error.blocked_by_creator", name))
}
val (isExistsAudioContent, _) = orderRepository.isExistOrderedAndOrderType(
@@ -86,6 +91,7 @@ class AudioContentCommentService(
} else {
member.nickname
},
titleMemberId = parent?.member?.id ?: member.id,
messageKey = if (parent != null) {
"content.comment.notification.reply"
} else {
@@ -140,7 +146,8 @@ class AudioContentCommentService(
audioContentId: Long,
memberId: Long,
timezone: String,
pageable: Pageable
pageable: Pageable,
lang: Lang? = null
): GetAudioContentCommentListResponse {
val audioContent = audioContentRepository.findByIdOrNull(id = audioContentId)
?: return GetAudioContentCommentListResponse(totalCount = 0, items = listOf())
@@ -157,7 +164,8 @@ class AudioContentCommentService(
isContentCreator = isContentCreator,
timezone = timezone,
offset = pageable.offset,
limit = pageable.pageSize
limit = pageable.pageSize,
lang = lang
)
val totalCount = repository.totalCountCommentByContentId(
contentId = audioContentId,
@@ -172,7 +180,8 @@ class AudioContentCommentService(
commentId: Long,
memberId: Long,
timezone: String,
pageable: Pageable
pageable: Pageable,
lang: Lang? = null
): GetAudioContentCommentListResponse {
val parentComment = repository.findByIdOrNull(id = commentId)
?: return GetAudioContentCommentListResponse(totalCount = 0, items = listOf())
@@ -187,7 +196,8 @@ class AudioContentCommentService(
memberId = memberId,
timezone = timezone,
offset = pageable.offset,
limit = pageable.pageSize
limit = pageable.pageSize,
lang = lang
)
val totalCount = repository.commentReplyCountByAudioContentCommentId(commentId, memberId)
@@ -10,7 +10,9 @@ import kr.co.vividnext.sodalive.content.main.QGetAudioContentMainItem
import kr.co.vividnext.sodalive.content.order.QOrder.order
import kr.co.vividnext.sodalive.content.playlist.ContentIdAndEndDateData
import kr.co.vividnext.sodalive.content.playlist.QContentIdAndEndDateData
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@@ -29,7 +31,8 @@ interface OrderQueryRepository {
coverImageHost: String,
memberId: Long,
offset: Long = 0,
limit: Long = 10
limit: Long = 10,
lang: Lang = Lang.KO
): List<GetAudioContentOrderListItem>
fun totalAudioContentOrderListCount(memberId: Long, dateTime: LocalDateTime): Int
@@ -38,12 +41,13 @@ interface OrderQueryRepository {
coverImageHost: String,
memberId: Long,
offset: Long = 0,
limit: Long = 20
limit: Long = 20,
lang: Lang = Lang.KO
): List<GetAudioContentMainItem>
fun findOrderedContent(contentIdList: List<Long>, memberId: Long): List<Long>
fun findEndDateByContentId(contentIdList: List<Long>, memberId: Long): List<ContentIdAndEndDateData>
fun findBuyerListByContentId(contentId: Long): List<ContentBuyer>
fun findBuyerListByContentId(contentId: Long, lang: Lang = Lang.KO): List<ContentBuyer>
fun findByMemberIdAndContentId(memberId: Long, contentId: Long, createdAt: LocalDateTime): Order?
}
@@ -134,8 +138,10 @@ class OrderQueryRepositoryImpl(
coverImageHost: String,
memberId: Long,
offset: Long,
limit: Long
limit: Long,
lang: Lang
): List<GetAudioContentOrderListItem> {
val creatorName = CreatorNameJoin(member, lang, "orderCreatorName")
val where = order.member.id.eq(memberId)
.and(order.isActive.isTrue)
.and(
@@ -153,7 +159,7 @@ class OrderQueryRepositoryImpl(
audioContent.id,
audioContent.coverImage.prepend("/").prepend(coverImageHost),
audioContent.member.id,
audioContent.member.nickname,
creatorName.displayName,
audioContent.title,
audioContent.theme.theme,
audioContent.duration,
@@ -165,6 +171,8 @@ class OrderQueryRepositoryImpl(
)
.from(order)
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where(where)
.offset(offset)
.limit(limit)
@@ -197,8 +205,10 @@ class OrderQueryRepositoryImpl(
coverImageHost: String,
memberId: Long,
offset: Long,
limit: Long
limit: Long,
lang: Lang
): List<GetAudioContentMainItem> {
val creatorName = CreatorNameJoin(member, lang, "mainOrderCreatorName")
return queryFactory
.select(
QGetAudioContentMainItem(
@@ -209,7 +219,7 @@ class OrderQueryRepositoryImpl(
member.profileImage
.prepend("/")
.prepend(coverImageHost),
member.nickname,
creatorName.displayName,
audioContent.price,
audioContent.duration,
audioContent.isPointAvailable
@@ -218,6 +228,7 @@ class OrderQueryRepositoryImpl(
.from(order)
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where(
order.member.id.eq(memberId)
.and(order.isActive.isTrue)
@@ -266,16 +277,18 @@ class OrderQueryRepositoryImpl(
.fetch()
}
override fun findBuyerListByContentId(contentId: Long): List<ContentBuyer> {
override fun findBuyerListByContentId(contentId: Long, lang: Lang): List<ContentBuyer> {
val creatorName = CreatorNameJoin(member, lang, "contentBuyerName")
return queryFactory
.select(
QContentBuyer(
member.nickname,
creatorName.displayName,
member.profileImage.prepend("/").prepend(imageHost)
)
)
.from(order)
.innerJoin(order.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where(
order.isActive.isTrue,
order.audioContent.id.eq(contentId),
@@ -9,6 +9,7 @@ import kr.co.vividnext.sodalive.content.PurchaseOption
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
import kr.co.vividnext.sodalive.content.like.AudioContentLikeRepository
import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.point.PointUsageService
import org.springframework.beans.factory.annotation.Value
@@ -26,6 +27,7 @@ class OrderService(
private val audioContentCommentQueryRepository: AudioContentCommentRepository,
private val audioContentLikeQueryRepository: AudioContentLikeRepository,
private val limitedEditionOrderRepository: LimitedEditionOrderRepository,
private val langContext: LangContext,
@Value("\${cloud.aws.cloud-front.host}")
private val audioContentCoverImageHost: String
@@ -123,7 +125,8 @@ class OrderService(
coverImageHost = audioContentCoverImageHost,
memberId = member.id!!,
offset = offset,
limit = limit
limit = limit,
lang = langContext.lang
)
.map {
val commentCount = audioContentCommentQueryRepository
@@ -154,7 +157,8 @@ class OrderService(
coverImageHost = audioContentCoverImageHost,
memberId = memberId,
offset = 0,
limit = 20
limit = 20,
lang = langContext.lang
)
}
}
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.content.playlist
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.AudioContentRepository
import kr.co.vividnext.sodalive.content.order.OrderRepository
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.live.roulette.RedisIdGenerator
import kr.co.vividnext.sodalive.member.Member
import org.springframework.data.repository.findByIdOrNull
@@ -16,7 +17,8 @@ class AudioContentPlaylistService(
private val idGenerator: RedisIdGenerator,
private val orderRepository: OrderRepository,
private val audioContentRepository: AudioContentRepository,
private val redisRepository: AudioContentPlaylistRedisRepository
private val redisRepository: AudioContentPlaylistRedisRepository,
private val langContext: LangContext
) {
fun createPlaylist(request: CreatePlaylistRequest, member: Member) {
if (request.contentIdAndOrderList.size >= 30) {
@@ -173,7 +175,8 @@ class AudioContentPlaylistService(
}
val contentList = audioContentRepository.fetchContentForPlaylist(
contentIdList = contentIdAndOrderList.map { it.contentId }
contentIdList = contentIdAndOrderList.map { it.contentId },
lang = langContext.lang
)
val orderMap = contentIdAndOrderList.sortedBy { it.order }
@@ -112,7 +112,8 @@ class AudioContentThemeService(
isAdult = memberContentPreferenceService.isAdultVisibleForQuery(member, isAdultContentVisible),
contentType = contentType,
offset = offset,
limit = limit
limit = limit,
lang = langContext.lang
)
return GetContentByThemeResponse(
@@ -29,6 +29,7 @@ import kr.co.vividnext.sodalive.live.room.LiveRoom
import kr.co.vividnext.sodalive.live.room.LiveRoomType
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.live.room.cancel.QLiveRoomCancel.liveRoomCancel
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.Member
@@ -40,6 +41,8 @@ import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing
import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag
import kr.co.vividnext.sodalive.member.tag.QMemberCreatorTag.memberCreatorTag
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.creatorNameContains
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Repository
import java.math.BigDecimal
@@ -122,13 +125,16 @@ class ExplorerQueryRepository(
creatorId: Long,
limit: Long,
offset: Long = 0,
withDonationCan: Boolean
withDonationCan: Boolean,
lang: Lang? = null
): List<MemberDonationRankingResponse> {
val member = QMember("user")
val creatorName = lang?.let { CreatorNameJoin(member, it, "legacyDonationRankName") }
val displayName = creatorName?.displayName ?: member.nickname
val donation = useCan.rewardCan.add(useCan.can).sum()
return queryFactory
.select(member, donation)
val query = queryFactory
.select(member, donation, displayName)
.from(useCanCalculate)
.innerJoin(useCanCalculate.useCan, useCan)
.innerJoin(useCan.member, member)
@@ -144,16 +150,17 @@ class ExplorerQueryRepository(
.or(useCan.canUsage.eq(CanUsage.LIVE))
)
)
.groupBy(useCan.member.id)
.groupBy(useCan.member.id, member.id, displayName)
.orderBy(donation.desc(), member.id.desc())
.fetch()
if (creatorName != null) query.leftJoin(creatorName.name).on(creatorName.condition)
return query.fetch()
.asSequence()
.map {
val account = it.get(member)!!
val donationCan = it.get(donation)!!
MemberDonationRankingResponse(
account.id!!,
account.nickname,
it.get(displayName)!!,
if (account.profileImage != null) {
"$cloudFrontHost/${account.profileImage}"
} else {
@@ -165,9 +172,12 @@ class ExplorerQueryRepository(
.toList()
}
fun getNewCreators(): List<Member> {
fun getNewCreators(lang: Lang = Lang.KO): List<GetExplorerSectionCreatorResponse> {
val creatorName = CreatorNameJoin(member, lang, "newExplorerCreatorName")
return queryFactory
.selectFrom(member)
.select(member, creatorName.displayName)
.from(member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where(
member.role.eq(MemberRole.CREATOR)
.and(member.createdAt.goe(LocalDateTime.now().minusDays(30)))
@@ -176,9 +186,13 @@ class ExplorerQueryRepository(
.orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
.limit(20)
.fetch()
.map { row ->
row.get(member)!!.toExplorerSectionCreator(cloudFrontHost).copy(nickname = row.get(creatorName.displayName)!!)
}
}
fun findCreatorByGender(gender: Int): List<Member> {
fun findCreatorByGender(gender: Int, lang: Lang = Lang.KO): List<GetExplorerSectionCreatorResponse> {
val creatorName = CreatorNameJoin(member, lang, "genderExplorerCreatorName")
val today = LocalDate.now()
val offset = (today.dayOfMonth - 1) % 26
val characters = ('A'..'Z').map { it.toString() }.plus((0..9).map { it.toString() })
@@ -192,7 +206,9 @@ class ExplorerQueryRepository(
val randomExpression = Expressions.numberTemplate(Double::class.java, "function('rand')").asc()
return queryFactory
.selectFrom(member)
.select(member, creatorName.displayName)
.from(member)
.leftJoin(creatorName.name).on(creatorName.condition)
.innerJoin(member.auth, auth)
.where(
member.role.eq(MemberRole.CREATOR)
@@ -204,17 +220,30 @@ class ExplorerQueryRepository(
.offset(0)
.limit(10)
.fetch()
.map { row ->
row.get(member)!!.toExplorerSectionCreator(cloudFrontHost).copy(nickname = row.get(creatorName.displayName)!!)
}
}
fun getSearchChannel(channel: String, memberId: Long): List<Member> {
return queryFactory.selectFrom(member)
fun getSearchChannel(channel: String, memberId: Long, lang: Lang? = null): List<GetRoomDetailUser> {
val creatorName = lang?.let { CreatorNameJoin(member, it, "channelSearchCreatorName") }
val displayName = creatorName?.displayName ?: member.nickname
val query = queryFactory.select(
Projections.constructor(
GetRoomDetailUser::class.java,
member.id,
displayName,
member.profileImage.coalesce("profile/default-profile.png").prepend("/").prepend(cloudFrontHost)
)
).from(member)
.where(
member.nickname.containsIgnoreCase(channel)
creatorNameContains(member, channel)
.and(member.isActive.isTrue)
.and(member.id.ne(memberId))
.and(member.role.eq(MemberRole.CREATOR))
)
.fetch()
if (creatorName != null) query.leftJoin(creatorName.name).on(creatorName.condition)
return query.fetch()
}
fun getMember(memberId: Long): Member? {
@@ -224,7 +253,8 @@ class ExplorerQueryRepository(
.fetchFirst()
}
fun getSimilarCreatorList(creatorId: Long): List<SimilarCreatorResponse> {
fun getSimilarCreatorList(creatorId: Long, lang: Lang = Lang.KO): List<SimilarCreatorResponse> {
val creatorName = CreatorNameJoin(member, lang, "similarCreatorName")
val creator = queryFactory
.selectFrom(member)
.where(member.id.eq(creatorId))
@@ -242,14 +272,15 @@ class ExplorerQueryRepository(
.and(member.tags.size().gt(0))
.and(memberCreatorTag.tag.isActive.isTrue)
return getRandomCreatorList(where = where, limit = 3)
return getRandomCreatorList(where = where, limit = 3, lang = lang)
}
val cnt = memberCreatorTag.member.count()
val similarCreators = queryFactory
.select(member, cnt)
.select(member, cnt, creatorName.displayName)
.from(memberCreatorTag)
.innerJoin(memberCreatorTag.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.innerJoin(memberCreatorTag.tag, creatorTag)
.leftJoin(member.auth, auth)
.where(
@@ -265,7 +296,7 @@ class ExplorerQueryRepository(
)
)
)
.groupBy(memberCreatorTag.member.id)
.groupBy(memberCreatorTag.member.id, member.id, creatorName.name.displayName)
.orderBy(cnt.desc())
.limit(3)
.fetch()
@@ -273,7 +304,7 @@ class ExplorerQueryRepository(
val account = it.get(member)!!
SimilarCreatorResponse(
account.id!!,
account.nickname,
it.get(creatorName.displayName)!!,
if (account.profileImage != null) {
"$cloudFrontHost/${account.profileImage}"
} else {
@@ -298,29 +329,32 @@ class ExplorerQueryRepository(
where = where.and(member.id.ne(userId))
}
val additionalCreator = getRandomCreatorList(where = where, limit = (3 - similarCreators.size).toLong())
val additionalCreator = getRandomCreatorList(where = where, limit = (3 - similarCreators.size).toLong(), lang = lang)
return similarCreators + additionalCreator
} else {
return similarCreators
}
}
private fun getRandomCreatorList(where: Predicate, limit: Long): List<SimilarCreatorResponse> {
private fun getRandomCreatorList(where: Predicate, limit: Long, lang: Lang): List<SimilarCreatorResponse> {
val creatorName = CreatorNameJoin(member, lang, "randomCreatorName")
val result = queryFactory
.select(member)
.select(member, creatorName.displayName)
.from(memberCreatorTag)
.join(memberCreatorTag.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.innerJoin(memberCreatorTag.tag, creatorTag)
.leftJoin(member.auth, auth)
.groupBy(memberCreatorTag.member.id)
.groupBy(memberCreatorTag.member.id, member.id, creatorName.name.displayName)
.where(where)
.limit(limit)
.orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
.fetch()
.map {
.map { row ->
val it = row.get(member)!!
SimilarCreatorResponse(
it.id!!,
it.nickname,
row.get(creatorName.displayName)!!,
if (it.profileImage != null) {
"$cloudFrontHost/${it.profileImage}"
} else {
@@ -474,7 +508,13 @@ class ExplorerQueryRepository(
.fetchFirst() ?: ""
}
fun getCheersList(creatorId: Long, memberId: Long, timezone: String, offset: Long, limit: Long): GetCheersResponse {
fun getCheersList(
creatorId: Long,
memberId: Long,
timezone: String,
offset: Long,
limit: Long
): GetCheersResponse {
val cheersDatePattern = messageSource
.getMessage("explorer.date.cheers.format", langContext.lang)
?: messageSource.getMessage("explorer.date.cheers.format", Lang.KO).orEmpty()
@@ -671,8 +711,10 @@ class ExplorerQueryRepository(
fun getFollowerList(
creatorId: Long,
offset: Long,
limit: Long
limit: Long,
lang: Lang = Lang.KO
): List<GetFollowerListDto> {
val creatorName = CreatorNameJoin(member, lang, "followerMemberName")
val blockMemberCondition = blockMember.member.id.eq(creatorId)
.and(blockMember.isActive.isTrue)
.and(blockMember.blockedMember.id.eq(member.id))
@@ -682,12 +724,13 @@ class ExplorerQueryRepository(
QGetFollowerListDto(
member.id,
member.profileImage.prepend("/").prepend(cloudFrontHost),
member.nickname,
creatorName.displayName,
member.role
)
)
.from(creatorFollowing)
.innerJoin(creatorFollowing.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.leftJoin(blockMember).on(blockMemberCondition)
.where(
member.isActive.isTrue
@@ -805,12 +848,17 @@ class ExplorerQueryRepository(
.fetchFirst()
}
fun getCreatorRankings(): List<Member> {
fun getCreatorRankings(lang: Lang = Lang.KO): List<GetExplorerSectionCreatorResponse> {
val creatorName = CreatorNameJoin(member, lang, "explorerRankCreatorName")
return queryFactory
.select(member)
.select(member, creatorName.displayName)
.from(creatorRanking)
.innerJoin(creatorRanking.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.orderBy(creatorRanking.ranking.asc())
.fetch()
.map { row ->
row.get(member)!!.toExplorerSectionCreator(cloudFrontHost).copy(nickname = row.get(creatorName.displayName)!!)
}
}
}
@@ -32,6 +32,7 @@ import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.MemberService
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkResponse
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.domain.Pageable
@@ -65,15 +66,15 @@ class ExplorerService(
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
private val localizedNameService: CreatorLocalizedNameService,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun getCreatorRank(memberId: Long): GetExplorerSectionResponse {
val creatorRankings = queryRepository
.getCreatorRankings()
.getCreatorRankings(langContext.lang)
.filter { !isBlockedBetweenMembers(memberId = memberId, otherMemberId = it.id!!) }
.map { it.toExplorerSectionCreator(cloudFrontHost) }
val currentDateTime = LocalDateTime.now()
val lastMonday = currentDateTime
@@ -107,9 +108,8 @@ class ExplorerService(
// 인기 크리에이터
val creatorRankings = queryRepository
.getCreatorRankings()
.getCreatorRankings(langContext.lang)
.filter { !isBlockedBetweenMembers(memberId = member.id!!, otherMemberId = it.id!!) }
.map { it.toExplorerSectionCreator(cloudFrontHost) }
val currentDateTime = LocalDateTime.now()
val lastMonday = currentDateTime
@@ -140,9 +140,8 @@ class ExplorerService(
// 새로 시작 (newCreators)
val newCreators = queryRepository
.getNewCreators()
.getNewCreators(langContext.lang)
.filter { !isBlockedBetweenMembers(memberId = member.id!!, otherMemberId = it.id!!) }
.map { it.toExplorerSectionCreator(cloudFrontHost) }
val newCreatorsSection = GetExplorerSectionResponse(
title = messageSource.getMessage("explorer.section.new_creators.title", langContext.lang).orEmpty(),
@@ -159,9 +158,8 @@ class ExplorerService(
.orEmpty(),
color = "39abde",
creators = queryRepository
.findCreatorByGender(1)
.findCreatorByGender(1, langContext.lang)
.filter { !isBlockedBetweenMembers(memberId = member.id!!, otherMemberId = it.id!!) }
.map { it.toExplorerSectionCreator(cloudFrontHost) }
)
val femaleCreatorSection = GetExplorerSectionResponse(
@@ -170,9 +168,8 @@ class ExplorerService(
.orEmpty(),
color = "ffa517",
creators = queryRepository
.findCreatorByGender(0)
.findCreatorByGender(0, langContext.lang)
.filter { !isBlockedBetweenMembers(memberId = member.id!!, otherMemberId = it.id!!) }
.map { it.toExplorerSectionCreator(cloudFrontHost) }
)
val randomInt = Random.nextInt(0, 2)
@@ -192,22 +189,23 @@ class ExplorerService(
throw SodaException(messageKey = "explorer.search.channel.min_length")
}
return queryRepository.getSearchChannel(channel, member.id!!)
return queryRepository.getSearchChannel(channel, member.id!!, langContext.lang)
.asSequence()
.filter { !isBlockedBetweenMembers(memberId = member.id!!, otherMemberId = it.id!!) }
.map { GetRoomDetailUser(it, cloudFrontHost) }
.toList()
}
fun getCreatorDetail(creatorId: Long, memberId: Long): GetCreatorDetailResponse {
val creatorAccount = queryRepository.getMember(creatorId)
?: throw SodaException(messageKey = "member.validation.user_not_found")
val creatorName = localizedNameService.findLocalizedNames(listOf(creatorId), langContext.lang)[creatorId]
?: creatorAccount.nickname
if (isBlockedBetweenMembers(memberId = memberId, otherMemberId = creatorId)) {
val messageTemplate = messageSource
.getMessage("explorer.creator.blocked_access", langContext.lang)
.orEmpty()
throw SodaException(message = String.format(messageTemplate, creatorAccount.nickname))
throw SodaException(message = String.format(messageTemplate, creatorName))
}
if (creatorAccount.role != MemberRole.CREATOR) {
@@ -239,7 +237,7 @@ class ExplorerService(
}
return GetCreatorDetailResponse(
nickname = creatorAccount.nickname,
nickname = creatorName,
profileImageUrl = if (creatorAccount.profileImage != null) {
"$cloudFrontHost/${creatorAccount.profileImage}"
} else {
@@ -266,6 +264,8 @@ class ExplorerService(
// 크리에이터(유저) 정보
val creatorAccount = queryRepository.getMember(creatorId)
?: throw SodaException(messageKey = "member.validation.user_not_found")
val creatorName = localizedNameService.findLocalizedNames(listOf(creatorId), langContext.lang)[creatorId]
?: creatorAccount.nickname
// 차단된 사용자 체크
val isBlockedByCreator = memberService.isBlocked(blockedMemberId = member.id!!, memberId = creatorId)
@@ -274,7 +274,7 @@ class ExplorerService(
val messageTemplate = messageSource
.getMessage("explorer.creator.blocked_access", langContext.lang)
.orEmpty()
throw SodaException(message = String.format(messageTemplate, creatorAccount.nickname))
throw SodaException(message = String.format(messageTemplate, creatorName))
}
val isCreator = creatorAccount.role == MemberRole.CREATOR
@@ -300,7 +300,7 @@ class ExplorerService(
// 추천 크리에이터
val similarCreatorList = if (isCreator) {
queryRepository.getSimilarCreatorList(creatorId)
queryRepository.getSimilarCreatorList(creatorId, langContext.lang)
} else {
listOf()
}
@@ -437,7 +437,7 @@ class ExplorerService(
} else {
"$cloudFrontHost/profile/default-profile.png"
},
nickname = creatorAccount.nickname,
nickname = creatorName,
tags = creatorAccount.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList(),
introduce = creatorAccount.introduce,
instagramUrl = creatorAccount.instagramUrl,
@@ -450,9 +450,9 @@ class ExplorerService(
isNotification = creatorFollowing?.isFollow ?: false,
notificationRecipientCount = notificationRecipientCount
),
userDonationRanking = memberDonationRanking,
userDonationRanking = renderDonationRanking(memberDonationRanking),
similarCreatorList = similarCreatorList,
liveRoomList = liveRoomList,
liveRoomList = liveRoomList.map { it.copy(managerNickname = creatorName) },
contentList = translatedContentList,
latestContent = latestContent,
totalContentCount = totalContentCount,
@@ -537,7 +537,7 @@ class ExplorerService(
null
},
totalCount = donationMemberTotal,
userDonationRanking = donationRanking
userDonationRanking = renderDonationRanking(donationRanking)
)
}
@@ -548,7 +548,12 @@ class ExplorerService(
): GetFollowerListResponse {
val totalCount = queryRepository.getFollowerListTotalCount(creatorId)
val followerList = queryRepository.getFollowerList(creatorId, pageable.offset, pageable.pageSize.toLong())
val followerList = queryRepository.getFollowerList(
creatorId,
pageable.offset,
pageable.pageSize.toLong(),
langContext.lang
)
.map {
val creatorFollowing = if (it.role == MemberRole.CREATOR) {
queryRepository.getCreatorFollowing(
@@ -684,6 +689,7 @@ class ExplorerService(
type = FcmEventType.CHANGE_NOTICE,
category = PushNotificationCategory.COMMUNITY,
title = member.nickname,
titleMemberId = member.id,
messageKey = "explorer.notice.fcm.message",
senderMemberId = member.id,
creatorId = member.id!!,
@@ -693,6 +699,11 @@ class ExplorerService(
)
}
private fun renderDonationRanking(items: List<MemberDonationRankingResponse>): List<MemberDonationRankingResponse> {
val names = localizedNameService.findCurrentNames(items.map { it.userId }, langContext.lang)
return items.map { it.copy(nickname = names[it.userId] ?: it.nickname) }
}
private fun isBlockedBetweenMembers(memberId: Long, otherMemberId: Long): Boolean {
return memberService.isBlocked(blockedMemberId = memberId, memberId = otherMemberId) ||
memberService.isBlocked(blockedMemberId = otherMemberId, memberId = memberId)
@@ -1,7 +1,11 @@
package kr.co.vividnext.sodalive.explorer.profile.channelDonation
import com.querydsl.core.types.dsl.CaseBuilder
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.QChannelDonationMessage.channelDonationMessage
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
@@ -18,8 +22,9 @@ interface ChannelDonationMessageQueryRepository {
offset: Long,
limit: Long,
startDateTime: LocalDateTime,
endDateTime: LocalDateTime
): List<ChannelDonationMessage>
endDateTime: LocalDateTime,
lang: Lang = Lang.KO
): List<Pair<ChannelDonationMessage, String>>
fun getChannelDonationMessageTotalCount(
creatorId: Long,
@@ -40,8 +45,13 @@ class ChannelDonationMessageQueryRepositoryImpl(
offset: Long,
limit: Long,
startDateTime: LocalDateTime,
endDateTime: LocalDateTime
): List<ChannelDonationMessage> {
endDateTime: LocalDateTime,
lang: Lang
): List<Pair<ChannelDonationMessage, String>> {
val creatorName = CreatorNameJoin(member, lang, "channelDonorName")
val originalName = CaseBuilder().`when`(member.nickname.startsWith("deleted_"))
.then(member.nickname.substring(8)).otherwise(member.nickname)
val displayName = creatorName.name.displayName.coalesce(originalName)
val where = whereCondition(
creatorId = creatorId,
memberId = memberId,
@@ -51,8 +61,10 @@ class ChannelDonationMessageQueryRepositoryImpl(
)
return queryFactory
.selectFrom(channelDonationMessage)
.leftJoin(channelDonationMessage.member).fetchJoin()
.select(channelDonationMessage, displayName)
.from(channelDonationMessage)
.leftJoin(channelDonationMessage.member, member).fetchJoin()
.leftJoin(creatorName.name).on(creatorName.condition)
.where(where)
.offset(offset)
.limit(limit)
@@ -61,6 +73,7 @@ class ChannelDonationMessageQueryRepositoryImpl(
channelDonationMessage.id.desc()
)
.fetch()
.map { row -> row.get(channelDonationMessage)!! to row.get(displayName)!! }
}
override fun getChannelDonationMessageTotalCount(
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.explorer.profile.channelDonation
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
import kr.co.vividnext.sodalive.can.use.CanUsage
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.Member
@@ -99,12 +98,13 @@ class ChannelDonationService(
offset = offset,
limit = limit,
startDateTime = startDateTime,
endDateTime = endDateTime
).map {
endDateTime = endDateTime,
lang = langContext.lang
).map { (it, nickname) ->
GetChannelDonationListItem(
id = it.id!!,
memberId = it.member!!.id!!,
nickname = it.member!!.nickname.removeDeletedNicknamePrefix(),
nickname = nickname,
profileUrl = if (it.member!!.profileImage != null) {
"$cloudFrontHost/${it.member!!.profileImage}"
} else {
@@ -140,6 +140,7 @@ class CreatorCommunityService(
type = FcmEventType.CHANGE_NOTICE,
category = PushNotificationCategory.COMMUNITY,
title = member.nickname,
titleMemberId = member.id,
messageKey = "creator.community.fcm.new_post",
senderMemberId = member.id,
creatorId = member.id!!,
@@ -533,6 +534,7 @@ class CreatorCommunityService(
type = FcmEventType.INDIVIDUAL,
category = PushNotificationCategory.COMMUNITY,
title = member.nickname,
titleMemberId = member.id,
messageKey = "creator.community.fcm.new_comment",
senderMemberId = member.id,
recipients = listOf(creatorId),
@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
@@ -70,8 +71,10 @@ class LiveRecommendRepository(
fun getOnAirRecommendChannelList(
isBlocked: (Long) -> Boolean,
isCreator: Boolean,
isAdult: Boolean
isAdult: Boolean,
lang: Lang
): List<GetRecommendChannelResponse> {
val creatorName = CreatorNameJoin(member, lang, "onAirRecommendedCreatorName")
var where = member.role.eq(MemberRole.CREATOR)
.and(member.isActive.isTrue)
@@ -88,20 +91,21 @@ class LiveRecommendRepository(
Projections.constructor(
GetRecommendChannelResponse::class.java,
member.id,
member.nickname,
creatorName.displayName,
member.profileImage.prepend("/").prepend(cloudFrontHost),
Expressions.asBoolean(true)
)
)
.from(liveRoom)
.rightJoin(liveRoom.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where(
where
.and(liveRoom.isActive.isTrue)
.and(liveRoom.channelName.isNotNull)
.and(liveRoom.channelName.isNotEmpty)
)
.groupBy(member.id)
.groupBy(member.id, member.nickname, member.profileImage, creatorName.name.displayName)
.orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
.limit(20)
.fetch()
@@ -113,8 +117,10 @@ class LiveRecommendRepository(
fun getRecommendChannelList(
withOutCreatorList: List<Long>,
limit: Long,
isBlocked: (Long) -> Boolean
isBlocked: (Long) -> Boolean,
lang: Lang
): List<GetRecommendChannelResponse> {
val creatorName = CreatorNameJoin(member, lang, "recommendedCreatorName")
val where = member.role.eq(MemberRole.CREATOR)
.and(member.isActive.isTrue)
@@ -123,12 +129,13 @@ class LiveRecommendRepository(
Projections.constructor(
GetRecommendChannelResponse::class.java,
member.id,
member.nickname,
creatorName.displayName,
member.profileImage.prepend("/").prepend(cloudFrontHost),
Expressions.asBoolean(false)
)
)
.from(member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where(where.and(member.id.notIn(withOutCreatorList)))
.orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
.limit(limit)
@@ -140,8 +147,10 @@ class LiveRecommendRepository(
memberId: Long,
isBlocked: (Long) -> Boolean,
isCreator: Boolean,
isAdult: Boolean
isAdult: Boolean,
lang: Lang
): List<GetRecommendChannelResponse> {
val creatorName = CreatorNameJoin(member, lang, "onAirFollowingCreatorName")
var where = member.role.eq(MemberRole.CREATOR)
.and(member.isActive.isTrue)
@@ -158,7 +167,7 @@ class LiveRecommendRepository(
Projections.constructor(
GetRecommendChannelResponse::class.java,
member.id,
member.nickname,
creatorName.displayName,
member.profileImage.prepend("/").prepend(cloudFrontHost),
Expressions.asBoolean(true)
)
@@ -166,6 +175,7 @@ class LiveRecommendRepository(
.from(liveRoom, creatorFollowing)
.rightJoin(liveRoom.member, member)
.innerJoin(creatorFollowing.creator, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where(
where
.and(liveRoom.member.id.eq(creatorFollowing.creator.id))
@@ -175,7 +185,7 @@ class LiveRecommendRepository(
.and(liveRoom.channelName.isNotNull)
.and(liveRoom.channelName.isNotEmpty)
)
.groupBy(member.id)
.groupBy(member.id, member.nickname, member.profileImage, creatorName.name.displayName)
.orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
.limit(20)
.fetch()
@@ -188,8 +198,10 @@ class LiveRecommendRepository(
memberId: Long,
withOutCreatorList: List<Long>,
limit: Long,
isBlocked: (Long) -> Boolean
isBlocked: (Long) -> Boolean,
lang: Lang
): List<GetRecommendChannelResponse> {
val creatorName = CreatorNameJoin(member, lang, "followingCreatorName")
val where = member.role.eq(MemberRole.CREATOR)
.and(member.isActive.isTrue)
@@ -198,13 +210,14 @@ class LiveRecommendRepository(
Projections.constructor(
GetRecommendChannelResponse::class.java,
member.id,
member.nickname,
creatorName.displayName,
member.profileImage.prepend("/").prepend(cloudFrontHost),
Expressions.asBoolean(false)
)
)
.from(creatorFollowing)
.innerJoin(creatorFollowing.creator, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where(
where
.and(member.id.notIn(withOutCreatorList))
@@ -244,8 +257,10 @@ class LiveRecommendRepository(
memberId: Long,
offset: Long,
limit: Long,
isBlocked: (Long) -> Boolean
isBlocked: (Long) -> Boolean,
lang: Lang
): List<GetCreatorFollowingAllListItem> {
val creatorName = CreatorNameJoin(member, lang, "allFollowingCreatorName")
val where = member.role.eq(MemberRole.CREATOR)
.and(member.isActive.isTrue)
.and(creatorFollowing.isActive.isTrue)
@@ -256,7 +271,7 @@ class LiveRecommendRepository(
.select(
QGetCreatorFollowingAllListItem(
member.id,
member.nickname,
creatorName.displayName,
member.profileImage.prepend("/").prepend(cloudFrontHost),
Expressions.asBoolean(true),
creatorFollowing.isNotify
@@ -264,10 +279,12 @@ class LiveRecommendRepository(
)
.from(creatorFollowing)
.innerJoin(creatorFollowing.creator, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where(where)
.groupBy(
member.id,
member.nickname,
creatorName.name.displayName,
member.profileImage,
creatorFollowing.isNotify
)
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.live.recommend
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
@@ -13,7 +14,8 @@ class LiveRecommendService(
private val repository: LiveRecommendRepository,
private val blockMemberRepository: BlockMemberRepository,
private val memberContentPreferenceService: MemberContentPreferenceService,
private val liveRecommendCacheService: LiveRecommendCacheService
private val liveRecommendCacheService: LiveRecommendCacheService,
private val langContext: LangContext
) {
fun getRecommendLive(member: Member?, lang: Lang): List<GetRecommendLiveResponse> {
val isAdult = if (member != null) {
@@ -45,7 +47,8 @@ class LiveRecommendService(
}
},
isCreator = member?.role == MemberRole.CREATOR,
isAdult = isAdult
isAdult = isAdult,
lang = langContext.lang
)
if (onAirChannelList.size >= 20) {
@@ -63,7 +66,8 @@ class LiveRecommendService(
} else {
false
}
}
},
lang = langContext.lang
)
return onAirChannelList + notOnAirCreatorList
@@ -76,7 +80,8 @@ class LiveRecommendService(
memberId = member.id!!,
isBlocked = { isBlockedBetweenMembers(memberId = member.id!!, creatorId = it) },
isCreator = member.role == MemberRole.CREATOR,
isAdult = isAdult
isAdult = isAdult,
lang = langContext.lang
)
if (onAirFollowingChannelList.size >= 20) {
@@ -89,7 +94,8 @@ class LiveRecommendService(
memberId = member.id!!,
withOutCreatorList = onAirCreatorIdList,
limit = (20 - onAirCreatorIdList.size).toLong(),
isBlocked = { isBlockedBetweenMembers(memberId = member.id!!, creatorId = it) }
isBlocked = { isBlockedBetweenMembers(memberId = member.id!!, creatorId = it) },
lang = langContext.lang
)
return onAirFollowingChannelList + notOnAirFollowingChannelList
@@ -105,7 +111,8 @@ class LiveRecommendService(
memberId = member.id!!,
offset = pageable.offset,
limit = pageable.pageSize.toLong(),
isBlocked = { isBlockedBetweenMembers(memberId = member.id!!, creatorId = it) }
isBlocked = { isBlockedBetweenMembers(memberId = member.id!!, creatorId = it) },
lang = langContext.lang
)
return GetCreatorFollowingAllListResponse(
@@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.live.reservation
import java.time.LocalDateTime
data class LiveReservationListRow(
val id: Long,
val roomId: Long,
val title: String,
val coverImage: String?,
val price: Int,
val masterNickname: String,
val beginDateTime: LocalDateTime
)
@@ -1,10 +1,17 @@
package kr.co.vividnext.sodalive.live.reservation
import com.querydsl.core.types.Projections
import com.querydsl.jpa.impl.JPAQuery
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.live.reservation.QLiveReservation.liveReservation
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.QMember
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@@ -12,29 +19,45 @@ import org.springframework.stereotype.Repository
interface LiveReservationRepository : JpaRepository<LiveReservation, Long>, LiveReservationQueryRepository
interface LiveReservationQueryRepository {
fun getReservationList(roomId: Long): List<LiveReservation>
fun getReservationList(roomId: Long, lang: Lang): List<GetRoomDetailUser>
fun cancelReservation(roomId: Long)
fun getReservationBookerList(roomId: Long): List<Member>
fun isExistsReservation(roomId: Long, memberId: Long): Boolean
fun getReservationListByMemberId(memberId: Long, active: Boolean): List<LiveReservation>
fun getReservationListByMemberId(memberId: Long, active: Boolean, lang: Lang): List<LiveReservationListRow>
fun getReservationByReservationAndMemberId(
reservationId: Long,
memberId: Long
): LiveReservation?
memberId: Long,
lang: Lang
): LiveReservationListRow?
fun getReservationCount(memberId: Long): Int
}
@Repository
class LiveReservationQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveReservationQueryRepository {
override fun getReservationList(roomId: Long): List<LiveReservation> {
class LiveReservationQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) : LiveReservationQueryRepository {
override fun getReservationList(roomId: Long, lang: Lang): List<GetRoomDetailUser> {
val bookerName = CreatorNameJoin(member, lang, "liveReservationBookerName")
return queryFactory
.selectFrom(liveReservation)
.select(
Projections.constructor(
GetRoomDetailUser::class.java,
member.id,
bookerName.displayName,
member.profileImage.coalesce("profile/default-profile.png").prepend("/").prepend(cloudFrontHost)
)
)
.from(liveReservation)
.innerJoin(liveReservation.room, liveRoom)
.innerJoin(liveReservation.member, member)
.leftJoin(bookerName.name).on(bookerName.condition)
.where(
liveRoom.id.eq(roomId)
.and(liveReservation.isActive.isTrue)
@@ -76,11 +99,8 @@ class LiveReservationQueryRepositoryImpl(private val queryFactory: JPAQueryFacto
.fetchFirst() != null
}
override fun getReservationListByMemberId(memberId: Long, active: Boolean): List<LiveReservation> {
return queryFactory
.selectFrom(liveReservation)
.innerJoin(liveReservation.member, member)
.innerJoin(liveReservation.room, liveRoom)
override fun getReservationListByMemberId(memberId: Long, active: Boolean, lang: Lang): List<LiveReservationListRow> {
return reservationRows(lang)
.where(
liveRoom.isActive.isTrue
.and(liveReservation.isActive.eq(active))
@@ -89,11 +109,12 @@ class LiveReservationQueryRepositoryImpl(private val queryFactory: JPAQueryFacto
.fetch()
}
override fun getReservationByReservationAndMemberId(reservationId: Long, memberId: Long): LiveReservation? {
return queryFactory
.selectFrom(liveReservation)
.innerJoin(liveReservation.member, member)
.innerJoin(liveReservation.room, liveRoom)
override fun getReservationByReservationAndMemberId(
reservationId: Long,
memberId: Long,
lang: Lang
): LiveReservationListRow? {
return reservationRows(lang)
.where(
liveReservation.id.eq(reservationId)
.and(member.id.eq(memberId))
@@ -101,6 +122,29 @@ class LiveReservationQueryRepositoryImpl(private val queryFactory: JPAQueryFacto
.fetchFirst()
}
private fun reservationRows(lang: Lang): JPAQuery<LiveReservationListRow> {
val owner = QMember("reservationRoomOwner")
val ownerName = CreatorNameJoin(owner, lang, "reservationRoomOwnerName")
return queryFactory
.select(
Projections.constructor(
LiveReservationListRow::class.java,
liveReservation.id,
liveRoom.id,
liveRoom.title,
liveRoom.coverImage,
liveRoom.price,
ownerName.displayName,
liveRoom.beginDateTime
)
)
.from(liveReservation)
.innerJoin(liveReservation.member, member)
.innerJoin(liveReservation.room, liveRoom)
.innerJoin(liveRoom.member, owner)
.leftJoin(ownerName.name).on(ownerName.condition)
}
override fun getReservationCount(memberId: Long): Int {
return queryFactory
.selectFrom(liveReservation)
@@ -8,6 +8,7 @@ import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.live.room.LiveRoomRepository
import kr.co.vividnext.sodalive.live.room.LiveRoomType
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
@@ -25,6 +26,7 @@ class LiveReservationService(
private val liveReservationCancelRepository: LiveReservationCancelRepository,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
private val localizedNameService: CreatorLocalizedNameService,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
@@ -78,10 +80,11 @@ class LiveReservationService(
"live.reservation.datetime_format",
langContext.lang
).orEmpty()
val names = localizedNameService.findLocalizedNames(listOf(room.member!!.id!!), langContext.lang)
return MakeLiveReservationResponse(
reservationId = reservation.id!!,
nickname = room.member!!.nickname,
nickname = names[room.member!!.id] ?: room.member!!.nickname,
title = room.title,
beginDateString = beginDateTime.format(DateTimeFormatter.ofPattern(reservationDateFormat)),
beginDateTimeUtc = room.beginDateTime
@@ -98,34 +101,35 @@ class LiveReservationService(
)
}
@Transactional(readOnly = true)
fun getReservationList(memberId: Long, active: Boolean, timezone: String): List<GetLiveReservationResponse> {
val detailDateFormat = messageSource.getMessage(
"live.room.datetime_format_detail",
langContext.lang
).orEmpty()
return repository
.getReservationListByMemberId(memberId, active)
val reservations = repository.getReservationListByMemberId(memberId, active, langContext.lang)
return reservations
.asSequence()
.map {
val beginDateTime = it.room!!.beginDateTime
val beginDateTime = it.beginDateTime
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of(timezone))
GetLiveReservationResponse(
reservationId = it.id!!,
roomId = it.room!!.id!!,
title = it.room!!.title,
coverImageUrl = if (it.room!!.coverImage != null) {
"$cloudFrontHost/${it.room!!.coverImage}"
reservationId = it.id,
roomId = it.roomId,
title = it.title,
coverImageUrl = if (it.coverImage != null) {
"$cloudFrontHost/${it.coverImage}"
} else {
"$cloudFrontHost/profile/default-profile.png"
},
price = it.room!!.price,
masterNickname = it.room!!.member!!.nickname,
price = it.price,
masterNickname = it.masterNickname,
beginDateTime = beginDateTime.format(
DateTimeFormatter.ofPattern(detailDateFormat)
),
beginDateTimeUtc = it.room!!.beginDateTime
beginDateTimeUtc = it.beginDateTime
.atZone(ZoneId.of("UTC"))
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
cancelable = beginDateTime.minusHours(4).isAfter(
@@ -138,33 +142,34 @@ class LiveReservationService(
.toList()
}
@Transactional(readOnly = true)
fun getReservation(reservationId: Long, memberId: Long, timezone: String): GetLiveReservationResponse {
val reservation = repository.getReservationByReservationAndMemberId(reservationId, memberId)
val reservation = repository.getReservationByReservationAndMemberId(reservationId, memberId, langContext.lang)
?: throw SodaException(messageKey = "live.reservation.invalid_reservation")
val detailDateFormat = messageSource.getMessage(
"live.room.datetime_format_detail",
langContext.lang
).orEmpty()
val beginDateTime = reservation.room!!.beginDateTime
val beginDateTime = reservation.beginDateTime
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of(timezone))
return GetLiveReservationResponse(
reservationId = reservation.id!!,
roomId = reservation.room!!.id!!,
title = reservation.room!!.title,
coverImageUrl = if (reservation.room!!.coverImage != null) {
"$cloudFrontHost/${reservation.room!!.coverImage}"
reservationId = reservation.id,
roomId = reservation.roomId,
title = reservation.title,
coverImageUrl = if (reservation.coverImage != null) {
"$cloudFrontHost/${reservation.coverImage}"
} else {
"$cloudFrontHost/profile/default-profile.png"
},
price = reservation.room!!.price,
masterNickname = reservation.room!!.member!!.nickname,
price = reservation.price,
masterNickname = reservation.masterNickname,
beginDateTime = beginDateTime.format(
DateTimeFormatter.ofPattern(detailDateFormat)
),
beginDateTimeUtc = reservation.room!!.beginDateTime
beginDateTimeUtc = reservation.beginDateTime
.atZone(ZoneId.of("UTC"))
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
cancelable = beginDateTime.minusHours(4).isAfter(
@@ -1,5 +1,7 @@
package kr.co.vividnext.sodalive.live.room
import kr.co.vividnext.sodalive.member.Member
data class GetLiveRoomUserProfileResponse(
val userId: Long,
val nickname: String,
@@ -19,3 +21,5 @@ data class GetLiveRoomUserProfileResponse(
val isFollowing: Boolean?,
val isBlock: Boolean
)
data class LiveRoomUserProfileRow(val member: Member, val nickname: String)
@@ -0,0 +1,3 @@
package kr.co.vividnext.sodalive.live.room
data class LiveRoomListRow(val room: LiveRoom, val creatorNickname: String)
@@ -8,17 +8,19 @@ import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
import kr.co.vividnext.sodalive.can.use.QUseCanCalculate.useCanCalculate
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.live.room.QQuarterLiveRankings.quarterLiveRankings
import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationItem
import kr.co.vividnext.sodalive.live.room.donation.QGetLiveRoomDonationItem
import kr.co.vividnext.sodalive.live.room.like.GetLiveRoomHeartListItem
import kr.co.vividnext.sodalive.live.room.like.QGetLiveRoomHeartListItem
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.beans.factory.annotation.Value
import org.springframework.cache.annotation.Cacheable
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
@@ -35,8 +37,9 @@ interface LiveRoomQueryRepository {
memberId: Long?,
isCreator: Boolean,
isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom>
effectiveGender: Gender?,
lang: Lang
): List<LiveRoomListRow>
fun getLiveRoomListReservationWithDate(
date: LocalDateTime,
@@ -45,30 +48,40 @@ interface LiveRoomQueryRepository {
memberId: Long?,
isCreator: Boolean,
isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom>
effectiveGender: Gender?,
lang: Lang
): List<LiveRoomListRow>
fun getLiveRoomListReservationWithoutDate(
timezone: String,
memberId: Long?,
isCreator: Boolean,
isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom>
effectiveGender: Gender?,
lang: Lang
): List<LiveRoomListRow>
fun getLiveRoom(id: Long): LiveRoom?
fun getLiveRoomDetail(id: Long, lang: Lang): LiveRoomListRow?
fun getLiveRoomUserProfile(userId: Long, lang: Lang): LiveRoomUserProfileRow?
fun getLiveRoomAndAccountId(roomId: Long, memberId: Long): LiveRoom?
fun getRecentRoomInfo(memberId: Long): GetRecentRoomInfoResponse?
fun getDonationTotal(roomId: Long): Int?
fun getDonationList(roomId: Long, isLiveCreator: Boolean, memberId: Long): List<GetLiveRoomDonationItem>
fun getDonationList(roomId: Long, isLiveCreator: Boolean, memberId: Long, lang: Lang): List<GetLiveRoomDonationItem>
fun getRoomActiveAndChannelNameIsNotNull(memberId: Long): List<LiveRoom>
fun getActiveRoomIdList(memberId: Long): Int
fun getTotalHeartCount(roomId: Long): Int?
fun getLiveRoomCreatorId(roomId: Long): Long?
fun getHeartList(roomId: Long): List<GetLiveRoomHeartListItem>
fun getHeartList(roomId: Long, lang: Lang): List<GetLiveRoomHeartListItem>
@Cacheable(
cacheNames = ["cache_ttl_10_minutes"],
key = "'getLatestFinishedLive:' + (#memberId ?: 'guest')"
)
fun getLatestFinishedLive(memberId: Long?): List<GetLatestFinishedLiveResponse>
}
@Repository
class LiveRoomQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@@ -82,8 +95,10 @@ class LiveRoomQueryRepositoryImpl(
memberId: Long?,
isCreator: Boolean,
isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> {
effectiveGender: Gender?,
lang: Lang
): List<LiveRoomListRow> {
val creatorName = CreatorNameJoin(member, lang, "liveRoomNowCreatorName")
var where = liveRoom.channelName.isNotNull
.and(liveRoom.channelName.isNotEmpty)
.and(liveRoom.isActive.isTrue)
@@ -116,8 +131,10 @@ class LiveRoomQueryRepositoryImpl(
}
var select = queryFactory
.selectFrom(liveRoom)
.innerJoin(liveRoom.member, member)
.select(liveRoom, creatorName.displayName)
.from(liveRoom)
.innerJoin(liveRoom.member, member).fetchJoin()
.leftJoin(creatorName.name).on(creatorName.condition)
.leftJoin(quarterLiveRankings).on(liveRoom.id.eq(quarterLiveRankings.roomId))
if (hasMemberId) {
@@ -146,6 +163,7 @@ class LiveRoomQueryRepositoryImpl(
liveRoom.member.createdAt.desc()
)
.fetch()
.map { LiveRoomListRow(it.get(liveRoom)!!, it.get(creatorName.displayName)!!) }
}
override fun getLiveRoomListReservationWithDate(
@@ -155,8 +173,10 @@ class LiveRoomQueryRepositoryImpl(
memberId: Long?,
isCreator: Boolean,
isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> {
effectiveGender: Gender?,
lang: Lang
): List<LiveRoomListRow> {
val creatorName = CreatorNameJoin(member, lang, "liveRoomReservationDateCreatorName")
var where = liveRoom.beginDateTime.goe(date)
.and(liveRoom.beginDateTime.lt(date.plusDays(1)))
.and(
@@ -192,8 +212,10 @@ class LiveRoomQueryRepositoryImpl(
}
var select = queryFactory
.selectFrom(liveRoom)
.innerJoin(liveRoom.member, member)
.select(liveRoom, creatorName.displayName)
.from(liveRoom)
.innerJoin(liveRoom.member, member).fetchJoin()
.leftJoin(creatorName.name).on(creatorName.condition)
if (memberId != null) {
val blockMemberCondition = blockMember.isActive.isTrue
@@ -216,6 +238,7 @@ class LiveRoomQueryRepositoryImpl(
.limit(limit)
.orderBy(liveRoom.beginDateTime.asc())
.fetch()
.map { LiveRoomListRow(it.get(liveRoom)!!, it.get(creatorName.displayName)!!) }
}
override fun getLiveRoomListReservationWithoutDate(
@@ -223,8 +246,10 @@ class LiveRoomQueryRepositoryImpl(
memberId: Long?,
isCreator: Boolean,
isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> {
effectiveGender: Gender?,
lang: Lang
): List<LiveRoomListRow> {
val creatorName = CreatorNameJoin(member, lang, "liveRoomReservationCreatorName")
var where = liveRoom.beginDateTime.gt(
LocalDateTime.now()
.atZone(ZoneId.of(timezone))
@@ -276,8 +301,10 @@ class LiveRoomQueryRepositoryImpl(
}
var select = queryFactory
.selectFrom(liveRoom)
.innerJoin(liveRoom.member, member)
.select(liveRoom, creatorName.displayName)
.from(liveRoom)
.innerJoin(liveRoom.member, member).fetchJoin()
.leftJoin(creatorName.name).on(creatorName.condition)
.limit(10)
if (memberId != null) {
@@ -299,6 +326,7 @@ class LiveRoomQueryRepositoryImpl(
.where(where)
.orderBy(*orderBy.toTypedArray())
.fetch()
.map { LiveRoomListRow(it.get(liveRoom)!!, it.get(creatorName.displayName)!!) }
}
override fun getLiveRoom(id: Long): LiveRoom? {
@@ -312,6 +340,29 @@ class LiveRoomQueryRepositoryImpl(
.fetchFirst()
}
override fun getLiveRoomDetail(id: Long, lang: Lang): LiveRoomListRow? {
val creatorName = CreatorNameJoin(member, lang, "liveRoomDetailCreatorName")
val row = queryFactory
.select(liveRoom, creatorName.displayName)
.from(liveRoom)
.innerJoin(liveRoom.member, member).fetchJoin()
.leftJoin(creatorName.name).on(creatorName.condition)
.where(liveRoom.id.eq(id).and(liveRoom.isActive.isTrue))
.fetchFirst() ?: return null
return LiveRoomListRow(row.get(liveRoom)!!, row.get(creatorName.displayName)!!)
}
override fun getLiveRoomUserProfile(userId: Long, lang: Lang): LiveRoomUserProfileRow? {
val profileName = CreatorNameJoin(member, lang, "liveRoomUserProfileName")
val row = queryFactory
.select(member, profileName.displayName)
.from(member)
.leftJoin(profileName.name).on(profileName.condition)
.where(member.id.eq(userId))
.fetchOne() ?: return null
return LiveRoomUserProfileRow(row.get(member)!!, row.get(profileName.displayName)!!)
}
override fun getLiveRoomAndAccountId(roomId: Long, memberId: Long): LiveRoom? {
return queryFactory
.selectFrom(liveRoom)
@@ -359,7 +410,13 @@ class LiveRoomQueryRepositoryImpl(
.fetchOne()
}
override fun getDonationList(roomId: Long, isLiveCreator: Boolean, memberId: Long): List<GetLiveRoomDonationItem> {
override fun getDonationList(
roomId: Long,
isLiveCreator: Boolean,
memberId: Long,
lang: Lang
): List<GetLiveRoomDonationItem> {
val donorName = CreatorNameJoin(member, lang, "liveRoomDonorName")
val where = liveRoom.id.eq(roomId)
.and(useCan.canUsage.eq(CanUsage.DONATION).or(useCan.canUsage.eq(CanUsage.SPIN_ROULETTE)))
.and(useCan.isRefund.isFalse)
@@ -380,7 +437,7 @@ class LiveRoomQueryRepositoryImpl(
.coalesce("profile/default-profile.png")
.prepend("/")
.prepend(cloudFrontHost),
member.nickname,
donorName.displayName,
member.id.coalesce(0),
Expressions.asNumber(
CaseBuilder()
@@ -406,7 +463,8 @@ class LiveRoomQueryRepositoryImpl(
.from(useCan)
.innerJoin(useCan.room, liveRoom)
.innerJoin(useCan.member, member)
.groupBy(useCan.member.id)
.leftJoin(donorName.name).on(donorName.condition)
.groupBy(useCan.member.id, member.nickname, donorName.name.displayName)
.where(where)
.orderBy(sortExpression.sum().desc())
.fetch()
@@ -461,26 +519,29 @@ class LiveRoomQueryRepositoryImpl(
.fetchFirst()
}
override fun getHeartList(roomId: Long): List<GetLiveRoomHeartListItem> {
override fun getHeartList(roomId: Long, lang: Lang): List<GetLiveRoomHeartListItem> {
val donorName = CreatorNameJoin(member, lang, "liveRoomHeartDonorName")
val where = liveRoom.id.eq(roomId)
.and(useCan.canUsage.eq(CanUsage.HEART))
.and(useCan.isRefund.isFalse)
return queryFactory
.select(
QGetLiveRoomHeartListItem(
Projections.constructor(
GetLiveRoomHeartListItem::class.java,
member.profileImage
.coalesce("profile/default-profile.png")
.prepend("/")
.prepend(cloudFrontHost),
member.nickname,
donorName.displayName,
useCan.can.add(useCan.rewardCan).sum()
)
)
.from(useCan)
.innerJoin(useCan.room, liveRoom)
.innerJoin(useCan.member, member)
.groupBy(useCan.member.id)
.leftJoin(donorName.name).on(donorName.condition)
.groupBy(useCan.member.id, member.nickname, donorName.name.displayName)
.where(where)
.orderBy(useCan.can.add(useCan.rewardCan).sum().desc())
.fetch()
@@ -26,6 +26,7 @@ import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType
import kr.co.vividnext.sodalive.fcm.PushTokenRepository
import kr.co.vividnext.sodalive.fcm.notification.PushNotificationCategory
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
@@ -67,8 +68,8 @@ import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.member.contentpreference.ViewerContentPreference
import kr.co.vividnext.sodalive.utils.generateFileName
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.springframework.beans.factory.annotation.Value
import org.springframework.cache.annotation.Cacheable
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.domain.Pageable
import org.springframework.data.repository.findByIdOrNull
@@ -89,6 +90,7 @@ class LiveRoomService(
private val menuService: LiveRoomMenuService,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
private val localizedNameService: CreatorLocalizedNameService,
private val repository: LiveRoomRepository,
private val rouletteRepository: NewRouletteRepository,
@@ -247,14 +249,14 @@ class LiveRoomService(
)
}
val creatorIds = roomList.mapNotNull { it.member?.id }.distinct()
val creatorIds = roomList.mapNotNull { it.room.member?.id }.distinct()
val languageTagByMemberId = buildLanguageTagMap(creatorIds)
return roomList
.map {
val roomInfo = roomInfoRepository.findByIdOrNull(it.id!!)
.map { (room, creatorNickname) ->
val roomInfo = roomInfoRepository.findByIdOrNull(room.id!!)
val reservations = it.reservations
val reservations = room.reservations
.filter { reservation ->
if (member?.id != null) {
reservation.member!!.id!! == member.id!! && reservation.isActive
@@ -263,7 +265,7 @@ class LiveRoomService(
}
}
val beginDateTime = it.beginDateTime
val beginDateTime = room.beginDateTime
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of(timezone))
.format(
@@ -272,42 +274,42 @@ class LiveRoomService(
.withLocale(langContext.lang.locale)
)
val beginDateTimeUtc = it.beginDateTime
val beginDateTimeUtc = room.beginDateTime
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
val tags = it.tags
val tags = room.tags
.filter { tag -> tag.tag.isActive }
.map { tag -> tag.tag.tag }
.let { list -> applyLanguageTagToRoomTags(it.member?.id, list, languageTagByMemberId) }
.let { list -> applyLanguageTagToRoomTags(room.member?.id, list, languageTagByMemberId) }
GetRoomListResponse(
roomId = it.id!!,
title = it.title,
content = it.notice,
roomId = room.id!!,
title = room.title,
content = room.notice,
beginDateTime = beginDateTime,
beginDateTimeUtc = beginDateTimeUtc,
numberOfParticipate = (roomInfo?.listenerCount ?: 0) +
(roomInfo?.speakerCount ?: 0) +
(roomInfo?.managerCount ?: 0),
numberOfPeople = it.numberOfPeople,
isAdult = it.isAdult,
price = it.price,
channelName = it.channelName,
creatorProfileImage = if (it.member!!.profileImage != null) {
"$cloudFrontHost/${it.member!!.profileImage}"
numberOfPeople = room.numberOfPeople,
isAdult = room.isAdult,
price = room.price,
channelName = room.channelName,
creatorProfileImage = if (room.member!!.profileImage != null) {
"$cloudFrontHost/${room.member!!.profileImage}"
} else {
"$cloudFrontHost/profile/default-profile.png"
},
creatorNickname = it.member!!.nickname,
creatorId = it.member!!.id!!,
creatorNickname = creatorNickname,
creatorId = room.member!!.id!!,
tags = tags,
coverImageUrl = if (it.coverImage!!.startsWith("https://")) {
it.coverImage!!
coverImageUrl = if (room.coverImage!!.startsWith("https://")) {
room.coverImage!!
} else {
"$cloudFrontHost/${it.coverImage!!}"
"$cloudFrontHost/${room.coverImage!!}"
},
isReservation = reservations.isNotEmpty(),
isPrivateRoom = it.type == LiveRoomType.PRIVATE
isPrivateRoom = room.type == LiveRoomType.PRIVATE
)
}
}
@@ -319,7 +321,7 @@ class LiveRoomService(
isCreator: Boolean,
isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> {
): List<LiveRoomListRow> {
return repository.getLiveRoomListNow(
offset = pageable.offset,
limit = pageable.pageSize.toLong(),
@@ -327,7 +329,8 @@ class LiveRoomService(
memberId = memberId,
isCreator = isCreator,
isAdult = isAdult,
effectiveGender = effectiveGender
effectiveGender = effectiveGender,
lang = langContext.lang
)
}
@@ -339,7 +342,7 @@ class LiveRoomService(
isCreator: Boolean,
isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> {
): List<LiveRoomListRow> {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val date = LocalDate.parse(dateString, dateTimeFormatter).atStartOfDay()
.atZone(ZoneId.of(timezone))
@@ -353,7 +356,8 @@ class LiveRoomService(
memberId = memberId,
isCreator = isCreator,
isAdult = isAdult,
effectiveGender = effectiveGender
effectiveGender = effectiveGender,
lang = langContext.lang
)
}
@@ -363,8 +367,15 @@ class LiveRoomService(
isCreator: Boolean,
isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> {
return repository.getLiveRoomListReservationWithoutDate(timezone, memberId, isCreator, isAdult, effectiveGender)
): List<LiveRoomListRow> {
return repository.getLiveRoomListReservationWithoutDate(
timezone,
memberId,
isCreator,
isAdult,
effectiveGender,
langContext.lang
)
}
@Transactional
@@ -504,6 +515,7 @@ class LiveRoomService(
type = FcmEventType.CREATE_LIVE,
category = PushNotificationCategory.LIVE,
title = createdRoom.member!!.nickname,
titleMemberId = createdRoom.member!!.id,
messageKey = if (createdRoom.channelName != null) {
"live.room.fcm.message.started"
} else {
@@ -525,7 +537,7 @@ class LiveRoomService(
}
fun getRoomDetail(roomId: Long, member: Member, timezone: String): GetRoomDetailResponse {
val room = repository.getLiveRoom(id = roomId)
val (room, creatorNickname) = repository.getLiveRoomDetail(id = roomId, lang = langContext.lang)
?: throw SodaException(messageKey = "live.room.already_ended")
val creatorId = room.member!!.id!!
@@ -573,7 +585,7 @@ class LiveRoomService(
genderRestriction = room.genderRestriction,
password = room.password
)
response.manager = GetRoomDetailManager(room.member!!, cloudFrontHost = cloudFrontHost)
response.manager = GetRoomDetailManager(room.member!!, cloudFrontHost, creatorNickname)
if (!room.channelName.isNullOrBlank()) {
val roomInfo = roomInfoRepository.findByIdOrNull(roomId)
@@ -618,21 +630,14 @@ class LiveRoomService(
}
)
response.participatingUsers = users
val names = findCurrentNames(users.map { it.id }, langContext.lang)
response.participatingUsers = users.map { it.copy(nickname = names[it.id] ?: it.nickname) }
response.numberOfParticipants = users.size
}
} else {
val reservationList = reservationRepository.getReservationList(roomId)
val reservationList = reservationRepository.getReservationList(roomId, langContext.lang)
response.isPaid = reservationList.any { it.id == member.id }
response.participatingUsers = reservationList
.asSequence()
.map {
if (it.member!!.id!! == member.id!!) {
response.isPaid = true
}
GetRoomDetailUser(it.member!!, cloudFrontHost)
}
.toList()
response.numberOfParticipants = reservationList.size
}
@@ -686,6 +691,7 @@ class LiveRoomService(
type = FcmEventType.START_LIVE,
category = PushNotificationCategory.LIVE,
title = room.member!!.nickname,
titleMemberId = room.member!!.id,
messageKey = "live.room.fcm.message.started",
senderMemberId = room.member!!.id,
args = listOf(room.title),
@@ -761,6 +767,7 @@ class LiveRoomService(
type = FcmEventType.CANCEL_LIVE,
category = PushNotificationCategory.LIVE,
title = room.member!!.nickname,
titleMemberId = room.member!!.id,
messageKey = "live.room.fcm.message.canceled",
senderMemberId = room.member!!.id,
args = listOf(room.title),
@@ -792,8 +799,9 @@ class LiveRoomService(
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = room.member!!.id!!)
if (isBlocked) {
val names = localizedNameService.findLocalizedNames(listOf(room.member!!.id!!), langContext.lang)
throw SodaException(
message = formatMessage("live.room.enter_blocked_by_host", room.member!!.nickname)
message = formatMessage("live.room.enter_blocked_by_host", names[room.member!!.id] ?: room.member!!.nickname)
)
}
@@ -807,8 +815,12 @@ class LiveRoomService(
val kickOutCount = kickOutService.getKickOutCount(roomId = room.id!!, userId = member.id!!)
if (kickOutCount >= 2) {
val names = localizedNameService.findLocalizedNames(listOf(room.member!!.id!!), langContext.lang)
throw SodaException(
message = formatMessage("live.room.participation_blocked_by_host", room.member!!.nickname)
message = formatMessage(
"live.room.participation_blocked_by_host",
names[room.member!!.id] ?: room.member!!.nickname
)
)
}
@@ -1057,6 +1069,10 @@ class LiveRoomService(
?.lowercase()
?.take(2)
val names = findCurrentNames(
listOf(creatorId) + (roomInfo.speakerList + roomInfo.listenerList + roomInfo.managerList).map { it.id },
langContext.lang
)
return GetRoomInfoResponse(
roomId = roomId,
title = room.title,
@@ -1079,7 +1095,7 @@ class LiveRoomService(
rtmToken = rtmToken,
v2vWorkerToken = v2vWorkerToken,
creatorId = room.member!!.id!!,
creatorNickname = room.member!!.nickname,
creatorNickname = names[creatorId] ?: room.member!!.nickname,
creatorProfileUrl = if (room.member!!.profileImage != null) {
"$cloudFrontHost/${room.member!!.profileImage}"
} else {
@@ -1090,9 +1106,9 @@ class LiveRoomService(
isCaptureRecordingAvailable = room.isCaptureRecordingAvailable,
participantsCount = roomInfo.listenerCount + roomInfo.speakerCount + roomInfo.managerCount,
totalAvailableParticipantsCount = room.numberOfPeople,
speakerList = roomInfo.speakerList,
listenerList = roomInfo.listenerList,
managerList = roomInfo.managerList,
speakerList = roomInfo.speakerList.map { it.copy(nickname = names[it.id] ?: it.nickname).apply { role = it.role } },
listenerList = roomInfo.listenerList.map { it.copy(nickname = names[it.id] ?: it.nickname).apply { role = it.role } },
managerList = roomInfo.managerList.map { it.copy(nickname = names[it.id] ?: it.nickname).apply { role = it.role } },
donationRankingTop3UserIds = donationRankingTop3UserIds,
menuPan = menuPan?.menu ?: "",
creatorLanguageCode = creatorLanguageCode,
@@ -1129,12 +1145,14 @@ class LiveRoomService(
val roomInfo = roomInfoRepository.findByIdOrNull(roomId)
?: throw SodaException(messageKey = "live.room.info_not_found")
return if (liveRoomCreatorId != member.id!!) {
val messages = if (liveRoomCreatorId != member.id!!) {
roomInfo.donationMessageList
.filter { !it.isSecret || it.memberId == member.id!! }
} else {
roomInfo.donationMessageList
}
val names = findCurrentNames(messages.map { it.memberId }, langContext.lang)
return messages.map { it.copy(nickname = names[it.memberId] ?: it.nickname) }
}
fun deleteDonationMessage(request: DeleteLiveRoomDonationMessage, member: Member) {
@@ -1161,7 +1179,7 @@ class LiveRoomService(
val roomInfo = roomInfoRepository.findByIdOrNull(roomId)
?: throw SodaException(messageKey = "live.room.info_not_found")
val user = memberRepository.findByIdOrNull(userId)
val (user, nickname) = repository.getLiveRoomUserProfile(userId, langContext.lang)
?: throw SodaException(messageKey = "common.error.invalid_request")
val isFollowing = if (user.role == MemberRole.CREATOR) {
@@ -1194,7 +1212,7 @@ class LiveRoomService(
return GetLiveRoomUserProfileResponse(
userId = user.id!!,
nickname = user.nickname,
nickname = nickname,
profileUrl = if (user.profileImage != null) {
"$cloudFrontHost/${user.profileImage}"
} else {
@@ -1439,7 +1457,8 @@ class LiveRoomService(
val donationList = repository.getDonationList(
roomId = room.id!!,
isLiveCreator = isLiveCreator,
memberId = memberId
memberId = memberId,
lang = langContext.lang
).filter { it.secretCan > 0 || it.can > 0 }
val totalCan = donationList.sumOf { it.can }
@@ -1492,6 +1511,10 @@ class LiveRoomService(
return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() }
}
private fun findCurrentNames(memberIds: Collection<Long>, lang: Lang): Map<Long, String> {
return localizedNameService.findCurrentNames(memberIds.distinct(), lang)
}
private fun resolvePreference(member: Member?): ViewerContentPreference {
if (member == null) {
return ViewerContentPreference(
@@ -1531,7 +1554,7 @@ class LiveRoomService(
}
fun getHeartList(roomId: Long): GetLiveRoomHeartListResponse {
val heartList = repository.getHeartList(roomId = roomId)
val heartList = repository.getHeartList(roomId = roomId, lang = langContext.lang)
return GetLiveRoomHeartListResponse(
heartList = heartList,
@@ -1541,11 +1564,10 @@ class LiveRoomService(
}
@Transactional(readOnly = true)
@Cacheable(
cacheNames = ["cache_ttl_10_minutes"],
key = "'getLatestFinishedLive:' + (#member?.id ?: 'guest')"
)
fun getLatestFinishedLive(member: Member?): List<GetLatestFinishedLiveResponse> {
return repository.getLatestFinishedLive(memberId = member?.id)
val items = repository.getLatestFinishedLive(memberId = member?.id)
val memberIds = items.map { it.memberId }.distinct()
val names = findCurrentNames(memberIds, langContext.lang)
return items.map { it.copy(nickname = names[it.memberId] ?: it.nickname) }
}
}
@@ -37,9 +37,9 @@ data class GetRoomDetailManager(
val profileImageUrl: String,
val isCreator: Boolean
) {
constructor(member: Member, cloudFrontHost: String) : this(
constructor(member: Member, cloudFrontHost: String, nickname: String) : this(
id = member.id!!,
nickname = member.nickname,
nickname = nickname,
introduce = member.introduce,
youtubeUrl = member.youtubeUrl,
instagramUrl = member.instagramUrl,
@@ -1,14 +1,12 @@
package kr.co.vividnext.sodalive.live.room.like
import com.querydsl.core.annotations.QueryProjection
data class GetLiveRoomHeartListResponse(
val heartList: List<GetLiveRoomHeartListItem>,
val totalCount: Int,
val totalHeart: Int
)
data class GetLiveRoomHeartListItem @QueryProjection constructor(
data class GetLiveRoomHeartListItem(
val profileImage: String,
val nickname: String,
val heart: Int
@@ -1,12 +1,17 @@
package kr.co.vividnext.sodalive.live.room.visit
import com.querydsl.core.types.Projections
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.QMember
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@@ -16,11 +21,15 @@ interface LiveRoomVisitRepository : JpaRepository<LiveRoomVisit, Long>, LiveRoom
interface LiveRoomVisitQueryRepository {
fun findByRoomIdAndMemberId(roomId: Long, memberId: Long): LiveRoomVisit?
fun findFirstByMemberIdOrderByUpdatedAtDesc(memberId: Long): LiveRoomVisit?
fun getRecentVisitRoomUsers(roomId: Long, member: Member): List<Member>
fun getRecentVisitRoomUsers(roomId: Long, member: Member, lang: Lang): List<GetRoomDetailUser>
}
@Repository
class LiveRoomVisitQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomVisitQueryRepository {
class LiveRoomVisitQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) : LiveRoomVisitQueryRepository {
override fun findByRoomIdAndMemberId(roomId: Long, memberId: Long): LiveRoomVisit? {
return queryFactory
.selectFrom(liveRoomVisit)
@@ -44,7 +53,9 @@ class LiveRoomVisitQueryRepositoryImpl(private val queryFactory: JPAQueryFactory
.fetchFirst()
}
override fun getRecentVisitRoomUsers(roomId: Long, member: Member): List<Member> {
override fun getRecentVisitRoomUsers(roomId: Long, member: Member, lang: Lang): List<GetRoomDetailUser> {
val visitor = QMember.member
val visitorName = CreatorNameJoin(visitor, lang, "recentLiveRoomVisitorName")
var where = liveRoomVisit.room.id.eq(roomId)
.and(liveRoomVisit.member.isActive.isTrue)
.and(liveRoomVisit.member.id.ne(member.id))
@@ -58,10 +69,18 @@ class LiveRoomVisitQueryRepositoryImpl(private val queryFactory: JPAQueryFactory
}
return queryFactory
.select(QMember.member)
.select(
Projections.constructor(
GetRoomDetailUser::class.java,
visitor.id,
visitorName.displayName,
visitor.profileImage.coalesce("profile/default-profile.png").prepend("/").prepend(cloudFrontHost)
)
)
.from(liveRoomVisit)
.innerJoin(liveRoomVisit.member, QMember.member)
.innerJoin(liveRoomVisit.member, visitor)
.innerJoin(liveRoomVisit.room, liveRoom)
.leftJoin(visitorName.name).on(visitorName.condition)
.where(where)
.fetch()
}
@@ -1,9 +1,9 @@
package kr.co.vividnext.sodalive.live.room.visit
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.live.room.LiveRoom
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.member.Member
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
@@ -12,9 +12,7 @@ import java.time.LocalDateTime
@Transactional(readOnly = true)
class LiveRoomVisitService(
private val repository: LiveRoomVisitRepository,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
private val langContext: LangContext
) {
@Transactional
fun roomVisit(room: LiveRoom, member: Member) {
@@ -34,9 +32,6 @@ class LiveRoomVisitService(
val roomVisit = repository.findFirstByMemberIdOrderByUpdatedAtDesc(member.id!!)
?: return emptyList()
return repository.getRecentVisitRoomUsers(roomVisit.room!!.id!!, member)
.asSequence()
.map { GetRoomDetailUser(it, cloudFrontHost) }
.toList()
return repository.getRecentVisitRoomUsers(roomVisit.room!!.id!!, member, langContext.lang)
}
}
@@ -1,12 +1,15 @@
package kr.co.vividnext.sodalive.member
import com.querydsl.core.types.Projections
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.fcm.PushTokenInfo
import kr.co.vividnext.sodalive.fcm.QPushToken.pushToken
import kr.co.vividnext.sodalive.fcm.QPushTokenInfo
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.live.reservation.QLiveReservation.liveReservation
import kr.co.vividnext.sodalive.live.room.GenderRestriction
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.auth.QAuth.auth
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
@@ -15,6 +18,8 @@ import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollow
import kr.co.vividnext.sodalive.member.nickname.QNicknameChangeLog.nicknameChangeLog
import kr.co.vividnext.sodalive.member.notification.QMemberNotification.memberNotification
import kr.co.vividnext.sodalive.message.QMessage.message
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.creatorNameContains
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Lock
@@ -38,7 +43,7 @@ interface MemberRepository : JpaRepository<Member, Long>, MemberQueryRepository
}
interface MemberQueryRepository {
fun findByNicknameAndOtherCondition(nickname: String, member: Member): List<Member>
fun findByNicknameAndOtherCondition(nickname: String, member: Member, lang: Lang? = null): List<GetRoomDetailUser>
fun findCreatorByIdOrNull(memberId: Long): Member?
fun getAllRecipientPushTokens(isAuth: Boolean?): List<PushTokenInfo>
fun getCreateLiveRoomNotificationRecipientPushTokens(
@@ -70,7 +75,7 @@ interface MemberQueryRepository {
fun getPushTokenFromReservationList(roomId: Long): List<PushTokenInfo>
fun getAuditionNoticeRecipientPushTokens(isAuth: Boolean): List<PushTokenInfo>
fun getMemberProfile(memberId: Long, myMemberId: Long): GetMemberProfileResponse
fun getMemberProfile(memberId: Long, myMemberId: Long, lang: Lang = Lang.KO): GetMemberProfileResponse
fun existsByNickname(nickname: String): Boolean
fun findNicknamesWithPrefix(prefix: String): List<String>
@@ -86,8 +91,10 @@ class MemberQueryRepositoryImpl(
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) : MemberQueryRepository {
override fun findByNicknameAndOtherCondition(nickname: String, member: Member): List<Member> {
var where = QMember.member.nickname.containsIgnoreCase(nickname)
override fun findByNicknameAndOtherCondition(nickname: String, member: Member, lang: Lang?): List<GetRoomDetailUser> {
val creatorName = lang?.let { CreatorNameJoin(QMember.member, it, "memberSearchName") }
val displayName = creatorName?.displayName ?: QMember.member.nickname
var where = creatorNameContains(QMember.member, nickname)
.and(QMember.member.id.ne(member.id))
.and(QMember.member.role.ne(MemberRole.ADMIN))
.and(QMember.member.role.ne(MemberRole.AGENT))
@@ -97,10 +104,19 @@ class MemberQueryRepositoryImpl(
where = where.and(QMember.member.role.ne(MemberRole.USER))
}
return queryFactory
.selectFrom(QMember.member)
val query = queryFactory
.select(
Projections.constructor(
GetRoomDetailUser::class.java,
QMember.member.id,
displayName,
QMember.member.profileImage.coalesce("profile/default-profile.png").prepend("/").prepend(cloudFrontHost)
)
)
.from(QMember.member)
.where(where)
.fetch()
if (creatorName != null) query.leftJoin(creatorName.name).on(creatorName.condition)
return query.fetch()
}
override fun findCreatorByIdOrNull(memberId: Long): Member? {
@@ -499,17 +515,19 @@ class MemberQueryRepositoryImpl(
.fetch()
}
override fun getMemberProfile(memberId: Long, myMemberId: Long): GetMemberProfileResponse {
override fun getMemberProfile(memberId: Long, myMemberId: Long, lang: Lang): GetMemberProfileResponse {
val creatorName = CreatorNameJoin(member, lang, "profileMemberName")
return queryFactory
.select(
QGetMemberProfileResponse(
member.id,
member.nickname,
creatorName.displayName,
member.profileImage.prepend("/").prepend(cloudFrontHost),
blockMember.id.isNotNull
)
)
.from(member)
.leftJoin(creatorName.name).on(creatorName.condition)
.leftJoin(blockMember)
.on(
member.id.eq(blockMember.blockedMember.id)
@@ -53,6 +53,7 @@ import kr.co.vividnext.sodalive.member.token.MemberTokenRepository
import kr.co.vividnext.sodalive.point.MemberPointRepository
import kr.co.vividnext.sodalive.utils.generateFileName
import kr.co.vividnext.sodalive.utils.generatePassword
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import kr.co.vividnext.sodalive.v2.home.following.port.out.HomeFollowingNewsInboxPort
import org.springframework.beans.factory.annotation.Value
import org.springframework.cache.CacheManager
@@ -105,6 +106,7 @@ class MemberService(
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
private val localizedNameService: CreatorLocalizedNameService,
private val countryContext: CountryContext,
private val memberContentPreferenceService: MemberContentPreferenceService,
@@ -308,7 +310,8 @@ class MemberService(
).sumOf { it.point }
return MyPageResponse(
nickname = member.nickname,
nickname = localizedNameService.findLocalizedNames(listOf(member.id!!), langContext.lang)[member.id]
?: member.nickname,
profileUrl = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {
@@ -542,8 +545,7 @@ class MemberService(
fun getBlockedMemberList(memberId: Long, offset: Long, limit: Long): GetBlockedMemberListResponse {
val totalCount = blockMemberRepository.getBlockedMemberTotalCount(memberId)
val items = blockMemberRepository.getBlockedMemberList(offset = offset, limit = limit, memberId = memberId)
val items = blockMemberRepository.getBlockedMemberList(offset, limit, memberId, langContext.lang)
return GetBlockedMemberListResponse(totalCount = totalCount, items = items)
}
@@ -610,14 +612,11 @@ class MemberService(
throw SodaException(messageKey = "member.validation.nickname_min_length")
}
return repository.findByNicknameAndOtherCondition(nickname, member)
return repository.findByNicknameAndOtherCondition(nickname, member, langContext.lang)
.filter {
!blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) &&
!blockMemberRepository.isBlocked(blockedMemberId = it.id!!, memberId = member.id!!)
}
.map {
GetRoomDetailUser(it, cloudFrontHost)
}
}
@Transactional
@@ -837,7 +836,7 @@ class MemberService(
}
fun getMemberProfile(memberId: Long, myMemberId: Long): GetMemberProfileResponse {
return repository.getMemberProfile(memberId, myMemberId)
return repository.getMemberProfile(memberId, myMemberId, langContext.lang)
}
private fun formatMessage(key: String, vararg args: Any): String {
@@ -1,8 +1,10 @@
package kr.co.vividnext.sodalive.member.block
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@@ -14,7 +16,7 @@ interface BlockMemberQueryRepository {
fun getBlockAccount(blockedMemberId: Long, memberId: Long): BlockMember?
fun isBlocked(blockedMemberId: Long, memberId: Long): Boolean
fun getBlockedMemberIdList(memberId: Long): List<Long>
fun getBlockedMemberList(offset: Long, limit: Long, memberId: Long): List<GetBlockedMemberListItem>
fun getBlockedMemberList(offset: Long, limit: Long, memberId: Long, lang: Lang = Lang.KO): List<GetBlockedMemberListItem>
fun getBlockedMemberTotalCount(memberId: Long): Int
}
@@ -61,18 +63,20 @@ class BlockMemberQueryRepositoryImpl(
.fetch()
}
override fun getBlockedMemberList(offset: Long, limit: Long, memberId: Long): List<GetBlockedMemberListItem> {
override fun getBlockedMemberList(offset: Long, limit: Long, memberId: Long, lang: Lang): List<GetBlockedMemberListItem> {
val creatorName = CreatorNameJoin(member, lang, "blockedMemberName")
return queryFactory
.select(
QGetBlockedMemberListItem(
blockMember.blockedMember.id,
blockMember.blockedMember.nickname,
creatorName.displayName,
blockMember.blockedMember.profileImage.prepend("/").prepend(cloudFrontHost),
blockMember.isActive
)
)
.from(blockMember)
.innerJoin(blockMember.blockedMember, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where(
blockMember.member.id.eq(memberId)
.and(blockMember.isActive.isTrue)
@@ -18,13 +18,15 @@ 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.SeriesState
import kr.co.vividnext.sodalive.explorer.GetExplorerSectionCreatorResponse
import kr.co.vividnext.sodalive.explorer.QCreatorRanking.creatorRanking
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag
import kr.co.vividnext.sodalive.member.tag.QMemberCreatorTag.memberCreatorTag
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
@@ -36,7 +38,9 @@ class RankingRepository(
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) {
fun getCreatorRankings(memberId: Long? = null): List<Member> {
fun getCreatorRankings(memberId: Long? = null, lang: Lang = Lang.KO): List<GetExplorerSectionCreatorResponse> {
val creatorName = CreatorNameJoin(member, lang, "rankingCreatorName")
val displayName = creatorName.displayName
val blockMemberCondition = if (memberId != null) {
blockMember.isActive.isTrue
.and(
@@ -52,11 +56,12 @@ class RankingRepository(
}
var select = queryFactory
.select(member)
.select(member, displayName)
.from(creatorRanking)
.innerJoin(creatorRanking.member, member)
.leftJoin(member.tags, memberCreatorTag).fetchJoin()
.leftJoin(memberCreatorTag.tag, creatorTag).fetchJoin()
select.leftJoin(creatorName.name).on(creatorName.condition)
if (memberId != null) {
select = select.leftJoin(blockMember).on(blockMemberCondition)
@@ -69,6 +74,9 @@ class RankingRepository(
return select
.orderBy(creatorRanking.ranking.asc())
.fetch()
.map { row ->
row.get(member)!!.toExplorerSectionCreator(imageHost).copy(nickname = row.get(displayName)!!)
}
.distinctBy { it.id }
}
@@ -84,6 +92,8 @@ class RankingRepository(
theme: String = "",
locale: String? = null
): List<GetAudioContentRankingItem> {
val creatorName = locale?.let { CreatorNameJoin(member, Lang.fromCode(it), "rankingContentCreatorName") }
val displayName = creatorName?.displayName ?: member.nickname
val blockMemberCondition = if (memberId != null) {
blockMember.isActive.isTrue
.and(
@@ -148,7 +158,7 @@ class RankingRepository(
audioContent.price,
audioContent.duration,
member.id,
member.nickname,
displayName,
audioContent.isPointAvailable,
creatorProfileImageUrl
)
@@ -199,6 +209,7 @@ class RankingRepository(
select = select.leftJoin(contentTranslation)
.on(contentTranslation.contentId.eq(audioContent.id).and(contentTranslation.locale.eq(locale)))
}
if (creatorName != null) select.leftJoin(creatorName.name).on(creatorName.condition)
if (memberId != null) {
where = where.and(blockMember.id.isNull)
@@ -269,6 +280,7 @@ class RankingRepository(
}
val results = select
.groupBy(member.id, displayName)
.offset(offset)
.limit(limit)
.fetch()
@@ -283,7 +295,7 @@ class RankingRepository(
val price = row.get(audioContent.price) ?: 0
val duration = row.get(audioContent.duration) ?: ""
val creatorId = row.get(member.id)!!
val creatorNickname = row.get(member.nickname)!!
val creatorNickname = row.get(displayName)!!
val isPointAvailable = row.get(audioContent.isPointAvailable) ?: false
val creatorProfileImageUrlValue = row.get(creatorProfileImageUrl)!!
@@ -26,7 +26,6 @@ class RankingService(
fun getCreatorRanking(memberId: Long?, rankingDate: String): GetExplorerSectionResponse {
val creatorRankings = repository
.getCreatorRankings(memberId = memberId)
.map { it.toExplorerSectionCreator(imageHost) }
return GetExplorerSectionResponse(
title = "인기 크리에이터",
@@ -10,11 +10,14 @@ import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentThe
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.keyword.QSeriesKeyword.seriesKeyword
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag
import kr.co.vividnext.sodalive.member.tag.QMemberCreatorTag.memberCreatorTag
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.creatorNameContains
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Repository
@@ -50,7 +53,7 @@ class SearchRepository(
.and(member.role.eq(MemberRole.CREATOR))
.and(creatorTag.isActive.isTrue.or(creatorTag.id.isNull))
.and(
member.nickname.containsIgnoreCase(keyword)
creatorNameContains(member, keyword)
.or(member.introduce.containsIgnoreCase(keyword))
.or(creatorTag.tag.containsIgnoreCase(keyword))
)
@@ -65,7 +68,8 @@ class SearchRepository(
keyword: String,
memberId: Long,
offset: Long,
limit: Long
limit: Long,
lang: Lang = Lang.KO
): List<SearchResponseItem> {
val blockMemberCondition = blockMember.isActive.isTrue
.and(
@@ -77,16 +81,18 @@ class SearchRepository(
)
)
val creatorName = CreatorNameJoin(member, lang, "searchCreatorName")
return queryFactory
.select(
QSearchResponseItem(
member.id,
member.profileImage.prepend("/").prepend(imageHost),
member.nickname,
member.nickname
creatorName.displayName,
creatorName.displayName
)
)
.from(member)
.leftJoin(creatorName.name).on(creatorName.condition)
.leftJoin(memberCreatorTag).on(memberCreatorTag.member.id.eq(member.id))
.leftJoin(creatorTag).on(memberCreatorTag.tag.id.eq(creatorTag.id))
.leftJoin(blockMember).on(blockMemberCondition)
@@ -95,13 +101,13 @@ class SearchRepository(
.and(member.role.eq(MemberRole.CREATOR))
.and(creatorTag.isActive.isTrue.or(creatorTag.id.isNull))
.and(
member.nickname.containsIgnoreCase(keyword)
creatorNameContains(member, keyword)
.or(member.introduce.containsIgnoreCase(keyword))
.or(creatorTag.tag.containsIgnoreCase(keyword))
)
.and(blockMember.id.isNull)
)
.groupBy(member.id)
.groupBy(member.id, creatorName.name.displayName)
.orderBy(member.id.desc())
.offset(offset)
.limit(limit)
@@ -131,7 +137,7 @@ class SearchRepository(
.and(audioContent.duration.isNotNull)
.and(
audioContent.title.containsIgnoreCase(keyword)
.or(member.nickname.containsIgnoreCase(keyword))
.or(creatorNameContains(member, keyword))
.or(audioContent.detail.containsIgnoreCase(keyword))
.or(audioContent.theme.theme.containsIgnoreCase(keyword))
.or(hashTag.tag.containsIgnoreCase(keyword).and(audioContentHashTag.isActive.isTrue))
@@ -177,7 +183,8 @@ class SearchRepository(
isAdult: Boolean,
contentType: ContentType,
offset: Long,
limit: Long
limit: Long,
lang: Lang = Lang.KO
): List<SearchResponseItem> {
val blockMemberCondition = blockMember.isActive.isTrue
.and(
@@ -196,7 +203,7 @@ class SearchRepository(
.and(audioContent.duration.isNotNull)
.and(
audioContent.title.containsIgnoreCase(keyword)
.or(member.nickname.containsIgnoreCase(keyword))
.or(creatorNameContains(member, keyword))
.or(audioContent.detail.containsIgnoreCase(keyword))
.or(audioContent.theme.theme.containsIgnoreCase(keyword))
.or(hashTag.tag.containsIgnoreCase(keyword).and(audioContentHashTag.isActive.isTrue))
@@ -222,23 +229,25 @@ class SearchRepository(
}
}
val creatorName = CreatorNameJoin(member, lang, "searchContentCreatorName")
return queryFactory
.select(
QSearchResponseItem(
audioContent.id,
audioContent.coverImage.prepend("/").prepend(imageHost),
audioContent.title,
audioContent.member.nickname
creatorName.displayName
)
)
.from(audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.leftJoin(audioContentTheme).on(audioContentTheme.id.eq(audioContent.theme.id))
.leftJoin(audioContentHashTag).on(audioContentHashTag.audioContent.id.eq(audioContent.id))
.leftJoin(hashTag).on(audioContentHashTag.hashTag.id.eq(hashTag.id))
.leftJoin(blockMember).on(blockMemberCondition)
.where(where)
.groupBy(audioContent.id)
.groupBy(audioContent.id, member.id, creatorName.name.displayName)
.orderBy(audioContent.id.desc())
.offset(offset)
.limit(limit)
@@ -270,7 +279,7 @@ class SearchRepository(
.and(audioContent.limited.isNull)
.and(
series.title.containsIgnoreCase(keyword)
.or(member.nickname.containsIgnoreCase(keyword))
.or(creatorNameContains(member, keyword))
.or(series.introduction.containsIgnoreCase(keyword))
.or(seriesGenre.genre.containsIgnoreCase(keyword))
.or(hashTag.tag.containsIgnoreCase(keyword).and(seriesKeyword.isActive.isTrue))
@@ -319,7 +328,8 @@ class SearchRepository(
isAdult: Boolean,
contentType: ContentType,
offset: Long,
limit: Long
limit: Long,
lang: Lang = Lang.KO
): List<SearchResponseItem> {
val blockMemberCondition = blockMember.isActive.isTrue
.and(
@@ -340,7 +350,7 @@ class SearchRepository(
.and(audioContent.limited.isNull)
.and(
series.title.containsIgnoreCase(keyword)
.or(member.nickname.containsIgnoreCase(keyword))
.or(creatorNameContains(member, keyword))
.or(series.introduction.containsIgnoreCase(keyword))
.or(seriesGenre.genre.containsIgnoreCase(keyword))
.or(hashTag.tag.containsIgnoreCase(keyword).and(seriesKeyword.isActive.isTrue))
@@ -367,25 +377,27 @@ class SearchRepository(
}
}
val creatorName = CreatorNameJoin(member, lang, "searchSeriesCreatorName")
return queryFactory
.select(
QSearchResponseItem(
series.id,
series.coverImage.prepend("/").prepend(imageHost),
series.title,
series.member.nickname
creatorName.displayName
)
)
.from(seriesContent)
.innerJoin(seriesContent.series, series)
.innerJoin(seriesContent.content, audioContent)
.innerJoin(series.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.leftJoin(seriesGenre).on(seriesGenre.id.eq(series.genre.id))
.leftJoin(seriesKeyword).on(seriesKeyword.series.id.eq(series.id))
.leftJoin(hashTag).on(hashTag.id.eq(seriesKeyword.keyword.id))
.leftJoin(blockMember).on(blockMemberCondition)
.where(where)
.groupBy(series.id, series.orders)
.groupBy(series.id, series.orders, member.id, creatorName.name.displayName)
.orderBy(series.orders.asc())
.offset(offset)
.limit(limit)
@@ -1,11 +1,12 @@
package kr.co.vividnext.sodalive.search
import kr.co.vividnext.sodalive.content.ContentType
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import org.springframework.stereotype.Service
@Service
class SearchService(private val repository: SearchRepository) {
class SearchService(private val repository: SearchRepository, private val langContext: LangContext) {
fun searchUnified(
keyword: String,
isAdult: Boolean,
@@ -16,7 +17,8 @@ class SearchService(private val repository: SearchRepository) {
keyword = keyword,
memberId = member.id!!,
offset = 0,
limit = 3
limit = 3,
lang = langContext.lang
)
.map {
it.type = SearchResponseType.CREATOR
@@ -29,7 +31,8 @@ class SearchService(private val repository: SearchRepository) {
isAdult = isAdult,
contentType = contentType,
offset = 0,
limit = 3
limit = 3,
lang = langContext.lang
)
.map {
it.type = SearchResponseType.CONTENT
@@ -42,7 +45,8 @@ class SearchService(private val repository: SearchRepository) {
isAdult = isAdult,
contentType = contentType,
offset = 0,
limit = 3
limit = 3,
lang = langContext.lang
)
.map {
it.type = SearchResponseType.SERIES
@@ -67,7 +71,8 @@ class SearchService(private val repository: SearchRepository) {
keyword = keyword,
memberId = member.id!!,
offset = offset,
limit = limit
limit = limit,
lang = langContext.lang
)
.map {
it.type = SearchResponseType.CREATOR
@@ -98,7 +103,8 @@ class SearchService(private val repository: SearchRepository) {
isAdult = isAdult,
contentType = contentType,
offset = offset,
limit = limit
limit = limit,
lang = langContext.lang
)
.map {
it.type = SearchResponseType.CONTENT
@@ -129,7 +135,8 @@ class SearchService(private val repository: SearchRepository) {
isAdult = isAdult,
contentType = contentType,
offset = offset,
limit = limit
limit = limit,
lang = langContext.lang
)
.map {
it.type = SearchResponseType.SERIES
@@ -18,6 +18,7 @@ import kr.co.vividnext.sodalive.query.recommend.RecommendChannelQueryService
import kr.co.vividnext.sodalive.rank.ContentRankingSortType
import kr.co.vividnext.sodalive.rank.RankingRepository
import kr.co.vividnext.sodalive.rank.RankingService
import kr.co.vividnext.sodalive.support.originalLocalizedNameService
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
@@ -68,6 +69,7 @@ class HomeServiceTest {
rankingRepository = rankingRepository,
explorerQueryRepository = explorerQueryRepository,
langContext = LangContext().apply { setLang(Lang.JA) },
localizedNameService = originalLocalizedNameService(),
memberContentPreferenceService = Mockito.mock(
kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService::class.java
),
@@ -99,7 +101,7 @@ class HomeServiceTest {
Mockito.doReturn(emptyList<Any>()).`when`(liveRoomService)
.getRoomList(null, LiveRoomStatus.NOW, Pageable.ofSize(10), null, timezone)
Mockito.`when`(rankingRepository.getCreatorRankings(null)).thenReturn(emptyList())
Mockito.`when`(rankingRepository.getCreatorRankings(null, Lang.JA)).thenReturn(emptyList())
Mockito.`when`(
contentThemeService.getActiveThemeOfContent(
false,
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.can
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
@@ -22,7 +23,7 @@ class CanServiceTest {
fun setUp() {
repository = mock(CanRepository::class.java)
countryContext = mock(CountryContext::class.java)
service = CanService(repository, countryContext)
service = CanService(repository, countryContext, LangContext())
}
@Test
@@ -0,0 +1,328 @@
package kr.co.vividnext.sodalive.content
import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationItem
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationQueryRepository
import kr.co.vividnext.sodalive.content.order.Order
import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.series.ContentSeriesRepository
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslation
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationPayload
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
import kr.co.vividnext.sodalive.content.translation.ContentTranslation
import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
import kr.co.vividnext.sodalive.explorer.CreatorRanking
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.block.BlockMember
import kr.co.vividnext.sodalive.rank.RankingRepository
import org.junit.jupiter.api.Assertions.assertAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.jdbc.core.JdbcTemplate
import java.time.LocalDateTime
import javax.persistence.EntityManager
@DataJpaTest(
properties = [
"spring.cache.type=none",
"spring.datasource.url=jdbc:h2:mem:legacy-content-owner-name;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
]
)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(QueryDslConfig::class, RankingRepository::class, AudioContentCurationQueryRepository::class)
class AudioContentRepositoryLocalizedNameTest @Autowired constructor(
private val repository: AudioContentRepository,
private val seriesRepository: ContentSeriesRepository,
private val rankingRepository: RankingRepository,
private val curationRepository: AudioContentCurationQueryRepository,
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate
) {
@Test
@DisplayName("JA 메인 콘텐츠 조회는 콘텐츠 ID가 아닌 실제 소유자 ID로 크리에이터 이름만 대체한다")
fun shouldLocalizeCurrentOwnerNameByCreatorIdWithoutChangingContentTitleOrUserName() {
val viewer = saveMember("owner-query-viewer", MemberRole.USER)
val creator = saveMember("owner-original", MemberRole.CREATOR)
val user = saveMember("user-original", MemberRole.USER)
val creatorContent = saveContent(creator, "Original audio title").takeIf { it.id != creator.id }
?: saveContent(creator, "Original audio title")
val userContent = saveContent(user, "User audio title")
saveAlias(creator, "Japanese owner")
saveAlias(user, "Must not replace USER")
entityManager.clear()
assertNotEquals(creator.id, creatorContent.id)
val items = findLatest(viewer.id!!, "ja").associateBy { it.contentId }
assertAll(
{ assertEquals("Japanese owner", items[creatorContent.id]!!.creatorNickname) },
{ assertEquals(creator.id, items[creatorContent.id]!!.creatorId) },
{ assertEquals("Original audio title", items[creatorContent.id]!!.title) },
{ assertEquals("user-original", items[userContent.id]!!.creatorNickname) },
{ assertEquals("User audio title", items[userContent.id]!!.title) },
{
assertEquals(
"owner-original",
findLatest(viewer.id!!, "ko").first { it.contentId == creatorContent.id }.creatorNickname
)
},
{
assertEquals(
"owner-original",
findLatest(viewer.id!!, "en").first { it.contentId == creatorContent.id }.creatorNickname
)
}
)
}
@Test
@DisplayName("메인 콘텐츠 이름 대체는 공개 필터를 유지하고 해제 후 변경된 현재 원본으로 돌아간다")
fun shouldPreserveVisibilityFiltersAndFallbackToRenamedOriginalAfterSoftClear() {
val viewer = saveMember("visibility-query-viewer", MemberRole.USER)
val creator = saveMember("before-rename", MemberRole.CREATOR)
val visible = saveContent(creator, "visible title")
saveContent(creator, "future title").releaseDate = LocalDateTime.now().plusDays(1)
saveContent(creator, "adult title").isAdult = true
saveContent(creator, "inactive title").isActive = false
saveContent(creator, "unfinished title").duration = null
val blockedCreator = saveMember("blocked-owner", MemberRole.CREATOR)
saveContent(blockedCreator, "blocked title")
entityManager.persist(
BlockMember(isActive = true).apply {
member = blockedCreator
blockedMember = viewer
}
)
saveAlias(creator, "Retained owner alias")
saveAlias(blockedCreator, "Blocked owner alias")
creator.nickname = "renamed-original"
entityManager.flush()
entityManager.clear()
val activeItems = findLatest(viewer.id!!, "ja")
assertEquals(listOf(visible.id), activeItems.map { it.contentId })
assertEquals("Retained owner alias", activeItems.single().creatorNickname)
jdbcTemplate.update(
"update creator_localized_name set is_active = false where member_id = ? and lang = 'JA'",
creator.id!!
)
entityManager.clear()
assertEquals("renamed-original", findLatest(viewer.id!!, "ja").single().creatorNickname)
}
@Test
@DisplayName("테마 공유 조회는 요청한 KO·JA 별칭과 기존 제목을 반환한다")
fun shouldLocalizeThemeNamesByRequestedLanguageWithoutChangingTitle() {
val viewer = saveMember("raw-theme-viewer", MemberRole.USER)
val creator = saveMember("raw-theme-original", MemberRole.CREATOR)
val content = saveContent(creator, "Unchanged theme title")
saveAlias(creator, "Japanese theme owner")
saveAlias(creator, "Korean theme owner", Lang.KO)
entityManager.clear()
val names = listOf(
Lang.KO to "Korean theme owner",
Lang.JA to "Japanese theme owner"
)
for ((lang, expected) in names) {
val item = repository.findByTheme(memberId = viewer.id!!, contentType = ContentType.ALL, lang = lang).single()
assertEquals(content.id, item.contentId)
assertEquals(expected, item.creatorNickname)
assertEquals("Unchanged theme title", item.title)
}
}
@Test
@DisplayName("큐레이션 조회는 활성 KO·JA 별칭이 있어도 원본 닉네임을 반환한다")
fun shouldKeepCurationNamesRawWithActiveAliases() {
val creator = saveMember("raw-curation-original", MemberRole.CREATOR)
val curation = AudioContentCuration(title = "curation", description = "description")
.also { entityManager.persist(it) }
val content = saveContent(creator, "Unchanged curation title").apply { this.curation = curation }
entityManager.persist(
AudioContentCurationItem().apply {
this.curation = curation
this.content = content
}
)
saveAlias(creator, "Japanese curation owner")
saveAlias(creator, "Korean curation owner", Lang.KO)
entityManager.clear()
val results = listOf(
repository.findContentByCurationId(curation.id!!, false, ContentType.ALL),
curationRepository.findByCurationId(curation.id!!, "https://cdn.test", contentType = ContentType.ALL)
)
for (items in results) {
assertEquals(content.id, items.single().contentId)
assertEquals("raw-curation-original", items.single().creatorNickname)
assertEquals("Unchanged curation title", items.single().title)
}
}
@Test
@DisplayName("랭킹 공유 조회는 요청한 KO·JA 별칭과 제목 번역·원본 fallback을 반환한다")
fun shouldLocalizeRankingNamesAndTitlesByRequestedLocale() {
val viewer = saveMember("raw-rank-viewer", MemberRole.USER)
val creator = saveMember("raw-rank-original", MemberRole.CREATOR).apply { profileImage = "profile.png" }
val content = saveContent(creator, "Original ranking title")
entityManager.persist(CreatorRanking(ranking = 1).apply { member = creator })
entityManager.persist(
Order(type = OrderType.KEEP).apply {
member = viewer
this.creator = creator
audioContent = content
}
)
entityManager.persist(
ContentTranslation(content.id!!, "ja", ContentTranslationPayload("Translated ranking title", "detail", ""))
)
saveAlias(creator, "Japanese ranking owner")
saveAlias(creator, "Korean ranking owner", Lang.KO)
// H2 JSON 컬럼에 문자열이 아닌 JSON 객체를 적재한다.
jdbcTemplate.update(
"update content_translation set rendered_payload = ? format json where content_id = ? and locale = 'ja'",
"""{"title":"Translated ranking title","detail":"detail","tags":""}""",
content.id!!
)
entityManager.clear()
val names = listOf(
Triple(Lang.KO, "Korean ranking owner", "Original ranking title"),
Triple(Lang.JA, "Japanese ranking owner", "Translated ranking title")
)
for ((lang, expected, expectedTitle) in names) {
assertEquals(expected, rankingRepository.getCreatorRankings(viewer.id, lang).single().nickname)
val item = rankingRepository.getAudioContentRanking(
memberId = viewer.id,
isAdult = false,
contentType = ContentType.ALL,
startDate = LocalDateTime.now().minusDays(1),
endDate = LocalDateTime.now().plusDays(1),
offset = 0,
limit = 10,
sortType = "판매량",
locale = lang.code
).single()
assertEquals(expected, item.creatorNickname)
assertEquals(expectedTitle, item.title)
}
assertEquals(
"Korean ranking owner",
rankingRepository.getCreatorRankings(viewer.id).single().nickname
)
}
@Test
@DisplayName("오리지널 시리즈 공유 조회는 요청한 언어의 제목 번역·원본 fallback을 반환한다")
fun shouldLocalizeOriginalSeriesTitlesByRequestedLocale() {
val creator = saveMember("raw-series-original", MemberRole.CREATOR)
val content = saveContent(creator, "Series content")
val genre = SeriesGenre(genre = "genre").also { entityManager.persist(it) }
val series = Series(title = "Original series title", introduction = "introduction", isOriginal = true).apply {
member = creator
this.genre = genre
coverImage = "series.png"
entityManager.persist(this)
}
entityManager.persist(
SeriesContent().apply {
this.series = series
this.content = content
}
)
entityManager.persist(
SeriesTranslation(series.id!!, "ja", SeriesTranslationPayload("Translated series title", "intro", emptyList()))
)
entityManager.flush()
// H2 JSON 컬럼에 문자열이 아닌 JSON 객체를 적재한다.
jdbcTemplate.update(
"update series_translation set rendered_payload = ? format json where series_id = ? and locale = 'ja'",
"""{"title":"Translated series title","introduction":"intro","keywords":[]}""",
series.id!!
)
entityManager.clear()
val titles = listOf(
"ja" to "Translated series title",
"ko" to "Original series title"
)
for ((locale, expectedTitle) in titles) {
val localized = seriesRepository.getOriginalAudioDramaList(
imageHost = "https://cdn.test",
isAdult = false,
contentType = ContentType.ALL,
locale = locale
).single()
assertEquals(expectedTitle, localized.title)
}
}
private fun findLatest(viewerId: Long, locale: String): List<AudioContentMainItem> {
return repository.getLatestContentByTheme(
memberId = viewerId,
theme = emptyList(),
contentType = ContentType.ALL,
offset = 0,
limit = 20,
sortType = SortType.NEWEST,
isFree = false,
isAdult = false,
locale = locale
)
}
private fun saveMember(nickname: String, role: MemberRole): Member {
return Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
profileImage = "profile.png",
role = role
).also {
entityManager.persist(it)
}
}
private fun saveContent(creator: Member, title: String): AudioContent {
val theme = AudioContentTheme(theme = "theme-$title", image = "theme.png")
entityManager.persist(theme)
return AudioContent(
title = title,
detail = "detail",
languageCode = "ko",
releaseDate = LocalDateTime.now().minusDays(1)
).apply {
member = creator
this.theme = theme
isActive = true
coverImage = "audio.png"
duration = "00:10:00"
entityManager.persist(this)
}
}
private fun saveAlias(member: Member, displayName: String, lang: Lang = Lang.JA) {
entityManager.flush()
jdbcTemplate.update(
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"values (?, ?, ?, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
member.id!!,
lang.name,
displayName
)
}
}
@@ -24,6 +24,7 @@ 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.MemberContentPreferenceService
import kr.co.vividnext.sodalive.support.originalLocalizedNameService
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
import kr.co.vividnext.sodalive.v2.recommendation.application.CreatorContentViewHistoryService
import org.junit.jupiter.api.Assertions.assertEquals
@@ -114,6 +115,7 @@ class AudioContentServiceTest {
applicationEventPublisher = applicationEventPublisher,
messageSource = SodaMessageSource(),
langContext = LangContext(),
localizedNameService = originalLocalizedNameService(),
contentThemeTranslationRepository = contentThemeTranslationRepository,
homeFollowingNewsPublishService = homeFollowingNewsPublishService,
memberContentPreferenceService = memberContentPreferenceService,
@@ -0,0 +1,91 @@
package kr.co.vividnext.sodalive.content.comment
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.AudioContentRepository
import kr.co.vividnext.sodalive.content.order.OrderRepository
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.support.LegacyCreatorNameTestConfiguration
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.annotation.Import
import javax.persistence.EntityManager
@DataJpaTest(properties = ["spring.cache.type=none"])
@Import(QueryDslConfig::class, LegacyCreatorNameTestConfiguration::class)
class AudioContentCommentNameMetadataTest @Autowired constructor(
private val entityManager: EntityManager,
private val repository: AudioContentCommentRepository,
private val blockRepository: BlockMemberRepository,
private val contentRepository: AudioContentRepository,
private val orderRepository: OrderRepository,
private val localizedNameService: CreatorLocalizedNameService
) {
@ParameterizedTest
@CsvSource("CREATOR, USER", "USER, CREATOR")
@DisplayName("답글 알림 제목은 부모 작성자 ID이고 snapshot 발신자 ID와 콘텐츠 제목 인자는 그대로다")
fun shouldKeepParentTitleOwnerSeparateFromActualCommenterAndResourceTitle(
parentRole: MemberRole,
commenterRole: MemberRole
) {
val parentWriter = Member(nickname = "parent-original", password = "password", role = parentRole)
val commenter = Member(nickname = "commenter-original", password = "password", role = commenterRole)
val contentOwner = Member(nickname = "content-owner-original", password = "password", role = MemberRole.CREATOR)
entityManager.persist(parentWriter)
entityManager.persist(commenter)
entityManager.persist(contentOwner)
val theme = AudioContentTheme(theme = "metadata-theme", image = "theme.png")
entityManager.persist(theme)
val content = AudioContent(title = "Unchanged content title", detail = "detail", languageCode = "ko").apply {
member = contentOwner
this.theme = theme
isActive = true
entityManager.persist(this)
}
val parent = AudioContentComment(comment = "parent comment", languageCode = "ko").apply {
member = parentWriter
audioContent = content
entityManager.persist(this)
}
entityManager.flush()
val publisher = Mockito.mock(ApplicationEventPublisher::class.java)
val service = AudioContentCommentService(
repository,
blockRepository,
contentRepository,
publisher,
orderRepository,
SodaMessageSource(),
LangContext(),
localizedNameService,
"https://cdn.test"
)
service.registerComment(commenter, "reply", content.id!!, parent.id!!, false, "ko")
val captor = ArgumentCaptor.forClass(FcmEvent::class.java)
Mockito.verify(publisher).publishEvent(captor.capture())
val event = captor.value
assertNotEquals(parentWriter.id, commenter.id)
assertEquals(parentWriter.id, event.titleMemberId)
assertEquals(commenter.id, event.senderMemberId)
assertEquals("parent-original", event.title)
assertEquals(listOf("Unchanged content title"), event.args)
assertEquals(emptyMap<Int, Long>(), event.nameArgumentMemberIds)
}
}
@@ -0,0 +1,259 @@
package kr.co.vividnext.sodalive.explorer
import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberAdapter
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.transaction.TestTransaction
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import javax.persistence.EntityManager
@SpringBootTest(
properties = [
"cloud.aws.cloud-front.host=https://cdn.test",
"spring.cache.type=none",
"spring.datasource.url=jdbc:h2:mem:legacy-creator-name-e2e;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
]
)
@AutoConfigureMockMvc
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
@Transactional
class ExplorerLocalizedNameEndToEndTest @Autowired constructor(
private val mockMvc: MockMvc,
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate
) {
@MockBean
private lateinit var countryContext: CountryContext
@BeforeEach
fun setUp() {
Mockito.doReturn("US").`when`(countryContext).countryCode
}
@Test
@DisplayName("레거시 프로필 이름은 현지화하되 팬톡 작성자·답글은 활성 JA·KO 별칭이 있어도 원본을 유지한다")
fun shouldLocalizeProfileButKeepOriginalFanTalkWriterAndReplyNamesDespiteActiveAliases() {
val viewer = saveMember("profile-viewer", MemberRole.USER)
val creator = saveMember("profile-original", MemberRole.CREATOR).apply { isVisibleDonationRank = false }
val fan = saveMember("fan-original", MemberRole.USER)
val peer = saveMember("deleted_peer-original", MemberRole.CREATOR)
val fanTalk = saveCheers(fan, creator, "fan talk")
saveCheers(peer, creator, "creator fan talk")
saveCheers(creator, creator, "creator reply", parent = fanTalk)
saveAlias(creator, "Japanese profile")
saveAlias(peer, "Japanese peer")
saveAlias(fan, "Must not replace fan")
saveAlias(creator, "Korean profile", lang = "KO")
saveAlias(peer, "Korean peer", lang = "KO")
entityManager.clear()
commitFixture()
mockMvc.perform(
get("/explorer/profile/${creator.id}")
.param("timezone", "Asia/Seoul")
.header("Accept-Language", "ja")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.creator.creatorId").value(creator.id!!))
.andExpect(jsonPath("$.data.creator.nickname").value("Japanese profile"))
.andExpect(jsonPath("$.data.cheers.totalCount").value(2))
.andExpect(jsonPath("$.data.cheers.cheers[0].memberId").value(peer.id!!))
.andExpect(jsonPath("$.data.cheers.cheers[0].nickname").value("peer-original"))
.andExpect(jsonPath("$.data.cheers.cheers[1].nickname").value("fan-original"))
.andExpect(jsonPath("$.data.cheers.cheers[1].replyList[0].memberId").value(creator.id!!))
.andExpect(jsonPath("$.data.cheers.cheers[1].replyList[0].nickname").value("profile-original"))
listOf("ko", "en").forEach { locale ->
mockMvc.perform(
get("/explorer/profile/${creator.id}")
.param("timezone", "Asia/Seoul")
.header("Accept-Language", locale)
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(
jsonPath("$.data.creator.nickname").value(if (locale == "ko") "Korean profile" else "profile-original")
)
.andExpect(jsonPath("$.data.cheers.cheers[0].nickname").value("peer-original"))
.andExpect(jsonPath("$.data.cheers.cheers[1].nickname").value("fan-original"))
.andExpect(jsonPath("$.data.cheers.cheers[1].replyList[0].nickname").value("profile-original"))
}
assertEquals("profile-original", entityManager.find(Member::class.java, creator.id).nickname)
}
@Test
@DisplayName("JA 상세는 원본 변경 후에도 별칭을 유지하고 해제·미등록이면 현재 원본을 반환한다")
fun shouldKeepAliasAfterOriginalRenameAndFallbackAfterSoftClearOrMissingAlias() {
val viewer = saveMember("detail-viewer", MemberRole.USER)
val creator = saveMember("before-rename", MemberRole.CREATOR)
val unregistered = saveMember("unregistered-original", MemberRole.CREATOR)
saveAlias(creator, "Retained profile alias")
creator.nickname = "renamed-original"
entityManager.flush()
entityManager.clear()
commitFixture()
mockMvc.perform(
get("/explorer/profile/${creator.id}/detail")
.header("Accept-Language", "ja")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.nickname").value("Retained profile alias"))
jdbcTemplate.update(
"update creator_localized_name set is_active = false where member_id = ? and lang = 'JA'",
creator.id!!
)
entityManager.clear()
mockMvc.perform(
get("/explorer/profile/${creator.id}/detail")
.header("Accept-Language", "ja")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.nickname").value("renamed-original"))
mockMvc.perform(
get("/explorer/profile/${unregistered.id}/detail")
.header("Accept-Language", "ja")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.nickname").value("unregistered-original"))
}
@Test
@DisplayName("통합 검색은 creator 제목·닉네임만 JA 이름으로 바꾸고 콘텐츠·시리즈 제목과 소유자 ID를 구분한다")
fun shouldLocalizeCreatorSearchTitleAndAllOwnerNamesWithoutReplacingContentOrSeriesTitles() {
val viewer = saveMember("unified-viewer", MemberRole.USER)
saveMember("unused-member", MemberRole.USER)
val creator = saveMember("unified-original", MemberRole.CREATOR)
val theme = AudioContentTheme(theme = "unified-theme", image = "theme.png")
entityManager.persist(theme)
val content = AudioContent(
title = "Unchanged content title",
detail = "description",
languageCode = "ko",
releaseDate = LocalDateTime.now().minusDays(1)
).apply {
member = creator
this.theme = theme
isActive = true
coverImage = "audio.png"
duration = "00:10:00"
entityManager.persist(this)
}
val genre = SeriesGenre(genre = "unified-genre", isAdult = false, isActive = true)
entityManager.persist(genre)
val series = Series(title = "Unchanged series title", introduction = "introduction").apply {
member = creator
this.genre = genre
coverImage = "series.png"
entityManager.persist(this)
}
entityManager.persist(
SeriesContent().apply {
this.series = series
this.content = content
}
)
saveAlias(creator, "Japanese unified creator")
entityManager.clear()
assertNotEquals(creator.id, content.id)
assertNotEquals(creator.id, series.id)
commitFixture()
mockMvc.perform(
get("/search")
.param("keyword", "unified-original")
.header("Accept-Language", "ja")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.creatorList[0].id").value(creator.id!!))
.andExpect(jsonPath("$.data.creatorList[0].title").value("Japanese unified creator"))
.andExpect(jsonPath("$.data.creatorList[0].nickname").value("Japanese unified creator"))
.andExpect(jsonPath("$.data.contentList[0].id").value(content.id!!))
.andExpect(jsonPath("$.data.contentList[0].title").value("Unchanged content title"))
.andExpect(jsonPath("$.data.contentList[0].nickname").value("Japanese unified creator"))
.andExpect(jsonPath("$.data.seriesList[0].id").value(series.id!!))
.andExpect(jsonPath("$.data.seriesList[0].title").value("Unchanged series title"))
.andExpect(jsonPath("$.data.seriesList[0].nickname").value("Japanese unified creator"))
mockMvc.perform(
get("/search/creators")
.param("keyword", "Japanese unified")
.header("Accept-Language", "ko")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.totalCount").value(1))
.andExpect(jsonPath("$.data.items[0].title").value("unified-original"))
.andExpect(jsonPath("$.data.items[0].nickname").value("unified-original"))
}
private fun commitFixture() {
TestTransaction.flagForCommit()
TestTransaction.end()
TestTransaction.start()
}
private fun saveMember(nickname: String, role: MemberRole): Member {
return Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
profileImage = "$nickname.png",
role = role
).also { entityManager.persist(it) }
}
private fun saveCheers(writer: Member, creator: Member, text: String, parent: CreatorCheers? = null): CreatorCheers {
return CreatorCheers(cheers = text, languageCode = "ko").apply {
member = writer
this.creator = creator
this.parent = parent
entityManager.persist(this)
}
}
private fun saveAlias(member: Member, displayName: String, lang: String = "JA") {
entityManager.flush()
jdbcTemplate.update(
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"values (?, ?, ?, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
member.id!!,
lang,
displayName
)
}
}
@@ -0,0 +1,90 @@
package kr.co.vividnext.sodalive.explorer
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import org.junit.jupiter.api.Assertions.assertAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.jdbc.core.JdbcTemplate
import javax.persistence.EntityManager
@DataJpaTest(properties = ["spring.cache.type=none"])
@Import(QueryDslConfig::class)
class ExplorerQueryRepositoryLocalizedNameTest @Autowired constructor(
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate,
queryFactory: JPAQueryFactory
) {
private val langContext = LangContext().apply { setLang(Lang.KO) }
private val repository = ExplorerQueryRepository(queryFactory, SodaMessageSource(), langContext, "https://cdn.test")
@Test
@DisplayName("KO 채널 검색도 원본 또는 활성 JA 이름을 찾고 self·비활성·USER를 제외한다")
fun shouldSearchOriginalOrActiveJaAliasInKoWithoutBypassingChannelFilters() {
val viewer = saveMember("Comet-viewer")
val original = saveMember("Comet-original")
val aliasOnly = saveMember("another-original")
val inactive = saveMember("Comet-inactive").apply { isActive = false }
val user = saveMember("Comet-user", MemberRole.USER)
listOf(viewer, original, aliasOnly, inactive, user).forEach { saveAlias(it, "Comet alias") }
entityManager.clear()
assertAll(
{
assertEquals(
setOf(original.id, aliasOnly.id),
repository.getSearchChannel("cOmEt", viewer.id!!).map { it.id }.toSet()
)
},
{ assertEquals(2, repository.getSearchChannel("cOmEt", viewer.id!!).size) },
{ assertEquals(listOf(original.id), repository.getSearchChannel("Comet-original", viewer.id!!).map { it.id }) }
)
}
@Test
@DisplayName("채널 검색은 해제한 JA와 EN 이름을 제외하고 동일 행 재활성화 후 별칭을 다시 찾는다")
fun shouldIgnoreSoftClearedAndNonJaChannelAliasesAndSearchReactivatedAlias() {
val viewer = saveMember("channel-viewer", MemberRole.USER)
val creator = saveMember("channel-original")
saveAlias(creator, "ChannelAlias", isActive = false)
saveAlias(creator, "EnglishChannel", lang = "EN")
entityManager.clear()
assertEquals(emptyList<Long>(), repository.getSearchChannel("ChannelAlias", viewer.id!!).map { it.id })
assertEquals(emptyList<Long>(), repository.getSearchChannel("EnglishChannel", viewer.id!!).map { it.id })
assertEquals(listOf(creator.id), repository.getSearchChannel("channel-original", viewer.id!!).map { it.id })
jdbcTemplate.update(
"update creator_localized_name set is_active = true where member_id = ? and lang = 'JA'",
creator.id!!
)
assertEquals(listOf(creator.id), repository.getSearchChannel("ChannelAlias", viewer.id!!).map { it.id })
}
private fun saveMember(nickname: String, role: MemberRole = MemberRole.CREATOR): Member {
return Member(email = "$nickname@test.com", password = "password", nickname = nickname, role = role).also {
entityManager.persist(it)
}
}
private fun saveAlias(member: Member, displayName: String, isActive: Boolean = true, lang: String = "JA") {
entityManager.flush()
jdbcTemplate.update(
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"values (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
member.id!!,
lang,
displayName,
isActive
)
}
}
@@ -11,12 +11,15 @@ import kr.co.vividnext.sodalive.explorer.profile.CreatorDonationRankingService
import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationService
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.MemberService
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
@@ -42,7 +45,28 @@ class ExplorerServiceTest {
}
}
private fun createService(queryRepository: ExplorerQueryRepository): ExplorerService {
@Test
@DisplayName("팬톡 수정 응답은 별칭 대신 작성자 원본 이름을 반환하고 deleted_ 접두사만 제거한다")
fun shouldKeepOriginalWriterNameAndRemoveDeletedPrefixWhenModifyingCheers() {
val queryRepository = Mockito.mock(ExplorerQueryRepository::class.java)
val localizedNameService = Mockito.mock(CreatorLocalizedNameService::class.java)
val service = createService(queryRepository, localizedNameService)
val writer = createMember(id = 10L, nickname = "deleted_writer", role = MemberRole.CREATOR)
val cheers = createCheers(writer, createMember(id = 20L, nickname = "creator", role = MemberRole.CREATOR))
Mockito.doReturn(cheers).`when`(queryRepository).getCheers(101L)
Mockito.`when`(localizedNameService.findLocalizedNames(listOf(10L), Lang.JA)).thenReturn(mapOf(10L to "alias"))
val response = service.modifyCheers(PutWriteCheersRequest(cheersId = 101L, content = "modified"), writer)
assertEquals("writer", response.writerNickname)
assertEquals("modified", response.content)
Mockito.verifyNoInteractions(localizedNameService)
}
private fun createService(
queryRepository: ExplorerQueryRepository,
localizedNameService: CreatorLocalizedNameService = Mockito.mock(CreatorLocalizedNameService::class.java)
): ExplorerService {
return ExplorerService(
memberService = Mockito.mock(MemberService::class.java),
memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java),
@@ -57,7 +81,8 @@ class ExplorerServiceTest {
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java),
contentTranslationRepository = Mockito.mock(ContentTranslationRepository::class.java),
messageSource = Mockito.mock(SodaMessageSource::class.java),
langContext = Mockito.mock(LangContext::class.java),
langContext = LangContext().apply { setLang(Lang.JA) },
localizedNameService = localizedNameService,
cloudFrontHost = "https://cdn.test"
)
}
@@ -72,9 +72,9 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
// then: 비공개 타인 메시지는 제외되고, createdAt desc/id desc 정렬이 적용돼야 한다.
assertEquals(3, list.size)
assertEquals(secretMine.id, list[0].id)
assertEquals(publicTieSecond.id, list[1].id)
assertEquals(publicTieFirst.id, list[2].id)
assertEquals(secretMine.id, list[0].first.id)
assertEquals(publicTieSecond.id, list[1].first.id)
assertEquals(publicTieFirst.id, list[2].first.id)
assertEquals(3, totalCount)
}
@@ -127,9 +127,9 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
// then: 본인 조회는 비공개 메시지를 포함하고 최신순으로 반환해야 한다.
assertEquals(3, list.size)
assertEquals(recentSecretOther.id, list[0].id)
assertEquals(recentSecretMine.id, list[1].id)
assertEquals(recentPublic.id, list[2].id)
assertEquals(recentSecretOther.id, list[0].first.id)
assertEquals(recentSecretMine.id, list[1].first.id)
assertEquals(recentPublic.id, list[2].first.id)
assertEquals(3, totalCount)
}
@@ -138,7 +138,7 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
fun shouldFetchMemberWithDonationMessages() {
// given: 크리에이터/후원자와 조회 대상 메시지를 준비한다.
val creator = saveMember(nickname = "creator3", role = MemberRole.CREATOR)
val viewer = saveMember(nickname = "viewer3", role = MemberRole.USER)
val viewer = saveMember(nickname = "deleted_viewer3", role = MemberRole.USER)
val message = saveMessage(member = viewer, creator = creator, can = 1, isSecret = false)
val monthStart = LocalDateTime.now().withDayOfMonth(1).toLocalDate().atStartOfDay()
val nextMonthStart = monthStart.plusMonths(1)
@@ -160,8 +160,9 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
// then: 조회 결과의 후원자 정보는 세션 없이도 접근 가능해야 한다.
assertEquals(1, list.size)
assertEquals(viewer.id, list[0].member!!.id)
assertEquals("viewer3", list[0].member!!.nickname)
assertEquals(viewer.id, list[0].first.member!!.id)
assertEquals("deleted_viewer3", list[0].first.member!!.nickname)
assertEquals("viewer3", list[0].second)
}
private fun saveMember(nickname: String, role: MemberRole): Member {
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.explorer.profile.channelDonation
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member
@@ -97,9 +98,10 @@ class ChannelDonationServiceTest {
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime()
anyLocalDateTime(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
).thenReturn(listOf(message))
).thenReturn(listOf(message to viewer.nickname))
// when: 채널 후원 목록 조회를 실행한다.
val result = service.getChannelDonationList(
@@ -129,7 +131,8 @@ class ChannelDonationServiceTest {
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime()
anyLocalDateTime(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
// then: 월 시작/다음 달 시작 범위를 사용해야 한다.
@@ -181,9 +184,10 @@ class ChannelDonationServiceTest {
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime()
anyLocalDateTime(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
).thenReturn(listOf(message))
).thenReturn(listOf(message to "withdrawn"))
// when: 채널 후원 목록 조회를 실행한다.
val result = service.getChannelDonationList(
@@ -228,9 +232,10 @@ class ChannelDonationServiceTest {
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime()
anyLocalDateTime(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
).thenReturn(listOf(message))
).thenReturn(listOf(message to viewer.nickname))
// when: 목록 조회를 실행한다.
val result = service.getChannelDonationList(
@@ -275,7 +280,8 @@ class ChannelDonationServiceTest {
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime()
anyLocalDateTime(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
).thenReturn(emptyList())
@@ -302,7 +308,8 @@ class ChannelDonationServiceTest {
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime()
anyLocalDateTime(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
// then: 월 시작/다음 달 시작 범위를 사용해야 한다.
@@ -150,6 +150,7 @@ class CreatorCommunityServiceTest {
assertEquals(FcmEventType.INDIVIDUAL, event.type)
assertEquals(PushNotificationCategory.COMMUNITY, event.category)
assertEquals("creator.community.fcm.new_comment", event.messageKey)
assertEquals(commenter.id, event.titleMemberId)
assertEquals(commenter.id, event.senderMemberId)
assertEquals(listOf(creator.id!!), event.recipients)
assertEquals(FcmDeepLinkValue.COMMUNITY, event.deepLinkValue)
@@ -94,7 +94,8 @@ class LiveRecommendRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
offset = 0,
limit = 20,
isBlocked = { false }
isBlocked = { false },
lang = Lang.KO
)
assertEquals(2, result.size)
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.live.recommend
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.auth.Auth
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
@@ -28,7 +29,8 @@ class LiveRecommendServiceTest {
repository = repository,
blockMemberRepository = blockMemberRepository,
memberContentPreferenceService = memberContentPreferenceService,
liveRecommendCacheService = liveRecommendCacheService
liveRecommendCacheService = liveRecommendCacheService,
langContext = LangContext()
)
}
@@ -0,0 +1,523 @@
package kr.co.vividnext.sodalive.live.room
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.UseCan
import kr.co.vividnext.sodalive.can.use.UseCanRepository
import kr.co.vividnext.sodalive.fcm.FcmService
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendService
import kr.co.vividnext.sodalive.live.reservation.LiveReservation
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
import kr.co.vividnext.sodalive.live.reservation.LiveReservationService
import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfo
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository
import kr.co.vividnext.sodalive.live.room.info.LiveRoomMemberRole
import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisitService
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import org.hibernate.SessionFactory
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.cache.CacheManager
import org.springframework.data.domain.PageRequest
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ContextConfiguration
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
import java.time.LocalDateTime
import javax.persistence.EntityManagerFactory
@SpringBootTest(
properties = [
"spring.cache.type=none",
"spring.jpa.properties.hibernate.generate_statistics=true",
"jwt.secret=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OWFiY2RlZg==",
"agora.app-id=0123456789abcdef0123456789abcdef",
"agora.app-certificate=0123456789abcdef0123456789abcdef"
]
)
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
@Transactional
class LiveRoomLocalizedNameIntegrationTest @Autowired constructor(
private val service: LiveRoomService,
private val reservationService: LiveReservationService,
private val reservationRepository: LiveReservationRepository,
private val visitService: LiveRoomVisitService,
private val recommendService: LiveRecommendService,
private val followingRepository: CreatorFollowingRepository,
private val useCanRepository: UseCanRepository,
private val roomRepository: LiveRoomRepository,
private val roomInfoRepository: LiveRoomInfoRedisRepository,
private val memberRepository: MemberRepository,
private val jdbcTemplate: JdbcTemplate,
private val cacheManager: CacheManager,
private val entityManagerFactory: EntityManagerFactory,
private val langContext: LangContext
) {
@MockBean
private lateinit var fcmService: FcmService
@MockBean
private lateinit var s3Uploader: S3Uploader
private val roomIds = mutableListOf<Long>()
private val finishedLiveCacheKeys = mutableListOf<String>()
@BeforeEach
fun setUpRequest() {
RequestContextHolder.setRequestAttributes(ServletRequestAttributes(MockHttpServletRequest()))
}
@AfterEach
fun resetRequestAndRemoveRoomInfo() {
roomIds.forEach { roomInfoRepository.deleteById(it) }
finishedLiveCacheKeys.forEach { cacheManager.getCache("cache_ttl_10_minutes")?.evict(it) }
RequestContextHolder.resetRequestAttributes()
}
@Test
@DisplayName("라이브 info는 모든 멤버 목록을 요청 언어로 복사하고 역할과 공유 Redis 원본을 보존한다")
fun shouldRenderRoomInfoMemberNamesWithoutMutatingSharedRedisMembership() {
val fixture = createFixture()
langContext.setLang(Lang.JA)
val japanese = service.getRoomInfo(fixture.roomId, fixture.viewer)
langContext.setLang(Lang.KO)
val korean = service.getRoomInfo(fixture.roomId, fixture.viewer)
val stored = roomInfoRepository.findById(fixture.roomId).orElseThrow()
assertAll(
{ assertEquals("日本-host", japanese.creatorNickname) },
{ assertEquals("日本-host", japanese.speakerList.single().nickname) },
{ assertEquals("日本-manager", japanese.managerList.single().nickname) },
{ assertEquals(listOf("viewer", "fallback"), japanese.listenerList.map { it.nickname }) },
{ assertEquals(LiveRoomMemberRole.SPEAKER, japanese.speakerList.single().role) },
{ assertEquals(LiveRoomMemberRole.MANAGER, japanese.managerList.single().role) },
{ assertEquals("host", korean.creatorNickname) },
{ assertEquals("host", korean.speakerList.single().nickname) },
{ assertEquals("manager", korean.managerList.single().nickname) },
{ assertEquals("host", stored.speakerList.single().nickname) },
{ assertEquals("manager", stored.managerList.single().nickname) },
{ assertEquals("host", memberRepository.findById(fixture.host.id!!).orElseThrow().nickname) }
)
}
@Test
@DisplayName("라이브 상세와 멤버 프로필의 현재 이름은 JA 별칭이며 USER와 비활성 별칭은 원본이다")
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
fun shouldRenderCurrentRoomDetailAndMemberProfileNames() {
val fixture = createFixture()
langContext.setLang(Lang.JA)
val detail = service.getRoomDetail(fixture.roomId, fixture.viewer, "UTC")
val hostProfile = service.getUserProfile(fixture.roomId, fixture.host.id!!, fixture.viewer)
val viewerProfile = service.getUserProfile(fixture.roomId, fixture.viewer.id!!, fixture.viewer)
assertAll(
{ assertEquals("日本-host", detail.manager!!.nickname) },
{ assertEquals("日本-host", detail.participatingUsers.single { it.id == fixture.host.id }.nickname) },
{ assertEquals("日本-manager", detail.participatingUsers.single { it.id == fixture.manager.id }.nickname) },
{ assertEquals("viewer", detail.participatingUsers.single { it.id == fixture.viewer.id }.nickname) },
{ assertEquals("fallback", detail.participatingUsers.single { it.id == fixture.fallback.id }.nickname) },
{ assertEquals("日本-host", hostProfile.nickname) },
{ assertEquals("viewer", viewerProfile.nickname) }
)
}
@Test
@DisplayName("현재 후원 룰렛 메시지 이름은 요청별로 렌더링하며 비밀 필터와 본문 UUID 및 Redis 원본은 보존한다")
fun shouldRenderDonationAndRouletteNamesOnCopiesAfterSecretFiltering() {
val fixture = createFixture()
val roomInfo = roomInfoRepository.findById(fixture.roomId).orElseThrow()
roomInfo.addDonationMessage(fixture.manager.id!!, "manager", false, 10, "manager 후원 본문")
roomInfo.addRouletteMessage(fixture.manager.id!!, "manager", "manager 룰렛 결과")
roomInfo.addDonationMessage(fixture.viewer.id!!, "viewer", false, 10, "viewer 본문")
roomInfo.addDonationMessage(fixture.manager.id!!, "manager", true, 10, "비밀 본문")
roomInfoRepository.save(roomInfo)
langContext.setLang(Lang.JA)
val japanese = service.getDonationMessageList(fixture.roomId, fixture.viewer)
langContext.setLang(Lang.KO)
val korean = service.getDonationMessageList(fixture.roomId, fixture.viewer)
val stored = roomInfoRepository.findById(fixture.roomId).orElseThrow().donationMessageList
assertAll(
{ assertEquals(listOf("日本-manager", "日本-manager", "viewer"), japanese.map { it.nickname }) },
{ assertEquals(listOf("manager", "manager", "viewer"), korean.map { it.nickname }) },
{ assertEquals(listOf("manager 후원 본문", "manager 룰렛 결과", "viewer 본문"), japanese.map { it.donationMessage }) },
{ assertEquals(korean.map { it.uuid }, japanese.map { it.uuid }) },
{ assertEquals(listOf("manager", "manager", "viewer", "manager"), stored.map { it.nickname }) },
{ assertEquals(4, stored.size) }
)
}
@Test
@DisplayName("최근 종료 라이브의 현재 이름은 같은 회원의 KO 조회 캐시 이후에도 JA 요청을 따른다")
fun shouldRenderJapaneseFinishedLiveNameAfterKoreanCacheRead() {
val fixture = createFixture()
val room = roomRepository.findById(fixture.roomId).orElseThrow()
room.isActive = false
fixture.host.profileImage = "profile/host.png"
roomRepository.flush()
val cacheKey = "getLatestFinishedLive:${fixture.viewer.id}"
finishedLiveCacheKeys.add(cacheKey)
val cache = requireNotNull(cacheManager.getCache("cache_ttl_10_minutes"))
cache.evict(cacheKey)
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
langContext.setLang(Lang.KO)
val korean = service.getLatestFinishedLive(fixture.viewer).single { it.memberId == fixture.host.id }
assertEquals(1L, currentNameQueryCount())
val roomListQuery = statistics.queries.single { it.contains("from LiveRoom") }
val roomListExecutions = statistics.getQueryStatistics(roomListQuery).executionCount
val cached = cache.get(cacheKey)?.get()
assertNotNull(cached)
assertEquals(1L, roomListExecutions)
assertEquals("host", (cached as List<*>).filterIsInstance<GetLatestFinishedLiveResponse>().single().nickname)
langContext.setLang(Lang.JA)
val nameQueryCountBeforeHit = currentNameQueryCount()
val japanese = service.getLatestFinishedLive(fixture.viewer).single { it.memberId == fixture.host.id }
assertEquals(1L, currentNameQueryCount() - nameQueryCountBeforeHit)
assertEquals(roomListExecutions, statistics.getQueryStatistics(roomListQuery).executionCount)
assertEquals(cached, cache.get(cacheKey)?.get())
fixture.host.nickname = "updated-host"
memberRepository.flush()
jdbcTemplate.update("UPDATE creator_localized_name SET is_active = false WHERE member_id = ?", fixture.host.id!!)
val nameQueryCountBeforeFallback = currentNameQueryCount()
val fallback = service.getLatestFinishedLive(fixture.viewer).single { it.memberId == fixture.host.id }
assertEquals(1L, currentNameQueryCount() - nameQueryCountBeforeFallback)
assertEquals("host", korean.nickname)
assertEquals("日本-host", japanese.nickname)
assertEquals("updated-host", fallback.nickname)
assertEquals("host", korean.nickname)
assertEquals(roomListExecutions, statistics.getQueryStatistics(roomListQuery).executionCount)
assertEquals(cached, cache.get(cacheKey)?.get())
jdbcTemplate.update(
"INSERT INTO creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"VALUES (?, 'EN', 'English host', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
fixture.host.id!!
)
langContext.setLang(Lang.EN)
val nameQueryCountBeforeEnglishHit = currentNameQueryCount()
val english = service.getLatestFinishedLive(fixture.viewer).single { it.memberId == fixture.host.id }
assertEquals("English host", english.nickname)
assertEquals(1L, currentNameQueryCount() - nameQueryCountBeforeEnglishHit)
assertEquals(roomListExecutions, statistics.getQueryStatistics(roomListQuery).executionCount)
assertEquals(cached, cache.get(cacheKey)?.get())
}
@Test
@DisplayName("언어 중립 live feed와 종료 캐시는 별칭 해제 후 변경된 현재 원본으로 fallback한다")
fun shouldFallBackToCurrentOriginalWithoutRewritingNeutralRedisNames() {
val fixture = createFixture()
val info = roomInfoRepository.findById(fixture.roomId).orElseThrow()
info.addDonationMessage(fixture.host.id!!, "host", false, 10, "host 본문")
roomInfoRepository.save(info)
fixture.host.nickname = "updated-host"
memberRepository.flush()
jdbcTemplate.update("UPDATE creator_localized_name SET is_active = false WHERE member_id = ?", fixture.host.id!!)
langContext.setLang(Lang.JA)
val response = service.getRoomInfo(fixture.roomId, fixture.viewer)
val messages = service.getDonationMessageList(fixture.roomId, fixture.viewer)
val stored = roomInfoRepository.findById(fixture.roomId).orElseThrow()
assertEquals("updated-host", response.creatorNickname)
assertEquals("updated-host", response.speakerList.single().nickname)
assertEquals("updated-host", messages.single().nickname)
assertEquals("host", stored.speakerList.single().nickname)
assertEquals("host", stored.donationMessageList.single().nickname)
assertEquals("host 본문", messages.single().donationMessage)
}
@Test
@DisplayName("예약 생성·목록·상세와 최근 방문자 조회는 실제 크리에이터 소유 ID로 JA 이름을 표시한다")
fun shouldLocalizeReservationAndRecentVisitorNames() {
val fixture = createFixture()
langContext.setLang(Lang.JA)
val reservation = reservationService.makeReservation(
MakeLiveReservationRequest(fixture.roomId, "web", "UTC"),
fixture.viewer.id!!
)
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
val listed = reservationService.getReservationList(fixture.viewer.id!!, true, "UTC").single()
val detail = reservationService.getReservation(reservation.reservationId, fixture.viewer.id!!, "UTC")
assertEquals(2L, statistics.queryExecutionCount, statistics.queries.joinToString("\n"))
val room = roomRepository.findById(fixture.roomId).orElseThrow()
visitService.roomVisit(room, fixture.viewer)
visitService.roomVisit(room, fixture.manager)
statistics.clear()
val visitor = visitService.getRecentVisitRoomUsers(fixture.viewer).single()
assertEquals(2L, statistics.queryExecutionCount, statistics.queries.joinToString("\n"))
assertEquals("日本-host", reservation.nickname)
assertEquals("日本-host", listed.masterNickname)
assertEquals("日本-host", detail.masterNickname)
assertEquals(fixture.manager.id, visitor.id)
assertEquals("日本-manager", visitor.nickname)
langContext.setLang(Lang.KO)
assertEquals("manager", visitService.getRecentVisitRoomUsers(fixture.viewer).single().nickname)
}
@Test
@DisplayName("추천과 팔로잉의 on-air 및 off-air 분기는 요청별 이름을 표시하고 count와 순서를 보존한다")
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
fun shouldLocalizeRecommendedAndFollowingChannels() {
val fixture = createFixture()
followingRepository.saveAndFlush(
CreatorFollowing().apply {
member = fixture.viewer
creator = fixture.host
}
)
followingRepository.saveAndFlush(
CreatorFollowing().apply {
member = fixture.viewer
creator = fixture.manager
}
)
langContext.setLang(Lang.JA)
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
val recommended = recommendService.getRecommendChannelList(null).associateBy { it.creatorId }
val aliasQueries = statistics.queries.filter {
Regex("""(?is)^select\s+\w+\.memberId,\s*\w+\.displayName\s+from\s+CreatorLocalizedNameEntity\b""")
.containsMatchIn(it)
}
val aliasQueryCount = aliasQueries.sumOf { statistics.getQueryStatistics(it).executionCount }
val queryCount = statistics.queryExecutionCount
val following = recommendService.getFollowingChannelList(fixture.viewer).associateBy { it.creatorId }
val all = recommendService.getFollowingAllChannelList(fixture.viewer, PageRequest.of(0, 20))
assertEquals("日本-host", recommended.getValue(fixture.host.id!!).nickname)
assertEquals("日本-manager", recommended.getValue(fixture.manager.id!!).nickname)
assertEquals("fallback", recommended.getValue(fixture.fallback.id!!).nickname)
assertEquals("日本-host", following.getValue(fixture.host.id!!).nickname)
assertEquals("日本-manager", following.getValue(fixture.manager.id!!).nickname)
assertEquals(2, all.totalCount)
assertEquals(setOf("日本-host", "日本-manager"), all.items.map { it.nickname }.toSet())
langContext.setLang(Lang.KO)
assertEquals(
setOf("host", "manager"),
recommendService.getFollowingAllChannelList(fixture.viewer, PageRequest.of(0, 20)).items.map { it.nickname }.toSet()
)
assertEquals(0L, aliasQueryCount, "Standalone alias SELECTs (total queries=$queryCount): $aliasQueries")
assertNoStandaloneNameQueries()
jdbcTemplate.update(
"INSERT INTO creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"VALUES (?, 'EN', 'English host', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
fixture.host.id!!
)
langContext.setLang(Lang.EN)
assertEquals(
setOf("English host", "manager"),
recommendService.getFollowingAllChannelList(fixture.viewer, PageRequest.of(0, 20)).items.map { it.nickname }.toSet()
)
assertNoStandaloneNameQueries()
}
@Test
@DisplayName("후원과 하트 집계는 donor ID로 이름만 바꾸고 USER·금액·비밀 합계와 공개 DTO 필드를 보존한다")
fun shouldLocalizeDonationAndHeartDonorNamesWithoutChangingAmounts() {
val fixture = createFixture()
val room = roomRepository.findById(fixture.roomId).orElseThrow()
listOf(CanUsage.DONATION, CanUsage.HEART).forEach { usage ->
useCanRepository.saveAndFlush(
UseCan(usage, 10, 2).apply {
member = fixture.manager
this.room = room
}
)
useCanRepository.saveAndFlush(
UseCan(usage, 5, 1).apply {
member = fixture.viewer
this.room = room
}
)
}
langContext.setLang(Lang.JA)
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
val donations = service.getDonationStatus(fixture.roomId, fixture.host.id!!)
val hearts = service.getHeartList(fixture.roomId)
assertEquals(3L, statistics.queryExecutionCount, statistics.queries.joinToString("\n"))
assertEquals(listOf("日本-manager", "viewer"), donations.donationList.map { it.nickname })
assertEquals(listOf("日本-manager", "viewer"), hearts.heartList.map { it.nickname })
assertEquals(18, donations.totalCan)
assertEquals(0, donations.totalSecretCan)
assertEquals(18, hearts.totalHeart)
assertEquals(2, hearts.totalCount)
}
@Test
@DisplayName("진행·날짜별 예약·예정 라이브 목록은 EN 이름을 조인하고 USER·미등록·비활성 소유자를 유지한다")
fun shouldJoinEnglishNamesInEveryDatabaseRoomListWithoutMutatingOwners() {
val fixture = createFixture()
val owners = listOf(fixture.host, fixture.manager, fixture.viewer, fixture.fallback)
listOf(fixture.host, fixture.viewer, fixture.fallback).forEach {
jdbcTemplate.update(
"INSERT INTO creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"VALUES (?, 'EN', ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
it.id!!,
"EN-${it.nickname}",
it != fixture.fallback
)
}
val rooms = listOf(roomRepository.findById(fixture.roomId).orElseThrow()) + owners.drop(1).map { owner ->
roomRepository.saveAndFlush(
LiveRoom("${owner.nickname} room", "notice", LocalDateTime.now(), 10, "cover.png", isAdult = false).apply {
member = owner
channelName = "live_${owner.id}"
}
)
}
val expected = mapOf(
fixture.host.id to "EN-host",
fixture.manager.id to "manager",
fixture.viewer.id to "viewer",
fixture.fallback.id to "fallback"
)
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
val current = roomRepository.getLiveRoomListNow(0, 20, "UTC", null, false, true, null, Lang.EN)
assertEquals(expected, current.associate { it.room.member!!.id to it.creatorNickname })
assertEquals(1L, statistics.queryExecutionCount)
owners.forEach { owner ->
reservationRepository.saveAndFlush(
LiveReservation().apply {
member = owner
room = rooms.first()
}
)
}
statistics.clear()
val bookers = reservationRepository.getReservationList(fixture.roomId, Lang.EN)
assertEquals(expected, bookers.associate { it.id to it.nickname })
assertEquals(1L, statistics.queryExecutionCount)
statistics.clear()
val reservation = reservationRepository.getReservationListByMemberId(fixture.viewer.id!!, true, Lang.EN).single()
val reserved = reservationRepository.getReservationByReservationAndMemberId(reservation.id, fixture.viewer.id!!, Lang.EN)
assertEquals("EN-host", reservation.masterNickname)
assertEquals("EN-host", reserved!!.masterNickname)
assertEquals(2L, statistics.queryExecutionCount)
val date = LocalDateTime.now().plusDays(1).withHour(12).withMinute(0)
rooms.forEach {
it.channelName = null
it.beginDateTime = date
}
roomRepository.flush()
statistics.clear()
val dated = roomRepository.getLiveRoomListReservationWithDate(
date.toLocalDate().atStartOfDay(),
0,
20,
null,
false,
true,
null,
Lang.EN
)
val upcoming = roomRepository.getLiveRoomListReservationWithoutDate("UTC", null, false, true, null, Lang.EN)
assertEquals(expected, dated.associate { it.room.member!!.id to it.creatorNickname })
assertEquals(expected, upcoming.associate { it.room.member!!.id to it.creatorNickname })
assertEquals(2L, statistics.queryExecutionCount)
assertEquals(listOf("host", "manager", "viewer", "fallback"), owners.map { it.nickname })
assertNoStandaloneNameQueries()
}
private fun assertNoStandaloneNameQueries() {
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
val nameQueries = statistics.queries.filter {
Regex("""(?is)\bfrom\s+CreatorLocalizedNameEntity\b""").containsMatchIn(it) ||
Regex("""(?is)^select\s+\w+\.id,\s*coalesce\([^)]*\)\s+from\s+Member\b""").containsMatchIn(it)
}
assertEquals(0L, nameQueries.sumOf { statistics.getQueryStatistics(it).executionCount }, nameQueries.joinToString("\n"))
}
private fun currentNameQueryCount(): Long {
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
return statistics.queries.filter { it.contains("from Member") && it.contains("CreatorLocalizedNameEntity") }
.sumOf { statistics.getQueryStatistics(it).executionCount }
}
private fun createFixture(): Fixture {
val host = savedMember("host", MemberRole.CREATOR)
val manager = savedMember("manager", MemberRole.CREATOR)
val viewer = savedMember("viewer", MemberRole.USER)
val fallback = savedMember("fallback", MemberRole.CREATOR, activeAlias = false)
val room = roomRepository.saveAndFlush(
LiveRoom(
title = "room title",
notice = "notice",
beginDateTime = LocalDateTime.now().minusMinutes(1),
numberOfPeople = 10,
coverImage = "cover.png",
isAdult = false
).apply {
member = host
channelName = "localized_live_${host.id}"
}
)
roomIds.add(room.id!!)
roomInfoRepository.save(
LiveRoomInfo(room.id!!).apply {
addSpeaker(host, "https://cdn.test")
addManager(manager, "https://cdn.test")
addListener(viewer, "https://cdn.test")
addListener(fallback, "https://cdn.test")
}
)
return Fixture(room.id!!, host, manager, viewer, fallback)
}
private fun savedMember(nickname: String, role: MemberRole, activeAlias: Boolean = true): Member {
val member = memberRepository.saveAndFlush(
Member(
email = "$nickname@localized-live.test",
password = "pw",
nickname = nickname,
profileImage = "profile/$nickname.png",
role = role,
isVisibleDonationRank = false
)
)
jdbcTemplate.update(
"INSERT INTO creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"VALUES (?, 'JA', ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
member.id!!,
"日本-$nickname",
activeAlias
)
return member
}
private data class Fixture(val roomId: Long, val host: Member, val manager: Member, val viewer: Member, val fallback: Member)
}
@@ -12,6 +12,7 @@ import kr.co.vividnext.sodalive.content.ContentType
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
import kr.co.vividnext.sodalive.explorer.profile.CreatorDonationRankingService
import kr.co.vividnext.sodalive.fcm.PushTokenRepository
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
@@ -30,6 +31,7 @@ import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.member.contentpreference.ViewerContentPreference
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
@@ -106,6 +108,7 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
menuService = menuService,
messageSource = messageSource,
langContext = langContext,
localizedNameService = mock<CreatorLocalizedNameService>(),
repository = repository,
rouletteRepository = rouletteRepository,
roomInfoRepository = roomInfoRepository,
@@ -152,7 +155,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
memberId = 100L,
isCreator = false,
isAdult = true,
effectiveGender = Gender.NONE
effectiveGender = Gender.NONE,
lang = Lang.KO
)
).thenReturn(emptyList())
@@ -172,7 +176,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
memberId = 100L,
isCreator = false,
isAdult = true,
effectiveGender = Gender.NONE
effectiveGender = Gender.NONE,
lang = Lang.KO
)
}
@@ -187,7 +192,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
memberId = null,
isCreator = false,
isAdult = true,
effectiveGender = null
effectiveGender = null,
lang = Lang.KO
)
).thenReturn(emptyList())
@@ -207,7 +213,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
memberId = null,
isCreator = false,
isAdult = true,
effectiveGender = null
effectiveGender = null,
lang = Lang.KO
)
}
@@ -222,7 +229,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
memberId = 200L,
isCreator = false,
isAdult = false,
effectiveGender = Gender.NONE
effectiveGender = Gender.NONE,
lang = Lang.KO
)
).thenReturn(emptyList())
@@ -240,7 +248,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
memberId = 200L,
isCreator = false,
isAdult = false,
effectiveGender = Gender.NONE
effectiveGender = Gender.NONE,
lang = Lang.KO
)
}
@@ -31,6 +31,7 @@ import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.auth.Auth
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
@@ -70,6 +71,7 @@ class LiveRoomServiceTest {
menuService = mock<LiveRoomMenuService>(),
messageSource = mock<SodaMessageSource>(),
langContext = LangContext(),
localizedNameService = mock<CreatorLocalizedNameService>(),
repository = repository,
rouletteRepository = rouletteRepository,
roomInfoRepository = roomInfoRepository,
@@ -0,0 +1,108 @@
package kr.co.vividnext.sodalive.member
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import org.junit.jupiter.api.Assertions.assertAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.jdbc.core.JdbcTemplate
import javax.persistence.EntityManager
@DataJpaTest(properties = ["spring.cache.type=none"])
@Import(QueryDslConfig::class)
class MemberRepositoryLocalizedNameTest @Autowired constructor(
private val repository: MemberRepository,
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate
) {
@Test
@DisplayName("크리에이터 요청의 회원 검색은 USER 원본 매칭과 CREATOR 별칭을 함께 유지한다")
fun shouldRetainUserOriginalMatchesAndAddOnlyCreatorAliasesForCreatorRequester() {
val requester = saveMember("needle-requester", MemberRole.CREATOR)
val creator = saveMember("creator-original", MemberRole.CREATOR)
val user = saveMember("needle-user", MemberRole.USER)
val aiUser = saveMember("ai-user-original", MemberRole.USER).apply { memberKind = MemberKind.AI_CHARACTER }
val admin = saveMember("needle-admin", MemberRole.ADMIN)
val agent = saveMember("needle-agent", MemberRole.AGENT)
val bot = saveMember("needle-bot", MemberRole.BOT)
listOf(requester, creator, user, aiUser, admin, agent, bot).forEach { saveAlias(it, "needle-alias") }
entityManager.clear()
val matches = repository.findByNicknameAndOtherCondition("NeEdLe", requester)
assertEquals(setOf(creator.id, user.id), matches.map { it.id }.toSet())
assertEquals(2, matches.size)
assertEquals(listOf(creator.id), repository.findByNicknameAndOtherCondition("creator-original", requester).map { it.id })
}
@Test
@DisplayName("USER 요청의 회원 검색은 기존 USER·self·관리 역할 제외 조건을 별칭에도 적용한다")
fun shouldPreserveUserRequesterRoleAndSelfExclusionsForAliases() {
val requester = saveMember("needle-viewer", MemberRole.USER)
val creator = saveMember("creator-original", MemberRole.CREATOR)
val user = saveMember("needle-user", MemberRole.USER)
val admin = saveMember("needle-admin", MemberRole.ADMIN)
val agent = saveMember("needle-agent", MemberRole.AGENT)
val bot = saveMember("needle-bot", MemberRole.BOT)
listOf(requester, creator, user, admin, agent, bot).forEach { saveAlias(it, "needle-alias") }
entityManager.clear()
assertEquals(listOf(creator.id), repository.findByNicknameAndOtherCondition("needle", requester).map { it.id })
}
@Test
@DisplayName("비활성 JA와 다른 언어 이름은 회원 별칭 검색에서 제외하고 현재 원본은 유지한다")
fun shouldExcludeInactiveAndNonJaAliasesWhileRetainingCurrentOriginalNickname() {
val requester = saveMember("member-clear-viewer", MemberRole.CREATOR)
val creator = saveMember("before-rename", MemberRole.CREATOR)
saveAlias(creator, "RetainedAlias", isActive = false)
saveAlias(creator, "EnglishAlias", lang = "EN")
creator.nickname = "after-rename"
entityManager.flush()
entityManager.clear()
assertEquals(emptyList<Long>(), repository.findByNicknameAndOtherCondition("RetainedAlias", requester).map { it.id })
assertEquals(emptyList<Long>(), repository.findByNicknameAndOtherCondition("EnglishAlias", requester).map { it.id })
assertEquals(listOf(creator.id), repository.findByNicknameAndOtherCondition("after-rename", requester).map { it.id })
jdbcTemplate.update(
"update creator_localized_name set is_active = true where member_id = ? and lang = 'JA'",
creator.id!!
)
assertAll(
{
assertEquals(
listOf(creator.id),
repository.findByNicknameAndOtherCondition("RetainedAlias", requester).map { it.id }
)
},
{
assertEquals(
listOf(creator.id),
repository.findByNicknameAndOtherCondition("after-rename", requester).map { it.id }
)
}
)
}
private fun saveMember(nickname: String, role: MemberRole): Member {
return Member(email = "$nickname@test.com", password = "password", nickname = nickname, role = role).also {
entityManager.persist(it)
}
}
private fun saveAlias(member: Member, displayName: String, isActive: Boolean = true, lang: String = "JA") {
entityManager.flush()
jdbcTemplate.update(
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"values (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
member.id!!,
lang,
displayName,
isActive
)
}
}
@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.member.block.BlockMember
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.member.block.MemberBlockRequest
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.support.originalLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -62,6 +63,7 @@ class MemberServiceCacheEvictionTest {
authenticationManagerBuilder = mock(),
messageSource = SodaMessageSource(),
langContext = LangContext(),
localizedNameService = originalLocalizedNameService(),
countryContext = CountryContext(),
memberContentPreferenceService = mock<MemberContentPreferenceService>(),
objectMapper = ObjectMapper(),
@@ -13,6 +13,7 @@ import kr.co.vividnext.sodalive.member.stipulation.Stipulation
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository
import kr.co.vividnext.sodalive.member.stipulation.StipulationIds
import kr.co.vividnext.sodalive.member.stipulation.StipulationRepository
import kr.co.vividnext.sodalive.support.originalLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
@@ -71,6 +72,7 @@ class MemberServiceContentPreferenceTest {
authenticationManagerBuilder = mock(),
messageSource = SodaMessageSource(),
langContext = LangContext(),
localizedNameService = originalLocalizedNameService(),
countryContext = CountryContext(),
memberContentPreferenceService = memberContentPreferenceService,
objectMapper = ObjectMapper(),
@@ -250,7 +250,7 @@ class OsivLazyLoadingRegressionTest @Autowired constructor(
val creators = rankingRepository.getCreatorRankings(memberId = null)
val response = creators.single().toExplorerSectionCreator("https://cdn.test")
val response = creators.single()
assertEquals("#creator-tag", response.tags)
}
@@ -0,0 +1,297 @@
package kr.co.vividnext.sodalive.search
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.ContentType
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.block.BlockMember
import kr.co.vividnext.sodalive.member.tag.CreatorTag
import kr.co.vividnext.sodalive.member.tag.MemberCreatorTag
import org.hibernate.SessionFactory
import org.junit.jupiter.api.Assertions.assertAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.jdbc.core.JdbcTemplate
import java.time.LocalDateTime
import javax.persistence.EntityManager
@DataJpaTest(properties = ["spring.cache.type=none", "spring.jpa.properties.hibernate.generate_statistics=true"])
@Import(QueryDslConfig::class)
class SearchRepositoryLocalizedNameTest @Autowired constructor(
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate,
queryFactory: JPAQueryFactory
) {
private val repository = SearchRepository(queryFactory, "https://cdn.test")
@Test
@DisplayName("크리에이터 검색은 원본과 활성 JA 별칭을 함께 찾고 중복 별칭도 회원별로 한 번씩 센다")
fun shouldSearchOriginalOrActiveJaAliasWithDistinctOwnersAndMatchingCount() {
val viewer = saveMember("search-viewer", MemberRole.USER)
val original = saveMember("Orion-original")
val aliasOnly = saveMember("second-original")
saveAlias(original, "Orion Japanese")
saveAlias(aliasOnly, "Orion Japanese")
entityManager.clear()
val items = repository.searchCreatorList("oRiOn", viewer.id!!, 0, 20)
val page = repository.searchCreatorList("oRiOn", viewer.id!!, 1, 1)
assertAll(
{ assertEquals(2, repository.searchCreatorTotalCount("oRiOn", viewer.id!!)) },
{ assertEquals(listOf(aliasOnly.id, original.id), items.map { it.id }) },
{ assertEquals(listOf(original.id), page.map { it.id }) },
{
assertEquals(
listOf(original.id),
repository.searchCreatorList("Orion-original", viewer.id!!, 0, 20).map { it.id }
)
}
)
}
@Test
@DisplayName("별칭 검색은 회원 활성·역할·태그 활성·양방향 차단 필터를 우회하지 않는다")
fun shouldPreserveCreatorVisibilityTagAndBidirectionalBlockFiltersForAliasMatches() {
val viewer = saveMember("filter-viewer", MemberRole.USER)
val visible = saveMember("visible-original")
val inactive = saveMember("inactive-original").apply { isActive = false }
val user = saveMember("user-original", MemberRole.USER)
val blockedByViewer = saveMember("blocked-original")
val blockingViewer = saveMember("blocking-original")
val inactiveTagOwner = saveMember("inactive-tag-original")
saveBlock(viewer, blockedByViewer)
saveBlock(blockingViewer, viewer)
val inactiveTag = CreatorTag(tag = "inactive-tag", isActive = false)
entityManager.persist(inactiveTag)
entityManager.persist(MemberCreatorTag(member = inactiveTagOwner, tag = inactiveTag))
listOf(visible, inactive, user, blockedByViewer, blockingViewer, inactiveTagOwner).forEach {
saveAlias(it, "AliasVisibility")
}
entityManager.clear()
assertAll(
{ assertEquals(1, repository.searchCreatorTotalCount("AliasVisibility", viewer.id!!)) },
{
assertEquals(
listOf(visible.id),
repository.searchCreatorList("AliasVisibility", viewer.id!!, 0, 20).map { it.id }
)
}
)
}
@Test
@DisplayName("비활성 JA와 다른 언어 별칭은 제외하고 원본 검색과 같은 행 재활성화는 유지한다")
fun shouldExcludeSoftClearedAndNonJaAliasesWithoutLosingOriginalSearch() {
val viewer = saveMember("clear-viewer", MemberRole.USER)
val creator = saveMember("current-original")
val content = saveContent(creator, "clear content title")
val series = saveSeries(creator, "clear series title", content)
saveAlias(creator, "ClearedAlias", isActive = false)
saveAlias(creator, "EnglishAlias", lang = "EN")
entityManager.clear()
assertEquals(0, repository.searchCreatorTotalCount("ClearedAlias", viewer.id!!))
assertEquals(emptyList<Long>(), repository.searchCreatorList("ClearedAlias", viewer.id!!, 0, 20).map { it.id })
assertEquals(0, repository.searchCreatorTotalCount("EnglishAlias", viewer.id!!))
assertEquals(1, repository.searchCreatorTotalCount("current-original", viewer.id!!))
assertEquals(0, repository.searchContentTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL))
assertEquals(0, repository.searchSeriesTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL))
assertEquals(1, repository.searchContentTotalCount("current-original", viewer.id!!, false, ContentType.ALL))
assertEquals(1, repository.searchSeriesTotalCount("current-original", viewer.id!!, false, ContentType.ALL))
repeat(2) {
jdbcTemplate.update(
"update creator_localized_name set is_active = true where member_id = ? and lang = 'JA'",
creator.id!!
)
assertAll(
{ assertEquals(1, repository.searchCreatorTotalCount("ClearedAlias", viewer.id!!)) },
{ assertEquals(1, repository.searchContentTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL)) },
{ assertEquals(1, repository.searchSeriesTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL)) },
{
assertEquals(
listOf(content.id),
repository.searchContentList("ClearedAlias", viewer.id!!, false, ContentType.ALL, 0, 20).map { it.id }
)
},
{
assertEquals(
listOf(series.id),
repository.searchSeriesList("ClearedAlias", viewer.id!!, false, ContentType.ALL, 0, 20).map { it.id }
)
},
{
assertEquals(
listOf(creator.id),
repository.searchCreatorList("ClearedAlias", viewer.id!!, 0, 20).map { it.id }
)
}
)
jdbcTemplate.update(
"update creator_localized_name set is_active = false where member_id = ? and lang = 'JA'",
creator.id!!
)
assertEquals(0, repository.searchCreatorTotalCount("ClearedAlias", viewer.id!!))
assertEquals(0, repository.searchContentTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL))
assertEquals(0, repository.searchSeriesTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL))
}
}
@Test
@DisplayName("콘텐츠 검색은 소유자 별칭을 찾되 성인·활성·완성·한정·차단 필터와 콘텐츠 제목을 보존한다")
fun shouldSearchContentOwnerAliasWithMatchingCountAndExistingContentFilters() {
val viewer = saveMember("content-search-viewer", MemberRole.USER)
val creator = saveMember("audio-owner-original")
val visible = saveContent(creator, "Unchanged audio title")
saveContent(creator, "adult audio").isAdult = true
saveContent(creator, "inactive audio").isActive = false
saveContent(creator, "unfinished audio").duration = null
saveContent(creator, "limited audio", limited = 5)
val blocked = saveMember("blocked-audio-owner")
saveContent(blocked, "blocked audio")
saveBlock(viewer, blocked)
saveAlias(creator, "AudioAlias")
saveAlias(creator, "English Audio Owner", lang = "EN")
saveAlias(blocked, "AudioAlias")
entityManager.clear()
val statistics = entityManager.entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
val items = repository.searchContentList("AudioAlias", viewer.id!!, false, ContentType.ALL, 0, 20, Lang.JA)
val aliasQueries = statistics.queries.filter {
Regex("""(?is)^select\s+\w+\.memberId,\s*\w+\.displayName\s+from\s+CreatorLocalizedNameEntity\b""")
.containsMatchIn(it)
}
val aliasQueryCount = aliasQueries.sumOf { statistics.getQueryStatistics(it).executionCount }
val queryCount = statistics.queryExecutionCount
assertAll(
{ assertEquals(1, repository.searchContentTotalCount("AudioAlias", viewer.id!!, false, ContentType.ALL)) },
{ assertEquals(listOf(visible.id), items.map { it.id }) },
{ assertEquals(listOf("Unchanged audio title"), items.map { it.title }) },
{ assertEquals(listOf("AudioAlias"), items.map { it.nickname }) },
{ assertEquals(1, repository.searchContentTotalCount("audio-owner-original", viewer.id!!, false, ContentType.ALL)) },
{ assertEquals(1, repository.searchContentTotalCount("Unchanged audio title", viewer.id!!, false, ContentType.ALL)) }
)
assertEquals(0L, aliasQueryCount, "Standalone alias SELECTs (total queries=$queryCount): $aliasQueries")
val englishItems = repository.searchContentList("AudioAlias", viewer.id!!, false, ContentType.ALL, 0, 20, Lang.EN)
assertEquals(listOf("English Audio Owner"), englishItems.map { it.nickname })
assertEquals(0, repository.searchContentTotalCount("English Audio Owner", viewer.id!!, false, ContentType.ALL))
}
@Test
@DisplayName("시리즈 검색은 시리즈 소유자 별칭을 찾고 콘텐츠 조인 중복과 기존 공개 필터를 보존한다")
fun shouldSearchSeriesOwnerAliasWithoutDuplicatingJoinedContentsOrChangingTitles() {
val viewer = saveMember("series-search-viewer", MemberRole.USER)
val creator = saveMember("series-owner-original")
val audioOwner = saveMember("different-audio-owner")
val visible = saveSeries(creator, "Unchanged series title", saveContent(audioOwner, "first episode"))
saveSeriesContent(visible, saveContent(audioOwner, "second episode"))
saveSeries(creator, "adult series", saveContent(creator, "public episode")).isAdult = true
saveSeries(creator, "adult episode series", saveContent(creator, "adult episode").apply { isAdult = true })
saveSeries(creator, "inactive series", saveContent(creator, "active episode")).isActive = false
saveSeries(creator, "unfinished series", saveContent(creator, "unfinished episode").apply { duration = null })
val blocked = saveMember("blocked-series-owner")
saveSeries(blocked, "blocked series", saveContent(blocked, "blocked episode"))
saveBlock(blocked, viewer)
saveAlias(creator, "SeriesAlias")
saveAlias(audioOwner, "EpisodeOwnerAlias")
saveAlias(blocked, "SeriesAlias")
entityManager.clear()
val items = repository.searchSeriesList("SeriesAlias", viewer.id!!, false, ContentType.ALL, 0, 20)
assertAll(
{ assertEquals(1, repository.searchSeriesTotalCount("SeriesAlias", viewer.id!!, false, ContentType.ALL)) },
{ assertEquals(listOf(visible.id), items.map { it.id }) },
{ assertEquals(listOf("Unchanged series title"), items.map { it.title }) },
{ assertEquals(0, repository.searchSeriesTotalCount("EpisodeOwnerAlias", viewer.id!!, false, ContentType.ALL)) },
{ assertEquals(1, repository.searchSeriesTotalCount("series-owner-original", viewer.id!!, false, ContentType.ALL)) },
{ assertEquals(1, repository.searchSeriesTotalCount("Unchanged series title", viewer.id!!, false, ContentType.ALL)) }
)
}
private fun saveMember(nickname: String, role: MemberRole = MemberRole.CREATOR): Member {
return Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
profileImage = "$nickname.png",
role = role
).also { entityManager.persist(it) }
}
private fun saveAlias(member: Member, displayName: String, isActive: Boolean = true, lang: String = "JA") {
entityManager.flush()
jdbcTemplate.update(
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"values (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
member.id!!,
lang,
displayName,
isActive
)
}
private fun saveBlock(member: Member, blockedMember: Member) {
entityManager.persist(
BlockMember(isActive = true).apply {
this.member = member
this.blockedMember = blockedMember
}
)
}
private fun saveContent(creator: Member, title: String, limited: Int? = null): AudioContent {
val theme = AudioContentTheme(theme = "theme-$title", image = "theme.png")
entityManager.persist(theme)
return AudioContent(
title = title,
detail = "description",
languageCode = "ko",
releaseDate = LocalDateTime.now().minusDays(1),
limited = limited
).apply {
member = creator
this.theme = theme
isActive = true
coverImage = "audio.png"
duration = "00:10:00"
entityManager.persist(this)
}
}
private fun saveSeries(creator: Member, title: String, content: AudioContent): Series {
val genre = SeriesGenre(genre = "genre-$title", isAdult = false, isActive = true)
entityManager.persist(genre)
return Series(title = title, introduction = "introduction", languageCode = "ko").apply {
member = creator
this.genre = genre
coverImage = "series.png"
entityManager.persist(this)
saveSeriesContent(this, content)
}
}
private fun saveSeriesContent(series: Series, content: AudioContent) {
entityManager.persist(
SeriesContent().apply {
this.series = series
this.content = content
}
)
}
}
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.search
import kr.co.vividnext.sodalive.content.ContentType
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
@@ -9,7 +10,7 @@ import org.mockito.Mockito
class SearchServiceTest {
private val repository: SearchRepository = Mockito.mock(SearchRepository::class.java)
private val service = SearchService(repository)
private val service = SearchService(repository, LangContext())
@Test
@DisplayName("콘텐츠 검색은 전달받은 isAdult 값을 그대로 사용한다")
@@ -132,6 +132,7 @@ class LegacyCommunityPostCharacterizationTest {
assertEquals(FcmEventType.CHANGE_NOTICE, event.type)
assertEquals(PushNotificationCategory.COMMUNITY, event.category)
assertEquals("creator.community.fcm.new_post", event.messageKey)
assertEquals(creator.id, event.titleMemberId)
assertEquals(creator.id, event.creatorId)
assertEquals(FcmDeepLinkValue.COMMUNITY, event.deepLinkValue)
assertEquals(creator.id, event.deepLinkId)
@@ -22,6 +22,7 @@ import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.MemberService
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
@@ -42,6 +43,7 @@ class LegacyFanTalkReplyCharacterizationTest {
private lateinit var cheersRepository: CreatorCheersRepository
private lateinit var applicationEventPublisher: ApplicationEventPublisher
private lateinit var messageSource: SodaMessageSource
private lateinit var localizedNameService: CreatorLocalizedNameService
private lateinit var service: ExplorerService
@BeforeEach
@@ -51,6 +53,7 @@ class LegacyFanTalkReplyCharacterizationTest {
cheersRepository = Mockito.mock(CreatorCheersRepository::class.java)
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
messageSource = Mockito.mock(SodaMessageSource::class.java)
localizedNameService = Mockito.mock(CreatorLocalizedNameService::class.java)
service = ExplorerService(
memberService = memberService,
memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java),
@@ -66,6 +69,7 @@ class LegacyFanTalkReplyCharacterizationTest {
contentTranslationRepository = Mockito.mock(ContentTranslationRepository::class.java),
messageSource = messageSource,
langContext = LangContext().apply { setLang(Lang.KO) },
localizedNameService = localizedNameService,
cloudFrontHost = "https://cdn.test"
)
}
@@ -177,6 +181,8 @@ class LegacyFanTalkReplyCharacterizationTest {
val root = createCheers(id = 300L, content = "root", member = creator, creator = creator)
Mockito.`when`(queryRepository.getMember(creator.id!!)).thenReturn(creator)
Mockito.`when`(queryRepository.getCreatorCheers(root.id!!)).thenReturn(root)
Mockito.`when`(localizedNameService.findLocalizedNames(listOf(creator.id!!), Lang.KO))
.thenReturn(mapOf(creator.id!! to "Korean creator alias"))
var nextId = 301L
Mockito.`when`(cheersRepository.save(Mockito.any(CreatorCheers::class.java))).thenAnswer { invocation ->
invocation.getArgument<CreatorCheers>(0).also { cheers ->
@@ -185,7 +191,7 @@ class LegacyFanTalkReplyCharacterizationTest {
}
}
service.writeCheers(
val response = service.writeCheers(
PostWriteCheersRequest(root.id, creator.id!!, "first reply", "ko"),
creator
)
@@ -198,6 +204,8 @@ class LegacyFanTalkReplyCharacterizationTest {
Mockito.verify(cheersRepository, Mockito.times(2)).save(cheersCaptor.capture())
assertEquals(listOf("first reply", "second reply"), cheersCaptor.allValues.map { it.cheers })
assertTrue(cheersCaptor.allValues.all { it.parent === root })
assertEquals("duplicate-creator", response.writerNickname)
Mockito.verifyNoInteractions(localizedNameService)
}
@Test
@@ -222,6 +230,8 @@ class LegacyFanTalkReplyCharacterizationTest {
Mockito.`when`(queryRepository.getMember(creator.id!!)).thenReturn(creator)
Mockito.`when`(memberService.isBlocked(writer.id!!, creator.id!!)).thenReturn(true)
Mockito.`when`(messageSource.getMessage("explorer.creator.blocked_cheers", Lang.KO)).thenReturn("blocked %s")
Mockito.`when`(localizedNameService.findLocalizedNames(listOf(creator.id!!), Lang.KO))
.thenReturn(mapOf(creator.id!! to "Korean blocked alias"))
val exception = assertThrows(SodaException::class.java) {
service.writeCheers(PostWriteCheersRequest(creatorId = creator.id!!, content = "reply"), writer)
@@ -229,6 +239,7 @@ class LegacyFanTalkReplyCharacterizationTest {
assertNull(exception.messageKey)
assertEquals("blocked blocked-creator", exception.message)
Mockito.verifyNoInteractions(localizedNameService)
Mockito.verifyNoInteractions(cheersRepository, applicationEventPublisher)
}
@@ -0,0 +1,467 @@
package kr.co.vividnext.sodalive.v2.creatorname.application
import kr.co.vividnext.sodalive.audition.Audition
import kr.co.vividnext.sodalive.audition.AuditionApplicant
import kr.co.vividnext.sodalive.audition.AuditionRole
import kr.co.vividnext.sodalive.audition.applicant.AuditionApplicantService
import kr.co.vividnext.sodalive.audition.applicant.AuditionApplicantSortType
import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront
import kr.co.vividnext.sodalive.can.CanService
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.UseCan
import kr.co.vividnext.sodalive.can.use.UseCanCalculate
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.AudioContentService
import kr.co.vividnext.sodalive.content.order.Order
import kr.co.vividnext.sodalive.content.order.OrderService
import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.playlist.AudioContentPlaylist
import kr.co.vividnext.sodalive.content.playlist.AudioContentPlaylistRedisRepository
import kr.co.vividnext.sodalive.content.playlist.AudioContentPlaylistService
import kr.co.vividnext.sodalive.content.playlist.PlaylistContentIdAndOrder
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
import kr.co.vividnext.sodalive.explorer.ExplorerService
import kr.co.vividnext.sodalive.explorer.MemberDonationRankingListResponse
import kr.co.vividnext.sodalive.explorer.MemberDonationRankingResponse
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationMessage
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationService
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.CreatorCommunityComment
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.MemberService
import kr.co.vividnext.sodalive.member.block.BlockMember
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.data.domain.PageRequest
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.context.ContextConfiguration
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import javax.persistence.EntityManager
@SpringBootTest(
properties = [
"spring.cache.type=none",
"cloud.aws.cloud-front.host=https://cdn.test",
"spring.datasource.url=jdbc:h2:mem:legacy-current-name-reads;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
]
)
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
@Transactional
class LegacyCurrentNameReadIntegrationTest @Autowired constructor(
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate,
private val memberService: MemberService,
private val orderService: OrderService,
private val playlistService: AudioContentPlaylistService,
private val playlistRepository: AudioContentPlaylistRedisRepository,
private val communityService: CreatorCommunityService,
private val donationService: ChannelDonationService,
private val explorerService: ExplorerService,
private val canService: CanService,
private val contentService: AudioContentService,
private val applicantService: AuditionApplicantService,
private val redisTemplate: RedisTemplate<String, Any>
) {
@MockBean
private lateinit var langContext: LangContext
@MockBean
private lateinit var countryContext: CountryContext
@MockBean
private lateinit var cloudFront: AudioContentCloudFront
@BeforeEach
fun setUp() {
jdbcTemplate.execute(
"CREATE ALIAS IF NOT EXISTS DATE_FORMAT FOR 'kr.co.vividnext.sodalive.support.H2MysqlDateFunctions.dateFormat'"
)
jdbcTemplate.execute(
"CREATE ALIAS IF NOT EXISTS CONVERT_TZ FOR 'kr.co.vividnext.sodalive.support.H2MysqlDateFunctions.convertTz'"
)
Mockito.doReturn(Lang.JA).`when`(langContext).lang
Mockito.doReturn("US").`when`(countryContext).countryCode
Mockito.doReturn("https://cdn.test/signed-audio").`when`(cloudFront)
.generateSignedURL(Mockito.anyString(), Mockito.anyLong())
}
@Test
@DisplayName("다른 회원 프로필과 차단 목록은 JA 이름을 표시하고 편집용 원본 프로필과 USER는 보존한다")
fun shouldLocalizeMemberDisplayAndBlockedListWithoutChangingRawEditorOrUserName() {
val viewer = saveMember("blocked-viewer", MemberRole.USER)
val creator = saveMember("blocked-creator", MemberRole.CREATOR)
val user = saveMember("blocked-user", MemberRole.USER)
listOf(creator, user).forEach { target ->
entityManager.persist(
BlockMember().apply {
member = viewer
blockedMember = target
}
)
}
saveAlias(creator, "Japanese blocked creator")
saveAlias(user, "Must not replace blocked USER")
entityManager.clear()
assertEquals("Japanese blocked creator", memberService.getMemberProfile(creator.id!!, viewer.id!!).nickname)
assertEquals("blocked-creator", memberService.getMember(creator.id!!, "web").nickname)
assertEquals("Japanese blocked creator", memberService.getMyPage(creator, "web").nickname)
val blocked = memberService.getBlockedMemberList(viewer.id!!, 0, 20)
assertEquals(2, blocked.totalCount)
assertEquals(
mapOf(creator.id to "Japanese blocked creator", user.id to "blocked-user"),
blocked.items.associate { it.memberId to it.nickname }
)
assertEquals("blocked-creator", entityManager.find(Member::class.java, creator.id).nickname)
}
@Test
@DisplayName("주문과 Redis 플레이리스트 상세는 콘텐츠 ID와 다른 소유자 이름을 현재 언어로 표시한다")
fun shouldLocalizeOrderAndPlaylistOwnersWithoutChangingTitlesOrStoredPlaylist() {
val viewer = saveMember("playlist-viewer", MemberRole.USER)
val creator = saveMember("playlist-creator", MemberRole.CREATOR)
val content = saveContent(creator, "Unchanged playlist title")
entityManager.persist(
Order(type = OrderType.KEEP).apply {
member = viewer
this.creator = creator
audioContent = content
}
)
saveAlias(creator, "Japanese playlist creator")
entityManager.clear()
assertNotEquals(creator.id, content.id)
val playlist = AudioContentPlaylist(
id = 100000L + viewer.id!!,
memberId = viewer.id!!,
title = "playlist metadata",
contentIdAndOrderList = listOf(PlaylistContentIdAndOrder(content.id!!, 1))
)
playlistRepository.save(playlist)
try {
val order = orderService.getAudioContentOrderList(viewer, 0, 20)
assertEquals("Japanese playlist creator", order.items.single().creatorNickname)
assertEquals("Unchanged playlist title", order.items.single().title)
val detail = playlistService.getPlaylistDetail(playlist.id, viewer)
assertEquals("Japanese playlist creator", detail.contentList.single().creatorNickname)
assertEquals("Unchanged playlist title", detail.contentList.single().title)
assertEquals(playlist, playlistRepository.findById(playlist.id).orElseThrow())
Mockito.doReturn(Lang.KO).`when`(langContext).lang
val korean = playlistService.getPlaylistDetail(playlist.id, viewer)
assertEquals("playlist-creator", korean.contentList.single().creatorNickname)
assertEquals("Japanese playlist creator", detail.contentList.single().creatorNickname)
} finally {
playlistRepository.delete(playlist)
}
}
@Test
@DisplayName("한정 콘텐츠 상세의 구매자 목록은 콘텐츠 소유자가 아닌 각 구매자 CREATOR 이름만 대체한다")
fun shouldLocalizeMixedBuyerNamesByBuyerIdWithoutAddingPublicOwnerFields() {
val owner = saveMember("buyer-content-owner", MemberRole.CREATOR)
val creatorBuyer = saveMember("buyer-creator", MemberRole.CREATOR)
val userBuyer = saveMember("buyer-user", MemberRole.USER)
val content = saveContent(owner, "Unchanged limited content title", limited = 5)
listOf(creatorBuyer, userBuyer).forEach { buyer ->
entityManager.persist(
Order(type = OrderType.KEEP).apply {
member = buyer
creator = owner
audioContent = content
}
)
}
saveAlias(owner, "Japanese content owner")
saveAlias(creatorBuyer, "Japanese buyer")
saveAlias(userBuyer, "Must not replace buyer USER")
entityManager.clear()
val detail = contentService.getDetail(content.id!!, owner, false, "Asia/Seoul")
assertEquals("Japanese content owner", detail.creator.nickname)
assertEquals(setOf("Japanese buyer", "buyer-user"), detail.buyerList.map { it.nickname }.toSet())
assertEquals("Unchanged limited content title", detail.title)
assertEquals("https://cdn.test/signed-audio", detail.contentUrl)
}
@Test
@DisplayName("레거시 커뮤니티 게시글·댓글·답글·구매 응답은 활성 JA·KO 별칭이 있어도 원본 이름을 유지한다")
fun shouldKeepOriginalCommunityNamesDespiteActiveJaAndKoAliases() {
val viewer = saveMember("community-viewer", MemberRole.USER)
val creator = saveMember("community-owner", MemberRole.CREATOR)
val peer = saveMember("deleted_community-peer", MemberRole.CREATOR)
val post = savePost(creator)
val parent = CreatorCommunityComment(comment = "unchanged creator comment").apply {
member = peer
creatorCommunity = post
entityManager.persist(this)
}
val reply = CreatorCommunityComment(comment = "unchanged USER reply").apply {
member = viewer
creatorCommunity = post
this.parent = parent
entityManager.persist(this)
}
val creatorReply = CreatorCommunityComment(comment = "unchanged creator reply").apply {
member = creator
creatorCommunity = post
this.parent = parent
entityManager.persist(this)
}
saveReceipt(viewer, CanUsage.PAID_COMMUNITY_POST, creator).communityPost = post
saveAlias(creator, "Japanese community owner")
saveAlias(peer, "Japanese community writer")
saveAlias(viewer, "Must not replace community USER")
listOf(creator, peer).forEach { writer ->
jdbcTemplate.update(
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"values (?, 'KO', ?, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
writer.id!!,
"Korean ${writer.nickname}"
)
}
entityManager.clear()
listOf(Lang.JA, Lang.KO).forEach { lang ->
Mockito.doReturn(lang).`when`(langContext).lang
val posts = communityService.getCommunityPostList(creator.id!!, viewer.id!!, "Asia/Seoul", 0, 20, false)
assertEquals("community-owner", posts.single().creatorNickname)
assertEquals("community-peer", posts.single().firstComment!!.nickname)
assertEquals("unchanged post content", posts.single().content)
val detail = communityService.getCommunityPostDetail(post.id!!, viewer.id!!, "Asia/Seoul", false)
assertEquals("community-owner", detail.creatorNickname)
val comments = communityService.getCommunityPostCommentList(post.id!!, viewer.id!!, "Asia/Seoul", 0, 20, false)
assertEquals("community-peer", comments.items.single().nickname)
val replies = communityService.getCommentReplyList(parent.id!!, viewer.id!!, "Asia/Seoul", 0, 20, false)
assertEquals(
mapOf(reply.id to "community-viewer", creatorReply.id to "community-owner"),
replies.items.associate { it.id to it.nickname }
)
assertEquals("unchanged USER reply", replies.items.single { it.id == reply.id }.comment)
val purchased = communityService.purchasePost(post.id!!, viewer.id!!, "Asia/Seoul", false, "web")
assertEquals("community-owner", purchased.creatorNickname)
assertEquals("community-peer", purchased.firstComment!!.nickname)
}
}
@Test
@DisplayName("채널 후원 목록의 이름 소유자는 채널이 아니라 후원자이며 USER는 원본이다")
fun shouldLocalizeCreatorDonorsIndependentlyOfTheChannelOwner() {
val viewer = saveMember("donation-viewer", MemberRole.USER)
val creator = saveMember("donation-channel", MemberRole.CREATOR)
val donor = saveMember("donation-creator-donor", MemberRole.CREATOR)
listOf(viewer, donor).forEach { writer ->
entityManager.persist(
ChannelDonationMessage(can = 100, isSecret = false).apply {
member = writer
this.creator = creator
}
)
}
saveAlias(creator, "Wrong channel owner name")
saveAlias(donor, "Japanese donor")
saveAlias(viewer, "Must not replace donor USER")
entityManager.clear()
val response = donationService.getChannelDonationList(creator.id!!, viewer, 0, 20)
assertEquals(2, response.totalCount)
assertEquals(
mapOf(donor.id to "Japanese donor", viewer.id to "donation-viewer"),
response.items.associate { it.memberId to it.nickname }
)
assertEquals(listOf(100, 100), response.items.map { it.can })
}
@Test
@DisplayName("receipt는 각 recipient·audio·community 소유자의 현재 이름을 쓰고 리소스 제목과 금액을 보존한다")
fun shouldLocalizeReceiptOwnerNamesWithoutReplacingResourceTitlesOrAmounts() {
val viewer = saveMember("receipt-viewer", MemberRole.USER)
val owner = saveMember("receipt-owner", MemberRole.CREATOR)
val recipient = saveMember("receipt-recipient", MemberRole.CREATOR)
val content = saveContent(owner, "Unchanged receipt content title")
val post = savePost(owner)
saveReceipt(viewer, CanUsage.DONATION, owner).audioContent = content
saveReceipt(viewer, CanUsage.PAID_COMMUNITY_POST, owner).communityPost = post
saveReceipt(viewer, CanUsage.CHANNEL_DONATION, recipient)
saveReceipt(viewer, CanUsage.CHANNEL_DONATION, viewer)
saveReceipt(viewer, CanUsage.ORDER_CONTENT, owner).audioContent = content
saveAlias(owner, "Japanese receipt owner")
saveAlias(recipient, "Japanese recipient")
saveAlias(viewer, "Must not replace receipt USER")
entityManager.clear()
val rows = canService.getCanUseStatus(viewer, PageRequest.of(0, 20), "UTC", "web")
assertEquals(
setOf(
"[콘텐츠 후원] Japanese receipt owner",
"[게시글 보기] Japanese receipt owner",
"[채널 후원] Japanese recipient",
"[채널 후원] receipt-viewer",
"[콘텐츠 구매] Unchanged receipt content title"
),
rows.map { it.title }.toSet()
)
assertEquals(5, rows.size)
assertEquals(listOf(100, 100, 100, 100, 100), rows.map { it.can })
}
@Test
@DisplayName("오디션 지원자는 applicant ID가 아닌 member ID로 이름을 표시하고 USER·count·정렬을 보존한다")
fun shouldLocalizeAuditionApplicantsByMemberIdAndKeepUserNamesAndCount() {
saveMember("applicant-unused-viewer", MemberRole.USER)
val creator = saveMember("applicant-creator", MemberRole.CREATOR)
val user = saveMember("applicant-user", MemberRole.USER)
val audition = Audition(title = "Unchanged audition title", information = "information")
entityManager.persist(audition)
val role = AuditionRole(name = "Unchanged role name", information = "information").apply {
this.audition = audition
entityManager.persist(this)
}
val creatorApplicant = AuditionApplicant(phoneNumber = "01000000000", voicePath = "voice.m4a").apply {
member = creator
this.role = role
entityManager.persist(this)
}
val userApplicant = AuditionApplicant(phoneNumber = "01000000001", voicePath = "voice.m4a").apply {
member = user
this.role = role
entityManager.persist(this)
}
saveAlias(creator, "Japanese applicant")
saveAlias(user, "Must not replace applicant USER")
entityManager.clear()
assertNotEquals(creator.id, creatorApplicant.id)
val response = applicantService.getAuditionApplicantList(role.id!!, AuditionApplicantSortType.NEWEST, 0, 20)
assertEquals(2, response.totalCount)
assertEquals(listOf(userApplicant.id, creatorApplicant.id), response.items.map { it.applicantId })
assertEquals(listOf("applicant-user", "Japanese applicant"), response.items.map { it.nickname })
}
@Test
@DisplayName("중립 후원랭킹 cache hit는 요청별 현재 이름을 복사하고 원본 변경·해제 후 현재 원본으로 돌아간다")
fun shouldRenderCachedDonorNamesAfterReadAndKeepCachedRowsImmutable() {
val viewer = saveMember("cached-viewer", MemberRole.USER)
val channel = saveMember("cached-channel", MemberRole.CREATOR)
val donor = saveMember("cached-donor-original", MemberRole.CREATOR)
saveAlias(donor, "Japanese cached donor")
entityManager.clear()
val key = "creator_donation_ranking_v2:${channel.id}:CUMULATIVE:0:20:false"
val totalKey = "creator_donation_ranking_member_total_v2:${channel.id}:CUMULATIVE"
val cached = MemberDonationRankingListResponse(
listOf(MemberDonationRankingResponse(donor.id!!, "stale cached original", "image.png", 0))
)
redisTemplate.opsForValue().set(key, cached)
redisTemplate.opsForValue().set(totalKey, 1)
try {
val japanese = explorerService.getCreatorProfileDonationRanking(
channel.id!!,
DonationRankingPeriod.CUMULATIVE,
PageRequest.of(0, 20),
viewer
)
assertEquals("Japanese cached donor", japanese.userDonationRanking.single().nickname)
jdbcTemplate.update("update member set nickname = ? where id = ?", "renamed donor", donor.id!!)
jdbcTemplate.update("update creator_localized_name set is_active = false where member_id = ?", donor.id!!)
entityManager.clear()
val cleared = explorerService.getCreatorProfileDonationRanking(
channel.id!!,
DonationRankingPeriod.CUMULATIVE,
PageRequest.of(0, 20),
viewer
)
assertEquals("renamed donor", cleared.userDonationRanking.single().nickname)
assertEquals("Japanese cached donor", japanese.userDonationRanking.single().nickname)
assertEquals(cached, redisTemplate.opsForValue().get(key))
} finally {
redisTemplate.delete(listOf(key, totalKey))
}
}
private fun saveMember(nickname: String, role: MemberRole): Member =
Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
profileImage = "profile.png",
role = role
).also { entityManager.persist(it) }
private fun saveContent(owner: Member, title: String, limited: Int? = null): AudioContent {
val theme = AudioContentTheme(theme = "theme-$title", image = "theme.png")
entityManager.persist(theme)
return AudioContent(
title = title,
detail = "detail",
languageCode = "ko",
releaseDate = LocalDateTime.now().minusDays(1),
limited = limited,
remaining = limited
)
.apply {
member = owner
this.theme = theme
isActive = true
content = "output/audio.m4a"
duration = "00:10:00"
coverImage = "content.png"
entityManager.persist(this)
}
}
private fun savePost(owner: Member): CreatorCommunity =
CreatorCommunity(
content = "unchanged post content",
price = 0,
isCommentAvailable = true,
isAdult = false,
languageCode = "ko"
).apply {
member = owner
entityManager.persist(this)
}
private fun saveReceipt(viewer: Member, usage: CanUsage, recipient: Member): UseCan {
entityManager.flush()
val receipt = UseCan(canUsage = usage, can = 100, rewardCan = 0).apply {
member = viewer
entityManager.persist(this)
}
entityManager.persist(
UseCanCalculate(100, PaymentGateway.PG, UseCanCalculateStatus.RECEIVED).apply {
useCan = receipt
recipientCreatorId = recipient.id!!
}
)
return receipt
}
private fun saveAlias(member: Member, name: String) {
entityManager.flush()
jdbcTemplate.update(
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"values (?, 'JA', ?, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
member.id!!,
name
)
}
}