test #449
+22
-1
@@ -1,9 +1,12 @@
|
||||
package kr.co.vividnext.sodalive.v2.home.following.application
|
||||
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.member.Gender
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowing
|
||||
import kr.co.vividnext.sodalive.v2.home.following.port.out.HomeFollowingQueryPort
|
||||
import org.springframework.stereotype.Service
|
||||
@@ -16,6 +19,8 @@ import java.time.LocalDateTime
|
||||
class HomeFollowingQueryService(
|
||||
private val queryPort: HomeFollowingQueryPort,
|
||||
private val memberContentPreferenceService: MemberContentPreferenceService,
|
||||
private val langContext: LangContext,
|
||||
private val creatorCommunityTranslationService: CreatorCommunityTranslationService,
|
||||
private val nowProvider: () -> LocalDateTime = { LocalDateTime.now(Clock.systemUTC()) }
|
||||
) {
|
||||
fun findHomeFollowing(member: Member): HomeFollowing {
|
||||
@@ -24,6 +29,15 @@ class HomeFollowingQueryService(
|
||||
val effectiveViewerGender = member.effectiveGender()
|
||||
val isViewerCreator = member.role == MemberRole.CREATOR
|
||||
val now = nowProvider()
|
||||
val recentNews = queryPort.findRecentNews(memberId, canViewAdultContent, now, RECENT_NEWS_LIMIT)
|
||||
val communityPostIds = recentNews.mapNotNull { news ->
|
||||
if (news.type == FollowingNewsType.COMMUNITY_POST) news.communityPost?.postId else null
|
||||
}
|
||||
val displayContents = if (communityPostIds.isEmpty()) {
|
||||
emptyMap()
|
||||
} else {
|
||||
creatorCommunityTranslationService.findDisplayContents(communityPostIds, langContext.lang.code)
|
||||
}
|
||||
|
||||
return HomeFollowing(
|
||||
followingCreators = queryPort.findFollowingCreators(memberId, FOLLOWING_CREATORS_LIMIT),
|
||||
@@ -43,7 +57,14 @@ class HomeFollowingQueryService(
|
||||
now,
|
||||
MONTHLY_SCHEDULES_LIMIT
|
||||
),
|
||||
recentNews = queryPort.findRecentNews(memberId, canViewAdultContent, now, RECENT_NEWS_LIMIT)
|
||||
recentNews = recentNews.map { news ->
|
||||
if (news.type != FollowingNewsType.COMMUNITY_POST) return@map news
|
||||
|
||||
val communityPost = news.communityPost ?: return@map news
|
||||
displayContents[communityPost.postId]
|
||||
?.let { displayContent -> news.copy(communityPost = communityPost.copy(content = displayContent.content)) }
|
||||
?: news
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+55
@@ -18,6 +18,8 @@ import kr.co.vividnext.sodalive.member.MemberKind
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityDisplayContent
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
|
||||
import kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInbox
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
|
||||
import kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.CreatorRankingSnapshot
|
||||
@@ -63,6 +65,9 @@ class HomeFollowingEndToEndTest @Autowired constructor(
|
||||
@MockBean
|
||||
private lateinit var countryContext: CountryContext
|
||||
|
||||
@MockBean
|
||||
private lateinit var creatorCommunityTranslationService: CreatorCommunityTranslationService
|
||||
|
||||
@Test
|
||||
@DisplayName("팔로잉 탭 API는 비회원에게 200 OK와 로그인 필요 빈 섹션 응답을 반환한다")
|
||||
fun shouldReturnLoginRequiredForAnonymous() {
|
||||
@@ -137,6 +142,56 @@ class HomeFollowingEndToEndTest @Autowired constructor(
|
||||
.andExpect(jsonPath("$.data.recentNews[?(@.communityPost.postId == ${fixture.paidPostId})]").isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("인증 회원 팔로잉 탭은 노출된 커뮤니티 소식만 영어 표시 본문으로 반환하고 다른 소식은 유지한다")
|
||||
fun shouldReturnEnglishCommunityTranslationForAuthenticatedFollowingTab() {
|
||||
Mockito.doReturn("US").`when`(countryContext).countryCode
|
||||
val fixture = createFixture()
|
||||
val fallbackPostId = transactionTemplate.execute {
|
||||
val creator = entityManager.find(Member::class.java, fixture.creatorId)
|
||||
val fallbackPost = saveCommunityPost(
|
||||
creator,
|
||||
"historical following fallback body",
|
||||
price = 0,
|
||||
imagePath = "community/fallback.png"
|
||||
)
|
||||
saveNews(
|
||||
memberId = fixture.viewer.id!!,
|
||||
creatorId = fixture.creatorId,
|
||||
sourceKey = "fallback-community-news",
|
||||
visibleFromAtUtc = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(10),
|
||||
newsType = FollowingNewsType.COMMUNITY_POST,
|
||||
targetId = fallbackPost.id!!
|
||||
)
|
||||
entityManager.flush()
|
||||
fallbackPost.id!!
|
||||
}!!
|
||||
Mockito.`when`(creatorCommunityTranslationService.findDisplayContents(listOf(fallbackPostId, fixture.freePostId), "en"))
|
||||
.thenReturn(
|
||||
mapOf(
|
||||
fallbackPostId to CreatorCommunityDisplayContent("current following fallback body en", false),
|
||||
fixture.freePostId to CreatorCommunityDisplayContent("translated following body en", true)
|
||||
)
|
||||
)
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/home/following")
|
||||
.header("Accept-Language", "en")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.recentNews[0].type").value("COMMUNITY_POST"))
|
||||
.andExpect(jsonPath("$.data.recentNews[0].communityPost.content").value("current following fallback body en"))
|
||||
.andExpect(jsonPath("$.data.recentNews[1].type").value("CREATOR_RANKING"))
|
||||
.andExpect(jsonPath("$.data.recentNews[2].type").value("COMMUNITY_POST"))
|
||||
.andExpect(jsonPath("$.data.recentNews[2].communityPost.content").value("translated following body en"))
|
||||
.andExpect(jsonPath("$.data.recentNews[3].type").value("AUDIO_CONTENT"))
|
||||
.andExpect(jsonPath("$.data.recentNews[3].audioContent.title").value("home-following-audio"))
|
||||
|
||||
Mockito.verify(creatorCommunityTranslationService).findDisplayContents(listOf(fallbackPostId, fixture.freePostId), "en")
|
||||
Mockito.verifyNoMoreInteractions(creatorCommunityTranslationService)
|
||||
}
|
||||
|
||||
private fun createFixture(): Fixture {
|
||||
return transactionTemplate.execute {
|
||||
val now = LocalDateTime.now(ZoneOffset.UTC)
|
||||
|
||||
+66
-3
@@ -1,10 +1,16 @@
|
||||
package kr.co.vividnext.sodalive.v2.home.following.application
|
||||
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.member.Gender
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.auth.Auth
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityDisplayContent
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCommunityPostNews
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreator
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingLive
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingNews
|
||||
@@ -23,10 +29,14 @@ import java.util.TimeZone
|
||||
class HomeFollowingQueryServiceTest {
|
||||
private val queryPort = RecordingHomeFollowingQueryPort()
|
||||
private val memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
||||
private val langContext = LangContext()
|
||||
private val creatorCommunityTranslationService = Mockito.mock(CreatorCommunityTranslationService::class.java)
|
||||
private val now = LocalDateTime.of(2026, 6, 25, 12, 0)
|
||||
private val service = HomeFollowingQueryService(
|
||||
queryPort,
|
||||
memberContentPreferenceService
|
||||
memberContentPreferenceService,
|
||||
langContext,
|
||||
creatorCommunityTranslationService
|
||||
) { now }
|
||||
|
||||
@Test
|
||||
@@ -86,7 +96,12 @@ class HomeFollowingQueryServiceTest {
|
||||
try {
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"))
|
||||
val defaultQueryPort = RecordingHomeFollowingQueryPort()
|
||||
val defaultService = HomeFollowingQueryService(defaultQueryPort, memberContentPreferenceService)
|
||||
val defaultService = HomeFollowingQueryService(
|
||||
defaultQueryPort,
|
||||
memberContentPreferenceService,
|
||||
langContext,
|
||||
creatorCommunityTranslationService
|
||||
)
|
||||
val member = member(12L)
|
||||
Mockito.`when`(memberContentPreferenceService.canViewAdultContent(member)).thenReturn(true)
|
||||
val beforeUtc = LocalDateTime.now(ZoneOffset.UTC).minusSeconds(1)
|
||||
@@ -103,10 +118,57 @@ class HomeFollowingQueryServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("팔로잉 최근 소식은 커뮤니티 게시물만 영어 표시 본문으로 일괄 치환하고 다른 소식과 원문 폴백을 유지한다")
|
||||
fun shouldTranslateOnlyCommunityPostNewsInOneEnglishBatch() {
|
||||
langContext.setLang(Lang.EN)
|
||||
val translatedNews = communityNews("community-1", 101L, "original-1")
|
||||
val audioNews = HomeFollowingNews(
|
||||
newsId = "audio-1",
|
||||
type = FollowingNewsType.AUDIO_CONTENT,
|
||||
visibleFromAtUtc = "2026-06-25T11:59:00Z"
|
||||
)
|
||||
val fallbackNews = communityNews("community-2", 102L, "historical-original-2")
|
||||
queryPort.recentNews = listOf(translatedNews, audioNews, fallbackNews)
|
||||
Mockito.`when`(creatorCommunityTranslationService.findDisplayContents(listOf(101L, 102L), "en"))
|
||||
.thenReturn(
|
||||
mapOf(
|
||||
101L to CreatorCommunityDisplayContent("translated-1-en", true),
|
||||
102L to CreatorCommunityDisplayContent("current-original-2", false)
|
||||
)
|
||||
)
|
||||
|
||||
val home = service.findHomeFollowing(member(14L))
|
||||
|
||||
assertEquals("translated-1-en", home.recentNews[0].communityPost?.content)
|
||||
assertEquals(audioNews, home.recentNews[1])
|
||||
assertEquals("current-original-2", home.recentNews[2].communityPost?.content)
|
||||
Mockito.verify(creatorCommunityTranslationService).findDisplayContents(listOf(101L, 102L), "en")
|
||||
Mockito.verifyNoMoreInteractions(creatorCommunityTranslationService)
|
||||
}
|
||||
|
||||
private fun member(id: Long): Member {
|
||||
return Member(email = "member-$id@test.com", password = "password", nickname = "member-$id").apply { this.id = id }
|
||||
}
|
||||
|
||||
private fun communityNews(newsId: String, postId: Long, content: String): HomeFollowingNews {
|
||||
return HomeFollowingNews(
|
||||
newsId = newsId,
|
||||
type = FollowingNewsType.COMMUNITY_POST,
|
||||
visibleFromAtUtc = "2026-06-25T12:00:00Z",
|
||||
communityPost = HomeFollowingCommunityPostNews(
|
||||
postId = postId,
|
||||
creatorProfileImage = "profile.png",
|
||||
creatorNickname = "creator",
|
||||
imageUrl = "image.png",
|
||||
content = content,
|
||||
createdAt = "2026-06-25T12:00:00Z",
|
||||
likeCount = 1,
|
||||
commentCount = 2
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private class RecordingHomeFollowingQueryPort : HomeFollowingQueryPort {
|
||||
var memberId: Long? = null
|
||||
var followingCreatorsLimit: Int? = null
|
||||
@@ -122,6 +184,7 @@ class HomeFollowingQueryServiceTest {
|
||||
var recentNewsLimit: Int? = null
|
||||
var recentNewsNow: LocalDateTime? = null
|
||||
var recentNewsCanViewAdultContent: Boolean? = null
|
||||
var recentNews: List<HomeFollowingNews> = emptyList()
|
||||
|
||||
override fun findFollowingCreators(memberId: Long, limit: Int): List<HomeFollowingCreator> {
|
||||
this.memberId = memberId
|
||||
@@ -168,7 +231,7 @@ class HomeFollowingQueryServiceTest {
|
||||
recentNewsCanViewAdultContent = canViewAdultContent
|
||||
recentNewsNow = nowUtc
|
||||
recentNewsLimit = limit
|
||||
return emptyList()
|
||||
return recentNews
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user