feat(creator): 커뮤니티 구매와 메뉴 동작을 정리한다

This commit is contained in:
2026-08-11 00:10:46 +09:00
parent 02cd583f56
commit 8b8b384b29
31 changed files with 1055 additions and 410 deletions

View File

@@ -6,12 +6,4 @@ sealed interface CommunityChange {
data object Created : CommunityChange data object Created : CommunityChange
data object Updated : CommunityChange data object Updated : CommunityChange
data class Deleted(
val postId: Long
) : CommunityChange
data class PinChanged(
val postId: Long
) : CommunityChange
} }

View File

@@ -33,6 +33,7 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelBinding 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.CreatorCommunityRepository
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityReportDialog
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyActivity 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.modify.ModifyCommunityPostRequest
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
@@ -60,8 +61,9 @@ import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType 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.audio.CreatorChannelAudioFragment
import kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragment 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.community.model.CreatorChannelCommunityMenuItem
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse 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.CreatorChannelScheduleResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
@@ -96,6 +98,7 @@ class CreatorChannelActivity :
private val liveViewModel: LiveViewModel by inject() private val liveViewModel: LiveViewModel by inject()
private val creatorCommunityRepository: CreatorCommunityRepository by inject() private val creatorCommunityRepository: CreatorCommunityRepository by inject()
private val creatorChannelRepository: CreatorChannelRepository by inject()
private var creatorId: Long = 0L private var creatorId: Long = 0L
private var currentHeader: CreatorChannelHeaderUiModel? = null private var currentHeader: CreatorChannelHeaderUiModel? = null
private var homeActionDelegate: CreatorChannelHomeFragment.HomeActionDelegate? = null private var homeActionDelegate: CreatorChannelHomeFragment.HomeActionDelegate? = null
@@ -107,6 +110,7 @@ class CreatorChannelActivity :
private var isOwnerFabAnimating: Boolean = false private var isOwnerFabAnimating: Boolean = false
private var isDonationFloatingButtonVisible: Boolean = false private var isDonationFloatingButtonVisible: Boolean = false
private var isFanTalkFixedPlusVisible: Boolean = false private var isFanTalkFixedPlusVisible: Boolean = false
private var isCommunityPostMutating: Boolean = false
private lateinit var loadingDialog: LoadingDialog private lateinit var loadingDialog: LoadingDialog
private val baseTitleBarHeight: Int by lazy { 60.dpToPx().toInt() } private val baseTitleBarHeight: Int by lazy { 60.dpToPx().toInt() }
private val liveActionCoordinator: LiveActionCoordinator by lazy { private val liveActionCoordinator: LiveActionCoordinator by lazy {
@@ -126,13 +130,6 @@ class CreatorChannelActivity :
resolveCommunityActivityResult(CommunityActivityResultSource.Write, result.resultCode) resolveCommunityActivityResult(CommunityActivityResultSource.Write, result.resultCode)
) )
} }
private val communityPostModifyLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
handleCommunityChange(
resolveCommunityActivityResult(CommunityActivityResultSource.Modify, result.resultCode)
)
}
private val communityDetailLauncher = registerForActivityResult( private val communityDetailLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartActivityForResult()
) { result -> ) { result ->
@@ -140,6 +137,13 @@ class CreatorChannelActivity :
resolveCommunityActivityResult(CommunityActivityResultSource.Detail, result.resultCode) resolveCommunityActivityResult(CommunityActivityResultSource.Detail, result.resultCode)
) )
} }
private val communityModifyLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
handleCommunityChange(
resolveCommunityActivityResult(CommunityActivityResultSource.Modify, result.resultCode)
)
}
private val fanTalkWriteLauncher = registerForActivityResult( private val fanTalkWriteLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartActivityForResult()
) { result -> ) { result ->
@@ -627,6 +631,142 @@ class CreatorChannelActivity :
handleCommunityAction(CommunityActionCommand.PostDetail(postId), communityDetailLauncher::launch) handleCommunityAction(CommunityActionCommand.PostDetail(postId), communityDetailLauncher::launch)
} }
override fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem) {
if (!item.showMore) return
CreatorCommunityPostMenuBottomSheetDialog(
isFixed = item.isPinned,
isCreator = item.isOwner,
onClickPin = { updateCreatorChannelCommunityPostFixed(item) },
onClickModify = {
if (item.isOwner) {
communityModifyLauncher.launch(
Intent(this, CreatorCommunityModifyActivity::class.java).apply {
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, item.postId)
}
)
}
},
onClickDelete = { showDeleteCreatorChannelCommunityPostDialog(item) },
onClickReport = {
CreatorCommunityReportDialog(
this,
layoutInflater
) { reason -> reportCreatorChannelCommunityPost(item, reason) }.show(screenWidth)
}
).show(supportFragmentManager, CreatorCommunityPostMenuBottomSheetDialog::class.java.simpleName)
}
private fun updateCreatorChannelCommunityPostFixed(item: CreatorChannelCommunityMenuItem) {
if (!item.isOwner || isCommunityPostMutating) return
isCommunityPostMutating = true
compositeDisposable.add(
creatorCommunityRepository.updateCommunityPostFixed(item.postId, !item.isPinned, authToken())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
isCommunityPostMutating = false
if (response.success) {
handleCommunityChange(CommunityChange.Updated)
} else {
showCommunityMutationFailureToast(response.message)
}
},
{
isCommunityPostMutating = false
showCommunityMutationFailureToast()
}
)
)
}
private fun showDeleteCreatorChannelCommunityPostDialog(item: CreatorChannelCommunityMenuItem) {
if (!item.isOwner) return
V2ModalDialog(
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)
).show(screenWidth)
}
private fun deleteCreatorChannelCommunityPost(item: CreatorChannelCommunityMenuItem) {
if (!item.isOwner || isCommunityPostMutating) return
val request = Gson().toJson(
ModifyCommunityPostRequest(
creatorCommunityId = item.postId,
isActive = false
)
).toRequestBody("text/plain".toMediaType())
isCommunityPostMutating = true
compositeDisposable.add(
creatorCommunityRepository.modifyCommunityPost(postImage = null, request = request, token = authToken())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
isCommunityPostMutating = false
if (response.success) {
handleCommunityChange(CommunityChange.Updated)
} else {
showCommunityMutationFailureToast(response.message)
}
},
{
isCommunityPostMutating = false
showCommunityMutationFailureToast()
}
)
)
}
private fun reportCreatorChannelCommunityPost(
item: CreatorChannelCommunityMenuItem,
reason: String
) {
if (item.isOwner || !item.showMore || reason.isBlank() || isCommunityPostMutating) return
isCommunityPostMutating = true
compositeDisposable.add(
creatorChannelRepository.reportCommunityPost(item.postId, reason, authToken())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
isCommunityPostMutating = false
if (response.success) {
showToast(
if (response.message.isNullOrBlank()) {
getString(R.string.character_comment_report_submitted)
} else {
response.message
}
)
} else {
showCommunityMutationFailureToast(response.message)
}
},
{
isCommunityPostMutating = false
showCommunityMutationFailureToast()
}
)
)
}
private fun showCommunityMutationFailureToast(message: String? = null) {
showToast(message.takeUnless { it.isNullOrBlank() } ?: getString(R.string.common_error_unknown))
}
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
override fun onCreatorChannelFanTalkContentChanged() { override fun onCreatorChannelFanTalkContentChanged() {
updateViewPagerHeight { updateViewPagerHeight {
postCheckCreatorChannelCurrentTabNeedsMore() postCheckCreatorChannelCurrentTabNeedsMore()
@@ -697,93 +837,6 @@ class CreatorChannelActivity :
).show(screenWidth) ).show(screenWidth)
} }
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) {
V2ModalDialog(
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) {
handleCommunityChange(CommunityChange.PinChanged(item.postId))
} 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) {
handleCommunityChange(CommunityChange.Deleted(item.postId))
} else {
response.message?.let(::showToast)
}
},
{ error -> error.message?.let(::showToast) }
)
)
}
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
private fun refreshCreatorChannelCommunity() { private fun refreshCreatorChannelCommunity() {
findCommunityFragment()?.onCreatorChannelCommunityRefreshRequested() findCommunityFragment()?.onCreatorChannelCommunityRefreshRequested()
} }
@@ -795,9 +848,10 @@ class CreatorChannelActivity :
homeActionDelegate?.refreshHome() homeActionDelegate?.refreshHome()
refreshCreatorChannelCommunity() refreshCreatorChannelCommunity()
} }
CommunityChange.Updated, CommunityChange.Updated -> {
is CommunityChange.Deleted, homeActionDelegate?.refreshHome()
is CommunityChange.PinChanged -> refreshCreatorChannelCommunity() refreshCreatorChannelCommunity()
}
} }
} }

View File

@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioConte
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse 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.CreatorChannelScheduleResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeUiState import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeUiState
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
@@ -28,7 +29,8 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
onSeriesClick = ::onSeriesClicked, onSeriesClick = ::onSeriesClicked,
onDonationClick = ::onDonationClicked, onDonationClick = ::onDonationClicked,
onSectionChevronClick = ::onSectionChevronClicked, onSectionChevronClick = ::onSectionChevronClicked,
onCommunityClick = ::onCommunityClicked onCommunityClick = ::onCommunityClicked,
onCommunityMoreClick = ::onCommunityMoreClicked
) )
private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L } private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L }
private val host: Host private val host: Host
@@ -134,6 +136,10 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
host.onCreatorChannelCommunityPostClicked(postId) host.onCreatorChannelCommunityPostClicked(postId)
} }
private fun onCommunityMoreClicked(item: CreatorChannelCommunityMenuItem) {
host.onCreatorChannelCommunityMoreClicked(item)
}
private fun onCurrentLiveClicked(live: CreatorChannelLiveResponse) { private fun onCurrentLiveClicked(live: CreatorChannelLiveResponse) {
host.onCreatorChannelCurrentLiveClicked(live) host.onCreatorChannelCurrentLiveClicked(live)
} }
@@ -150,6 +156,7 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
fun onCreatorChannelDonationClicked() fun onCreatorChannelDonationClicked()
fun onCreatorChannelHomeTabRequested(tab: CreatorChannelTab) fun onCreatorChannelHomeTabRequested(tab: CreatorChannelTab)
fun onCreatorChannelCommunityPostClicked(postId: Long) fun onCreatorChannelCommunityPostClicked(postId: Long)
fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)
fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse) fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)
} }

View File

@@ -11,11 +11,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.databinding.FragmentCreatorChannelCommunityBinding import kr.co.vividnext.sodalive.databinding.FragmentCreatorChannelCommunityBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityContentItem
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityMediaPlayerManager
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityViewMode import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityViewMode
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.CreatorChannelCommunityGridAdapter import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.CreatorChannelCommunityGridAdapter
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.calculateCreatorChannelCommunityGridItemSize import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.calculateCreatorChannelCommunityGridItemSize
@@ -29,14 +27,11 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
private val viewModel: CreatorChannelCommunityViewModel by viewModel() private val viewModel: CreatorChannelCommunityViewModel by viewModel()
private val listAdapter = CreatorChannelCommunityListAdapter( private val listAdapter = CreatorChannelCommunityListAdapter(
onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) }, onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) },
onPlayClick = { item -> toggleCommunityAudio(item) }, onMoreClick = { item -> host.onCreatorChannelCommunityMoreClicked(item) }
onOwnerMoreClick = { item -> host.onCreatorChannelCommunityOwnerMoreClicked(item) },
isPlayingContent = { postId -> mediaPlayerManager?.isPlayingContent(postId) == true }
) )
private val gridAdapter = CreatorChannelCommunityGridAdapter( private val gridAdapter = CreatorChannelCommunityGridAdapter(
onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) } onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) }
) )
private var mediaPlayerManager: CreatorCommunityMediaPlayerManager? = null
private var currentContentState: CreatorChannelCommunityUiState.Content? = null private var currentContentState: CreatorChannelCommunityUiState.Content? = null
private var lastContentLayoutKey: CreatorChannelCommunityContentLayoutKey? = null private var lastContentLayoutKey: CreatorChannelCommunityContentLayoutKey? = null
private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L } private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L }
@@ -45,7 +40,6 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
mediaPlayerManager = CreatorCommunityMediaPlayerManager(requireContext()) { listAdapter.notifyDataSetChanged() }
bindLoading() bindLoading()
setupCommunityList() setupCommunityList()
setupClickListeners() setupClickListeners()
@@ -53,19 +47,12 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
} }
override fun onDestroyView() { override fun onDestroyView() {
mediaPlayerManager?.stopContent()
mediaPlayerManager = null
currentContentState = null currentContentState = null
lastContentLayoutKey = null lastContentLayoutKey = null
binding.rvCreatorChannelCommunity.adapter = null binding.rvCreatorChannelCommunity.adapter = null
super.onDestroyView() super.onDestroyView()
} }
override fun onPause() {
mediaPlayerManager?.pauseContent()
super.onPause()
}
private fun setupCommunityList() = with(binding.rvCreatorChannelCommunity) { private fun setupCommunityList() = with(binding.rvCreatorChannelCommunity) {
layoutManager = LinearLayoutManager(requireContext()) layoutManager = LinearLayoutManager(requireContext())
adapter = listAdapter adapter = listAdapter
@@ -220,16 +207,11 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
layoutCreatorChannelCommunityEmpty.updatePadding(bottom = bottomPadding) layoutCreatorChannelCommunityEmpty.updatePadding(bottom = bottomPadding)
} }
private fun toggleCommunityAudio(item: CreatorChannelCommunityPostUiModel) {
val audioUrl = item.audioUrl ?: return
mediaPlayerManager?.toggleContent(CreatorCommunityContentItem(item.postId, audioUrl))
}
interface Host { interface Host {
fun isCreatorChannelOwner(): Boolean fun isCreatorChannelOwner(): Boolean
fun onCreatorChannelCommunityContentChanged() fun onCreatorChannelCommunityContentChanged()
fun onCreatorChannelCommunityPostClicked(postId: Long) fun onCreatorChannelCommunityPostClicked(postId: Long)
fun onCreatorChannelCommunityOwnerMoreClicked(item: CreatorChannelCommunityPostUiModel) fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)
} }
companion object { companion object {

View File

@@ -23,10 +23,14 @@ import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityReportDialog
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.PurchaseCommunityPostDialog import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.PurchaseCommunityPostDialog
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityContentItem import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityContentItem
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityMediaPlayerManager import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityMediaPlayerManager
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyActivity
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
@@ -40,6 +44,12 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
) { ) {
private val viewModel: CreatorChannelCommunityDetailViewModel by viewModel() private val viewModel: CreatorChannelCommunityDetailViewModel by viewModel()
private val modifyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK && postId > 0L) {
setResult(Activity.RESULT_OK)
viewModel.loadDetail(postId)
}
}
private val replyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val replyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK && postId > 0L) { if (result.resultCode == Activity.RESULT_OK && postId > 0L) {
setResult(Activity.RESULT_OK) setResult(Activity.RESULT_OK)
@@ -65,6 +75,11 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
} }
binding.btnCreatorChannelCommunityDetailBack.setOnClickListener { finish() } binding.btnCreatorChannelCommunityDetailBack.setOnClickListener { finish() }
binding.btnCreatorChannelCommunityDetailMore.setOnClickListener {
val content = viewModel.detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
?: return@setOnClickListener
showPostMenu(content.post)
}
binding.layoutCreatorChannelCommunityDetailImageContainer.clipToOutline = true binding.layoutCreatorChannelCommunityDetailImageContainer.clipToOutline = true
binding.layoutCreatorChannelCommunityDetailImageContainer.outlineProvider = roundedOutlineProvider() binding.layoutCreatorChannelCommunityDetailImageContainer.outlineProvider = roundedOutlineProvider()
binding.rvCreatorChannelCommunityDetailComments.layoutManager = LinearLayoutManager(this) binding.rvCreatorChannelCommunityDetailComments.layoutManager = LinearLayoutManager(this)
@@ -103,6 +118,13 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
viewModel.consumePostChangedEvent() viewModel.consumePostChangedEvent()
} }
} }
viewModel.postDeletedEventLiveData.observe(this) { deleted ->
if (deleted == true) {
setResult(Activity.RESULT_OK)
viewModel.consumePostDeletedEvent()
finish()
}
}
viewModel.toastLiveData.observe(this) { event -> viewModel.toastLiveData.observe(this) { event ->
event.consume()?.let { toast -> event.consume()?.let { toast ->
val message = toast.message ?: toast.resId?.let(::getString) val message = toast.message ?: toast.resId?.let(::getString)
@@ -150,6 +172,7 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
} }
tvCreatorChannelCommunityDetailNickname.text = post.creatorNickname tvCreatorChannelCommunityDetailNickname.text = post.creatorNickname
tvCreatorChannelCommunityDetailTime.text = post.createdAtText tvCreatorChannelCommunityDetailTime.text = post.createdAtText
btnCreatorChannelCommunityDetailMore.isVisible = post.showMore
tvCreatorChannelCommunityDetailBody.text = post.content tvCreatorChannelCommunityDetailBody.text = post.content
layoutCreatorChannelCommunityDetailReaction.isVisible = post.showReaction || content.isCommentAvailable layoutCreatorChannelCommunityDetailReaction.isVisible = post.showReaction || content.isCommentAvailable
tvCreatorChannelCommunityDetailLikeCount.text = post.likeCount.moneyFormat() tvCreatorChannelCommunityDetailLikeCount.text = post.likeCount.moneyFormat()
@@ -163,9 +186,6 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
ivCreatorChannelCommunityDetailHeart.setImageResource( ivCreatorChannelCommunityDetailHeart.setImageResource(
if (post.isLiked) R.drawable.ic_feed_community_heart_fill else R.drawable.ic_feed_community_heart if (post.isLiked) R.drawable.ic_feed_community_heart_fill else R.drawable.ic_feed_community_heart
) )
layoutCreatorChannelCommunityDetailPrice.isVisible = post.isLocked
tvCreatorChannelCommunityDetailPrice.text = post.price.moneyFormat()
tvCreatorChannelCommunityDetailPurchased.isVisible = post.existOrdered && post.price > 0
layoutCreatorChannelCommunityDetailImageContainer.isVisible = post.imageUrl != null || post.showPaywall layoutCreatorChannelCommunityDetailImageContainer.isVisible = post.imageUrl != null || post.showPaywall
layoutCreatorChannelCommunityDetailImageContainer.layoutParams = layoutCreatorChannelCommunityDetailImageContainer.layoutParams =
layoutCreatorChannelCommunityDetailImageContainer.layoutParams.apply { layoutCreatorChannelCommunityDetailImageContainer.layoutParams.apply {
@@ -296,6 +316,37 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
).show(screenWidth) ).show(screenWidth)
} }
private fun showPostMenu(post: CreatorChannelCommunityPostDetailUiModel) {
CreatorCommunityPostMenuBottomSheetDialog(
isFixed = post.isPinned,
isCreator = post.isOwner,
onClickPin = viewModel::updateCommunityPostFixed,
onClickModify = {
modifyLauncher.launch(
Intent(this, CreatorCommunityModifyActivity::class.java).apply {
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, post.postId)
}
)
},
onClickDelete = ::showDeletePostDialog,
onClickReport = {
CreatorCommunityReportDialog(this, layoutInflater, viewModel::reportCommunityPost).show(screenWidth)
}
).show(supportFragmentManager, CreatorCommunityPostMenuBottomSheetDialog::class.java.simpleName)
}
private fun showDeletePostDialog() {
V2ModalDialog(
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 = viewModel::deleteCommunityPost,
cancelButtonTitle = getString(R.string.cancel)
).show(screenWidth)
}
private fun showDeleteCommentDialog(commentId: Long) { private fun showDeleteCommentDialog(commentId: Long) {
V2ModalDialog( V2ModalDialog(
activity = this, activity = this,

View File

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.community.detail
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.google.gson.Gson
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
@@ -12,12 +13,15 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.ModifyCommunityPostRequest
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentsResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentsResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityPostDetailResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityPostDetailResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityReplyResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityReplyResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
class CreatorChannelCommunityDetailViewModel( class CreatorChannelCommunityDetailViewModel(
private val repository: CreatorChannelRepository, private val repository: CreatorChannelRepository,
@@ -37,6 +41,10 @@ class CreatorChannelCommunityDetailViewModel(
val postChangedEventLiveData: LiveData<Boolean> val postChangedEventLiveData: LiveData<Boolean>
get() = _postChangedEventLiveData get() = _postChangedEventLiveData
private val _postDeletedEventLiveData = MutableLiveData<Boolean>()
val postDeletedEventLiveData: LiveData<Boolean>
get() = _postDeletedEventLiveData
private val _toastLiveData = MutableLiveData<CreatorChannelEvent<ToastMessage>>() private val _toastLiveData = MutableLiveData<CreatorChannelEvent<ToastMessage>>()
val toastLiveData: LiveData<CreatorChannelEvent<ToastMessage>> val toastLiveData: LiveData<CreatorChannelEvent<ToastMessage>>
get() = _toastLiveData get() = _toastLiveData
@@ -49,6 +57,7 @@ class CreatorChannelCommunityDetailViewModel(
private var isSendingComment = false private var isSendingComment = false
private var isModifyingComment = false private var isModifyingComment = false
private var isPurchasingPost = false private var isPurchasingPost = false
private var isMutatingPost = false
fun loadDetail(postId: Long) { fun loadDetail(postId: Long) {
if (postId <= 0) return if (postId <= 0) return
@@ -181,6 +190,10 @@ class CreatorChannelCommunityDetailViewModel(
_postChangedEventLiveData.value = false _postChangedEventLiveData.value = false
} }
fun consumePostDeletedEvent() {
_postDeletedEventLiveData.value = false
}
fun startCommentEdit(commentId: Long, comment: String) { fun startCommentEdit(commentId: Long, comment: String) {
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
if (commentId <= 0) return if (commentId <= 0) return
@@ -245,6 +258,97 @@ class CreatorChannelCommunityDetailViewModel(
) )
} }
fun updateCommunityPostFixed() {
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
if (!content.post.isOwner || isMutatingPost) return
isMutatingPost = true
compositeDisposable.add(
legacyRepository.updateCommunityPostFixed(postId, !content.post.isPinned, authToken())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
isMutatingPost = false
if (response.success) {
_postChangedEventLiveData.value = true
loadDetail(postId)
} else {
showMutationFailureToast(response.message)
}
},
{
isMutatingPost = false
showMutationFailureToast()
}
)
)
}
fun deleteCommunityPost() {
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
if (!content.post.isOwner || isMutatingPost) return
val request = Gson().toJson(
ModifyCommunityPostRequest(
creatorCommunityId = postId,
isActive = false
)
).toRequestBody("text/plain".toMediaType())
isMutatingPost = true
compositeDisposable.add(
legacyRepository.modifyCommunityPost(postImage = null, request = request, token = authToken())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
isMutatingPost = false
if (response.success) {
_postDeletedEventLiveData.value = true
} else {
showMutationFailureToast(response.message)
}
},
{
isMutatingPost = false
showMutationFailureToast()
}
)
)
}
fun reportCommunityPost(reason: String) {
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
if (reason.isBlank() || content.post.isOwner || !content.post.showMore || isMutatingPost) return
isMutatingPost = true
compositeDisposable.add(
repository.reportCommunityPost(postId, reason, authToken())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
isMutatingPost = false
if (response.success) {
_toastLiveData.value = CreatorChannelEvent(
if (response.message.isNullOrBlank()) {
ToastMessage(resId = R.string.character_comment_report_submitted)
} else {
ToastMessage(message = response.message)
}
)
} else {
showMutationFailureToast(response.message)
}
},
{
isMutatingPost = false
showMutationFailureToast()
}
)
)
}
private fun submitCommentEdit( private fun submitCommentEdit(
content: CreatorChannelCommunityDetailUiState.Content, content: CreatorChannelCommunityDetailUiState.Content,
commentId: Long, commentId: Long,
@@ -425,25 +529,30 @@ class CreatorChannelCommunityDetailViewModel(
) )
} }
private fun CreatorChannelCommunityPostDetailResponse.toUiModel() = CreatorChannelCommunityPostDetailUiModel( private fun CreatorChannelCommunityPostDetailResponse.toUiModel(): CreatorChannelCommunityPostDetailUiModel {
postId = postId, val isOwner = creatorId == SharedPreferenceManager.userId
creatorId = creatorId, return CreatorChannelCommunityPostDetailUiModel(
creatorNickname = creatorNickname, postId = postId,
creatorProfileUrl = creatorProfileUrl, creatorId = creatorId,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc), creatorNickname = creatorNickname,
content = content, creatorProfileUrl = creatorProfileUrl,
imageUrl = imageUrl.takeUnless { isLocked() }, createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
audioUrl = audioUrl.takeUnless { isLocked() }, content = content,
price = price, imageUrl = imageUrl.takeUnless { isLocked() },
existOrdered = existOrdered, audioUrl = audioUrl.takeUnless { isLocked() },
likeCount = likeCount, price = price,
commentCount = commentCount, existOrdered = existOrdered,
isLiked = isLiked, likeCount = likeCount,
isPinned = isPinned, commentCount = commentCount,
isLocked = isLocked(), isLiked = isLiked,
showPaywall = isLocked(), isPinned = isPinned,
showReaction = !isLocked() isOwner = isOwner,
) showMore = isOwner || price == 0 || existOrdered,
isLocked = isLocked(),
showPaywall = isLocked(),
showReaction = !isLocked()
)
}
private fun CreatorChannelCommunityPostDetailResponse.isLocked(): Boolean { private fun CreatorChannelCommunityPostDetailResponse.isLocked(): Boolean {
return price > 0 && !existOrdered && creatorId != SharedPreferenceManager.userId return price > 0 && !existOrdered && creatorId != SharedPreferenceManager.userId
@@ -526,6 +635,8 @@ data class CreatorChannelCommunityPostDetailUiModel(
val commentCount: Int, val commentCount: Int,
val isLiked: Boolean, val isLiked: Boolean,
val isPinned: Boolean, val isPinned: Boolean,
val isOwner: Boolean,
val showMore: Boolean,
val isLocked: Boolean, val isLocked: Boolean,
val showPaywall: Boolean, val showPaywall: Boolean,
val showReaction: Boolean val showReaction: Boolean

View File

@@ -21,7 +21,6 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel(
val isLocked = price > 0 && !existOrdered && !isOwner val isLocked = price > 0 && !existOrdered && !isOwner
val showOwnerActions = isOwner && creatorId == currentUserId val showOwnerActions = isOwner && creatorId == currentUserId
val visibleImageUrl = imageUrl.takeUnless { isLocked } val visibleImageUrl = imageUrl.takeUnless { isLocked }
val showPlayButton = !isLocked && !audioUrl.isNullOrBlank() && !visibleImageUrl.isNullOrBlank()
return CreatorChannelCommunityPostUiModel( return CreatorChannelCommunityPostUiModel(
postId = postId, postId = postId,
creatorId = creatorId, creatorId = creatorId,
@@ -30,7 +29,6 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel(
createdAtText = relativeTimeTextFormatter.format(createdAtUtc), createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
content = content, content = content,
imageUrl = visibleImageUrl, imageUrl = visibleImageUrl,
audioUrl = audioUrl,
price = price, price = price,
existOrdered = existOrdered, existOrdered = existOrdered,
likeCount = likeCount, likeCount = likeCount,
@@ -40,11 +38,17 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel(
showNotice = isPinned, showNotice = isPinned,
isPinned = isPinned, isPinned = isPinned,
isLocked = isLocked, isLocked = isLocked,
showOwnerMore = showOwnerActions,
showOwnerTopPrice = showOwnerActions && price > 0, showOwnerTopPrice = showOwnerActions && price > 0,
showPlayButton = showPlayButton,
gridPreviewText = content.toGridPreviewText(), gridPreviewText = content.toGridPreviewText(),
imageMode = toImageMode(isLocked, visibleImageUrl) imageMode = toImageMode(isLocked, visibleImageUrl),
menuItem = CreatorChannelCommunityMenuItem(
postId = postId,
creatorId = creatorId,
currentUserId = currentUserId,
price = price,
existOrdered = existOrdered,
isPinned = isPinned
)
) )
} }

View File

@@ -0,0 +1,16 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community.model
data class CreatorChannelCommunityMenuItem(
val postId: Long,
val creatorId: Long,
val currentUserId: Long,
val price: Int,
val existOrdered: Boolean,
val isPinned: Boolean
) {
val isOwner: Boolean
get() = creatorId == currentUserId
val showMore: Boolean
get() = isOwner || price == 0 || existOrdered
}

View File

@@ -32,7 +32,6 @@ data class CreatorChannelCommunityPostUiModel(
val createdAtText: String, val createdAtText: String,
val content: String, val content: String,
val imageUrl: String?, val imageUrl: String?,
val audioUrl: String?,
val price: Int, val price: Int,
val existOrdered: Boolean, val existOrdered: Boolean,
val likeCount: Int, val likeCount: Int,
@@ -42,9 +41,8 @@ data class CreatorChannelCommunityPostUiModel(
val showNotice: Boolean, val showNotice: Boolean,
val isPinned: Boolean, val isPinned: Boolean,
val isLocked: Boolean, val isLocked: Boolean,
val showOwnerMore: Boolean,
val showOwnerTopPrice: Boolean, val showOwnerTopPrice: Boolean,
val showPlayButton: Boolean,
val gridPreviewText: String, val gridPreviewText: String,
val imageMode: CreatorChannelCommunityImageMode val imageMode: CreatorChannelCommunityImageMode,
val menuItem: CreatorChannelCommunityMenuItem
) )

View File

@@ -19,14 +19,13 @@ import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityListBindi
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
import kr.co.vividnext.sodalive.v2.widget.feed.calculateFeedCommunityImageHeight import kr.co.vividnext.sodalive.v2.widget.feed.calculateFeedCommunityImageHeight
class CreatorChannelCommunityListAdapter( class CreatorChannelCommunityListAdapter(
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}, private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
private val onPlayClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}, private val onMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {}
private val onOwnerMoreClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
private val isPlayingContent: (Long) -> Boolean = { false }
) : RecyclerView.Adapter<CreatorChannelCommunityListAdapter.ViewHolder>() { ) : RecyclerView.Adapter<CreatorChannelCommunityListAdapter.ViewHolder>() {
private var items: List<CreatorChannelCommunityPostUiModel> = emptyList() private var items: List<CreatorChannelCommunityPostUiModel> = emptyList()
@@ -40,9 +39,7 @@ class CreatorChannelCommunityListAdapter(
return ViewHolder( return ViewHolder(
ItemCreatorChannelCommunityListBinding.inflate(LayoutInflater.from(parent.context), parent, false), ItemCreatorChannelCommunityListBinding.inflate(LayoutInflater.from(parent.context), parent, false),
onPostClick, onPostClick,
onPlayClick, onMoreClick
onOwnerMoreClick,
isPlayingContent
) )
} }
@@ -55,9 +52,7 @@ class CreatorChannelCommunityListAdapter(
class ViewHolder( class ViewHolder(
private val binding: ItemCreatorChannelCommunityListBinding, private val binding: ItemCreatorChannelCommunityListBinding,
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit, private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit,
private val onPlayClick: (CreatorChannelCommunityPostUiModel) -> Unit, private val onMoreClick: (CreatorChannelCommunityMenuItem) -> Unit
private val onOwnerMoreClick: (CreatorChannelCommunityPostUiModel) -> Unit,
private val isPlayingContent: (Long) -> Boolean
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
init { init {
@@ -86,7 +81,7 @@ class CreatorChannelCommunityListAdapter(
val visibleImageUrl = item.imageUrl.takeUnless { item.isLocked } val visibleImageUrl = item.imageUrl.takeUnless { item.isLocked }
layoutCreatorChannelCommunityListImageContainer.isVisible = layoutCreatorChannelCommunityListImageContainer.isVisible =
visibleImageUrl != null || item.isLocked || item.showPlayButton visibleImageUrl != null || item.isLocked
ivCreatorChannelCommunityListImage.isVisible = visibleImageUrl != null ivCreatorChannelCommunityListImage.isVisible = visibleImageUrl != null
resetCommunityImageHeight() resetCommunityImageHeight()
if (visibleImageUrl != null) { if (visibleImageUrl != null) {
@@ -119,19 +114,14 @@ class CreatorChannelCommunityListAdapter(
} }
layoutCreatorChannelCommunityListLockedOverlay.isVisible = item.isLocked layoutCreatorChannelCommunityListLockedOverlay.isVisible = item.isLocked
ivCreatorChannelCommunityListLock.isVisible = item.isLocked ivCreatorChannelCommunityListLock.isVisible = item.isLocked
tvCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked layoutCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked
tvCreatorChannelCommunityListLockedPrice.text = item.price.moneyFormat() tvCreatorChannelCommunityListLockedPrice.text = item.price.moneyFormat()
ivCreatorChannelCommunityListPlay.isVisible = item.showPlayButton
ivCreatorChannelCommunityListPlay.setImageResource(
if (isPlayingContent(item.postId)) R.drawable.ic_player_pause else R.drawable.ic_new_player_play
)
ivCreatorChannelCommunityListPlay.setOnClickListener { onPlayClick(item) }
layoutCreatorChannelCommunityListTopActions.isVisible = item.showOwnerMore || item.showOwnerTopPrice layoutCreatorChannelCommunityListTopActions.isVisible = item.showOwnerTopPrice || item.menuItem.showMore
layoutCreatorChannelCommunityListTopPrice.isVisible = item.showOwnerTopPrice layoutCreatorChannelCommunityListTopPrice.isVisible = item.showOwnerTopPrice
tvCreatorChannelCommunityListTopPrice.text = item.price.moneyFormat() tvCreatorChannelCommunityListTopPrice.text = item.price.moneyFormat()
ivCreatorChannelCommunityListOwnerMore.isVisible = item.showOwnerMore btnCreatorChannelCommunityListMore.isVisible = item.menuItem.showMore
ivCreatorChannelCommunityListOwnerMore.setOnClickListener { onOwnerMoreClick(item) } btnCreatorChannelCommunityListMore.setOnClickListener { onMoreClick(item.menuItem) }
} }
private fun resetCommunityImageHeight() { private fun resetCommunityImageHeight() {

View File

@@ -103,7 +103,9 @@ data class CreatorChannelCommunityPostResponse(
@SerializedName("dateUtc") val dateUtc: String, @SerializedName("dateUtc") val dateUtc: String,
@SerializedName("existOrdered") val existOrdered: Boolean, @SerializedName("existOrdered") val existOrdered: Boolean,
@SerializedName("likeCount") val likeCount: Int, @SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentCount") val commentCount: Int @SerializedName("commentCount") val commentCount: Int,
@SerializedName("isPinned") val isPinned: Boolean,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean
) )
@Keep @Keep

View File

@@ -185,6 +185,11 @@ class CreatorChannelRepository(
token = token token = token
) )
fun reportCommunityPost(postId: Long, reason: String, token: String) = reportRepository.report(
request = ReportRequest(ReportType.COMMUNITY_POST, reason, communityPostId = postId),
token = token
)
fun deleteFanTalk(fanTalkId: Long, token: String) = explorerRepository.modifyCheers( fun deleteFanTalk(fanTalkId: Long, token: String) = explorerRepository.modifyCheers(
request = PutModifyCheersRequest(cheersId = fanTalkId, isActive = false), request = PutModifyCheersRequest(cheersId = fanTalkId, isActive = false),
token = token token = token

View File

@@ -18,7 +18,8 @@ fun CreatorChannelHomeResponse.toUiContent(currentMemberId: Long): CreatorChanne
?.let { add(CreatorChannelHomeSection.Schedules(it)) } ?.let { add(CreatorChannelHomeSection.Schedules(it)) }
audioContents.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.AudioContents(it)) } audioContents.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.AudioContents(it)) }
series.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Series(it)) } series.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Series(it)) }
communities.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Communities(it)) } communities.takeIf { it.isNotEmpty() }
?.let { add(CreatorChannelHomeSection.Communities(it, currentMemberId)) }
add(CreatorChannelHomeSection.FanTalk(fanTalk)) add(CreatorChannelHomeSection.FanTalk(fanTalk))
introduce.takeIf { it.isNotBlank() }?.let { add(CreatorChannelHomeSection.Introduce(it)) } introduce.takeIf { it.isNotBlank() }?.let { add(CreatorChannelHomeSection.Introduce(it)) }
add(CreatorChannelHomeSection.Activity(activity)) add(CreatorChannelHomeSection.Activity(activity))

View File

@@ -57,7 +57,10 @@ sealed interface CreatorChannelHomeSection {
data class Schedules(val schedules: List<CreatorChannelScheduleResponse>) : CreatorChannelHomeSection data class Schedules(val schedules: List<CreatorChannelScheduleResponse>) : CreatorChannelHomeSection
data class AudioContents(val audioContents: List<CreatorChannelAudioContentResponse>) : CreatorChannelHomeSection data class AudioContents(val audioContents: List<CreatorChannelAudioContentResponse>) : CreatorChannelHomeSection
data class Series(val series: List<CreatorChannelSeriesResponse>) : CreatorChannelHomeSection data class Series(val series: List<CreatorChannelSeriesResponse>) : CreatorChannelHomeSection
data class Communities(val communities: List<CreatorChannelCommunityPostResponse>) : CreatorChannelHomeSection data class Communities(
val communities: List<CreatorChannelCommunityPostResponse>,
val currentMemberId: Long
) : CreatorChannelHomeSection
data class FanTalk(val fanTalk: CreatorChannelFanTalkSummaryResponse) : CreatorChannelHomeSection data class FanTalk(val fanTalk: CreatorChannelFanTalkSummaryResponse) : CreatorChannelHomeSection
data class Introduce(val introduce: String) : CreatorChannelHomeSection data class Introduce(val introduce: String) : CreatorChannelHomeSection
data class Activity(val activity: CreatorChannelActivityResponse) : CreatorChannelHomeSection data class Activity(val activity: CreatorChannelActivityResponse) : CreatorChannelHomeSection

View File

@@ -25,6 +25,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelCommunityP
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse 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.CreatorChannelScheduleResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeSection import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeSection
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
import kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView import kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView
@@ -43,7 +44,8 @@ class CreatorChannelHomeSectionAdapter(
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit = {}, private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit = {},
private val onDonationClick: () -> Unit = {}, private val onDonationClick: () -> Unit = {},
private val onSectionChevronClick: (CreatorChannelTab) -> Unit = {}, private val onSectionChevronClick: (CreatorChannelTab) -> Unit = {},
private val onCommunityClick: (Long) -> Unit = {} private val onCommunityClick: (Long) -> Unit = {},
private val onCommunityMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {}
) : RecyclerView.Adapter<CreatorChannelHomeSectionAdapter.SectionViewHolder>() { ) : RecyclerView.Adapter<CreatorChannelHomeSectionAdapter.SectionViewHolder>() {
private var items: List<CreatorChannelHomeSection> = emptyList() private var items: List<CreatorChannelHomeSection> = emptyList()
@@ -65,7 +67,8 @@ class CreatorChannelHomeSectionAdapter(
onSeriesClick, onSeriesClick,
onDonationClick, onDonationClick,
onSectionChevronClick, onSectionChevronClick,
onCommunityClick onCommunityClick,
onCommunityMoreClick
) )
} }
@@ -83,7 +86,8 @@ class CreatorChannelHomeSectionAdapter(
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit, private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit,
private val onDonationClick: () -> Unit, private val onDonationClick: () -> Unit,
private val onSectionChevronClick: (CreatorChannelTab) -> Unit, private val onSectionChevronClick: (CreatorChannelTab) -> Unit,
private val onCommunityClick: (Long) -> Unit private val onCommunityClick: (Long) -> Unit,
private val onCommunityMoreClick: (CreatorChannelCommunityMenuItem) -> Unit
) : RecyclerView.ViewHolder(view) { ) : RecyclerView.ViewHolder(view) {
private val title: TextView? = view.findViewById(R.id.tv_section_title) private val title: TextView? = view.findViewById(R.id.tv_section_title)
private val sectionTitleChevron: ImageView? = view.findViewById(R.id.iv_section_title_chevron) private val sectionTitleChevron: ImageView? = view.findViewById(R.id.iv_section_title_chevron)
@@ -366,6 +370,14 @@ class CreatorChannelHomeSectionAdapter(
communityMoreButton?.setOnClickListener { onSectionChevronClick(CreatorChannelTab.Community) } communityMoreButton?.setOnClickListener { onSectionChevronClick(CreatorChannelTab.Community) }
val visibleCommunities = item.communities.take(MAX_COMMUNITY_ITEM_COUNT) val visibleCommunities = item.communities.take(MAX_COMMUNITY_ITEM_COUNT)
visibleCommunities.forEachIndexed { index, community -> visibleCommunities.forEachIndexed { index, community ->
val menuItem = CreatorChannelCommunityMenuItem(
postId = community.postId,
creatorId = community.creatorId,
currentUserId = item.currentMemberId,
price = community.price,
existOrdered = community.existOrdered,
isPinned = community.isPinned
)
val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp( val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp(
itemView.resources.configuration.screenWidthDp itemView.resources.configuration.screenWidthDp
) )
@@ -381,7 +393,8 @@ class CreatorChannelHomeSectionAdapter(
) )
) )
setHideEmptyTextRows(true) setHideEmptyTextRows(true)
bind(community.toFeedCommunityItem()) bind(community.toFeedCommunityItem(showMore = menuItem.showMore))
setOnMoreClick { onCommunityMoreClick(menuItem) }
} }
bindCommunityImages(row, community) bindCommunityImages(row, community)
row.layoutParams = LinearLayout.LayoutParams( row.layoutParams = LinearLayout.LayoutParams(
@@ -443,7 +456,9 @@ class CreatorChannelHomeSectionAdapter(
} }
} }
private fun CreatorChannelCommunityPostResponse.toFeedCommunityItem(): FeedItem.Community = FeedItem.Community( private fun CreatorChannelCommunityPostResponse.toFeedCommunityItem(
showMore: Boolean
): FeedItem.Community = FeedItem.Community(
feedId = postId.toString(), feedId = postId.toString(),
creatorId = creatorId.toString(), creatorId = creatorId.toString(),
creatorName = creatorNickname, creatorName = creatorNickname,
@@ -458,7 +473,11 @@ class CreatorChannelHomeSectionAdapter(
audioUrl = audioUrl, audioUrl = audioUrl,
price = price, price = price,
existOrdered = existOrdered, existOrdered = existOrdered,
showKeyword = false isPinned = isPinned,
isCommentAvailable = isCommentAvailable,
hideReactionWhenLocked = true,
showKeyword = false,
showMore = showMore
) )
private fun bindIntroduce(item: CreatorChannelHomeSection.Introduce) { private fun bindIntroduce(item: CreatorChannelHomeSection.Introduce) {

View File

@@ -20,6 +20,8 @@ class FeedCommunityView @JvmOverloads constructor(
) : LinearLayout(context, attrs, defStyleAttr) { ) : LinearLayout(context, attrs, defStyleAttr) {
private var profileImage: ImageView? = null private var profileImage: ImageView? = null
private var noticeRow: View? = null
private var moreButton: View? = null
private var creatorText: TextView? = null private var creatorText: TextView? = null
private var createdAtText: TextView? = null private var createdAtText: TextView? = null
private var bodyText: TextView? = null private var bodyText: TextView? = null
@@ -27,15 +29,21 @@ class FeedCommunityView @JvmOverloads constructor(
private var communityImage: ImageView? = null private var communityImage: ImageView? = null
private var paidOverlay: View? = null private var paidOverlay: View? = null
private var priceText: TextView? = null private var priceText: TextView? = null
private var reactionRow: View? = null
private var commentIcon: ImageView? = null
private var commentCountText: TextView? = null private var commentCountText: TextView? = null
private var likeIcon: ImageView? = null
private var likeCountText: TextView? = null private var likeCountText: TextView? = null
private var currentItem: FeedItem.Community? = null private var currentItem: FeedItem.Community? = null
private var clickListener: ((FeedItem) -> Unit)? = null private var clickListener: ((FeedItem) -> Unit)? = null
private var moreClickListener: ((FeedItem.Community) -> Unit)? = null
private var hideEmptyTextRows: Boolean = false private var hideEmptyTextRows: Boolean = false
override fun onFinishInflate() { override fun onFinishInflate() {
super.onFinishInflate() super.onFinishInflate()
noticeRow = findViewById(R.id.ll_feed_community_notice)
profileImage = findViewById(R.id.iv_feed_community_profile) profileImage = findViewById(R.id.iv_feed_community_profile)
moreButton = findViewById(R.id.btn_feed_community_more)
creatorText = findViewById(R.id.tv_feed_community_creator) creatorText = findViewById(R.id.tv_feed_community_creator)
createdAtText = findViewById(R.id.tv_feed_community_created_at) createdAtText = findViewById(R.id.tv_feed_community_created_at)
bodyText = findViewById(R.id.tv_feed_community_body) bodyText = findViewById(R.id.tv_feed_community_body)
@@ -43,7 +51,10 @@ class FeedCommunityView @JvmOverloads constructor(
communityImage = findViewById(R.id.iv_feed_community_image) communityImage = findViewById(R.id.iv_feed_community_image)
paidOverlay = findViewById(R.id.ll_feed_community_paid_overlay) paidOverlay = findViewById(R.id.ll_feed_community_paid_overlay)
priceText = findViewById(R.id.tv_feed_community_price) priceText = findViewById(R.id.tv_feed_community_price)
reactionRow = findViewById(R.id.ll_feed_community_reaction)
commentIcon = findViewById(R.id.iv_feed_community_comment)
commentCountText = findViewById(R.id.tv_feed_community_comment_count) commentCountText = findViewById(R.id.tv_feed_community_comment_count)
likeIcon = findViewById(R.id.iv_feed_community_like)
likeCountText = findViewById(R.id.tv_feed_community_like_count) likeCountText = findViewById(R.id.tv_feed_community_like_count)
clipToOutline = true clipToOutline = true
outlineProvider = roundedOutlineProvider(CARD_RADIUS_DP) outlineProvider = roundedOutlineProvider(CARD_RADIUS_DP)
@@ -59,7 +70,8 @@ class FeedCommunityView @JvmOverloads constructor(
requireNotNull(createdAtText).text = item.createdAtText requireNotNull(createdAtText).text = item.createdAtText
requireNotNull(bodyText).text = item.bodyText requireNotNull(bodyText).text = item.bodyText
requireNotNull(bodyText).visibility = visibilityForText(item.bodyText) requireNotNull(bodyText).visibility = visibilityForText(item.bodyText)
val isLocked = item.price > 0 && !item.existOrdered requireNotNull(noticeRow).isVisible = item.isPinned
val isLocked = item.isLocked
val hasImage = !item.imageUrl.isNullOrBlank() val hasImage = !item.imageUrl.isNullOrBlank()
requireNotNull(communityImageContainer).isVisible = hasImage || isLocked requireNotNull(communityImageContainer).isVisible = hasImage || isLocked
resetCommunityImageHeight() resetCommunityImageHeight()
@@ -69,9 +81,15 @@ class FeedCommunityView @JvmOverloads constructor(
} }
requireNotNull(paidOverlay).isVisible = isLocked requireNotNull(paidOverlay).isVisible = isLocked
requireNotNull(priceText).text = item.price.toString() requireNotNull(priceText).text = item.price.toString()
requireNotNull(reactionRow).isVisible = item.showReaction
requireNotNull(commentIcon).isVisible = item.showComment
requireNotNull(commentCountText).text = item.commentCount.toString() requireNotNull(commentCountText).text = item.commentCount.toString()
requireNotNull(commentCountText).isVisible = item.showComment
requireNotNull(likeIcon).isVisible = item.showReaction
requireNotNull(likeCountText).text = item.likeCount.toString() requireNotNull(likeCountText).text = item.likeCount.toString()
requireNotNull(likeCountText).isVisible = item.showReaction
applyClickState(item) applyClickState(item)
applyMoreClickState(item)
} }
fun profileImageView(): ImageView = requireNotNull(profileImage) fun profileImageView(): ImageView = requireNotNull(profileImage)
@@ -116,6 +134,11 @@ class FeedCommunityView @JvmOverloads constructor(
currentItem?.let(::applyClickState) currentItem?.let(::applyClickState)
} }
fun setOnMoreClick(listener: ((FeedItem.Community) -> Unit)?) {
moreClickListener = listener
currentItem?.let(::applyMoreClickState)
}
fun setHideEmptyTextRows(hide: Boolean) { fun setHideEmptyTextRows(hide: Boolean) {
hideEmptyTextRows = hide hideEmptyTextRows = hide
currentItem?.let(::bind) currentItem?.let(::bind)
@@ -130,6 +153,14 @@ class FeedCommunityView @JvmOverloads constructor(
isClickable = listener != null isClickable = listener != null
} }
private fun applyMoreClickState(item: FeedItem.Community) {
val listener = moreClickListener
requireNotNull(moreButton).isVisible = item.showMore
requireNotNull(moreButton).setOnClickListener(
if (listener == null) null else View.OnClickListener { listener(item) }
)
}
private fun updateRootWidth(width: Int) { private fun updateRootWidth(width: Int) {
val currentLayoutParams = layoutParams val currentLayoutParams = layoutParams
layoutParams = if (currentLayoutParams == null) { layoutParams = if (currentLayoutParams == null) {

View File

@@ -47,6 +47,19 @@ sealed class FeedItem(open val feedId: String, val variant: FeedVariant) {
val audioUrl: String? = null, val audioUrl: String? = null,
val price: Int = 0, val price: Int = 0,
val existOrdered: Boolean = false, val existOrdered: Boolean = false,
val showKeyword: Boolean = true val isPinned: Boolean = false,
) : FeedItem(feedId, FeedVariant.Community) val isCommentAvailable: Boolean = true,
val hideReactionWhenLocked: Boolean = false,
val showKeyword: Boolean = true,
val showMore: Boolean = false
) : FeedItem(feedId, FeedVariant.Community) {
val isLocked: Boolean
get() = price > 0 && !existOrdered
val showReaction: Boolean
get() = !hideReactionWhenLocked || !isLocked
val showComment: Boolean
get() = showReaction && isCommentAvailable
}
} }

View File

@@ -73,7 +73,7 @@
android:includeFontPadding="false" android:includeFontPadding="false"
android:maxLines="1" android:maxLines="1"
android:textColor="@color/white" android:textColor="@color/white"
app:layout_constraintEnd_toStartOf="@id/layout_creator_channel_community_detail_price" app:layout_constraintEnd_toStartOf="@id/btn_creator_channel_community_detail_more"
app:layout_constraintStart_toEndOf="@id/iv_creator_channel_community_detail_profile" app:layout_constraintStart_toEndOf="@id/iv_creator_channel_community_detail_profile"
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile" app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile"
tools:text="크리에이터" /> tools:text="크리에이터" />
@@ -91,36 +91,17 @@
app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_community_detail_nickname" app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_community_detail_nickname"
tools:text="2분 전" /> tools:text="2분 전" />
<LinearLayout <ImageButton
android:id="@+id/layout_creator_channel_community_detail_price" android:id="@+id/btn_creator_channel_community_detail_more"
android:layout_width="wrap_content" android:layout_width="42dp"
android:layout_height="wrap_content" android:layout_height="42dp"
android:background="@drawable/bg_creator_channel_community_price" android:background="@android:color/transparent"
android:gravity="center" android:contentDescription="@string/read_more"
android:orientation="horizontal" android:src="@drawable/ic_seemore_vertical"
android:paddingHorizontal="@dimen/spacing_4"
android:paddingVertical="2dp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile" app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile"
tools:visibility="visible"> tools:visibility="visible" />
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:contentDescription="@null"
android:src="@drawable/ic_bar_cash" />
<TextView
android:id="@+id/tv_creator_channel_community_detail_price"
style="@style/Typography.Caption3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:includeFontPadding="false"
android:textColor="@color/black"
tools:text="300" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<TextView <TextView
@@ -222,22 +203,6 @@
tools:text="Audio" tools:text="Audio"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView
android:id="@+id/tv_creator_channel_community_detail_purchased"
style="@style/Typography.Caption2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="@dimen/spacing_8"
android:background="@drawable/bg_creator_channel_community_price"
android:includeFontPadding="false"
android:paddingHorizontal="@dimen/spacing_8"
android:paddingVertical="@dimen/spacing_4"
android:text="@string/creator_channel_community_purchased_badge"
android:textColor="@color/black"
android:visibility="gone"
tools:visibility="visible" />
<LinearLayout <LinearLayout
android:id="@+id/layout_creator_channel_community_detail_reaction" android:id="@+id/layout_creator_channel_community_detail_reaction"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -122,13 +122,13 @@
tools:text="300" /> tools:text="300" />
</LinearLayout> </LinearLayout>
<ImageView <ImageButton
android:id="@+id/iv_creator_channel_community_list_owner_more" android:id="@+id/btn_creator_channel_community_list_more"
android:layout_width="24dp" android:layout_width="42dp"
android:layout_height="24dp" android:layout_height="42dp"
android:layout_marginStart="@dimen/spacing_4" android:background="@android:color/transparent"
android:contentDescription="@null" android:contentDescription="@string/read_more"
android:src="@drawable/ic_new_more" android:src="@drawable/ic_seemore_vertical"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
</LinearLayout> </LinearLayout>
@@ -187,32 +187,34 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView <LinearLayout
android:id="@+id/tv_creator_channel_community_list_locked_price" android:id="@+id/layout_creator_channel_community_list_locked_price"
style="@style/Typography.Body3"
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="36dp" android:layout_height="36dp"
android:layout_marginTop="@dimen/spacing_4" android:layout_marginTop="@dimen/spacing_4"
android:background="@drawable/bg_creator_channel_community_price" android:background="@drawable/bg_creator_channel_community_price"
android:drawableStart="@drawable/ic_bar_cash"
android:drawablePadding="@dimen/spacing_6"
android:gravity="center" android:gravity="center"
android:includeFontPadding="false" android:orientation="horizontal"
android:textColor="@color/black"
android:visibility="gone" android:visibility="gone"
tools:text="30" tools:visibility="visible">
tools:visibility="visible" />
</LinearLayout>
<ImageView <ImageView
android:id="@+id/iv_creator_channel_community_list_play" android:layout_width="18dp"
android:layout_width="48dp" android:layout_height="18dp"
android:layout_height="48dp" android:contentDescription="@null"
android:layout_gravity="center" android:src="@drawable/ic_bar_cash" />
android:contentDescription="@null"
android:src="@drawable/ic_new_player_play" <TextView
android:visibility="gone" android:id="@+id/tv_creator_channel_community_list_locked_price"
tools:visibility="visible" /> style="@style/Typography.Body3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_6"
android:includeFontPadding="false"
android:textColor="@color/black"
tools:text="30" />
</LinearLayout>
</LinearLayout>
</FrameLayout> </FrameLayout>
<LinearLayout <LinearLayout

View File

@@ -8,7 +8,33 @@
android:padding="@dimen/spacing_14"> android:padding="@dimen/spacing_14">
<LinearLayout <LinearLayout
android:id="@+id/ll_feed_community_notice"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/spacing_14"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:contentDescription="@null"
android:src="@drawable/ic_pin" />
<TextView
style="@style/Typography.Body5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:includeFontPadding="false"
android:text="@string/creator_channel_community_notice"
android:textColor="@color/green_400" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="42dp" android:layout_height="42dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
@@ -22,9 +48,10 @@
tools:src="@drawable/ic_launcher_background" /> tools:src="@drawable/ic_launcher_background" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_8" android:layout_marginStart="@dimen/spacing_8"
android:layout_weight="1"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
@@ -50,6 +77,16 @@
android:textColor="@color/gray_500" android:textColor="@color/gray_500"
tools:text="2분 전" /> tools:text="2분 전" />
</LinearLayout> </LinearLayout>
<ImageButton
android:id="@+id/btn_feed_community_more"
android:layout_width="42dp"
android:layout_height="42dp"
android:background="@android:color/transparent"
android:contentDescription="@string/read_more"
android:src="@drawable/ic_seemore_vertical"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout> </LinearLayout>
<TextView <TextView
@@ -125,6 +162,7 @@
</FrameLayout> </FrameLayout>
<LinearLayout <LinearLayout
android:id="@+id/ll_feed_community_reaction"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginTop="@dimen/spacing_16" android:layout_marginTop="@dimen/spacing_16"
@@ -132,6 +170,7 @@
android:orientation="horizontal"> android:orientation="horizontal">
<ImageView <ImageView
android:id="@+id/iv_feed_community_comment"
android:layout_width="18dp" android:layout_width="18dp"
android:layout_height="18dp" android:layout_height="18dp"
android:contentDescription="@null" android:contentDescription="@null"
@@ -148,6 +187,7 @@
tools:text="5" /> tools:text="5" />
<ImageView <ImageView
android:id="@+id/iv_feed_community_like"
android:layout_width="18dp" android:layout_width="18dp"
android:layout_height="18dp" android:layout_height="18dp"
android:layout_marginStart="15dp" android:layout_marginStart="15dp"

View File

@@ -44,13 +44,4 @@ class CommunityChangeTest {
assertEquals(List(CommunityActivityResultSource.entries.size) { CommunityChange.Ignored }, changes) assertEquals(List(CommunityActivityResultSource.entries.size) { CommunityChange.Ignored }, changes)
} }
@Test
fun `삭제와 고정 변경은 post ID를 가진 명시적 변경으로 표현한다`() {
val deleted = CommunityChange.Deleted(postId = 31L)
val pinChanged = CommunityChange.PinChanged(postId = 32L)
assertEquals(31L, deleted.postId)
assertEquals(32L, pinChanged.postId)
}
} }

View File

@@ -119,6 +119,164 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("private fun handleCommunityChange(change: CommunityChange)")) assertTrue(source.contains("private fun handleCommunityChange(change: CommunityChange)"))
} }
@Test
fun `홈과 커뮤니티 list 더보기는 동일 menu item을 Activity Host까지 전달한다`() {
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
).readText()
val homeFragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt"
).readText()
val communityFragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt"
).readText()
val activity = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
assertTrue(
adapter.contains("private val onCommunityMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {}")
)
assertTrue(adapter.contains("currentUserId = item.currentMemberId"))
assertTrue(adapter.contains("showMore = menuItem.showMore"))
assertTrue(adapter.contains("setOnMoreClick { onCommunityMoreClick(menuItem) }"))
assertTrue(adapter.contains("row.setOnClickListener { onCommunityClick(community.postId) }"))
assertTrue(homeFragment.contains("onCommunityMoreClick = ::onCommunityMoreClicked"))
assertTrue(homeFragment.contains("host.onCreatorChannelCommunityMoreClicked(item)"))
assertTrue(
homeFragment.contains("fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)")
)
assertTrue(communityFragment.contains("host.onCreatorChannelCommunityMoreClicked(item)"))
assertTrue(
communityFragment.contains("fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)")
)
assertTrue(
activity.contains(
"override fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)"
)
)
}
@Test
fun `커뮤니티 더보기는 하나의 Activity callback에서 권한별 기존 메뉴와 수정 결과를 연결한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val handlerSource = sourceSection(
source = source,
startMarker = "override fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)",
endMarker = "override fun onCreatorChannelFanTalkContentChanged"
)
assertEquals(
1,
Regex(
Regex.escape(
"override fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)"
)
)
.findAll(source)
.count()
)
assertTrue(handlerSource.contains("if (!item.showMore) return"))
assertTrue(handlerSource.contains("CreatorCommunityPostMenuBottomSheetDialog("))
assertTrue(handlerSource.contains("isFixed = item.isPinned"))
assertTrue(handlerSource.contains("isCreator = item.isOwner"))
assertTrue(handlerSource.contains("onClickPin = { updateCreatorChannelCommunityPostFixed(item) }"))
assertTrue(handlerSource.contains("onClickDelete = { showDeleteCreatorChannelCommunityPostDialog(item) }"))
assertTrue(handlerSource.contains("CreatorCommunityReportDialog("))
assertTrue(handlerSource.contains("if (item.isOwner)"))
assertFalse(handlerSource.contains("isCreatorChannelOwner()"))
assertTrue(source.contains("private val communityModifyLauncher = registerForActivityResult"))
assertTrue(source.contains("CommunityActivityResultSource.Modify"))
assertTrue(source.contains("CreatorCommunityModifyActivity::class.java"))
assertTrue(source.contains("putExtra(Constants.EXTRA_COMMUNITY_POST_ID, item.postId)"))
}
@Test
fun `커뮤니티 고정과 삭제 mutation은 역할과 중복을 재확인하고 성공만 목록을 갱신한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val pinSource = sourceSection(
source = source,
startMarker = "private fun updateCreatorChannelCommunityPostFixed(item: CreatorChannelCommunityMenuItem)",
endMarker = "private fun showDeleteCreatorChannelCommunityPostDialog"
)
val deleteSource = sourceSection(
source = source,
startMarker = "private fun deleteCreatorChannelCommunityPost(item: CreatorChannelCommunityMenuItem)",
endMarker = "private fun reportCreatorChannelCommunityPost"
)
val deleteDialogSource = sourceSection(
source = source,
startMarker = "private fun showDeleteCreatorChannelCommunityPostDialog",
endMarker = "private fun deleteCreatorChannelCommunityPost"
)
listOf(pinSource, deleteSource).forEach { mutationSource ->
assertTrue(
mutationSource.contains(
"if (!item.isOwner || isCommunityPostMutating) return"
)
)
assertFalse(mutationSource.contains("isCreatorChannelOwner()"))
assertTrue(mutationSource.contains("isCommunityPostMutating = true"))
assertEquals(2, "isCommunityPostMutating = false".toRegex().findAll(mutationSource).count())
assertTrue(mutationSource.contains("if (response.success)"))
assertTrue(mutationSource.contains("handleCommunityChange(CommunityChange.Updated)"))
assertTrue(mutationSource.contains("showCommunityMutationFailureToast"))
}
assertTrue(
pinSource.contains(
"creatorCommunityRepository.updateCommunityPostFixed(item.postId, !item.isPinned, authToken())"
)
)
assertTrue(deleteDialogSource.contains("if (!item.isOwner) return"))
assertFalse(deleteDialogSource.contains("isCreatorChannelOwner()"))
assertTrue(deleteSource.contains("ModifyCommunityPostRequest("))
assertTrue(deleteSource.contains("creatorCommunityId = item.postId"))
assertTrue(deleteSource.contains("isActive = false"))
assertTrue(deleteSource.contains("Gson().toJson"))
assertTrue(deleteSource.contains("toRequestBody(\"text/plain\".toMediaType())"))
assertTrue(
deleteSource.contains(
"creatorCommunityRepository.modifyCommunityPost(postImage = null, request = request, token = authToken())"
)
)
}
@Test
fun `커뮤니티 신고 mutation은 방문자 역할과 중복을 재확인하고 성공 토스트만 표시한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val reportSource = sourceSection(
source = source,
startMarker = "private fun reportCreatorChannelCommunityPost(",
endMarker = "private fun showCommunityMutationFailureToast"
)
assertTrue(
reportSource.contains(
"if (item.isOwner || !item.showMore || reason.isBlank() || " +
"isCommunityPostMutating) return"
)
)
assertFalse(reportSource.contains("isCreatorChannelOwner()"))
assertTrue(reportSource.contains("isCommunityPostMutating = true"))
assertEquals(2, "isCommunityPostMutating = false".toRegex().findAll(reportSource).count())
assertTrue(
reportSource.contains(
"creatorChannelRepository.reportCommunityPost(item.postId, reason, authToken())"
)
)
assertTrue(reportSource.contains("response.message.isNullOrBlank()"))
assertTrue(reportSource.contains("R.string.character_comment_report_submitted"))
assertTrue(reportSource.contains("showCommunityMutationFailureToast"))
assertFalse(reportSource.contains("handleCommunityChange("))
}
@Test @Test
fun `follow notify source는 Phase 11 직접 팔로우 알림 액션을 연결한다`() { fun `follow notify source는 Phase 11 직접 팔로우 알림 액션을 연결한다`() {
val source = projectFile( val source = projectFile(
@@ -653,80 +811,12 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("iconResId = R.drawable.ic_new_upload_community_post")) 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("textResId = R.string.creator_channel_owner_fab_community"))
assertTrue(source.contains("CreatorChannelTab.Community.ordinal -> onOwnerFabCommunityClicked()")) assertTrue(source.contains("CreatorChannelTab.Community.ordinal -> onOwnerFabCommunityClicked()"))
assertTrue(source.contains("private val communityPostModifyLauncher")) assertFalse(source.contains("communityPostModifyLauncher"))
assertTrue(source.contains("CreatorCommunityModifyActivity::class.java")) assertFalse(source.contains("onCreatorChannelCommunityOwnerMoreClicked"))
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()")) 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("fun onCreatorChannelCommunityRefreshRequested()"))
assertTrue( assertFalse(fragment.contains("CreatorCommunityMediaPlayerManager"))
assertFalse(fragment.contains("toggleCommunityAudio"))
fragment.contains(
"CreatorCommunityMediaPlayerManager(requireContext()) { listAdapter.notifyDataSetChanged() }"
)
)
assertTrue(fragment.contains("mediaPlayerManager?.toggleContent(CreatorCommunityContentItem(item.postId, audioUrl))"))
assertTrue(fragment.contains("mediaPlayerManager?.stopContent()"))
}
@Test
fun `커뮤니티 고정 성공은 실제 post id로 단일 변경 handler를 호출한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val fixedSource = sourceSection(
source = source,
startMarker = "private fun updateCreatorChannelCommunityPostFixed",
endMarker = "private fun deleteCreatorChannelCommunityPost"
)
val successMarker = "if (response.success)"
val beforeSuccessSource = fixedSource.substring(0, fixedSource.indexOf(successMarker))
val successSource = fixedSource.substring(
fixedSource.indexOf(successMarker),
fixedSource.indexOf("} else", fixedSource.indexOf(successMarker))
)
assertTrue(fixedSource.contains("postId = item.postId"))
assertTrue(fixedSource.contains("isFixed = !item.isPinned"))
assertTrue(successSource.contains("handleCommunityChange(CommunityChange.PinChanged(item.postId))"))
assertFalse(beforeSuccessSource.contains("CommunityChange.PinChanged"))
assertEquals(1, fixedSource.split("CommunityChange.PinChanged").size - 1)
assertFalse(fixedSource.contains("CommunityChange.PinChanged(creatorId)"))
assertFalse(fixedSource.contains("refreshCreatorChannelCommunity()"))
}
@Test
fun `커뮤니티 삭제 성공은 실제 post id로 단일 변경 handler를 호출한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val deleteSource = sourceSection(
source = source,
startMarker = "private fun deleteCreatorChannelCommunityPost",
endMarker = "private fun authToken()"
)
val successMarker = "if (response.success)"
val beforeSuccessSource = deleteSource.substring(0, deleteSource.indexOf(successMarker))
val successSource = deleteSource.substring(
deleteSource.indexOf(successMarker),
deleteSource.indexOf("} else", deleteSource.indexOf(successMarker))
)
assertTrue(deleteSource.contains("creatorCommunityId = item.postId"))
assertTrue(deleteSource.contains("isActive = false"))
assertTrue(successSource.contains("handleCommunityChange(CommunityChange.Deleted(item.postId))"))
assertFalse(beforeSuccessSource.contains("CommunityChange.Deleted"))
assertEquals(1, deleteSource.split("CommunityChange.Deleted").size - 1)
assertFalse(deleteSource.contains("CommunityChange.Deleted(creatorId)"))
assertFalse(deleteSource.contains("refreshCreatorChannelCommunity()"))
} }
@Test @Test
@@ -743,18 +833,14 @@ class CreatorChannelActivitySourceTest {
handlerSource.indexOf("CommunityChange.Created"), handlerSource.indexOf("CommunityChange.Created"),
handlerSource.indexOf("CommunityChange.Updated") handlerSource.indexOf("CommunityChange.Updated")
) )
val mutationSource = handlerSource.substring(handlerSource.indexOf("CommunityChange.Updated")) val updatedSource = handlerSource.substring(handlerSource.indexOf("CommunityChange.Updated"))
assertTrue(createdSource.contains("homeActionDelegate?.refreshHome()")) assertTrue(createdSource.contains("homeActionDelegate?.refreshHome()"))
assertTrue(createdSource.contains("refreshCreatorChannelCommunity()")) assertTrue(createdSource.contains("refreshCreatorChannelCommunity()"))
assertTrue( assertTrue(updatedSource.contains("homeActionDelegate?.refreshHome()"))
mutationSource.contains( assertTrue(updatedSource.contains("refreshCreatorChannelCommunity()"))
"CommunityChange.Updated,\n" + assertFalse(updatedSource.contains("CommunityChange.Deleted"))
" is CommunityChange.Deleted,\n" + assertFalse(updatedSource.contains("CommunityChange.PinChanged"))
" is CommunityChange.PinChanged -> refreshCreatorChannelCommunity()"
)
)
assertFalse(mutationSource.contains("homeActionDelegate?.refreshHome()"))
} }
@Test @Test
@@ -2003,7 +2089,7 @@ class CreatorChannelActivitySourceTest {
assertTrue(adapter.contains("val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp(")) assertTrue(adapter.contains("val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp("))
assertTrue(adapter.contains("rootWidthDp = communityWidthDp")) assertTrue(adapter.contains("rootWidthDp = communityWidthDp"))
assertTrue(adapter.contains("setHideEmptyTextRows(true)")) assertTrue(adapter.contains("setHideEmptyTextRows(true)"))
assertTrue(adapter.contains("bind(community.toFeedCommunityItem())")) assertTrue(adapter.contains("bind(community.toFeedCommunityItem(showMore = menuItem.showMore))"))
assertTrue(adapter.contains("bindCommunityImages(row, community)")) assertTrue(adapter.contains("bindCommunityImages(row, community)"))
assertTrue(adapter.contains("BlurTransformation(itemView.context, 25f, 2.5f)")) assertTrue(adapter.contains("BlurTransformation(itemView.context, 25f, 2.5f)"))
assertTrue(adapter.contains("row.layoutParams = LinearLayout.LayoutParams(")) assertTrue(adapter.contains("row.layoutParams = LinearLayout.LayoutParams("))

View File

@@ -61,6 +61,16 @@ class CreatorChannelHomeMapperTest {
assertFalse(visitorContent.sections.filterIsInstance<CreatorChannelHomeSection.Donations>().single().isOwner) assertFalse(visitorContent.sections.filterIsInstance<CreatorChannelHomeSection.Donations>().single().isOwner)
} }
@Test
fun `현재 회원 id를 커뮤니티 section에 보존한다`() {
val communities = response().toUiContent(currentMemberId = 77L)
.sections
.filterIsInstance<CreatorChannelHomeSection.Communities>()
.single()
assertEquals(77L, communities.currentMemberId)
}
@Test @Test
fun `null 단건 콘텐츠와 빈 리스트와 blank SNS는 후원 empty와 팬Talk empty section을 생성한다`() { fun `null 단건 콘텐츠와 빈 리스트와 blank SNS는 후원 empty와 팬Talk empty section을 생성한다`() {
val content = response( val content = response(
@@ -267,7 +277,9 @@ class CreatorChannelHomeMapperTest {
dateUtc = "2026-06-11T12:00:00Z", dateUtc = "2026-06-11T12:00:00Z",
existOrdered = false, existOrdered = false,
likeCount = 1, likeCount = 1,
commentCount = 2 commentCount = 2,
isPinned = false,
isCommentAvailable = true
) )
private fun fanTalk() = CreatorChannelFanTalkSummaryResponse( private fun fanTalk() = CreatorChannelFanTalkSummaryResponse(

View File

@@ -4,7 +4,9 @@ import com.google.gson.Gson
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeResponse
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
class CreatorChannelHomeModelsTest { class CreatorChannelHomeModelsTest {
@@ -36,6 +38,15 @@ class CreatorChannelHomeModelsTest {
assertEquals("https://x.example", response.sns.xUrl) assertEquals("https://x.example", response.sns.xUrl)
} }
@Test
fun `home response preserves community pinned and comment availability`() {
val response = gson.fromJson(homeJson(scheduleType = "AUDIO"), CreatorChannelHomeResponse::class.java)
val community = response.communities.single()
assertTrue(community.isPinned)
assertFalse(community.isCommentAvailable)
}
private fun homeJson(scheduleType: String): String = """ private fun homeJson(scheduleType: String): String = """
{ {
"creator": { "creator": {
@@ -63,7 +74,24 @@ class CreatorChannelHomeModelsTest {
], ],
"audioContents": [], "audioContents": [],
"series": [], "series": [],
"communities": [], "communities": [
{
"postId": 21,
"creatorId": 1,
"creatorNickname": "creator",
"creatorProfileUrl": "https://example.com/profile.png",
"imageUrl": null,
"audioUrl": null,
"content": "community",
"price": 0,
"dateUtc": "2026-06-12T00:00:00Z",
"existOrdered": false,
"likeCount": 6,
"commentCount": 5,
"isPinned": true,
"isCommentAvailable": false
}
],
"fanTalk": { "fanTalk": {
"totalCount": 0, "totalCount": 0,
"latestFanTalk": null "latestFanTalk": null

View File

@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
@@ -29,6 +30,7 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
val reply = inflateView(R.layout.activity_creator_channel_community_reply) val reply = inflateView(R.layout.activity_creator_channel_community_reply)
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_back)) assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_back))
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_more))
assertNotNull(detail.findViewById<TextView>(R.id.tv_creator_channel_community_detail_comment_header)) assertNotNull(detail.findViewById<TextView>(R.id.tv_creator_channel_community_detail_comment_header))
assertNotNull(detail.findViewById<RecyclerView>(R.id.rv_creator_channel_community_detail_comments)) assertNotNull(detail.findViewById<RecyclerView>(R.id.rv_creator_channel_community_detail_comments))
assertNotNull(detail.findViewById<View>(R.id.layout_creator_channel_community_detail_paywall)) assertNotNull(detail.findViewById<View>(R.id.layout_creator_channel_community_detail_paywall))
@@ -39,6 +41,9 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_send)) assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_send))
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_edit_cancel)) assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_edit_cancel))
assertRemovedId("tv_creator_channel_community_detail_title") assertRemovedId("tv_creator_channel_community_detail_title")
assertRemovedId("layout_creator_channel_community_detail_price")
assertRemovedId("tv_creator_channel_community_detail_price")
assertRemovedId("tv_creator_channel_community_detail_purchased")
assertRemovedId("tv_creator_channel_community_detail_comment_unavailable") assertRemovedId("tv_creator_channel_community_detail_comment_unavailable")
assertNotNull(reply.findViewById<ImageButton>(R.id.btn_creator_channel_community_reply_back)) assertNotNull(reply.findViewById<ImageButton>(R.id.btn_creator_channel_community_reply_back))
@@ -156,6 +161,23 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
assertTrue(detailActivity.contains("btn_audio_content_play")) assertTrue(detailActivity.contains("btn_audio_content_play"))
} }
@Test
fun `상세 게시글 더보기는 기존 관리 신고 UI와 수정 launcher를 재사용한다`() {
val detailActivity = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/" +
"CreatorChannelCommunityDetailActivity.kt"
).readText()
assertTrue(detailActivity.contains("CreatorCommunityPostMenuBottomSheetDialog"))
assertTrue(detailActivity.contains("CreatorCommunityReportDialog"))
assertTrue(detailActivity.contains("CreatorCommunityModifyActivity"))
assertTrue(detailActivity.contains("modifyLauncher"))
assertTrue(detailActivity.contains("Constants.EXTRA_COMMUNITY_POST_ID"))
assertTrue(detailActivity.contains("btnCreatorChannelCommunityDetailMore"))
assertFalse(detailActivity.contains("layoutCreatorChannelCommunityDetailPrice"))
assertFalse(detailActivity.contains("tvCreatorChannelCommunityDetailPurchased"))
}
@Test @Test
fun `댓글 답글 Adapter는 PopupMenu 대신 regular font popup을 사용한다`() { fun `댓글 답글 Adapter는 PopupMenu 대신 regular font popup을 사용한다`() {
val commentAdapter = projectFile( val commentAdapter = projectFile(

View File

@@ -31,6 +31,8 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.kotlin.any import org.mockito.kotlin.any
import org.mockito.kotlin.argThat import org.mockito.kotlin.argThat
import org.mockito.kotlin.eq
import org.mockito.kotlin.isNull
import org.mockito.kotlin.never import org.mockito.kotlin.never
import org.mockito.kotlin.times import org.mockito.kotlin.times
import org.mockito.kotlin.verify import org.mockito.kotlin.verify
@@ -469,6 +471,99 @@ class CreatorChannelCommunityDetailViewModelTest {
verify(repository, times(2)).getCommunityPostDetail(POST_ID, AUTH_TOKEN) verify(repository, times(2)).getCommunityPostDetail(POST_ID, AUTH_TOKEN)
} }
@Test
fun `게시글 더보기는 작성자 또는 무료 또는 구매 완료일 때만 표시한다`() {
SharedPreferenceManager.userId = 10L
val cases = listOf(
detailResponse(isCommentAvailable = false, creatorId = 10L, price = 300, existOrdered = false),
detailResponse(isCommentAvailable = false, creatorId = 999L, price = 0, existOrdered = false),
detailResponse(isCommentAvailable = false, creatorId = 999L, price = 300, existOrdered = true),
detailResponse(isCommentAvailable = false, creatorId = 999L, price = 300, existOrdered = false)
)
val results = cases.map { response ->
stubDetail(response)
viewModel.loadDetail(POST_ID)
(viewModel.detailStateLiveData.requireValue() as CreatorChannelCommunityDetailUiState.Content).post
}
assertTrue(results[0].isOwner)
assertTrue(results[0].showMore)
assertFalse(results[1].isOwner)
assertTrue(results[1].showMore)
assertTrue(results[2].showMore)
assertFalse(results[3].showMore)
}
@Test
fun `게시글 고정 성공은 반전 값을 요청하고 상세 재조회와 변경 이벤트를 노출한다`() {
whenever(repository.getCommunityPostDetail(POST_ID, AUTH_TOKEN)).thenReturn(
Single.just(ApiResponse(true, detailResponse(isCommentAvailable = false, creatorId = 10L, isPinned = false), null)),
Single.just(ApiResponse(true, detailResponse(isCommentAvailable = false, creatorId = 10L, isPinned = true), null))
)
whenever(legacyRepository.updateCommunityPostFixed(POST_ID, true, AUTH_TOKEN))
.thenReturn(Single.just(ApiResponse(true, Any(), null)))
viewModel.loadDetail(POST_ID)
viewModel.updateCommunityPostFixed()
val state = viewModel.detailStateLiveData.requireValue() as CreatorChannelCommunityDetailUiState.Content
assertTrue(state.post.isPinned)
assertTrue(viewModel.postChangedEventLiveData.requireValue() == true)
verify(legacyRepository).updateCommunityPostFixed(POST_ID, true, AUTH_TOKEN)
verify(repository, times(2)).getCommunityPostDetail(POST_ID, AUTH_TOKEN)
}
@Test
fun `게시글 삭제 성공은 삭제 이벤트를 노출한다`() {
stubDetail(detailResponse(isCommentAvailable = false, creatorId = 10L))
whenever(legacyRepository.modifyCommunityPost(isNull(), any(), eq(AUTH_TOKEN)))
.thenReturn(Single.just(ApiResponse(true, Any(), null)))
viewModel.loadDetail(POST_ID)
viewModel.deleteCommunityPost()
assertTrue(viewModel.postDeletedEventLiveData.requireValue() == true)
verify(legacyRepository).modifyCommunityPost(
isNull(),
argThat { requestBody ->
okio.Buffer().also(requestBody::writeTo).readUtf8().contains("\"isActive\":false")
},
eq(AUTH_TOKEN)
)
}
@Test
fun `게시글 신고 성공은 repository 호출과 toast만 노출한다`() {
stubDetail(detailResponse(isCommentAvailable = false, creatorId = 999L))
whenever(repository.reportCommunityPost(POST_ID, "스팸", AUTH_TOKEN))
.thenReturn(Single.just(ApiResponse(true, Any(), "접수됨")))
viewModel.loadDetail(POST_ID)
viewModel.reportCommunityPost("스팸")
verify(repository).reportCommunityPost(POST_ID, "스팸", AUTH_TOKEN)
assertEquals("접수됨", viewModel.toastLiveData.requireValue()?.consume()?.message)
assertFalse(viewModel.postChangedEventLiveData.requireValue() == true)
}
@Test
fun `게시글 mutation 진행 중에는 중복 요청을 막고 실패 시 성공 이벤트를 노출하지 않는다`() {
stubDetail(detailResponse(isCommentAvailable = false, creatorId = 10L))
val subject = PublishSubject.create<ApiResponse<Any>>()
whenever(legacyRepository.updateCommunityPostFixed(POST_ID, true, AUTH_TOKEN))
.thenReturn(subject.firstOrError())
viewModel.loadDetail(POST_ID)
viewModel.updateCommunityPostFixed()
viewModel.updateCommunityPostFixed()
subject.onNext(ApiResponse(false, null, "고정 실패"))
verify(legacyRepository, times(1)).updateCommunityPostFixed(POST_ID, true, AUTH_TOKEN)
assertEquals("고정 실패", viewModel.toastLiveData.requireValue()?.consume()?.message)
assertFalse(viewModel.postChangedEventLiveData.requireValue() == true)
}
@Test @Test
fun `댓글 UI 모델은 답글 화면 재포맷을 위해 원본 createdAtUtc를 보존한다`() { fun `댓글 UI 모델은 답글 화면 재포맷을 위해 원본 createdAtUtc를 보존한다`() {
stubDetail(detailResponse(isCommentAvailable = true, commentIds = listOf(11L))) stubDetail(detailResponse(isCommentAvailable = true, commentIds = listOf(11L)))
@@ -571,7 +666,8 @@ class CreatorChannelCommunityDetailViewModelTest {
existOrdered: Boolean = true, existOrdered: Boolean = true,
creatorId: Long = 100L, creatorId: Long = 100L,
commentIds: List<Long> = emptyList(), commentIds: List<Long> = emptyList(),
commentsHasNext: Boolean = false commentsHasNext: Boolean = false,
isPinned: Boolean = false
) = CreatorChannelCommunityPostDetailResponse( ) = CreatorChannelCommunityPostDetailResponse(
postId = POST_ID, postId = POST_ID,
creatorId = creatorId, creatorId = creatorId,
@@ -587,7 +683,7 @@ class CreatorChannelCommunityDetailViewModelTest {
likeCount = likeCount, likeCount = likeCount,
commentCount = 2, commentCount = 2,
isLiked = isLiked, isLiked = isLiked,
isPinned = false, isPinned = isPinned,
comments = commentsResponse(ids = commentIds, hasNext = commentsHasNext) comments = commentsResponse(ids = commentIds, hasNext = commentsHasNext)
) )

View File

@@ -11,6 +11,7 @@ import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.calculateCreatorChannelCommunityGridItemSize import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.calculateCreatorChannelCommunityGridItemSize
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertSame import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@@ -62,7 +63,7 @@ class CreatorChannelCommunityFragmentLayoutTest {
} }
@Test @Test
fun `커뮤니티 list item layout은 프로필 본문 이미지 잠금 재생 반응 owner 영역을 제공한다`() { fun `커뮤니티 list item layout은 프로필 본문 이미지 잠금 반응과 작성자 금액을 제공한다`() {
val item = inflateView(R.layout.item_creator_channel_community_list) val item = inflateView(R.layout.item_creator_channel_community_list)
val itemLayout = projectFile("app/src/main/res/layout/item_creator_channel_community_list.xml").readText() val itemLayout = projectFile("app/src/main/res/layout/item_creator_channel_community_list.xml").readText()
val imageContainer = requireNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_image_container)) val imageContainer = requireNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_image_container))
@@ -77,24 +78,29 @@ class CreatorChannelCommunityFragmentLayoutTest {
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_image)) assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_image))
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_locked_overlay)) assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_locked_overlay))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_locked_price)) assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_locked_price))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_play))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_comment_count)) assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_comment_count))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_like_count)) assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_like_count))
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions)) assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions))
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_top_price)) assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_top_price))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_owner_more))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_top_price)) assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_top_price))
val moreButton = requireNotNull(item.findViewById<View>(R.id.btn_creator_channel_community_list_more))
assertSame(imageContainer, (lockIcon.parent as View).parent) assertSame(imageContainer, (lockIcon.parent as View).parent)
assertSame(imageContainer, (lockedPrice.parent as View).parent) assertSame(imageContainer, ((lockedPrice.parent as View).parent as View).parent)
assertSame( assertSame(
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions), item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions),
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_price).parent item.findViewById<View>(R.id.layout_creator_channel_community_list_top_price).parent
) )
assertSame(
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions),
moreButton.parent
)
assertTrue(itemLayout.contains("android:id=\"@+id/layout_creator_channel_community_list_locked_overlay\"")) assertTrue(itemLayout.contains("android:id=\"@+id/layout_creator_channel_community_list_locked_overlay\""))
assertTrue(itemLayout.contains("android:id=\"@+id/tv_creator_channel_community_list_locked_price\"")) assertTrue(itemLayout.contains("android:id=\"@+id/tv_creator_channel_community_list_locked_price\""))
assertTrue(itemLayout.contains("android:background=\"@drawable/bg_creator_channel_community_price\"")) assertTrue(itemLayout.contains("android:background=\"@drawable/bg_creator_channel_community_price\""))
assertTrue(itemLayout.contains("android:drawableStart=\"@drawable/ic_bar_cash\"")) assertFalse(itemLayout.contains("android:drawableStart=\"@drawable/ic_bar_cash\""))
assertTrue(itemLayout.contains("android:id=\"@+id/iv_creator_channel_community_list_play\"")) assertTrue(itemLayout.contains("android:src=\"@drawable/ic_bar_cash\""))
assertFalse(itemLayout.contains("iv_creator_channel_community_list_play"))
assertFalse(itemLayout.contains("iv_creator_channel_community_list_owner_more"))
} }
@Test @Test
@@ -138,7 +144,7 @@ class CreatorChannelCommunityFragmentLayoutTest {
} }
@Test @Test
fun `커뮤니티 fragment source는 pagination view mode owner padding stopContent 계약을 사용한다`() { fun `커뮤니티 fragment source는 pagination view mode owner padding과 상세 이동 계약을 사용한다`() {
val fragment = projectFile( val fragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt" "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt"
).readText() ).readText()
@@ -161,8 +167,10 @@ class CreatorChannelCommunityFragmentLayoutTest {
assertTrue(fragment.contains("calculateCreatorChannelCommunityGridItemSize(")) assertTrue(fragment.contains("calculateCreatorChannelCommunityGridItemSize("))
assertTrue(fragment.contains("viewModel.consumePaginationErrorMessage()")) assertTrue(fragment.contains("viewModel.consumePaginationErrorMessage()"))
assertTrue(fragment.contains("applyOwnerCtaPadding")) assertTrue(fragment.contains("applyOwnerCtaPadding"))
assertTrue(fragment.contains("pauseContent")) assertFalse(fragment.contains("CreatorCommunityMediaPlayerManager"))
assertTrue(fragment.contains("stopContent")) assertFalse(fragment.contains("toggleCommunityAudio"))
assertFalse(fragment.contains("onPlayClick"))
assertFalse(fragment.contains("onCreatorChannelCommunityOwnerMoreClicked"))
} }
@Test @Test
@@ -181,19 +189,25 @@ class CreatorChannelCommunityFragmentLayoutTest {
assertTrue(listAdapter.contains("showComment") && listAdapter.contains("isVisible = item.showComment")) assertTrue(listAdapter.contains("showComment") && listAdapter.contains("isVisible = item.showComment"))
assertTrue(listAdapter.contains("isLocked")) assertTrue(listAdapter.contains("isLocked"))
assertTrue(gridAdapter.contains("isLocked")) assertTrue(gridAdapter.contains("isLocked"))
assertTrue(listAdapter.contains("showPlayButton"))
assertTrue(listAdapter.contains("showOwnerMore"))
assertTrue(listAdapter.contains("isPlayingContent"))
assertTrue(listAdapter.contains("R.drawable.ic_player_pause"))
assertTrue(listAdapter.contains("R.drawable.ic_new_player_play"))
assertTrue(listAdapter.contains("private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}")) assertTrue(listAdapter.contains("private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}"))
assertTrue(listAdapter.contains("private val onMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {}"))
assertTrue(listAdapter.contains("root.setOnClickListener { onPostClick(item) }")) assertTrue(listAdapter.contains("root.setOnClickListener { onPostClick(item) }"))
assertTrue(listAdapter.contains("ivCreatorChannelCommunityListPlay.setOnClickListener { onPlayClick(item) }")) assertTrue(
assertTrue(listAdapter.contains("onOwnerMoreClick(item)")) listAdapter.contains(
assertTrue(listAdapter.contains("ivCreatorChannelCommunityListOwnerMore.setOnClickListener { onOwnerMoreClick(item) }")) "btnCreatorChannelCommunityListMore.setOnClickListener { onMoreClick(item.menuItem) }"
)
)
assertTrue(listAdapter.contains("item.showOwnerTopPrice || item.menuItem.showMore"))
assertFalse(listAdapter.contains("onPlayClick"))
assertFalse(listAdapter.contains("onOwnerMoreClick"))
assertFalse(listAdapter.contains("isPlayingContent"))
assertFalse(listAdapter.contains("showPlayButton"))
assertFalse(listAdapter.contains("showOwnerMore"))
assertTrue(gridAdapter.contains("private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}")) assertTrue(gridAdapter.contains("private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}"))
assertTrue(gridAdapter.contains("root.setOnClickListener { onPostClick(item) }")) assertTrue(gridAdapter.contains("root.setOnClickListener { onPostClick(item) }"))
assertTrue(listAdapter.contains("tvCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked")) assertFalse(gridAdapter.contains("onMoreClick"))
assertFalse(gridAdapter.contains("CreatorChannelCommunityMenuItem"))
assertTrue(listAdapter.contains("layoutCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked"))
assertTrue(listAdapter.contains("layoutCreatorChannelCommunityListTopPrice.isVisible = item.showOwnerTopPrice")) assertTrue(listAdapter.contains("layoutCreatorChannelCommunityListTopPrice.isVisible = item.showOwnerTopPrice"))
assertTrue(!listAdapter.contains("item.isLocked || item.showOwnerTopPrice")) assertTrue(!listAdapter.contains("item.isLocked || item.showOwnerTopPrice"))
assertTrue(gridAdapter.contains(".asBitmap()")) assertTrue(gridAdapter.contains(".asBitmap()"))
@@ -211,8 +225,17 @@ class CreatorChannelCommunityFragmentLayoutTest {
).readText() ).readText()
assertTrue(fragment.contains("onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) }")) assertTrue(fragment.contains("onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) }"))
assertTrue(fragment.contains("onMoreClick = { item -> host.onCreatorChannelCommunityMoreClicked(item) }"))
assertTrue(fragment.contains("private val gridAdapter = CreatorChannelCommunityGridAdapter(")) assertTrue(fragment.contains("private val gridAdapter = CreatorChannelCommunityGridAdapter("))
assertTrue(fragment.contains("fun onCreatorChannelCommunityPostClicked(postId: Long)")) assertTrue(fragment.contains("fun onCreatorChannelCommunityPostClicked(postId: Long)"))
assertTrue(fragment.contains("fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)"))
assertFalse(fragment.contains("onPlayClick"))
assertFalse(fragment.contains("onCreatorChannelCommunityOwnerMoreClicked"))
assertFalse(
fragment.substringAfter("private val gridAdapter = CreatorChannelCommunityGridAdapter(")
.substringBefore(")\n private var currentContentState")
.contains("onMoreClick")
)
} }
@Test @Test

View File

@@ -66,7 +66,7 @@ class CreatorChannelCommunityMapperTest {
} }
@Test @Test
fun `유료 미구매 타인 게시글은 잠금 상태이고 play button을 숨긴다`() { fun `유료 미구매 타인 게시글은 잠금 상태이고 이미지를 숨긴다`() {
val item = listOf( val item = listOf(
communityPost(price = 100, existOrdered = false, imageUrl = "image.png", audioUrl = "audio.mp3") communityPost(price = 100, existOrdered = false, imageUrl = "image.png", audioUrl = "audio.mp3")
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single() ).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single()
@@ -74,7 +74,6 @@ class CreatorChannelCommunityMapperTest {
assertTrue(item.isLocked) assertTrue(item.isLocked)
assertNull(item.imageUrl) assertNull(item.imageUrl)
assertEquals(CreatorChannelCommunityImageMode.LockedGray, item.imageMode) assertEquals(CreatorChannelCommunityImageMode.LockedGray, item.imageMode)
assertFalse(item.showPlayButton)
} }
@Test @Test
@@ -93,7 +92,7 @@ class CreatorChannelCommunityMapperTest {
} }
@Test @Test
fun `고정 게시글 여부는 owner more 정책에서 사용할 수 있도록 UI 모델에 보존한다`() { fun `고정 게시글 여부는 UI 모델에 보존한다`() {
val pinnedItem = listOf(communityPost(isPinned = true)) val pinnedItem = listOf(communityPost(isPinned = true))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L) .toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L)
.single() .single()
@@ -108,33 +107,32 @@ class CreatorChannelCommunityMapperTest {
} }
@Test @Test
fun `본인 또는 구매한 사용자는 이미지와 오디오가 있으면 play button을 표시한다`() { fun `메뉴 권한은 작성자 무료 구매 여부에 따라 계산하고 고정 상태를 보존한다`() {
val ownerItem = listOf( val authorPaidNotOrdered = listOf(communityPost(price = 100, existOrdered = false, isPinned = true))
communityPost(price = 100, existOrdered = false, imageUrl = "image.png", audioUrl = "audio.mp3") .toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L)
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L).single() .single()
val orderedItem = listOf( val visitorFree = listOf(communityPost(price = 0, existOrdered = false))
communityPost(price = 100, existOrdered = true, imageUrl = "image.png", audioUrl = "audio.mp3") .toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L)
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single() .single()
val visitorPaidOrdered = listOf(communityPost(price = 100, existOrdered = true))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L)
.single()
val visitorPaidNotOrdered = listOf(communityPost(price = 100, existOrdered = false))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L)
.single()
assertFalse(ownerItem.isLocked) assertTrue(authorPaidNotOrdered.menuItem.isOwner)
assertTrue(ownerItem.showPlayButton) assertTrue(authorPaidNotOrdered.menuItem.showMore)
assertFalse(orderedItem.isLocked) assertTrue(authorPaidNotOrdered.menuItem.isPinned)
assertTrue(orderedItem.showPlayButton) assertTrue(authorPaidNotOrdered.isPinned)
assertTrue(visitorFree.menuItem.showMore)
assertTrue(visitorPaidOrdered.menuItem.showMore)
assertFalse(visitorPaidNotOrdered.menuItem.isOwner)
assertFalse(visitorPaidNotOrdered.menuItem.showMore)
} }
@Test @Test
fun `오디오 또는 이미지가 없으면 play button을 숨긴다`() { fun `본인 채널에 본인이 쓴 유료 게시글에서만 top price를 표시한다`() {
val items = listOf(
communityPost(postId = 1L, imageUrl = null, audioUrl = "audio.mp3"),
communityPost(postId = 2L, imageUrl = "image.png", audioUrl = null),
communityPost(postId = 3L, imageUrl = "image.png", audioUrl = " ")
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L)
assertEquals(listOf(false, false, false), items.map { it.showPlayButton })
}
@Test
fun `본인 채널에 본인이 쓴 게시글에서만 owner more와 유료 top price를 표시한다`() {
val ownerPaid = listOf(communityPost(price = 100, creatorId = 10L)) val ownerPaid = listOf(communityPost(price = 100, creatorId = 10L))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L) .toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L)
.single() .single()
@@ -148,13 +146,9 @@ class CreatorChannelCommunityMapperTest {
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L) .toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L)
.single() .single()
assertTrue(ownerPaid.showOwnerMore)
assertTrue(ownerPaid.showOwnerTopPrice) assertTrue(ownerPaid.showOwnerTopPrice)
assertTrue(ownerFree.showOwnerMore)
assertFalse(ownerFree.showOwnerTopPrice) assertFalse(ownerFree.showOwnerTopPrice)
assertFalse(otherCreator.showOwnerMore)
assertFalse(otherCreator.showOwnerTopPrice) assertFalse(otherCreator.showOwnerTopPrice)
assertFalse(otherChannel.showOwnerMore)
assertFalse(otherChannel.showOwnerTopPrice) assertFalse(otherChannel.showOwnerTopPrice)
} }

View File

@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.v2.main.home
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.drawable.ColorDrawable
import android.text.Spanned import android.text.Spanned
import android.text.TextUtils import android.text.TextUtils
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
@@ -659,6 +658,19 @@ class HomeMainFragmentLayoutTest {
assertEquals("https://example.com/audio.m4a", item.toUiModel().item.audioUrl) assertEquals("https://example.com/audio.m4a", item.toUiModel().item.audioUrl)
} }
@Test
fun `popular community mapper preserves locked item reactions`() {
val item = popularCommunityData(
audioUrl = null,
price = 100,
existOrdered = false
).toUiModel().item
assertTrue(item.isLocked)
assertTrue(item.showReaction)
assertTrue(item.showComment)
}
@Test @Test
fun `home relative time formatter converts active creator utc timestamp to relative time text`() { fun `home relative time formatter converts active creator utc timestamp to relative time text`() {
val context = ApplicationProvider.getApplicationContext<Context>() val context = ApplicationProvider.getApplicationContext<Context>()
@@ -1509,7 +1521,9 @@ class HomeMainFragmentLayoutTest {
private fun popularCommunityData( private fun popularCommunityData(
audioUrl: String?, audioUrl: String?,
createdAt: String = "2분 전" createdAt: String = "2분 전",
price: Int = 0,
existOrdered: Boolean = false
): HomePopularCommunityPostItem { ): HomePopularCommunityPostItem {
return HomePopularCommunityPostItem( return HomePopularCommunityPostItem(
postId = 1L, postId = 1L,
@@ -1519,11 +1533,11 @@ class HomeMainFragmentLayoutTest {
imageUrl = null, imageUrl = null,
audioUrl = audioUrl, audioUrl = audioUrl,
content = "본문", content = "본문",
price = 0, price = price,
createdAt = createdAt, createdAt = createdAt,
likeCount = 6L, likeCount = 6L,
commentCount = 5L, commentCount = 5L,
existOrdered = false existOrdered = existOrdered
) )
} }

View File

@@ -1,6 +1,8 @@
package kr.co.vividnext.sodalive.v2.widget.feed package kr.co.vividnext.sodalive.v2.widget.feed
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import org.junit.Test import org.junit.Test
@@ -102,4 +104,71 @@ class FeedItemTest {
assertEquals(5, item.commentCount) assertEquals(5, item.commentCount)
assertEquals(6, item.likeCount) assertEquals(6, item.likeCount)
} }
@Test
fun `locked paid community item hides reactions and comments`() {
val item = communityItem(
price = 100,
existOrdered = false,
hideReactionWhenLocked = true
)
assertTrue(item.isLocked)
assertFalse(item.showReaction)
assertFalse(item.showComment)
}
@Test
fun `locked paid community item preserves reactions by default`() {
val item = communityItem(price = 100, existOrdered = false)
assertTrue(item.isLocked)
assertTrue(item.showReaction)
assertTrue(item.showComment)
}
@Test
fun `free community item with comments disabled keeps likes only`() {
val item = communityItem(price = 0, isCommentAvailable = false)
assertFalse(item.isLocked)
assertTrue(item.showReaction)
assertFalse(item.showComment)
}
@Test
fun `community item preserves pinned state`() {
val item = communityItem(isPinned = true)
assertTrue(item.isPinned)
}
@Test
fun `community item hides more action by default`() {
assertFalse(communityItem().showMore)
}
private fun communityItem(
price: Int = 0,
existOrdered: Boolean = false,
isPinned: Boolean = false,
isCommentAvailable: Boolean = true,
hideReactionWhenLocked: Boolean = false
) = FeedItem.Community(
feedId = "feed-community-policy",
creatorId = "creator-1",
creatorName = "크리에이터 이름",
creatorImageUrl = "https://example.com/profile.png",
postId = "post-policy",
bodyText = "커뮤니티 본문",
keywordText = "",
createdAtText = "2분 전",
commentCount = 5,
likeCount = 6,
price = price,
existOrdered = existOrdered,
isPinned = isPinned,
isCommentAvailable = isCommentAvailable,
hideReactionWhenLocked = hideReactionWhenLocked
)
} }

View File

@@ -63,6 +63,28 @@ class FeedViewTest {
assertEquals(View.GONE, view.findViewById<TextView>(R.id.tv_feed_community_body).visibility) assertEquals(View.GONE, view.findViewById<TextView>(R.id.tv_feed_community_body).visibility)
} }
@Test
fun `community more click은 현재 bind된 item만 전달한다`() {
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
val first = sampleCommunityItem(bodyText = "첫 번째", keywordText = "", showMore = true)
val second = sampleCommunityItem(bodyText = "두 번째", keywordText = "", showMore = true)
var clickedItem: FeedItem.Community? = null
var rootClickedItem: FeedItem? = null
view.setOnMoreClick { clickedItem = it }
view.setOnFeedClick { rootClickedItem = it }
view.bind(first)
view.bind(second)
view.findViewById<View>(R.id.btn_feed_community_more).performClick()
assertSame(second, clickedItem)
assertEquals(null, rootClickedItem)
view.performClick()
assertSame(second, rootClickedItem)
}
@Test @Test
fun `community layout uses runtime image height instead of fixed crop height`() { fun `community layout uses runtime image height instead of fixed crop height`() {
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community) val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
@@ -201,7 +223,8 @@ class FeedViewTest {
audioUrl: String? = null, audioUrl: String? = null,
price: Int = 0, price: Int = 0,
existOrdered: Boolean = false, existOrdered: Boolean = false,
showKeyword: Boolean = true showKeyword: Boolean = true,
showMore: Boolean = false
) = FeedItem.Community( ) = FeedItem.Community(
feedId = "feed-community-1", feedId = "feed-community-1",
creatorId = "creator-1", creatorId = "creator-1",
@@ -217,7 +240,8 @@ class FeedViewTest {
audioUrl = audioUrl, audioUrl = audioUrl,
price = price, price = price,
existOrdered = existOrdered, existOrdered = existOrdered,
showKeyword = showKeyword showKeyword = showKeyword,
showMore = showMore
) )
private fun Int.dpToPx(): Int { private fun Int.dpToPx(): Int {