feat(home-following): 팔로잉 탭 facade를 통합한다
This commit is contained in:
@@ -2,22 +2,23 @@ package kr.co.vividnext.sodalive.v2.api.home.following.application
|
||||
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponse
|
||||
import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService
|
||||
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingQueryService
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class HomeFollowingFacade {
|
||||
class HomeFollowingFacade(
|
||||
private val homeFollowingQueryService: HomeFollowingQueryService,
|
||||
private val chatRoomListService: ChatRoomListService
|
||||
) {
|
||||
fun getFollowingTab(member: Member?): HomeFollowingTabResponse {
|
||||
if (member == null) {
|
||||
return HomeFollowingTabResponse.loginRequired()
|
||||
}
|
||||
|
||||
return HomeFollowingTabResponse(
|
||||
isLoginRequired = false,
|
||||
followingCreators = emptyList(),
|
||||
onAirLives = emptyList(),
|
||||
recentChats = emptyList(),
|
||||
monthlySchedules = emptyList(),
|
||||
recentNews = emptyList()
|
||||
)
|
||||
val home = homeFollowingQueryService.findHomeFollowing(member)
|
||||
val recentChats = chatRoomListService.getRooms(member, filter = "ALL", cursor = null, limit = 10).rooms
|
||||
|
||||
return HomeFollowingTabResponse.from(home.copy(recentChats = recentChats))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
package kr.co.vividnext.sodalive.v2.api.home.following.adapter.`in`.web
|
||||
|
||||
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.live.room.LiveRoom
|
||||
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.member.following.CreatorFollowing
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
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.usercreatorchat.UserCreatorChatMessage
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessageType
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatParticipant
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatRoom
|
||||
import org.hamcrest.Matchers.nullValue
|
||||
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.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
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.support.TransactionTemplate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@SpringBootTest(
|
||||
properties = [
|
||||
"cloud.aws.cloud-front.host=https://cdn.test",
|
||||
"spring.cache.type=none",
|
||||
"spring.datasource.url=jdbc:h2:mem:home-following-e2e;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
|
||||
]
|
||||
)
|
||||
@AutoConfigureMockMvc
|
||||
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
||||
class HomeFollowingEndToEndTest @Autowired constructor(
|
||||
private val mockMvc: MockMvc,
|
||||
private val entityManager: EntityManager,
|
||||
private val transactionTemplate: TransactionTemplate
|
||||
) {
|
||||
@MockBean
|
||||
private lateinit var countryContext: CountryContext
|
||||
|
||||
@Test
|
||||
@DisplayName("팔로잉 탭 API는 비회원에게 200 OK와 로그인 필요 빈 섹션 응답을 반환한다")
|
||||
fun shouldReturnLoginRequiredForAnonymous() {
|
||||
mockMvc.perform(get("/api/v2/home/following"))
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.isLoginRequired").value(true))
|
||||
.andExpect(jsonPath("$.data.followingCreators").isEmpty)
|
||||
.andExpect(jsonPath("$.data.onAirLives").isEmpty)
|
||||
.andExpect(jsonPath("$.data.recentChats").isEmpty)
|
||||
.andExpect(jsonPath("$.data.monthlySchedules").isEmpty)
|
||||
.andExpect(jsonPath("$.data.recentNews").isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("팔로잉 탭 API는 인증 회원의 팔로잉/On Air/최근 대화/스케줄/최근 소식을 조립해 반환한다")
|
||||
fun shouldAssembleFollowingTabForMember() {
|
||||
Mockito.doReturn("US").`when`(countryContext).countryCode
|
||||
val fixture = createFixture()
|
||||
|
||||
mockMvc.perform(get("/api/v2/home/following").with(user(MemberAdapter(fixture.viewer))))
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.isLoginRequired").value(false))
|
||||
.andExpect(jsonPath("$.data.followingCreators[0].creatorId").value(fixture.creatorId))
|
||||
.andExpect(jsonPath("$.data.followingCreators[0].creatorNickname").value("home-following-creator"))
|
||||
.andExpect(jsonPath("$.data.onAirLives[0].liveId").value(fixture.liveId))
|
||||
.andExpect(jsonPath("$.data.onAirLives[0].title").value("home-following-live"))
|
||||
.andExpect(jsonPath("$.data.recentChats[0].roomId").value(fixture.chatRoomId))
|
||||
.andExpect(jsonPath("$.data.recentChats[0].chatType").value("DM"))
|
||||
.andExpect(jsonPath("$.data.recentChats[0].targetName").value("home-following-creator"))
|
||||
.andExpect(jsonPath("$.data.recentChats[0].lastMessage").value("recent dm"))
|
||||
.andExpect(jsonPath("$.data.monthlySchedules[0].scheduleId").value("LIVE:${fixture.liveId}"))
|
||||
.andExpect(jsonPath("$.data.monthlySchedules[1].scheduleId").value("AUDIO:${fixture.audioId}"))
|
||||
.andExpect(jsonPath("$.data.recentNews[0].newsId").value(fixture.rankedNewsId.toString()))
|
||||
.andExpect(jsonPath("$.data.recentNews[0].creatorId").doesNotExist())
|
||||
.andExpect(jsonPath("$.data.recentNews[0].ranking").doesNotExist())
|
||||
.andExpect(jsonPath("$.data.recentNews[0].rank").value(7))
|
||||
.andExpect(jsonPath("$.data.recentNews[1].rank").value(nullValue()))
|
||||
}
|
||||
|
||||
private fun createFixture(): Fixture {
|
||||
return transactionTemplate.execute {
|
||||
val now = LocalDateTime.now(ZoneOffset.UTC)
|
||||
val viewer = saveMember("home-following-viewer", MemberRole.USER)
|
||||
val creator = saveMember("home-following-creator", MemberRole.CREATOR, profileImage = "creator.png")
|
||||
saveFollowing(viewer, creator)
|
||||
val live = saveLiveRoom(creator, now.plusHours(1), channelName = "on-air")
|
||||
val theme = saveTheme()
|
||||
val audio = saveAudioContent(creator, theme, now.plusDays(1))
|
||||
val oldNews = saveNews(viewer.id!!, creator.id!!, "old-news", now.minusHours(2), rank = null)
|
||||
val rankedNews = saveNews(viewer.id!!, creator.id!!, "ranked-news", now.minusHours(1), rank = 7)
|
||||
val chatRoom = saveDmChatRoom(viewer, creator, now.minusMinutes(10))
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
Fixture(
|
||||
viewer = viewer,
|
||||
creatorId = creator.id!!,
|
||||
liveId = live.id!!,
|
||||
audioId = audio.id!!,
|
||||
chatRoomId = chatRoom.id!!,
|
||||
rankedNewsId = rankedNews.id!!,
|
||||
oldNewsId = oldNews.id!!
|
||||
)
|
||||
}!!
|
||||
}
|
||||
|
||||
private fun saveMember(seed: String, role: MemberRole, profileImage: String? = null): Member {
|
||||
val member = Member(
|
||||
email = "$seed@test.com",
|
||||
password = "password",
|
||||
nickname = seed,
|
||||
profileImage = profileImage,
|
||||
role = role,
|
||||
countryCode = "US"
|
||||
)
|
||||
entityManager.persist(member)
|
||||
return member
|
||||
}
|
||||
|
||||
private fun saveFollowing(member: Member, creator: Member): CreatorFollowing {
|
||||
val following = CreatorFollowing(isActive = true).apply {
|
||||
this.member = member
|
||||
this.creator = creator
|
||||
}
|
||||
entityManager.persist(following)
|
||||
return following
|
||||
}
|
||||
|
||||
private fun saveLiveRoom(creator: Member, beginDateTime: LocalDateTime, channelName: String?): LiveRoom {
|
||||
val liveRoom = LiveRoom(
|
||||
title = "home-following-live",
|
||||
notice = "notice",
|
||||
beginDateTime = beginDateTime,
|
||||
numberOfPeople = 0,
|
||||
isAdult = false
|
||||
).apply {
|
||||
member = creator
|
||||
this.channelName = channelName
|
||||
}
|
||||
entityManager.persist(liveRoom)
|
||||
return liveRoom
|
||||
}
|
||||
|
||||
private fun saveTheme(): AudioContentTheme {
|
||||
val theme = AudioContentTheme(theme = "home-following-theme", image = "theme.png", isActive = true)
|
||||
entityManager.persist(theme)
|
||||
return theme
|
||||
}
|
||||
|
||||
private fun saveAudioContent(creator: Member, theme: AudioContentTheme, releaseDate: LocalDateTime): AudioContent {
|
||||
val audio = AudioContent(
|
||||
title = "home-following-audio",
|
||||
detail = "detail",
|
||||
languageCode = "ko",
|
||||
releaseDate = releaseDate
|
||||
).apply {
|
||||
member = creator
|
||||
this.theme = theme
|
||||
duration = "00:10:00"
|
||||
isActive = true
|
||||
}
|
||||
entityManager.persist(audio)
|
||||
return audio
|
||||
}
|
||||
|
||||
private fun saveNews(
|
||||
memberId: Long,
|
||||
creatorId: Long,
|
||||
sourceKey: String,
|
||||
visibleFromAtUtc: LocalDateTime,
|
||||
rank: Int?
|
||||
): HomeFollowingNewsInbox {
|
||||
val news = HomeFollowingNewsInbox(
|
||||
memberId = memberId,
|
||||
creatorId = creatorId,
|
||||
newsType = FollowingNewsType.CREATOR_RANKING,
|
||||
sourceKey = sourceKey,
|
||||
targetId = creatorId,
|
||||
occurredAtUtc = visibleFromAtUtc.minusMinutes(30),
|
||||
visibleFromAtUtc = visibleFromAtUtc,
|
||||
creatorNickname = "home-following-creator",
|
||||
creatorProfileImagePath = "creator.png",
|
||||
title = "news-$sourceKey",
|
||||
body = "news body",
|
||||
thumbnailImagePath = null,
|
||||
rank = rank,
|
||||
isAdult = false
|
||||
)
|
||||
entityManager.persist(news)
|
||||
return news
|
||||
}
|
||||
|
||||
private fun saveDmChatRoom(viewer: Member, creator: Member, messageCreatedAt: LocalDateTime): UserCreatorChatRoom {
|
||||
val room = UserCreatorChatRoom()
|
||||
entityManager.persist(room)
|
||||
val viewerParticipant = UserCreatorChatParticipant(room, viewer)
|
||||
val creatorParticipant = UserCreatorChatParticipant(room, creator)
|
||||
entityManager.persist(viewerParticipant)
|
||||
entityManager.persist(creatorParticipant)
|
||||
val message = UserCreatorChatMessage(
|
||||
chatRoom = room,
|
||||
participant = creatorParticipant,
|
||||
messageType = UserCreatorChatMessageType.TEXT,
|
||||
textMessage = "recent dm"
|
||||
)
|
||||
entityManager.persist(message)
|
||||
entityManager.flush()
|
||||
message.createdAt = messageCreatedAt
|
||||
message.updatedAt = messageCreatedAt
|
||||
return room
|
||||
}
|
||||
|
||||
private data class Fixture(
|
||||
val viewer: Member,
|
||||
val creatorId: Long,
|
||||
val liveId: Long,
|
||||
val audioId: Long,
|
||||
val chatRoomId: Long,
|
||||
val rankedNewsId: Long,
|
||||
val oldNewsId: Long
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package kr.co.vividnext.sodalive.v2.api.home.following.application
|
||||
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListItemResponse
|
||||
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListPageResponse
|
||||
import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService
|
||||
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
|
||||
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingQueryService
|
||||
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.domain.HomeFollowingCreator
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingLive
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingNews
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingSchedule
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
|
||||
class HomeFollowingFacadeTest {
|
||||
private val queryService = Mockito.mock(HomeFollowingQueryService::class.java)
|
||||
private val chatRoomListService = Mockito.mock(ChatRoomListService::class.java)
|
||||
private val facade = HomeFollowingFacade(queryService, chatRoomListService)
|
||||
|
||||
@Test
|
||||
@DisplayName("비로그인 회원은 로그인 필요 응답을 반환하고 조회/채팅 서비스를 호출하지 않는다")
|
||||
fun shouldReturnLoginRequiredWithoutCallingServicesForAnonymous() {
|
||||
val response = facade.getFollowingTab(null)
|
||||
|
||||
assertTrue(response.isLoginRequired)
|
||||
assertTrue(response.followingCreators.isEmpty())
|
||||
assertTrue(response.onAirLives.isEmpty())
|
||||
assertTrue(response.recentChats.isEmpty())
|
||||
assertTrue(response.monthlySchedules.isEmpty())
|
||||
assertTrue(response.recentNews.isEmpty())
|
||||
Mockito.verifyNoInteractions(queryService, chatRoomListService)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("로그인 회원은 팔로잉 홈 조회 결과에 최근 대화 10개를 조립해 반환한다")
|
||||
fun shouldAssembleFollowingHomeWithRecentChatsForMember() {
|
||||
val member = Member(
|
||||
email = "viewer@test.com",
|
||||
password = "password",
|
||||
nickname = "viewer",
|
||||
role = MemberRole.USER
|
||||
).apply { id = 10L }
|
||||
val home = homeFollowing()
|
||||
val recentChat = ChatRoomListItemResponse(
|
||||
roomId = 30L,
|
||||
chatType = "DM",
|
||||
targetName = "creator",
|
||||
targetImageUrl = "https://cdn.test/creator.png",
|
||||
lastMessage = "hello",
|
||||
lastMessageAt = "2026-06-25T01:00:00Z"
|
||||
)
|
||||
Mockito.doReturn(home).`when`(queryService).findHomeFollowing(member)
|
||||
Mockito.doReturn(ChatRoomListPageResponse(rooms = listOf(recentChat), hasMore = false, nextCursor = null))
|
||||
.`when`(chatRoomListService).getRooms(member, filter = "ALL", cursor = null, limit = 10)
|
||||
|
||||
val response = facade.getFollowingTab(member)
|
||||
|
||||
assertFalse(response.isLoginRequired)
|
||||
assertEquals(1L, response.followingCreators.single().creatorId)
|
||||
assertEquals(2L, response.onAirLives.single().liveId)
|
||||
assertEquals(listOf(recentChat), response.recentChats)
|
||||
assertEquals("LIVE:4", response.monthlySchedules.single().scheduleId)
|
||||
assertEquals("news-5", response.recentNews.single().newsId)
|
||||
Mockito.verify(queryService).findHomeFollowing(member)
|
||||
Mockito.verify(chatRoomListService).getRooms(member, filter = "ALL", cursor = null, limit = 10)
|
||||
}
|
||||
|
||||
private fun homeFollowing(): HomeFollowing {
|
||||
return HomeFollowing(
|
||||
followingCreators = listOf(
|
||||
HomeFollowingCreator(
|
||||
creatorId = 1L,
|
||||
creatorNickname = "creator",
|
||||
creatorProfileImageUrl = "https://cdn.test/creator.png"
|
||||
)
|
||||
),
|
||||
onAirLives = listOf(
|
||||
HomeFollowingLive(
|
||||
liveId = 2L,
|
||||
creatorProfileImageUrl = "https://cdn.test/live.png",
|
||||
creatorNickname = "creator",
|
||||
title = "live",
|
||||
startedAtUtc = "2026-06-25T00:00:00Z"
|
||||
)
|
||||
),
|
||||
recentChats = emptyList(),
|
||||
monthlySchedules = listOf(
|
||||
HomeFollowingSchedule(
|
||||
scheduleId = "LIVE:4",
|
||||
creatorId = 1L,
|
||||
creatorProfileImageUrl = "https://cdn.test/creator.png",
|
||||
creatorNickname = "creator",
|
||||
title = "schedule",
|
||||
type = CreatorActivityType.LIVE,
|
||||
targetId = 4L,
|
||||
scheduledAtUtc = "2026-06-25T02:00:00Z",
|
||||
isOnAir = false
|
||||
)
|
||||
),
|
||||
recentNews = listOf(
|
||||
HomeFollowingNews(
|
||||
newsId = "news-5",
|
||||
type = FollowingNewsType.CREATOR_RANKING,
|
||||
creatorProfileImageUrl = "https://cdn.test/news.png",
|
||||
creatorNickname = "creator",
|
||||
title = "news",
|
||||
body = "body",
|
||||
thumbnailImageUrl = null,
|
||||
targetId = 1L,
|
||||
occurredAtUtc = "2026-06-25T03:00:00Z",
|
||||
visibleFromAtUtc = "2026-06-25T04:00:00Z",
|
||||
rank = 7
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user