test #426
@@ -0,0 +1,146 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.home.following.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
|
||||||
|
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingNewsSourceKey
|
||||||
|
import kr.co.vividnext.sodalive.v2.home.following.port.out.HomeFollowingNewsInboxPort
|
||||||
|
import kr.co.vividnext.sodalive.v2.home.following.port.out.HomeFollowingNewsInboxRecord
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Propagation
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class HomeFollowingNewsPublishService(
|
||||||
|
private val inboxPort: HomeFollowingNewsInboxPort
|
||||||
|
) {
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
fun publishCommunityPostCreated(
|
||||||
|
postId: Long,
|
||||||
|
creatorId: Long,
|
||||||
|
creatorNickname: String,
|
||||||
|
creatorProfileImagePath: String?,
|
||||||
|
title: String,
|
||||||
|
body: String,
|
||||||
|
thumbnailImagePath: String?,
|
||||||
|
occurredAtUtc: LocalDateTime,
|
||||||
|
isAdult: Boolean
|
||||||
|
): Int {
|
||||||
|
return publishToFollowers(
|
||||||
|
creatorId = creatorId,
|
||||||
|
newsType = FollowingNewsType.COMMUNITY_POST,
|
||||||
|
sourceKey = HomeFollowingNewsSourceKey.communityPost(postId),
|
||||||
|
targetId = postId,
|
||||||
|
occurredAtUtc = occurredAtUtc,
|
||||||
|
visibleFromAtUtc = occurredAtUtc,
|
||||||
|
creatorNickname = creatorNickname,
|
||||||
|
creatorProfileImagePath = creatorProfileImagePath,
|
||||||
|
title = title,
|
||||||
|
body = body,
|
||||||
|
thumbnailImagePath = thumbnailImagePath,
|
||||||
|
rank = null,
|
||||||
|
isAdult = isAdult
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
fun publishContentUploaded(
|
||||||
|
contentId: Long,
|
||||||
|
creatorId: Long,
|
||||||
|
creatorNickname: String,
|
||||||
|
creatorProfileImagePath: String?,
|
||||||
|
title: String,
|
||||||
|
body: String,
|
||||||
|
thumbnailImagePath: String?,
|
||||||
|
occurredAtUtc: LocalDateTime,
|
||||||
|
visibleFromAtUtc: LocalDateTime,
|
||||||
|
isAdult: Boolean
|
||||||
|
): Int {
|
||||||
|
return publishToFollowers(
|
||||||
|
creatorId = creatorId,
|
||||||
|
newsType = FollowingNewsType.AUDIO_CONTENT,
|
||||||
|
sourceKey = HomeFollowingNewsSourceKey.audioContent(contentId),
|
||||||
|
targetId = contentId,
|
||||||
|
occurredAtUtc = occurredAtUtc,
|
||||||
|
visibleFromAtUtc = visibleFromAtUtc,
|
||||||
|
creatorNickname = creatorNickname,
|
||||||
|
creatorProfileImagePath = creatorProfileImagePath,
|
||||||
|
title = title,
|
||||||
|
body = body,
|
||||||
|
thumbnailImagePath = thumbnailImagePath,
|
||||||
|
rank = null,
|
||||||
|
isAdult = isAdult
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
fun publishCreatorRankingVisible(
|
||||||
|
creatorId: Long,
|
||||||
|
creatorNickname: String,
|
||||||
|
creatorProfileImagePath: String?,
|
||||||
|
aggregationStartAtUtc: LocalDateTime,
|
||||||
|
visibleFromAtUtc: LocalDateTime,
|
||||||
|
rank: Int
|
||||||
|
): Int {
|
||||||
|
return publishToFollowers(
|
||||||
|
creatorId = creatorId,
|
||||||
|
newsType = FollowingNewsType.CREATOR_RANKING,
|
||||||
|
sourceKey = HomeFollowingNewsSourceKey.creatorRanking(creatorId, aggregationStartAtUtc),
|
||||||
|
targetId = creatorId,
|
||||||
|
occurredAtUtc = visibleFromAtUtc,
|
||||||
|
visibleFromAtUtc = visibleFromAtUtc,
|
||||||
|
creatorNickname = creatorNickname,
|
||||||
|
creatorProfileImagePath = creatorProfileImagePath,
|
||||||
|
title = creatorNickname,
|
||||||
|
body = "$rank",
|
||||||
|
thumbnailImagePath = creatorProfileImagePath,
|
||||||
|
rank = rank,
|
||||||
|
isAdult = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun publishToFollowers(
|
||||||
|
creatorId: Long,
|
||||||
|
newsType: FollowingNewsType,
|
||||||
|
sourceKey: String,
|
||||||
|
targetId: Long,
|
||||||
|
occurredAtUtc: LocalDateTime,
|
||||||
|
visibleFromAtUtc: LocalDateTime,
|
||||||
|
creatorNickname: String,
|
||||||
|
creatorProfileImagePath: String?,
|
||||||
|
title: String,
|
||||||
|
body: String,
|
||||||
|
thumbnailImagePath: String?,
|
||||||
|
rank: Int?,
|
||||||
|
isAdult: Boolean
|
||||||
|
): Int {
|
||||||
|
val followerIds = inboxPort.findActiveFollowerIds(creatorId)
|
||||||
|
if (followerIds.isEmpty()) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val records = followerIds.map { memberId ->
|
||||||
|
HomeFollowingNewsInboxRecord(
|
||||||
|
memberId = memberId,
|
||||||
|
creatorId = creatorId,
|
||||||
|
newsType = newsType.name,
|
||||||
|
sourceKey = sourceKey,
|
||||||
|
targetId = targetId,
|
||||||
|
occurredAtUtc = occurredAtUtc,
|
||||||
|
visibleFromAtUtc = visibleFromAtUtc,
|
||||||
|
creatorNickname = creatorNickname,
|
||||||
|
creatorProfileImagePath = creatorProfileImagePath,
|
||||||
|
title = title.take(TITLE_MAX_LENGTH),
|
||||||
|
body = body.take(BODY_MAX_LENGTH),
|
||||||
|
thumbnailImagePath = thumbnailImagePath,
|
||||||
|
rank = rank,
|
||||||
|
isAdult = isAdult
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return inboxPort.insertIgnoreAll(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TITLE_MAX_LENGTH = 255
|
||||||
|
private const val BODY_MAX_LENGTH = 1_000
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.home.following.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
|
||||||
|
import kr.co.vividnext.sodalive.v2.home.following.port.out.HomeFollowingNewsInboxPort
|
||||||
|
import kr.co.vividnext.sodalive.v2.home.following.port.out.HomeFollowingNewsInboxRecord
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
class HomeFollowingNewsPublishServiceTest {
|
||||||
|
@Test
|
||||||
|
@DisplayName("커뮤니티 게시글 발행은 현재 활성 팔로워에게만 inbox record를 생성한다")
|
||||||
|
fun shouldPublishCommunityPostCreatedToActiveFollowers() {
|
||||||
|
val inboxPort = FakeHomeFollowingNewsInboxPort(activeFollowerIds = listOf(1L, 2L))
|
||||||
|
val service = HomeFollowingNewsPublishService(inboxPort)
|
||||||
|
val occurredAtUtc = LocalDateTime.of(2026, 6, 25, 1, 2, 3)
|
||||||
|
|
||||||
|
service.publishCommunityPostCreated(
|
||||||
|
postId = 100L,
|
||||||
|
creatorId = 9L,
|
||||||
|
creatorNickname = "creator",
|
||||||
|
creatorProfileImagePath = "profile.png",
|
||||||
|
title = "새 커뮤니티 글",
|
||||||
|
body = "본문",
|
||||||
|
thumbnailImagePath = "post.png",
|
||||||
|
occurredAtUtc = occurredAtUtc,
|
||||||
|
isAdult = true
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(9L, inboxPort.findActiveFollowerIdsCreatorId)
|
||||||
|
assertEquals(listOf(1L, 2L), inboxPort.records.map { it.memberId })
|
||||||
|
val record = inboxPort.records.first()
|
||||||
|
assertEquals(FollowingNewsType.COMMUNITY_POST.name, record.newsType)
|
||||||
|
assertEquals("COMMUNITY_POST:100", record.sourceKey)
|
||||||
|
assertEquals(100L, record.targetId)
|
||||||
|
assertEquals(occurredAtUtc, record.visibleFromAtUtc)
|
||||||
|
assertEquals("post.png", record.thumbnailImagePath)
|
||||||
|
assertEquals(true, record.isAdult)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("오디오 콘텐츠 발행은 공개 시각을 visibleFromAtUtc로 저장한다")
|
||||||
|
fun shouldPublishContentUploadedWithVisibleFromAtUtc() {
|
||||||
|
val inboxPort = FakeHomeFollowingNewsInboxPort(activeFollowerIds = listOf(3L))
|
||||||
|
val service = HomeFollowingNewsPublishService(inboxPort)
|
||||||
|
val occurredAtUtc = LocalDateTime.of(2026, 6, 25, 2, 0)
|
||||||
|
val visibleFromAtUtc = LocalDateTime.of(2026, 6, 25, 9, 0)
|
||||||
|
|
||||||
|
service.publishContentUploaded(
|
||||||
|
contentId = 200L,
|
||||||
|
creatorId = 8L,
|
||||||
|
creatorNickname = "audio-creator",
|
||||||
|
creatorProfileImagePath = null,
|
||||||
|
title = "오디오 제목",
|
||||||
|
body = "오디오 설명",
|
||||||
|
thumbnailImagePath = "cover.jpg",
|
||||||
|
occurredAtUtc = occurredAtUtc,
|
||||||
|
visibleFromAtUtc = visibleFromAtUtc,
|
||||||
|
isAdult = false
|
||||||
|
)
|
||||||
|
|
||||||
|
val record = inboxPort.records.single()
|
||||||
|
assertEquals(FollowingNewsType.AUDIO_CONTENT.name, record.newsType)
|
||||||
|
assertEquals("AUDIO_CONTENT:200", record.sourceKey)
|
||||||
|
assertEquals(occurredAtUtc, record.occurredAtUtc)
|
||||||
|
assertEquals(visibleFromAtUtc, record.visibleFromAtUtc)
|
||||||
|
assertEquals("cover.jpg", record.thumbnailImagePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("발행 record의 title과 body는 inbox 컬럼 길이에 맞게 잘린다")
|
||||||
|
fun shouldTruncateTitleAndBodyToInboxColumnLimits() {
|
||||||
|
val inboxPort = FakeHomeFollowingNewsInboxPort(activeFollowerIds = listOf(5L))
|
||||||
|
val service = HomeFollowingNewsPublishService(inboxPort)
|
||||||
|
|
||||||
|
service.publishContentUploaded(
|
||||||
|
contentId = 201L,
|
||||||
|
creatorId = 8L,
|
||||||
|
creatorNickname = "audio-creator",
|
||||||
|
creatorProfileImagePath = null,
|
||||||
|
title = "가".repeat(300),
|
||||||
|
body = "나".repeat(1_200),
|
||||||
|
thumbnailImagePath = null,
|
||||||
|
occurredAtUtc = LocalDateTime.of(2026, 6, 25, 2, 0),
|
||||||
|
visibleFromAtUtc = LocalDateTime.of(2026, 6, 25, 9, 0),
|
||||||
|
isAdult = false
|
||||||
|
)
|
||||||
|
|
||||||
|
val record = inboxPort.records.single()
|
||||||
|
assertEquals(255, record.title.length)
|
||||||
|
assertEquals(1_000, record.body.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("크리에이터 랭킹 발행은 rank와 스냅샷 노출 시각을 저장한다")
|
||||||
|
fun shouldPublishCreatorRankingVisibleWithRankAndVisibleFromAtUtc() {
|
||||||
|
val inboxPort = FakeHomeFollowingNewsInboxPort(activeFollowerIds = listOf(4L))
|
||||||
|
val service = HomeFollowingNewsPublishService(inboxPort)
|
||||||
|
val aggregationStartAtUtc = LocalDateTime.of(2026, 5, 31, 15, 0)
|
||||||
|
val visibleFromAtUtc = LocalDateTime.of(2026, 6, 8, 0, 0)
|
||||||
|
|
||||||
|
service.publishCreatorRankingVisible(
|
||||||
|
creatorId = 7L,
|
||||||
|
creatorNickname = "ranker",
|
||||||
|
creatorProfileImagePath = "ranker.png",
|
||||||
|
aggregationStartAtUtc = aggregationStartAtUtc,
|
||||||
|
visibleFromAtUtc = visibleFromAtUtc,
|
||||||
|
rank = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
val record = inboxPort.records.single()
|
||||||
|
assertEquals(FollowingNewsType.CREATOR_RANKING.name, record.newsType)
|
||||||
|
assertEquals("CREATOR_RANKING:7:2026-05-31T15:00", record.sourceKey)
|
||||||
|
assertEquals(7L, record.targetId)
|
||||||
|
assertEquals(visibleFromAtUtc, record.occurredAtUtc)
|
||||||
|
assertEquals(visibleFromAtUtc, record.visibleFromAtUtc)
|
||||||
|
assertEquals(2, record.rank)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeHomeFollowingNewsInboxPort(
|
||||||
|
private val activeFollowerIds: List<Long>
|
||||||
|
) : HomeFollowingNewsInboxPort {
|
||||||
|
val records = mutableListOf<HomeFollowingNewsInboxRecord>()
|
||||||
|
var findActiveFollowerIdsCreatorId: Long? = null
|
||||||
|
|
||||||
|
override fun insertIgnoreAll(records: List<HomeFollowingNewsInboxRecord>): Int {
|
||||||
|
this.records.addAll(records)
|
||||||
|
return records.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deactivateByMemberIdAndCreatorId(memberId: Long, creatorId: Long): Long = 0
|
||||||
|
|
||||||
|
override fun findActiveFollowerIds(creatorId: Long): List<Long> {
|
||||||
|
findActiveFollowerIdsCreatorId = creatorId
|
||||||
|
return activeFollowerIds
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user