Compare commits

...

3 Commits

8 changed files with 124 additions and 22 deletions

View File

@@ -10,7 +10,7 @@ import kr.co.vividnext.sodalive.v2.api.home.dto.HomeCreatorItem
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeFirstAudioContentItem import kr.co.vividnext.sodalive.v2.api.home.dto.HomeFirstAudioContentItem
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeGenreCreatorGroupItem import kr.co.vividnext.sodalive.v2.api.home.dto.HomeGenreCreatorGroupItem
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeLiveItem import kr.co.vividnext.sodalive.v2.api.home.dto.HomeLiveItem
import kr.co.vividnext.sodalive.v2.api.home.dto.HomePopularCommunityItem import kr.co.vividnext.sodalive.v2.api.home.dto.HomePopularCommunityPostItem
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeRecommendationPageResponse import kr.co.vividnext.sodalive.v2.api.home.dto.HomeRecommendationPageResponse
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeRecommendationResponse import kr.co.vividnext.sodalive.v2.api.home.dto.HomeRecommendationResponse
import kr.co.vividnext.sodalive.v2.api.home.dto.imageUrl import kr.co.vividnext.sodalive.v2.api.home.dto.imageUrl
@@ -81,7 +81,7 @@ class HomeRecommendationFacade(
).map { it.toItem() }, ).map { it.toItem() },
cheerCreators = queryService.findCheerCreatorRecommendations(HOME_CHEER_CREATOR_LIMIT, member?.id) cheerCreators = queryService.findCheerCreatorRecommendations(HOME_CHEER_CREATOR_LIMIT, member?.id)
.map { it.toCreatorItem() }, .map { it.toCreatorItem() },
popularCommunities = queryService.findPopularCommunityRecommendations( popularCommunityPosts = queryService.findPopularCommunityRecommendations(
limit = HOME_POPULAR_COMMUNITY_LIMIT, limit = HOME_POPULAR_COMMUNITY_LIMIT,
memberId = member?.id, memberId = member?.id,
includeAdultCommunities = includeAdult includeAdultCommunities = includeAdult
@@ -205,7 +205,7 @@ class HomeRecommendationFacade(
if (aiCharacters.isEmpty()) add("aiCharacters") if (aiCharacters.isEmpty()) add("aiCharacters")
if (genreCreators.isEmpty()) add("genreCreators") if (genreCreators.isEmpty()) add("genreCreators")
if (cheerCreators.isEmpty()) add("cheerCreators") if (cheerCreators.isEmpty()) add("cheerCreators")
if (popularCommunities.isEmpty()) add("popularCommunities") if (popularCommunityPosts.isEmpty()) add("popularCommunityPosts")
} }
} }
@@ -301,15 +301,18 @@ class HomeRecommendationFacade(
creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage) creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage)
) )
private fun HomePopularCommunityRecommendationRecord.toItem() = HomePopularCommunityItem( private fun HomePopularCommunityRecommendationRecord.toItem() = HomePopularCommunityPostItem(
communityId = communityId, postId = communityId,
creatorId = creatorId, creatorId = creatorId,
creatorNickname = creatorNickname, creatorNickname = creatorNickname,
creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage), creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage),
imageUrl = imageUrl(cloudFrontHost, imagePath),
audioUrl = imageUrl(cloudFrontHost, audioPath),
content = content, content = content,
createdAt = createdAt.toUtcIso(), createdAt = createdAt.toUtcIso(),
likeCount = likeCount, likeCount = likeCount,
commentCount = commentCount commentCount = commentCount,
existOrdered = existOrdered
) )
companion object { companion object {

View File

@@ -21,7 +21,7 @@ data class HomeRecommendationResponse(
val aiCharacters: List<HomeAiCharacterItem>, val aiCharacters: List<HomeAiCharacterItem>,
val genreCreators: List<HomeGenreCreatorGroupItem>, val genreCreators: List<HomeGenreCreatorGroupItem>,
val cheerCreators: List<HomeCreatorItem>, val cheerCreators: List<HomeCreatorItem>,
val popularCommunities: List<HomePopularCommunityItem> val popularCommunityPosts: List<HomePopularCommunityPostItem>
) )
data class HomeLiveItem( data class HomeLiveItem(
@@ -87,13 +87,16 @@ data class HomeGenreCreatorGroupItem(
val creators: List<HomeCreatorItem> val creators: List<HomeCreatorItem>
) )
data class HomePopularCommunityItem( data class HomePopularCommunityPostItem(
val communityId: Long, val postId: Long,
val creatorId: Long, val creatorId: Long,
val creatorNickname: String, val creatorNickname: String,
val creatorProfileImage: String?, val creatorProfileImage: String?,
val imageUrl: String?,
val audioUrl: String?,
val content: String, val content: String,
val createdAt: String, val createdAt: String,
val likeCount: Long, val likeCount: Long,
val commentCount: Long val commentCount: Long,
val existOrdered: Boolean
) )

View File

@@ -6,6 +6,8 @@ import com.querydsl.core.types.dsl.BooleanExpression
import com.querydsl.core.types.dsl.Expressions import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.JPAExpressions import com.querydsl.jpa.JPAExpressions
import com.querydsl.jpa.impl.JPAQueryFactory 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.chat.character.QChatCharacter.chatCharacter import kr.co.vividnext.sodalive.chat.character.QChatCharacter.chatCharacter
import kr.co.vividnext.sodalive.chat.original.QOriginalWork import kr.co.vividnext.sodalive.chat.original.QOriginalWork
import kr.co.vividnext.sodalive.chat.room.ParticipantType import kr.co.vividnext.sodalive.chat.room.ParticipantType
@@ -773,10 +775,13 @@ class DefaultHomeRecommendationQueryRepository(
member.id, member.id,
member.nickname, member.nickname,
member.profileImage, member.profileImage,
creatorCommunity.imagePath,
creatorCommunity.audioPath,
creatorCommunity.content, creatorCommunity.content,
creatorCommunity.createdAt, creatorCommunity.createdAt,
creatorCommunityLike.id.countDistinct(), creatorCommunityLike.id.countDistinct(),
creatorCommunityComment.id.countDistinct() creatorCommunityComment.id.countDistinct(),
orderedCommunityPostCondition(memberId)
) )
) )
.from(creatorCommunity) .from(creatorCommunity)
@@ -792,7 +797,6 @@ class DefaultHomeRecommendationQueryRepository(
.where( .where(
creatorCommunity.isActive.isTrue, creatorCommunity.isActive.isTrue,
member.isActive.isTrue, member.isActive.isTrue,
creatorCommunity.price.eq(0),
creatorCommunity.isFixed.isFalse, creatorCommunity.isFixed.isFalse,
includeAdultCommunityCondition(includeAdultCommunities), includeAdultCommunityCondition(includeAdultCommunities),
notBlockedCreatorCondition(memberId, member.id), notBlockedCreatorCondition(memberId, member.id),
@@ -803,6 +807,8 @@ class DefaultHomeRecommendationQueryRepository(
member.id, member.id,
member.nickname, member.nickname,
member.profileImage, member.profileImage,
creatorCommunity.imagePath,
creatorCommunity.audioPath,
creatorCommunity.content, creatorCommunity.content,
creatorCommunity.createdAt creatorCommunity.createdAt
) )
@@ -1054,6 +1060,20 @@ class DefaultHomeRecommendationQueryRepository(
.notExists() .notExists()
} }
private fun orderedCommunityPostCondition(memberId: Long?): BooleanExpression {
if (memberId == null) return Expressions.FALSE
return JPAExpressions
.selectOne()
.from(useCan)
.where(
useCan.member.id.eq(memberId),
useCan.isRefund.isFalse,
useCan.communityPost.id.eq(creatorCommunity.id),
useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST)
)
.exists()
}
private fun notBlockedCreatorSql(creatorIdExpression: String): String { private fun notBlockedCreatorSql(creatorIdExpression: String): String {
return """ return """
not exists ( not exists (

View File

@@ -151,10 +151,13 @@ data class HomePopularCommunityRecommendationRecord(
val creatorId: Long, val creatorId: Long,
val creatorNickname: String, val creatorNickname: String,
val creatorProfileImage: String?, val creatorProfileImage: String?,
val imagePath: String?,
val audioPath: String?,
val content: String, val content: String,
val createdAt: LocalDateTime, val createdAt: LocalDateTime,
val likeCount: Long, val likeCount: Long,
val commentCount: Long val commentCount: Long,
val existOrdered: Boolean
) )
data class HomeGenreCreatorRecommendationGroup( data class HomeGenreCreatorRecommendationGroup(

View File

@@ -219,7 +219,7 @@ class HomeRecommendationControllerTest @Autowired constructor(
.andExpect(jsonPath("$.data.aiCharacters").isArray) .andExpect(jsonPath("$.data.aiCharacters").isArray)
.andExpect(jsonPath("$.data.genreCreators").isArray) .andExpect(jsonPath("$.data.genreCreators").isArray)
.andExpect(jsonPath("$.data.cheerCreators").isArray) .andExpect(jsonPath("$.data.cheerCreators").isArray)
.andExpect(jsonPath("$.data.popularCommunities").isArray) .andExpect(jsonPath("$.data.popularCommunityPosts").isArray)
assertTrue(output.out.contains("event=home_recommendations_query_success")) assertTrue(output.out.contains("event=home_recommendations_query_success"))
assertTrue(output.out.contains("emptySections=")) assertTrue(output.out.contains("emptySections="))

View File

@@ -39,7 +39,21 @@ class HomeRecommendationResponseTest {
), ),
genreCreators = emptyList(), genreCreators = emptyList(),
cheerCreators = emptyList(), cheerCreators = emptyList(),
popularCommunities = emptyList() popularCommunityPosts = listOf(
HomePopularCommunityPostItem(
postId = 5L,
creatorId = 6L,
creatorNickname = "community-creator",
creatorProfileImage = "https://cdn.test/profile/community.png",
imageUrl = "https://cdn.test/community/image.png",
audioUrl = "https://cdn.test/community/audio.mp3",
content = "community content",
createdAt = "2026-06-01T00:00:00Z",
likeCount = 7L,
commentCount = 8L,
existOrdered = true
)
)
) )
val json = objectMapper.readTree(objectMapper.writeValueAsString(response)) val json = objectMapper.readTree(objectMapper.writeValueAsString(response))
@@ -47,5 +61,9 @@ class HomeRecommendationResponseTest {
assertEquals(true, json["firstAudioContents"][0]["isPointAvailable"].asBoolean()) assertEquals(true, json["firstAudioContents"][0]["isPointAvailable"].asBoolean())
assertFalse(json["firstAudioContents"][0].has("pointAvailable")) assertFalse(json["firstAudioContents"][0].has("pointAvailable"))
assertEquals("https://cdn.test/profile/character.png", json["aiCharacters"][0]["profileImage"].asText()) assertEquals("https://cdn.test/profile/character.png", json["aiCharacters"][0]["profileImage"].asText())
assertEquals(5L, json["popularCommunityPosts"][0]["postId"].asLong())
assertEquals("https://cdn.test/community/image.png", json["popularCommunityPosts"][0]["imageUrl"].asText())
assertEquals("https://cdn.test/community/audio.mp3", json["popularCommunityPosts"][0]["audioUrl"].asText())
assertEquals(true, json["popularCommunityPosts"][0]["existOrdered"].asBoolean())
} }
} }

View File

@@ -1234,7 +1234,12 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
val creator = saveMember("community-detail-creator", MemberRole.CREATOR) val creator = saveMember("community-detail-creator", MemberRole.CREATOR)
val inactiveCreator = saveMember("community-detail-inactive-creator", MemberRole.CREATOR, isActive = false) val inactiveCreator = saveMember("community-detail-inactive-creator", MemberRole.CREATOR, isActive = false)
val member = saveMember("community-detail-member", MemberRole.USER) val member = saveMember("community-detail-member", MemberRole.USER)
val eligible = saveCommunity(creator, isCommentAvailable = true) val eligible = saveCommunity(
creator,
isCommentAvailable = true,
imagePath = "community/detail-image.png",
audioPath = "community/detail-audio.mp3"
)
val paid = saveCommunity(creator, isCommentAvailable = true, price = 10) val paid = saveCommunity(creator, isCommentAvailable = true, price = 10)
val fixed = saveCommunity(creator, isCommentAvailable = true, isFixed = true) val fixed = saveCommunity(creator, isCommentAvailable = true, isFixed = true)
val adult = saveCommunity(creator, isCommentAvailable = true, isAdult = true) val adult = saveCommunity(creator, isCommentAvailable = true, isAdult = true)
@@ -1249,23 +1254,46 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
updateCreatedAt("CreatorCommunityLike", like1.id!!, LocalDateTime.of(2026, 5, 29, 2, 0)) updateCreatedAt("CreatorCommunityLike", like1.id!!, LocalDateTime.of(2026, 5, 29, 2, 0))
updateCreatedAt("CreatorCommunityLike", like2.id!!, LocalDateTime.of(2026, 5, 29, 3, 0)) updateCreatedAt("CreatorCommunityLike", like2.id!!, LocalDateTime.of(2026, 5, 29, 3, 0))
updateCreatedAt("CreatorCommunityComment", comment1.id!!, LocalDateTime.of(2026, 5, 29, 4, 0)) updateCreatedAt("CreatorCommunityComment", comment1.id!!, LocalDateTime.of(2026, 5, 29, 4, 0))
saveCommunityOrder(member, paid, isRefund = false)
flushAndClear() flushAndClear()
val details = repository.findPopularCommunityRecommendationDetails( val details = repository.findPopularCommunityRecommendationDetails(
listOf(eligible.id!!, paid.id!!, fixed.id!!, adult.id!!, inactivePost.id!!, inactiveCreatorPost.id!!, 999L), listOf(eligible.id!!, paid.id!!, fixed.id!!, adult.id!!, inactivePost.id!!, inactiveCreatorPost.id!!, 999L),
memberId = member.id,
includeAdultCommunities = false includeAdultCommunities = false
) )
val detailById = details.associateBy { it.communityId } val detailById = details.associateBy { it.communityId }
assertEquals(setOf(eligible.id), detailById.keys) assertEquals(setOf(eligible.id, paid.id), detailById.keys)
assertEquals("content", detailById[eligible.id]!!.content) assertEquals("content", detailById[eligible.id]!!.content)
assertEquals("community/detail-image.png", detailById[eligible.id]!!.imagePath)
assertEquals("community/detail-audio.mp3", detailById[eligible.id]!!.audioPath)
assertEquals(LocalDateTime.of(2026, 5, 29, 1, 0), detailById[eligible.id]!!.createdAt) assertEquals(LocalDateTime.of(2026, 5, 29, 1, 0), detailById[eligible.id]!!.createdAt)
assertEquals(2L, detailById[eligible.id]!!.likeCount) assertEquals(2L, detailById[eligible.id]!!.likeCount)
assertEquals(1L, detailById[eligible.id]!!.commentCount) assertEquals(1L, detailById[eligible.id]!!.commentCount)
assertEquals(false, detailById[eligible.id]!!.existOrdered)
assertEquals(true, detailById[paid.id]!!.existOrdered)
assertEquals(creator.id, detailById[eligible.id]!!.creatorId) assertEquals(creator.id, detailById[eligible.id]!!.creatorId)
assertEquals("community-detail-creator", detailById[eligible.id]!!.creatorNickname) assertEquals("community-detail-creator", detailById[eligible.id]!!.creatorNickname)
} }
@Test
@DisplayName("인기 커뮤니티 상세는 비회원에게 구매 여부를 false로 반환한다")
fun shouldReturnFalseOrderStatusForAnonymousPopularCommunityDetails() {
val creator = saveMember("anonymous-community-creator", MemberRole.CREATOR)
val paid = saveCommunity(creator, isCommentAvailable = true, price = 10)
flushAndClear()
val details = repository.findPopularCommunityRecommendationDetails(
listOf(paid.id!!),
memberId = null,
includeAdultCommunities = false
)
assertEquals(listOf(paid.id), details.map { it.communityId })
assertEquals(listOf(false), details.map { it.existOrdered })
}
@Test @Test
@DisplayName("인기 커뮤니티 상세는 성인 노출 가능 회원에게 성인 게시글을 포함한다") @DisplayName("인기 커뮤니티 상세는 성인 노출 가능 회원에게 성인 게시글을 포함한다")
fun shouldFindAdultPopularCommunityDetailsWhenAdultVisible() { fun shouldFindAdultPopularCommunityDetailsWhenAdultVisible() {
@@ -1656,13 +1684,17 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
price: Int = 0, price: Int = 0,
isAdult: Boolean = false, isAdult: Boolean = false,
isActive: Boolean = true, isActive: Boolean = true,
isFixed: Boolean = false isFixed: Boolean = false,
imagePath: String? = null,
audioPath: String? = null
): CreatorCommunity { ): CreatorCommunity {
val community = CreatorCommunity( val community = CreatorCommunity(
content = "content", content = "content",
price = price, price = price,
isCommentAvailable = isCommentAvailable, isCommentAvailable = isCommentAvailable,
isAdult = isAdult, isAdult = isAdult,
audioPath = audioPath,
imagePath = imagePath,
isActive = isActive, isActive = isActive,
isFixed = isFixed isFixed = isFixed
) )
@@ -1671,6 +1703,14 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
return community return community
} }
private fun saveCommunityOrder(member: Member, community: CreatorCommunity, isRefund: Boolean): UseCan {
val useCan = UseCan(canUsage = CanUsage.PAID_COMMUNITY_POST, can = community.price, rewardCan = 0, isRefund = isRefund)
useCan.member = member
useCan.communityPost = community
entityManager.persist(useCan)
return useCan
}
private fun saveAudioContent( private fun saveAudioContent(
creator: Member, creator: Member,
releaseDate: LocalDateTime, releaseDate: LocalDateTime,

View File

@@ -227,30 +227,39 @@ class HomeRecommendationQueryServiceTest {
creatorId = 10L, creatorId = 10L,
creatorNickname = "creator-10", creatorNickname = "creator-10",
creatorProfileImage = "profile-10.png", creatorProfileImage = "profile-10.png",
imagePath = "community-1.png",
audioPath = "community-1.mp3",
content = "content-1", content = "content-1",
createdAt = LocalDateTime.of(2026, 5, 29, 1, 0), createdAt = LocalDateTime.of(2026, 5, 29, 1, 0),
likeCount = 3L, likeCount = 3L,
commentCount = 2L commentCount = 2L,
existOrdered = true
), ),
HomePopularCommunityRecommendationRecord( HomePopularCommunityRecommendationRecord(
communityId = 2L, communityId = 2L,
creatorId = 10L, creatorId = 10L,
creatorNickname = "creator-10", creatorNickname = "creator-10",
creatorProfileImage = "profile-10.png", creatorProfileImage = "profile-10.png",
imagePath = null,
audioPath = null,
content = "content-2", content = "content-2",
createdAt = LocalDateTime.of(2026, 5, 29, 2, 0), createdAt = LocalDateTime.of(2026, 5, 29, 2, 0),
likeCount = 1L, likeCount = 1L,
commentCount = 1L commentCount = 1L,
existOrdered = false
), ),
HomePopularCommunityRecommendationRecord( HomePopularCommunityRecommendationRecord(
communityId = 3L, communityId = 3L,
creatorId = 11L, creatorId = 11L,
creatorNickname = "creator-11", creatorNickname = "creator-11",
creatorProfileImage = null, creatorProfileImage = null,
imagePath = null,
audioPath = null,
content = "content-3", content = "content-3",
createdAt = LocalDateTime.of(2026, 5, 29, 3, 0), createdAt = LocalDateTime.of(2026, 5, 29, 3, 0),
likeCount = 0L, likeCount = 0L,
commentCount = 0L commentCount = 0L,
existOrdered = false
) )
) )
@@ -262,6 +271,9 @@ class HomeRecommendationQueryServiceTest {
assertEquals(listOf(1L, 3L), communities.map { it.communityId }) assertEquals(listOf(1L, 3L), communities.map { it.communityId })
assertEquals(listOf(10L, 11L), communities.map { it.creatorId }) assertEquals(listOf(10L, 11L), communities.map { it.creatorId })
assertEquals(LocalDateTime.of(2026, 5, 29, 1, 0), communities.first().createdAt) assertEquals(LocalDateTime.of(2026, 5, 29, 1, 0), communities.first().createdAt)
assertEquals("community-1.png", communities.first().imagePath)
assertEquals("community-1.mp3", communities.first().audioPath)
assertEquals(true, communities.first().existOrdered)
} }
@Test @Test
@@ -281,10 +293,13 @@ class HomeRecommendationQueryServiceTest {
creatorId = if (communityId <= 10L) 1L else communityId, creatorId = if (communityId <= 10L) 1L else communityId,
creatorNickname = "creator-$communityId", creatorNickname = "creator-$communityId",
creatorProfileImage = null, creatorProfileImage = null,
imagePath = null,
audioPath = null,
content = "content-$communityId", content = "content-$communityId",
createdAt = LocalDateTime.of(2026, 5, 29, 1, 0).plusMinutes(communityId), createdAt = LocalDateTime.of(2026, 5, 29, 1, 0).plusMinutes(communityId),
likeCount = 0L, likeCount = 0L,
commentCount = 0L commentCount = 0L,
existOrdered = false
) )
} }