feat(home): AI 캐릭터 크리에이터 이동을 연결한다

This commit is contained in:
2026-06-23 11:33:35 +09:00
parent 74efe45d05
commit 51b50eed75
6 changed files with 84 additions and 18 deletions

View File

@@ -7,7 +7,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.ToastMessage import kr.co.vividnext.sodalive.common.ToastMessage
@@ -18,6 +17,7 @@ import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCo
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
import kr.co.vividnext.sodalive.v2.main.home.model.HomeCreatorRankingUiState import kr.co.vividnext.sodalive.v2.main.home.model.HomeCreatorRankingUiState
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationAiCharacterSection import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationAiCharacterSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationAiCharacterUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerSection import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCheerCreatorSection import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCheerCreatorSection
@@ -35,6 +35,8 @@ import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyAct
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationUiState import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationUiState
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerIntent import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerIntent
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerRoute import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerRoute
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationAiCharacterIntent
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationAiCharacterRoute
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationRecentlyActiveCreatorIntent import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationRecentlyActiveCreatorIntent
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationRecentlyActiveCreatorRoute import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationRecentlyActiveCreatorRoute
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups
@@ -63,7 +65,7 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
private val recentActivityCreatorAdapter = HomeRecentActivityCreatorAdapter { onRecentActivityClick(it) } private val recentActivityCreatorAdapter = HomeRecentActivityCreatorAdapter { onRecentActivityClick(it) }
private val recentDebutCreatorAdapter = HomeRecentDebutCreatorAdapter { openCreatorProfile(it.creatorId) } private val recentDebutCreatorAdapter = HomeRecentDebutCreatorAdapter { openCreatorProfile(it.creatorId) }
private val firstAudioAdapter = HomeFirstAudioAdapter { openAudioContentDetail(it) } private val firstAudioAdapter = HomeFirstAudioAdapter { openAudioContentDetail(it) }
private val aiCharacterAdapter = HomeAiCharacterAdapter { openCharacterDetail(it.characterId) } private val aiCharacterAdapter = HomeAiCharacterAdapter { onAiCharacterClick(it) }
private val genreCreatorAdapter = HomeGenreCreatorAdapter( private val genreCreatorAdapter = HomeGenreCreatorAdapter(
onFollowAllClick = { creatorIds -> onGenreFollowAllClick(creatorIds) }, onFollowAllClick = { creatorIds -> onGenreFollowAllClick(creatorIds) },
onCreatorClick = { creator -> openCreatorProfile(creator.creatorId) } onCreatorClick = { creator -> openCreatorProfile(creator.creatorId) }
@@ -339,6 +341,11 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
startActivity(route.toHomeRecommendationRecentlyActiveCreatorIntent(requireContext())) startActivity(route.toHomeRecommendationRecentlyActiveCreatorIntent(requireContext()))
} }
private fun onAiCharacterClick(item: HomeRecommendationAiCharacterUiModel) {
val route = item.toHomeRecommendationAiCharacterRoute() ?: return
startActivity(route.toHomeRecommendationAiCharacterIntent(requireContext()))
}
private fun openCreatorRankingProfile(item: CreatorRankingItem) { private fun openCreatorRankingProfile(item: CreatorRankingItem) {
if (item.creatorId <= 0L) return if (item.creatorId <= 0L) return
openCreatorProfile(item.creatorId) openCreatorProfile(item.creatorId)
@@ -358,14 +365,6 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
) )
} }
private fun openCharacterDetail(characterId: Long) {
startActivity(
Intent(requireContext(), CharacterDetailActivity::class.java).apply {
putExtra(CharacterDetailActivity.EXTRA_CHARACTER_ID, characterId)
}
)
}
private fun openPopularCommunityPost(item: FeedItem.Community) { private fun openPopularCommunityPost(item: FeedItem.Community) {
val creatorId = item.creatorId.toLongOrNull() ?: return val creatorId = item.creatorId.toLongOrNull() ?: return
val postId = item.postId.toLongOrNull() ?: return val postId = item.postId.toLongOrNull() ?: return

View File

@@ -64,6 +64,7 @@ data class HomeFirstAudioContentItem(
@Keep @Keep
data class HomeAiCharacterItem( data class HomeAiCharacterItem(
@SerializedName("characterId") val characterId: Long, @SerializedName("characterId") val characterId: Long,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("name") val name: String, @SerializedName("name") val name: String,
@SerializedName("description") val description: String, @SerializedName("description") val description: String,
@SerializedName("profileImage") val profileImage: String?, @SerializedName("profileImage") val profileImage: String?,

View File

@@ -79,6 +79,7 @@ fun HomeFirstAudioContentItem.toUiModel(): HomeRecommendationFirstAudioContentUi
) )
fun HomeAiCharacterItem.toUiModel(): HomeRecommendationAiCharacterUiModel = HomeRecommendationAiCharacterUiModel( fun HomeAiCharacterItem.toUiModel(): HomeRecommendationAiCharacterUiModel = HomeRecommendationAiCharacterUiModel(
creatorId = creatorId,
item = CharacterChatThumbnailItem( item = CharacterChatThumbnailItem(
characterId = characterId, characterId = characterId,
imageUrl = profileImage.orEmpty(), imageUrl = profileImage.orEmpty(),

View File

@@ -191,9 +191,24 @@ data class HomeRecommendationFirstAudioContentUiModel(
) )
data class HomeRecommendationAiCharacterUiModel( data class HomeRecommendationAiCharacterUiModel(
val creatorId: Long,
val item: CharacterChatThumbnailItem val item: CharacterChatThumbnailItem
) )
sealed interface HomeRecommendationAiCharacterRoute {
data class Creator(val creatorId: Long) : HomeRecommendationAiCharacterRoute
}
fun HomeRecommendationAiCharacterUiModel.toHomeRecommendationAiCharacterRoute(): HomeRecommendationAiCharacterRoute? {
return creatorId.takeIf { it > 0L }?.let(HomeRecommendationAiCharacterRoute::Creator)
}
fun HomeRecommendationAiCharacterRoute.toHomeRecommendationAiCharacterIntent(context: Context): Intent {
return when (this) {
is HomeRecommendationAiCharacterRoute.Creator -> CreatorChannelActivity.newIntent(context, creatorId)
}
}
data class HomeRecommendationGenreCreatorGroupUiModel( data class HomeRecommendationGenreCreatorGroupUiModel(
val genre: String, val genre: String,
val creators: List<HomeRecommendationCreatorUiModel>, val creators: List<HomeRecommendationCreatorUiModel>,

View File

@@ -6,11 +6,10 @@ import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationAiCharacterUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationAiCharacterUiModel
import kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.CharacterChatThumbnailItem
import kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.CharacterChatThumbnailView import kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.CharacterChatThumbnailView
class HomeAiCharacterAdapter( class HomeAiCharacterAdapter(
private val onClickItem: (CharacterChatThumbnailItem) -> Unit = {} private val onClickItem: (HomeRecommendationAiCharacterUiModel) -> Unit = {}
) : RecyclerView.Adapter<HomeAiCharacterAdapter.CharacterViewHolder>() { ) : RecyclerView.Adapter<HomeAiCharacterAdapter.CharacterViewHolder>() {
private var items: List<HomeRecommendationAiCharacterUiModel> = emptyList() private var items: List<HomeRecommendationAiCharacterUiModel> = emptyList()
@@ -31,19 +30,19 @@ class HomeAiCharacterAdapter(
} }
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) { override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
holder.bind(items[position].item) holder.bind(items[position])
} }
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
class CharacterViewHolder( class CharacterViewHolder(
private val view: CharacterChatThumbnailView, private val view: CharacterChatThumbnailView,
private val onClickItem: (CharacterChatThumbnailItem) -> Unit private val onClickItem: (HomeRecommendationAiCharacterUiModel) -> Unit
) : RecyclerView.ViewHolder(view) { ) : RecyclerView.ViewHolder(view) {
fun bind(item: CharacterChatThumbnailItem) { fun bind(item: HomeRecommendationAiCharacterUiModel) {
view.bind(item) view.bind(item.item)
view.imageView().loadUrl(item.imageUrl) view.imageView().loadUrl(item.item.imageUrl)
view.setOnCharacterClick(onClickItem) view.setOnCharacterClick { onClickItem(item) }
} }
} }
} }

View File

@@ -32,6 +32,7 @@ import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.event.EventItem import kr.co.vividnext.sodalive.settings.event.EventItem
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
import kr.co.vividnext.sodalive.v2.main.home.data.HomeActiveCreatorItem import kr.co.vividnext.sodalive.v2.main.home.data.HomeActiveCreatorItem
import kr.co.vividnext.sodalive.v2.main.home.data.HomeAiCharacterItem
import kr.co.vividnext.sodalive.v2.main.home.data.HomeBannerItem import kr.co.vividnext.sodalive.v2.main.home.data.HomeBannerItem
import kr.co.vividnext.sodalive.v2.main.home.data.HomeCreatorItem import kr.co.vividnext.sodalive.v2.main.home.data.HomeCreatorItem
import kr.co.vividnext.sodalive.v2.main.home.data.HomeFirstAudioContentItem import kr.co.vividnext.sodalive.v2.main.home.data.HomeFirstAudioContentItem
@@ -40,6 +41,7 @@ import kr.co.vividnext.sodalive.v2.main.home.data.HomeLiveItem
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerSection import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerRoute import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerRoute
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationAiCharacterRoute
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationFirstAudioContentUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationFirstAudioContentUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCreatorUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCreatorUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationGenreCreatorGroupUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationGenreCreatorGroupUiModel
@@ -53,6 +55,8 @@ import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyAct
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerIntent import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerIntent
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerRoute import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerRoute
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationAiCharacterIntent
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationAiCharacterRoute
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationRecentlyActiveCreatorIntent import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationRecentlyActiveCreatorIntent
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationRecentlyActiveCreatorRoute import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationRecentlyActiveCreatorRoute
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomePopularCommunityPosts import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomePopularCommunityPosts
@@ -1087,6 +1091,41 @@ class HomeMainFragmentLayoutTest {
assertEquals(13L, communityIntent.getLongExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, 0L)) assertEquals(13L, communityIntent.getLongExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, 0L))
} }
@Test
fun `home ai character mapper preserves creator id for routing and character id for display`() {
val item = HomeAiCharacterItem(
characterId = 11L,
creatorId = 22L,
name = "캐릭터",
description = "설명",
profileImage = "https://example.com/character.png",
totalChatCount = 33L,
originalWorkTitle = "원작"
).toUiModel()
assertEquals(22L, item.creatorId)
assertEquals(11L, item.item.characterId)
}
@Test
fun `home ai character route uses creator id and ignores invalid ids`() {
assertEquals(
HomeRecommendationAiCharacterRoute.Creator(22L),
aiCharacter(creatorId = 22L).toHomeRecommendationAiCharacterRoute()
)
assertEquals(null, aiCharacter(creatorId = 0L).toHomeRecommendationAiCharacterRoute())
assertEquals(null, aiCharacter(creatorId = -1L).toHomeRecommendationAiCharacterRoute())
}
@Test
fun `home ai character route creates creator channel intent`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val intent = HomeRecommendationAiCharacterRoute.Creator(22L).toHomeRecommendationAiCharacterIntent(context)
assertEquals(CreatorChannelActivity::class.java.name, intent.component?.className)
assertEquals(22L, intent.getLongExtra(CreatorChannelActivity.EXTRA_CREATOR_ID, 0L))
}
@Test @Test
fun `home popular community adapter applies blur when locked paid post image is loaded`() { fun `home popular community adapter applies blur when locked paid post image is loaded`() {
val source = projectFile( val source = projectFile(
@@ -1517,6 +1556,18 @@ class HomeMainFragmentLayoutTest {
) )
} }
private fun aiCharacter(creatorId: Long): kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationAiCharacterUiModel {
return HomeAiCharacterItem(
characterId = 11L,
creatorId = creatorId,
name = "캐릭터",
description = "설명",
profileImage = null,
totalChatCount = 33L,
originalWorkTitle = null
).toUiModel()
}
private fun popularCommunityData( private fun popularCommunityData(
audioUrl: String?, audioUrl: String?,
createdAt: String = "2분 전" createdAt: String = "2분 전"