From 12b446c4aea73424e7f2592056819b58f2ad3bf4 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 1 Jun 2026 22:40:05 +0900 Subject: [PATCH] =?UTF-8?q?feat(recommend):=20=EC=9D=B8=EA=B8=B0=20?= =?UTF-8?q?=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=83=81=EC=84=B8=20=ED=95=84=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...efaultHomeRecommendationQueryRepository.kt | 24 +++++++++- .../port/out/HomeRecommendationQueryPort.kt | 5 +- ...ltHomeRecommendationQueryRepositoryTest.kt | 46 +++++++++++++++++-- .../HomeRecommendationQueryServiceTest.kt | 23 ++++++++-- 4 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt index 0db0d797..65941901 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt @@ -6,6 +6,8 @@ import com.querydsl.core.types.dsl.BooleanExpression import com.querydsl.core.types.dsl.Expressions import com.querydsl.jpa.JPAExpressions 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.original.QOriginalWork import kr.co.vividnext.sodalive.chat.room.ParticipantType @@ -773,10 +775,13 @@ class DefaultHomeRecommendationQueryRepository( member.id, member.nickname, member.profileImage, + creatorCommunity.imagePath, + creatorCommunity.audioPath, creatorCommunity.content, creatorCommunity.createdAt, creatorCommunityLike.id.countDistinct(), - creatorCommunityComment.id.countDistinct() + creatorCommunityComment.id.countDistinct(), + orderedCommunityPostCondition(memberId) ) ) .from(creatorCommunity) @@ -792,7 +797,6 @@ class DefaultHomeRecommendationQueryRepository( .where( creatorCommunity.isActive.isTrue, member.isActive.isTrue, - creatorCommunity.price.eq(0), creatorCommunity.isFixed.isFalse, includeAdultCommunityCondition(includeAdultCommunities), notBlockedCreatorCondition(memberId, member.id), @@ -803,6 +807,8 @@ class DefaultHomeRecommendationQueryRepository( member.id, member.nickname, member.profileImage, + creatorCommunity.imagePath, + creatorCommunity.audioPath, creatorCommunity.content, creatorCommunity.createdAt ) @@ -1054,6 +1060,20 @@ class DefaultHomeRecommendationQueryRepository( .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 { return """ not exists ( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/HomeRecommendationQueryPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/HomeRecommendationQueryPort.kt index 165c36a7..47f1687c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/HomeRecommendationQueryPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/HomeRecommendationQueryPort.kt @@ -151,10 +151,13 @@ data class HomePopularCommunityRecommendationRecord( val creatorId: Long, val creatorNickname: String, val creatorProfileImage: String?, + val imagePath: String?, + val audioPath: String?, val content: String, val createdAt: LocalDateTime, val likeCount: Long, - val commentCount: Long + val commentCount: Long, + val existOrdered: Boolean ) data class HomeGenreCreatorRecommendationGroup( diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt index 8659a60c..c6ffda85 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt @@ -1234,7 +1234,12 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( val creator = saveMember("community-detail-creator", MemberRole.CREATOR) val inactiveCreator = saveMember("community-detail-inactive-creator", MemberRole.CREATOR, isActive = false) 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 fixed = saveCommunity(creator, isCommentAvailable = true, isFixed = 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", like2.id!!, LocalDateTime.of(2026, 5, 29, 3, 0)) updateCreatedAt("CreatorCommunityComment", comment1.id!!, LocalDateTime.of(2026, 5, 29, 4, 0)) + saveCommunityOrder(member, paid, isRefund = false) flushAndClear() val details = repository.findPopularCommunityRecommendationDetails( listOf(eligible.id!!, paid.id!!, fixed.id!!, adult.id!!, inactivePost.id!!, inactiveCreatorPost.id!!, 999L), + memberId = member.id, includeAdultCommunities = false ) 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("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(2L, detailById[eligible.id]!!.likeCount) 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("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 @DisplayName("인기 커뮤니티 상세는 성인 노출 가능 회원에게 성인 게시글을 포함한다") fun shouldFindAdultPopularCommunityDetailsWhenAdultVisible() { @@ -1656,13 +1684,17 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( price: Int = 0, isAdult: Boolean = false, isActive: Boolean = true, - isFixed: Boolean = false + isFixed: Boolean = false, + imagePath: String? = null, + audioPath: String? = null ): CreatorCommunity { val community = CreatorCommunity( content = "content", price = price, isCommentAvailable = isCommentAvailable, isAdult = isAdult, + audioPath = audioPath, + imagePath = imagePath, isActive = isActive, isFixed = isFixed ) @@ -1671,6 +1703,14 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( 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( creator: Member, releaseDate: LocalDateTime, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryServiceTest.kt index 43181b47..12b2958f 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryServiceTest.kt @@ -227,30 +227,39 @@ class HomeRecommendationQueryServiceTest { creatorId = 10L, creatorNickname = "creator-10", creatorProfileImage = "profile-10.png", + imagePath = "community-1.png", + audioPath = "community-1.mp3", content = "content-1", createdAt = LocalDateTime.of(2026, 5, 29, 1, 0), likeCount = 3L, - commentCount = 2L + commentCount = 2L, + existOrdered = true ), HomePopularCommunityRecommendationRecord( communityId = 2L, creatorId = 10L, creatorNickname = "creator-10", creatorProfileImage = "profile-10.png", + imagePath = null, + audioPath = null, content = "content-2", createdAt = LocalDateTime.of(2026, 5, 29, 2, 0), likeCount = 1L, - commentCount = 1L + commentCount = 1L, + existOrdered = false ), HomePopularCommunityRecommendationRecord( communityId = 3L, creatorId = 11L, creatorNickname = "creator-11", creatorProfileImage = null, + imagePath = null, + audioPath = null, content = "content-3", createdAt = LocalDateTime.of(2026, 5, 29, 3, 0), likeCount = 0L, - commentCount = 0L + commentCount = 0L, + existOrdered = false ) ) @@ -262,6 +271,9 @@ class HomeRecommendationQueryServiceTest { assertEquals(listOf(1L, 3L), communities.map { it.communityId }) assertEquals(listOf(10L, 11L), communities.map { it.creatorId }) 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 @@ -281,10 +293,13 @@ class HomeRecommendationQueryServiceTest { creatorId = if (communityId <= 10L) 1L else communityId, creatorNickname = "creator-$communityId", creatorProfileImage = null, + imagePath = null, + audioPath = null, content = "content-$communityId", createdAt = LocalDateTime.of(2026, 5, 29, 1, 0).plusMinutes(communityId), likeCount = 0L, - commentCount = 0L + commentCount = 0L, + existOrdered = false ) }