feat(creator): 커뮤니티 탭 activity 동작을 연결한다

This commit is contained in:
2026-06-22 01:44:29 +09:00
parent e29ae4fedb
commit a36c3b74e8
4 changed files with 211 additions and 11 deletions

View File

@@ -7,10 +7,10 @@ import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.view.Gravity
import android.view.animation.Interpolator
import android.view.LayoutInflater
import android.view.View
import android.view.View.MeasureSpec
import android.view.animation.Interpolator
import android.widget.LinearLayout
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
@@ -24,11 +24,11 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayoutMediator
import com.google.gson.Gson
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.base.SodaDialog
@@ -37,10 +37,16 @@ import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyActivity
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.ModifyCommunityPostRequest
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog
import kr.co.vividnext.sodalive.mypage.MyPageViewModel
import kr.co.vividnext.sodalive.mypage.auth.Auth
@@ -50,11 +56,13 @@ import kr.co.vividnext.sodalive.report.ProfileReportDialog
import kr.co.vividnext.sodalive.report.UserReportDialog
import kr.co.vividnext.sodalive.settings.ContentSettingsActivity
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment
import kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragment
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment
import kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragment
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelScrollState
@@ -65,6 +73,8 @@ import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivity
import kr.co.vividnext.sodalive.splash.SplashActivity
import kr.co.vividnext.sodalive.user.login.LoginActivity
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.koin.android.ext.android.inject
class CreatorChannelActivity :
@@ -72,10 +82,12 @@ class CreatorChannelActivity :
CreatorChannelHomeFragment.Host,
CreatorChannelLiveFragment.Host,
CreatorChannelAudioFragment.Host,
CreatorChannelSeriesFragment.Host {
CreatorChannelSeriesFragment.Host,
CreatorChannelCommunityFragment.Host {
private val liveViewModel: LiveViewModel by inject()
private val myPageViewModel: MyPageViewModel by inject()
private val creatorCommunityRepository: CreatorCommunityRepository by inject()
private var creatorId: Long = 0L
private var currentHeader: CreatorChannelHeaderUiModel? = null
private var homeActionDelegate: CreatorChannelHomeFragment.HomeActionDelegate? = null
@@ -103,6 +115,14 @@ class CreatorChannelActivity :
) { result ->
if (result.resultCode == RESULT_OK) {
homeActionDelegate?.refreshHome()
refreshCreatorChannelCommunity()
}
}
private val communityPostModifyLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
refreshCreatorChannelCommunity()
}
}
private val liveRoomCreateLauncher = registerForActivityResult(
@@ -407,6 +427,9 @@ class CreatorChannelActivity :
CreatorChannelTab.Series.ordinal -> binding.viewPager.post {
findSeriesFragment()?.onCreatorChannelSeriesTabSelected()
}
CreatorChannelTab.Community.ordinal -> binding.viewPager.post {
findCommunityFragment()?.onCreatorChannelCommunityTabSelected()
}
}
}
}
@@ -430,6 +453,11 @@ class CreatorChannelActivity :
findSeriesFragment()?.onCreatorChannelSeriesTabSelected()
}
}
if (binding.viewPager.currentItem == CreatorChannelTab.Community.ordinal) {
binding.viewPager.post {
findCommunityFragment()?.onCreatorChannelCommunityTabSelected()
}
}
}
override fun onCreatorChannelFollowProgressChanged(inProgress: Boolean) {
@@ -495,6 +523,102 @@ class CreatorChannelActivity :
postCheckCreatorChannelCurrentTabNeedsMore()
}
override fun onCreatorChannelCommunityContentChanged() {
updateViewPagerHeight()
postCheckCreatorChannelCurrentTabNeedsMore()
}
override fun onCreatorChannelCommunityOwnerMoreClicked(item: CreatorChannelCommunityPostUiModel) {
CreatorCommunityPostMenuBottomSheetDialog(
isFixed = item.isPinned,
isCreator = true,
onClickPin = {
updateCreatorChannelCommunityPostFixed(item)
},
onClickModify = {
communityPostModifyLauncher.launch(
Intent(this, CreatorCommunityModifyActivity::class.java).apply {
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, item.postId)
}
)
},
onClickDelete = {
showCreatorChannelCommunityDeleteDialog(item)
},
onClickReport = {}
).show(supportFragmentManager, CreatorCommunityPostMenuBottomSheetDialog::class.java.simpleName)
}
private fun showCreatorChannelCommunityDeleteDialog(item: CreatorChannelCommunityPostUiModel) {
SodaDialog(
activity = this,
layoutInflater = layoutInflater,
title = getString(R.string.screen_creator_community_delete_title),
desc = getString(R.string.screen_creator_community_delete_desc),
confirmButtonTitle = getString(R.string.confirm_delete_title),
confirmButtonClick = {
deleteCreatorChannelCommunityPost(item)
},
cancelButtonTitle = getString(R.string.cancel),
cancelButtonClick = {}
).show(screenWidth)
}
private fun updateCreatorChannelCommunityPostFixed(item: CreatorChannelCommunityPostUiModel) {
compositeDisposable.add(
creatorCommunityRepository.updateCommunityPostFixed(
postId = item.postId,
isFixed = !item.isPinned,
token = authToken()
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
if (response.success) {
refreshCreatorChannelCommunity()
} else {
response.message?.let(::showToast)
}
},
{ error -> error.message?.let(::showToast) }
)
)
}
private fun deleteCreatorChannelCommunityPost(item: CreatorChannelCommunityPostUiModel) {
val request = ModifyCommunityPostRequest(
creatorCommunityId = item.postId,
isActive = false
)
val requestJson = Gson().toJson(request)
compositeDisposable.add(
creatorCommunityRepository.modifyCommunityPost(
postImage = null,
request = requestJson.toRequestBody("text/plain".toMediaType()),
token = authToken()
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
if (response.success) {
refreshCreatorChannelCommunity()
} else {
response.message?.let(::showToast)
}
},
{ error -> error.message?.let(::showToast) }
)
)
}
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
private fun refreshCreatorChannelCommunity() {
findCommunityFragment()?.onCreatorChannelCommunityRefreshRequested()
}
private fun setupOwnerFabInsets() {
binding.viewPager.updatePadding(bottom = OWNER_FAB_CONTENT_BOTTOM_PADDING_DP.dpToPx().toInt())
}
@@ -517,10 +641,17 @@ class CreatorChannelActivity :
iconResId = R.drawable.ic_new_upload_audio,
textResId = R.string.creator_channel_audio_upload_button
)
CreatorChannelTab.Community -> bindOwnerCta(
iconResId = R.drawable.ic_new_upload_community_post,
textResId = R.string.creator_channel_owner_fab_community
)
else -> Unit
}
findLiveFragment()?.onCreatorChannelLiveOwnerCtaVisibilityChanged(ownerCtaTab == CreatorChannelTab.Live)
findAudioFragment()?.onCreatorChannelAudioOwnerCtaVisibilityChanged(ownerCtaTab == CreatorChannelTab.Audio)
findCommunityFragment()?.onCreatorChannelCommunityOwnerCtaVisibilityChanged(
ownerCtaTab == CreatorChannelTab.Community
)
}
private fun bindOwnerCta(iconResId: Int, textResId: Int) {
@@ -533,6 +664,7 @@ class CreatorChannelActivity :
return when (binding.viewPager.currentItem) {
CreatorChannelTab.Live.ordinal -> CreatorChannelTab.Live
CreatorChannelTab.Audio.ordinal -> CreatorChannelTab.Audio
CreatorChannelTab.Community.ordinal -> CreatorChannelTab.Community
else -> null
}
}
@@ -630,6 +762,7 @@ class CreatorChannelActivity :
when (binding.viewPager.currentItem) {
CreatorChannelTab.Live.ordinal -> onOwnerFabLiveClicked()
CreatorChannelTab.Audio.ordinal -> onOwnerFabAudioClicked()
CreatorChannelTab.Community.ordinal -> onOwnerFabCommunityClicked()
}
}
@@ -675,18 +808,25 @@ class CreatorChannelActivity :
return supportFragmentManager.findFragmentByTag(fragmentTag) as? CreatorChannelSeriesFragment
}
private fun findCommunityFragment(): CreatorChannelCommunityFragment? {
val fragmentTag = "f${CreatorChannelTab.Community.ordinal}"
return supportFragmentManager.findFragmentByTag(fragmentTag) as? CreatorChannelCommunityFragment
}
private fun notifyCurrentCreatorChannelTabScrolledToBottom() {
when (binding.viewPager.currentItem) {
CreatorChannelTab.Live.ordinal -> findLiveFragment()?.onCreatorChannelLiveScrolledToBottom()
CreatorChannelTab.Audio.ordinal -> findAudioFragment()?.onCreatorChannelAudioScrolledToBottom()
CreatorChannelTab.Series.ordinal -> findSeriesFragment()?.onCreatorChannelSeriesScrolledToBottom()
CreatorChannelTab.Community.ordinal -> findCommunityFragment()?.onCreatorChannelCommunityScrolledToBottom()
}
}
private fun isCreatorChannelLoadMoreTab(position: Int): Boolean {
return position == CreatorChannelTab.Live.ordinal ||
position == CreatorChannelTab.Audio.ordinal ||
position == CreatorChannelTab.Series.ordinal
position == CreatorChannelTab.Series.ordinal ||
position == CreatorChannelTab.Community.ordinal
}
private fun ensureLoginAndAdultAuth(isAdult: Boolean, onAuthed: () -> Unit) {

View File

@@ -96,6 +96,10 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
viewModel.loadMore()
}
fun onCreatorChannelCommunityRefreshRequested() {
viewModel.refreshCommunity()
}
fun onCreatorChannelCommunityOwnerCtaVisibilityChanged(isVisible: Boolean) {
applyOwnerCtaPadding(isVisible)
}
@@ -196,10 +200,7 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
interface Host {
fun isCreatorChannelOwner(): Boolean
fun onCreatorChannelCommunityContentChanged()
fun onCreatorChannelCommunityOwnerMoreClicked(item: CreatorChannelCommunityPostUiModel) {
onCreatorChannelCommunityOwnerMoreClicked(item.postId)
}
fun onCreatorChannelCommunityOwnerMoreClicked(postId: Long)
fun onCreatorChannelCommunityOwnerMoreClicked(item: CreatorChannelCommunityPostUiModel)
}
companion object {

View File

@@ -52,6 +52,12 @@ class CreatorChannelCommunityViewModel(
loadFirstPage()
}
fun refreshCommunity() {
if (creatorId <= 0) return
loadFirstPage()
}
fun loadMore() {
val content = _communityStateLiveData.value as? CreatorChannelCommunityUiState.Content ?: return
if (!content.hasNext || content.isLoadingMore || creatorId <= 0) return

View File

@@ -416,7 +416,6 @@ class CreatorChannelActivitySourceTest {
assertFalse(source.contains("if (tab != CreatorChannelTab.Home) return"))
assertTrue(pagerAdapter.contains("CreatorChannelTab.Audio -> CreatorChannelAudioFragment.newInstance(creatorId)"))
assertTrue(pagerAdapter.contains("CreatorChannelTab.Series -> CreatorChannelSeriesFragment.newInstance(creatorId)"))
assertFalse(source.contains("CreatorChannelTab.Community ->"))
assertFalse(source.contains("CreatorChannelTab.FanTalk ->"))
assertFalse(source.contains("CreatorChannelTab.Donation ->"))
}
@@ -440,6 +439,60 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("putExtra(Constants.EXTRA_SERIES_ID, seriesId)"))
}
@Test
fun `Community tab source는 Fragment Host pagination owner CTA를 Activity에 연결한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt"
).readText()
val fragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt"
).readText()
assertTrue(adapter.contains("CreatorChannelCommunityFragment.newInstance(creatorId)"))
assertTrue(source.contains("CreatorChannelCommunityFragment.Host"))
assertTrue(
source.contains(
"import kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragment"
)
)
assertTrue(source.contains("private fun findCommunityFragment(): CreatorChannelCommunityFragment?"))
assertTrue(source.contains("findCommunityFragment()?.onCreatorChannelCommunityTabSelected()"))
assertTrue(source.contains("if (binding.viewPager.currentItem == CreatorChannelTab.Community.ordinal)"))
assertTrue(source.contains("findCommunityFragment()?.onCreatorChannelCommunityScrolledToBottom()"))
assertTrue(source.contains("position == CreatorChannelTab.Community.ordinal"))
assertTrue(source.contains("override fun onCreatorChannelCommunityContentChanged()"))
assertTrue(source.contains("findCommunityFragment()?.onCreatorChannelCommunityOwnerCtaVisibilityChanged("))
assertTrue(source.contains("ownerCtaTab == CreatorChannelTab.Community"))
assertTrue(source.contains("CreatorChannelTab.Community.ordinal -> CreatorChannelTab.Community"))
assertTrue(source.contains("iconResId = R.drawable.ic_new_upload_community_post"))
assertTrue(source.contains("textResId = R.string.creator_channel_owner_fab_community"))
assertTrue(source.contains("CreatorChannelTab.Community.ordinal -> onOwnerFabCommunityClicked()"))
assertTrue(source.contains("private val communityPostModifyLauncher"))
assertTrue(source.contains("CreatorCommunityModifyActivity::class.java"))
assertTrue(source.contains("putExtra(Constants.EXTRA_COMMUNITY_POST_ID, item.postId)"))
assertTrue(source.contains("creatorCommunityRepository.updateCommunityPostFixed("))
assertTrue(source.contains("isFixed = !item.isPinned"))
assertTrue(source.contains("creatorCommunityRepository.modifyCommunityPost("))
assertTrue(source.contains("isActive = false"))
assertTrue(source.contains("findCommunityFragment()?.onCreatorChannelCommunityRefreshRequested()"))
assertFalse(source.contains("onCreatorChannelCommunityOwnerMoreClicked(postId: Long)"))
assertFalse(fragment.contains("onCreatorChannelCommunityOwnerMoreClicked(item.postId)"))
assertFalse(source.contains("onClickPin = {},"))
assertFalse(source.contains("onClickModify = {},"))
assertFalse(source.contains("onClickDelete = {},"))
assertTrue(fragment.contains("fun onCreatorChannelCommunityRefreshRequested()"))
assertTrue(
fragment.contains(
"CreatorCommunityMediaPlayerManager(requireContext()) { listAdapter.notifyDataSetChanged() }"
)
)
assertTrue(fragment.contains("mediaPlayerManager?.toggleContent(CreatorCommunityContentItem(item.postId, audioUrl))"))
assertTrue(fragment.contains("mediaPlayerManager?.stopContent()"))
}
@Test
fun `section adapter source는 활동 지표를 행 단위 resource label로 표시한다`() {
val adapter = projectFile(