From 8b8b384b291f869761d31d5ef7d9af8b9118c9cb Mon Sep 17 00:00:00 2001 From: klaus Date: Tue, 11 Aug 2026 00:10:46 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B5=AC=EB=A7=A4=EC=99=80=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=EC=9D=84=20=EC=A0=95=EB=A6=AC=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/community/action/CommunityChange.kt | 8 - .../creator/channel/CreatorChannelActivity.kt | 250 +++++++++++------- .../channel/CreatorChannelHomeFragment.kt | 9 +- .../CreatorChannelCommunityFragment.kt | 24 +- .../CreatorChannelCommunityDetailActivity.kt | 57 +++- .../CreatorChannelCommunityDetailViewModel.kt | 149 +++++++++-- .../model/CreatorChannelCommunityMappers.kt | 14 +- .../model/CreatorChannelCommunityMenuItem.kt | 16 ++ .../model/CreatorChannelCommunityUiModels.kt | 6 +- .../ui/CreatorChannelCommunityListAdapter.kt | 28 +- .../channel/data/CreatorChannelHomeModels.kt | 4 +- .../channel/data/CreatorChannelRepository.kt | 5 + .../model/CreatorChannelHomeMappers.kt | 3 +- .../model/CreatorChannelHomeUiModels.kt | 5 +- .../ui/CreatorChannelHomeSectionAdapter.kt | 31 ++- .../v2/widget/feed/FeedCommunityView.kt | 33 ++- .../sodalive/v2/widget/feed/FeedItem.kt | 17 +- ...ivity_creator_channel_community_detail.xml | 53 +--- .../item_creator_channel_community_list.xml | 54 ++-- .../main/res/layout/view_feed_community.xml | 42 ++- .../community/action/CommunityChangeTest.kt | 9 - .../CreatorChannelActivitySourceTest.kt | 250 ++++++++++++------ .../channel/CreatorChannelHomeMapperTest.kt | 14 +- .../channel/CreatorChannelHomeModelsTest.kt | 30 ++- ...annelCommunityDetailReplyUiContractTest.kt | 22 ++ ...atorChannelCommunityDetailViewModelTest.kt | 100 ++++++- ...eatorChannelCommunityFragmentLayoutTest.kt | 59 +++-- .../CreatorChannelCommunityMapperTest.kt | 54 ++-- .../main/home/HomeMainFragmentLayoutTest.kt | 22 +- .../sodalive/v2/widget/feed/FeedItemTest.kt | 69 +++++ .../sodalive/v2/widget/feed/FeedViewTest.kt | 28 +- 31 files changed, 1055 insertions(+), 410 deletions(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMenuItem.kt diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/community/action/CommunityChange.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/community/action/CommunityChange.kt index f7928289..cebd93a1 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/community/action/CommunityChange.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/community/action/CommunityChange.kt @@ -6,12 +6,4 @@ sealed interface CommunityChange { data object Created : CommunityChange data object Updated : CommunityChange - - data class Deleted( - val postId: Long - ) : CommunityChange - - data class PinChanged( - val postId: Long - ) : CommunityChange } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt index 71425817..5e178bbe 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt @@ -33,6 +33,7 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelBinding import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog +import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityReportDialog import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyActivity import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.ModifyCommunityPostRequest import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity @@ -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.creator.channel.audio.CreatorChannelAudioFragment import kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragment -import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel +import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem 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.CreatorChannelScheduleResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse @@ -96,6 +98,7 @@ class CreatorChannelActivity : private val liveViewModel: LiveViewModel by inject() private val creatorCommunityRepository: CreatorCommunityRepository by inject() + private val creatorChannelRepository: CreatorChannelRepository by inject() private var creatorId: Long = 0L private var currentHeader: CreatorChannelHeaderUiModel? = null private var homeActionDelegate: CreatorChannelHomeFragment.HomeActionDelegate? = null @@ -107,6 +110,7 @@ class CreatorChannelActivity : private var isOwnerFabAnimating: Boolean = false private var isDonationFloatingButtonVisible: Boolean = false private var isFanTalkFixedPlusVisible: Boolean = false + private var isCommunityPostMutating: Boolean = false private lateinit var loadingDialog: LoadingDialog private val baseTitleBarHeight: Int by lazy { 60.dpToPx().toInt() } private val liveActionCoordinator: LiveActionCoordinator by lazy { @@ -126,13 +130,6 @@ class CreatorChannelActivity : resolveCommunityActivityResult(CommunityActivityResultSource.Write, result.resultCode) ) } - private val communityPostModifyLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - handleCommunityChange( - resolveCommunityActivityResult(CommunityActivityResultSource.Modify, result.resultCode) - ) - } private val communityDetailLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> @@ -140,6 +137,13 @@ class CreatorChannelActivity : resolveCommunityActivityResult(CommunityActivityResultSource.Detail, result.resultCode) ) } + private val communityModifyLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + handleCommunityChange( + resolveCommunityActivityResult(CommunityActivityResultSource.Modify, result.resultCode) + ) + } private val fanTalkWriteLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> @@ -627,6 +631,142 @@ class CreatorChannelActivity : 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() { updateViewPagerHeight { postCheckCreatorChannelCurrentTabNeedsMore() @@ -697,93 +837,6 @@ class CreatorChannelActivity : ).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() { findCommunityFragment()?.onCreatorChannelCommunityRefreshRequested() } @@ -795,9 +848,10 @@ class CreatorChannelActivity : homeActionDelegate?.refreshHome() refreshCreatorChannelCommunity() } - CommunityChange.Updated, - is CommunityChange.Deleted, - is CommunityChange.PinChanged -> refreshCreatorChannelCommunity() + CommunityChange.Updated -> { + homeActionDelegate?.refreshHome() + refreshCreatorChannelCommunity() + } } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt index 50750979..ae49347c 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt @@ -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.CreatorChannelScheduleResponse 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.CreatorChannelHomeUiState import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab @@ -28,7 +29,8 @@ class CreatorChannelHomeFragment : BaseFragment host.onCreatorChannelCommunityPostClicked(item.postId) }, - onPlayClick = { item -> toggleCommunityAudio(item) }, - onOwnerMoreClick = { item -> host.onCreatorChannelCommunityOwnerMoreClicked(item) }, - isPlayingContent = { postId -> mediaPlayerManager?.isPlayingContent(postId) == true } + onMoreClick = { item -> host.onCreatorChannelCommunityMoreClicked(item) } ) private val gridAdapter = CreatorChannelCommunityGridAdapter( onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) } ) - private var mediaPlayerManager: CreatorCommunityMediaPlayerManager? = null private var currentContentState: CreatorChannelCommunityUiState.Content? = null private var lastContentLayoutKey: CreatorChannelCommunityContentLayoutKey? = null private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L } @@ -45,7 +40,6 @@ class CreatorChannelCommunityFragment : BaseFragment + if (result.resultCode == Activity.RESULT_OK && postId > 0L) { + setResult(Activity.RESULT_OK) + viewModel.loadDetail(postId) + } + } private val replyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK && postId > 0L) { setResult(Activity.RESULT_OK) @@ -65,6 +75,11 @@ class CreatorChannelCommunityDetailActivity : BaseActivity + if (deleted == true) { + setResult(Activity.RESULT_OK) + viewModel.consumePostDeletedEvent() + finish() + } + } viewModel.toastLiveData.observe(this) { event -> event.consume()?.let { toast -> val message = toast.message ?: toast.resId?.let(::getString) @@ -150,6 +172,7 @@ class CreatorChannelCommunityDetailActivity : BaseActivity 0 layoutCreatorChannelCommunityDetailImageContainer.isVisible = post.imageUrl != null || post.showPaywall layoutCreatorChannelCommunityDetailImageContainer.layoutParams = layoutCreatorChannelCommunityDetailImageContainer.layoutParams.apply { @@ -296,6 +316,37 @@ class CreatorChannelCommunityDetailActivity : BaseActivity get() = _postChangedEventLiveData + private val _postDeletedEventLiveData = MutableLiveData() + val postDeletedEventLiveData: LiveData + get() = _postDeletedEventLiveData + private val _toastLiveData = MutableLiveData>() val toastLiveData: LiveData> get() = _toastLiveData @@ -49,6 +57,7 @@ class CreatorChannelCommunityDetailViewModel( private var isSendingComment = false private var isModifyingComment = false private var isPurchasingPost = false + private var isMutatingPost = false fun loadDetail(postId: Long) { if (postId <= 0) return @@ -181,6 +190,10 @@ class CreatorChannelCommunityDetailViewModel( _postChangedEventLiveData.value = false } + fun consumePostDeletedEvent() { + _postDeletedEventLiveData.value = false + } + fun startCommentEdit(commentId: Long, comment: String) { val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: 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( content: CreatorChannelCommunityDetailUiState.Content, commentId: Long, @@ -425,25 +529,30 @@ class CreatorChannelCommunityDetailViewModel( ) } - private fun CreatorChannelCommunityPostDetailResponse.toUiModel() = CreatorChannelCommunityPostDetailUiModel( - postId = postId, - creatorId = creatorId, - creatorNickname = creatorNickname, - creatorProfileUrl = creatorProfileUrl, - createdAtText = relativeTimeTextFormatter.format(createdAtUtc), - content = content, - imageUrl = imageUrl.takeUnless { isLocked() }, - audioUrl = audioUrl.takeUnless { isLocked() }, - price = price, - existOrdered = existOrdered, - likeCount = likeCount, - commentCount = commentCount, - isLiked = isLiked, - isPinned = isPinned, - isLocked = isLocked(), - showPaywall = isLocked(), - showReaction = !isLocked() - ) + private fun CreatorChannelCommunityPostDetailResponse.toUiModel(): CreatorChannelCommunityPostDetailUiModel { + val isOwner = creatorId == SharedPreferenceManager.userId + return CreatorChannelCommunityPostDetailUiModel( + postId = postId, + creatorId = creatorId, + creatorNickname = creatorNickname, + creatorProfileUrl = creatorProfileUrl, + createdAtText = relativeTimeTextFormatter.format(createdAtUtc), + content = content, + imageUrl = imageUrl.takeUnless { isLocked() }, + audioUrl = audioUrl.takeUnless { isLocked() }, + price = price, + existOrdered = existOrdered, + likeCount = likeCount, + commentCount = commentCount, + isLiked = isLiked, + isPinned = isPinned, + isOwner = isOwner, + showMore = isOwner || price == 0 || existOrdered, + isLocked = isLocked(), + showPaywall = isLocked(), + showReaction = !isLocked() + ) + } private fun CreatorChannelCommunityPostDetailResponse.isLocked(): Boolean { return price > 0 && !existOrdered && creatorId != SharedPreferenceManager.userId @@ -526,6 +635,8 @@ data class CreatorChannelCommunityPostDetailUiModel( val commentCount: Int, val isLiked: Boolean, val isPinned: Boolean, + val isOwner: Boolean, + val showMore: Boolean, val isLocked: Boolean, val showPaywall: Boolean, val showReaction: Boolean diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMappers.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMappers.kt index 560a9dae..73e58a94 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMappers.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMappers.kt @@ -21,7 +21,6 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel( val isLocked = price > 0 && !existOrdered && !isOwner val showOwnerActions = isOwner && creatorId == currentUserId val visibleImageUrl = imageUrl.takeUnless { isLocked } - val showPlayButton = !isLocked && !audioUrl.isNullOrBlank() && !visibleImageUrl.isNullOrBlank() return CreatorChannelCommunityPostUiModel( postId = postId, creatorId = creatorId, @@ -30,7 +29,6 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel( createdAtText = relativeTimeTextFormatter.format(createdAtUtc), content = content, imageUrl = visibleImageUrl, - audioUrl = audioUrl, price = price, existOrdered = existOrdered, likeCount = likeCount, @@ -40,11 +38,17 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel( showNotice = isPinned, isPinned = isPinned, isLocked = isLocked, - showOwnerMore = showOwnerActions, showOwnerTopPrice = showOwnerActions && price > 0, - showPlayButton = showPlayButton, 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 + ) ) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMenuItem.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMenuItem.kt new file mode 100644 index 00000000..0887c579 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMenuItem.kt @@ -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 +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityUiModels.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityUiModels.kt index 4b9cc011..9c1e72fc 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityUiModels.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityUiModels.kt @@ -32,7 +32,6 @@ data class CreatorChannelCommunityPostUiModel( val createdAtText: String, val content: String, val imageUrl: String?, - val audioUrl: String?, val price: Int, val existOrdered: Boolean, val likeCount: Int, @@ -42,9 +41,8 @@ data class CreatorChannelCommunityPostUiModel( val showNotice: Boolean, val isPinned: Boolean, val isLocked: Boolean, - val showOwnerMore: Boolean, val showOwnerTopPrice: Boolean, - val showPlayButton: Boolean, val gridPreviewText: String, - val imageMode: CreatorChannelCommunityImageMode + val imageMode: CreatorChannelCommunityImageMode, + val menuItem: CreatorChannelCommunityMenuItem ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityListAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityListAdapter.kt index accbaf9c..f8337f3c 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityListAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityListAdapter.kt @@ -19,14 +19,13 @@ import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityListBindi import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.moneyFormat +import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel import kr.co.vividnext.sodalive.v2.widget.feed.calculateFeedCommunityImageHeight class CreatorChannelCommunityListAdapter( private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}, - private val onPlayClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}, - private val onOwnerMoreClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}, - private val isPlayingContent: (Long) -> Boolean = { false } + private val onMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {} ) : RecyclerView.Adapter() { private var items: List = emptyList() @@ -40,9 +39,7 @@ class CreatorChannelCommunityListAdapter( return ViewHolder( ItemCreatorChannelCommunityListBinding.inflate(LayoutInflater.from(parent.context), parent, false), onPostClick, - onPlayClick, - onOwnerMoreClick, - isPlayingContent + onMoreClick ) } @@ -55,9 +52,7 @@ class CreatorChannelCommunityListAdapter( class ViewHolder( private val binding: ItemCreatorChannelCommunityListBinding, private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit, - private val onPlayClick: (CreatorChannelCommunityPostUiModel) -> Unit, - private val onOwnerMoreClick: (CreatorChannelCommunityPostUiModel) -> Unit, - private val isPlayingContent: (Long) -> Boolean + private val onMoreClick: (CreatorChannelCommunityMenuItem) -> Unit ) : RecyclerView.ViewHolder(binding.root) { init { @@ -86,7 +81,7 @@ class CreatorChannelCommunityListAdapter( val visibleImageUrl = item.imageUrl.takeUnless { item.isLocked } layoutCreatorChannelCommunityListImageContainer.isVisible = - visibleImageUrl != null || item.isLocked || item.showPlayButton + visibleImageUrl != null || item.isLocked ivCreatorChannelCommunityListImage.isVisible = visibleImageUrl != null resetCommunityImageHeight() if (visibleImageUrl != null) { @@ -119,19 +114,14 @@ class CreatorChannelCommunityListAdapter( } layoutCreatorChannelCommunityListLockedOverlay.isVisible = item.isLocked ivCreatorChannelCommunityListLock.isVisible = item.isLocked - tvCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked + layoutCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked 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 tvCreatorChannelCommunityListTopPrice.text = item.price.moneyFormat() - ivCreatorChannelCommunityListOwnerMore.isVisible = item.showOwnerMore - ivCreatorChannelCommunityListOwnerMore.setOnClickListener { onOwnerMoreClick(item) } + btnCreatorChannelCommunityListMore.isVisible = item.menuItem.showMore + btnCreatorChannelCommunityListMore.setOnClickListener { onMoreClick(item.menuItem) } } private fun resetCommunityImageHeight() { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt index c96aa102..e57b4d52 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt @@ -103,7 +103,9 @@ data class CreatorChannelCommunityPostResponse( @SerializedName("dateUtc") val dateUtc: String, @SerializedName("existOrdered") val existOrdered: Boolean, @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 diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt index 92c086a4..ad7a3748 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt @@ -185,6 +185,11 @@ class CreatorChannelRepository( 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( request = PutModifyCheersRequest(cheersId = fanTalkId, isActive = false), token = token diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeMappers.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeMappers.kt index 1e3897db..27917101 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeMappers.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeMappers.kt @@ -18,7 +18,8 @@ fun CreatorChannelHomeResponse.toUiContent(currentMemberId: Long): CreatorChanne ?.let { add(CreatorChannelHomeSection.Schedules(it)) } audioContents.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.AudioContents(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)) introduce.takeIf { it.isNotBlank() }?.let { add(CreatorChannelHomeSection.Introduce(it)) } add(CreatorChannelHomeSection.Activity(activity)) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeUiModels.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeUiModels.kt index 87c4ddda..867f6a02 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeUiModels.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeUiModels.kt @@ -57,7 +57,10 @@ sealed interface CreatorChannelHomeSection { data class Schedules(val schedules: List) : CreatorChannelHomeSection data class AudioContents(val audioContents: List) : CreatorChannelHomeSection data class Series(val series: List) : CreatorChannelHomeSection - data class Communities(val communities: List) : CreatorChannelHomeSection + data class Communities( + val communities: List, + val currentMemberId: Long + ) : CreatorChannelHomeSection data class FanTalk(val fanTalk: CreatorChannelFanTalkSummaryResponse) : CreatorChannelHomeSection data class Introduce(val introduce: String) : CreatorChannelHomeSection data class Activity(val activity: CreatorChannelActivityResponse) : CreatorChannelHomeSection diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt index c13c3c2c..4475630b 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt @@ -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.CreatorChannelScheduleResponse 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.CreatorChannelTab import kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView @@ -43,7 +44,8 @@ class CreatorChannelHomeSectionAdapter( private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit = {}, private val onDonationClick: () -> Unit = {}, private val onSectionChevronClick: (CreatorChannelTab) -> Unit = {}, - private val onCommunityClick: (Long) -> Unit = {} + private val onCommunityClick: (Long) -> Unit = {}, + private val onCommunityMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {} ) : RecyclerView.Adapter() { private var items: List = emptyList() @@ -65,7 +67,8 @@ class CreatorChannelHomeSectionAdapter( onSeriesClick, onDonationClick, onSectionChevronClick, - onCommunityClick + onCommunityClick, + onCommunityMoreClick ) } @@ -83,7 +86,8 @@ class CreatorChannelHomeSectionAdapter( private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit, private val onDonationClick: () -> Unit, private val onSectionChevronClick: (CreatorChannelTab) -> Unit, - private val onCommunityClick: (Long) -> Unit + private val onCommunityClick: (Long) -> Unit, + private val onCommunityMoreClick: (CreatorChannelCommunityMenuItem) -> Unit ) : RecyclerView.ViewHolder(view) { private val title: TextView? = view.findViewById(R.id.tv_section_title) private val sectionTitleChevron: ImageView? = view.findViewById(R.id.iv_section_title_chevron) @@ -366,6 +370,14 @@ class CreatorChannelHomeSectionAdapter( communityMoreButton?.setOnClickListener { onSectionChevronClick(CreatorChannelTab.Community) } val visibleCommunities = item.communities.take(MAX_COMMUNITY_ITEM_COUNT) 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( itemView.resources.configuration.screenWidthDp ) @@ -381,7 +393,8 @@ class CreatorChannelHomeSectionAdapter( ) ) setHideEmptyTextRows(true) - bind(community.toFeedCommunityItem()) + bind(community.toFeedCommunityItem(showMore = menuItem.showMore)) + setOnMoreClick { onCommunityMoreClick(menuItem) } } bindCommunityImages(row, community) 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(), creatorId = creatorId.toString(), creatorName = creatorNickname, @@ -458,7 +473,11 @@ class CreatorChannelHomeSectionAdapter( audioUrl = audioUrl, price = price, existOrdered = existOrdered, - showKeyword = false + isPinned = isPinned, + isCommentAvailable = isCommentAvailable, + hideReactionWhenLocked = true, + showKeyword = false, + showMore = showMore ) private fun bindIntroduce(item: CreatorChannelHomeSection.Introduce) { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt index a1ff3c43..b92d9261 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt @@ -20,6 +20,8 @@ class FeedCommunityView @JvmOverloads constructor( ) : LinearLayout(context, attrs, defStyleAttr) { private var profileImage: ImageView? = null + private var noticeRow: View? = null + private var moreButton: View? = null private var creatorText: TextView? = null private var createdAtText: TextView? = null private var bodyText: TextView? = null @@ -27,15 +29,21 @@ class FeedCommunityView @JvmOverloads constructor( private var communityImage: ImageView? = null private var paidOverlay: View? = null private var priceText: TextView? = null + private var reactionRow: View? = null + private var commentIcon: ImageView? = null private var commentCountText: TextView? = null + private var likeIcon: ImageView? = null private var likeCountText: TextView? = null private var currentItem: FeedItem.Community? = null private var clickListener: ((FeedItem) -> Unit)? = null + private var moreClickListener: ((FeedItem.Community) -> Unit)? = null private var hideEmptyTextRows: Boolean = false override fun onFinishInflate() { super.onFinishInflate() + noticeRow = findViewById(R.id.ll_feed_community_notice) profileImage = findViewById(R.id.iv_feed_community_profile) + moreButton = findViewById(R.id.btn_feed_community_more) creatorText = findViewById(R.id.tv_feed_community_creator) createdAtText = findViewById(R.id.tv_feed_community_created_at) bodyText = findViewById(R.id.tv_feed_community_body) @@ -43,7 +51,10 @@ class FeedCommunityView @JvmOverloads constructor( communityImage = findViewById(R.id.iv_feed_community_image) paidOverlay = findViewById(R.id.ll_feed_community_paid_overlay) 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) + likeIcon = findViewById(R.id.iv_feed_community_like) likeCountText = findViewById(R.id.tv_feed_community_like_count) clipToOutline = true outlineProvider = roundedOutlineProvider(CARD_RADIUS_DP) @@ -59,7 +70,8 @@ class FeedCommunityView @JvmOverloads constructor( requireNotNull(createdAtText).text = item.createdAtText requireNotNull(bodyText).text = 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() requireNotNull(communityImageContainer).isVisible = hasImage || isLocked resetCommunityImageHeight() @@ -69,9 +81,15 @@ class FeedCommunityView @JvmOverloads constructor( } requireNotNull(paidOverlay).isVisible = isLocked requireNotNull(priceText).text = item.price.toString() + requireNotNull(reactionRow).isVisible = item.showReaction + requireNotNull(commentIcon).isVisible = item.showComment requireNotNull(commentCountText).text = item.commentCount.toString() + requireNotNull(commentCountText).isVisible = item.showComment + requireNotNull(likeIcon).isVisible = item.showReaction requireNotNull(likeCountText).text = item.likeCount.toString() + requireNotNull(likeCountText).isVisible = item.showReaction applyClickState(item) + applyMoreClickState(item) } fun profileImageView(): ImageView = requireNotNull(profileImage) @@ -116,6 +134,11 @@ class FeedCommunityView @JvmOverloads constructor( currentItem?.let(::applyClickState) } + fun setOnMoreClick(listener: ((FeedItem.Community) -> Unit)?) { + moreClickListener = listener + currentItem?.let(::applyMoreClickState) + } + fun setHideEmptyTextRows(hide: Boolean) { hideEmptyTextRows = hide currentItem?.let(::bind) @@ -130,6 +153,14 @@ class FeedCommunityView @JvmOverloads constructor( 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) { val currentLayoutParams = layoutParams layoutParams = if (currentLayoutParams == null) { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt index 21cbf5a4..00ddedf1 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt @@ -47,6 +47,19 @@ sealed class FeedItem(open val feedId: String, val variant: FeedVariant) { val audioUrl: String? = null, val price: Int = 0, val existOrdered: Boolean = false, - val showKeyword: Boolean = true - ) : FeedItem(feedId, FeedVariant.Community) + val isPinned: Boolean = false, + 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 + } } diff --git a/app/src/main/res/layout/activity_creator_channel_community_detail.xml b/app/src/main/res/layout/activity_creator_channel_community_detail.xml index 270fa755..dd9e1086 100644 --- a/app/src/main/res/layout/activity_creator_channel_community_detail.xml +++ b/app/src/main/res/layout/activity_creator_channel_community_detail.xml @@ -73,7 +73,7 @@ android:includeFontPadding="false" android:maxLines="1" 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_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile" tools:text="크리에이터" /> @@ -91,36 +91,17 @@ app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_community_detail_nickname" tools:text="2분 전" /> - - - - - - + tools:visibility="visible" /> - - - @@ -187,32 +187,34 @@ android:visibility="gone" tools:visibility="visible" /> - - + tools:visibility="visible"> - + + + + + + + + + + + + @@ -22,9 +48,10 @@ tools:src="@drawable/ic_launcher_background" /> + + 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 fun `follow notify source는 Phase 11 직접 팔로우 알림 액션을 연결한다`() { val source = projectFile( @@ -653,80 +811,12 @@ class CreatorChannelActivitySourceTest { assertTrue(source.contains("iconResId = R.drawable.ic_new_upload_community_post")) assertTrue(source.contains("textResId = R.string.creator_channel_owner_fab_community")) assertTrue(source.contains("CreatorChannelTab.Community.ordinal -> onOwnerFabCommunityClicked()")) - assertTrue(source.contains("private val communityPostModifyLauncher")) - assertTrue(source.contains("CreatorCommunityModifyActivity::class.java")) - assertTrue(source.contains("putExtra(Constants.EXTRA_COMMUNITY_POST_ID, item.postId)")) - assertTrue(source.contains("creatorCommunityRepository.updateCommunityPostFixed(")) - assertTrue(source.contains("isFixed = !item.isPinned")) - assertTrue(source.contains("creatorCommunityRepository.modifyCommunityPost(")) - assertTrue(source.contains("isActive = false")) + assertFalse(source.contains("communityPostModifyLauncher")) + assertFalse(source.contains("onCreatorChannelCommunityOwnerMoreClicked")) assertTrue(source.contains("findCommunityFragment()?.onCreatorChannelCommunityRefreshRequested()")) - assertFalse(source.contains("onCreatorChannelCommunityOwnerMoreClicked(postId: Long)")) - assertFalse(fragment.contains("onCreatorChannelCommunityOwnerMoreClicked(item.postId)")) - assertFalse(source.contains("onClickPin = {},")) - assertFalse(source.contains("onClickModify = {},")) - assertFalse(source.contains("onClickDelete = {},")) assertTrue(fragment.contains("fun onCreatorChannelCommunityRefreshRequested()")) - assertTrue( - - fragment.contains( - "CreatorCommunityMediaPlayerManager(requireContext()) { listAdapter.notifyDataSetChanged() }" - ) - ) - assertTrue(fragment.contains("mediaPlayerManager?.toggleContent(CreatorCommunityContentItem(item.postId, audioUrl))")) - assertTrue(fragment.contains("mediaPlayerManager?.stopContent()")) - } - - @Test - fun `커뮤니티 고정 성공은 실제 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()")) + assertFalse(fragment.contains("CreatorCommunityMediaPlayerManager")) + assertFalse(fragment.contains("toggleCommunityAudio")) } @Test @@ -743,18 +833,14 @@ class CreatorChannelActivitySourceTest { handlerSource.indexOf("CommunityChange.Created"), 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("refreshCreatorChannelCommunity()")) - assertTrue( - mutationSource.contains( - "CommunityChange.Updated,\n" + - " is CommunityChange.Deleted,\n" + - " is CommunityChange.PinChanged -> refreshCreatorChannelCommunity()" - ) - ) - assertFalse(mutationSource.contains("homeActionDelegate?.refreshHome()")) + assertTrue(updatedSource.contains("homeActionDelegate?.refreshHome()")) + assertTrue(updatedSource.contains("refreshCreatorChannelCommunity()")) + assertFalse(updatedSource.contains("CommunityChange.Deleted")) + assertFalse(updatedSource.contains("CommunityChange.PinChanged")) } @Test @@ -2003,7 +2089,7 @@ class CreatorChannelActivitySourceTest { assertTrue(adapter.contains("val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp(")) assertTrue(adapter.contains("rootWidthDp = communityWidthDp")) 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("BlurTransformation(itemView.context, 25f, 2.5f)")) assertTrue(adapter.contains("row.layoutParams = LinearLayout.LayoutParams(")) diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeMapperTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeMapperTest.kt index 4f6bda7a..59013b8f 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeMapperTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeMapperTest.kt @@ -61,6 +61,16 @@ class CreatorChannelHomeMapperTest { assertFalse(visitorContent.sections.filterIsInstance().single().isOwner) } + @Test + fun `현재 회원 id를 커뮤니티 section에 보존한다`() { + val communities = response().toUiContent(currentMemberId = 77L) + .sections + .filterIsInstance() + .single() + + assertEquals(77L, communities.currentMemberId) + } + @Test fun `null 단건 콘텐츠와 빈 리스트와 blank SNS는 후원 empty와 팬Talk empty section을 생성한다`() { val content = response( @@ -267,7 +277,9 @@ class CreatorChannelHomeMapperTest { dateUtc = "2026-06-11T12:00:00Z", existOrdered = false, likeCount = 1, - commentCount = 2 + commentCount = 2, + isPinned = false, + isCommentAvailable = true ) private fun fanTalk() = CreatorChannelFanTalkSummaryResponse( diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeModelsTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeModelsTest.kt index 7e36987b..35368e00 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeModelsTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeModelsTest.kt @@ -4,7 +4,9 @@ import com.google.gson.Gson import kr.co.vividnext.sodalive.v2.common.CreatorActivityType import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeResponse import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Test class CreatorChannelHomeModelsTest { @@ -36,6 +38,15 @@ class CreatorChannelHomeModelsTest { 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 = """ { "creator": { @@ -63,7 +74,24 @@ class CreatorChannelHomeModelsTest { ], "audioContents": [], "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": { "totalCount": 0, "latestFanTalk": null diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailReplyUiContractTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailReplyUiContractTest.kt index bf4edb2d..993d74d0 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailReplyUiContractTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailReplyUiContractTest.kt @@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ApplicationProvider import kr.co.vividnext.sodalive.R import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test @@ -29,6 +30,7 @@ class CreatorChannelCommunityDetailReplyUiContractTest { val reply = inflateView(R.layout.activity_creator_channel_community_reply) assertNotNull(detail.findViewById(R.id.btn_creator_channel_community_detail_back)) + assertNotNull(detail.findViewById(R.id.btn_creator_channel_community_detail_more)) assertNotNull(detail.findViewById(R.id.tv_creator_channel_community_detail_comment_header)) assertNotNull(detail.findViewById(R.id.rv_creator_channel_community_detail_comments)) assertNotNull(detail.findViewById(R.id.layout_creator_channel_community_detail_paywall)) @@ -39,6 +41,9 @@ class CreatorChannelCommunityDetailReplyUiContractTest { assertNotNull(detail.findViewById(R.id.btn_creator_channel_community_detail_send)) assertNotNull(detail.findViewById(R.id.btn_creator_channel_community_detail_edit_cancel)) 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") assertNotNull(reply.findViewById(R.id.btn_creator_channel_community_reply_back)) @@ -156,6 +161,23 @@ class CreatorChannelCommunityDetailReplyUiContractTest { 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 fun `댓글 답글 Adapter는 PopupMenu 대신 regular font popup을 사용한다`() { val commentAdapter = projectFile( diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailViewModelTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailViewModelTest.kt index 8c3e1cc5..319ddb20 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailViewModelTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailViewModelTest.kt @@ -31,6 +31,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.argThat +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -469,6 +471,99 @@ class CreatorChannelCommunityDetailViewModelTest { 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>() + 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 fun `댓글 UI 모델은 답글 화면 재포맷을 위해 원본 createdAtUtc를 보존한다`() { stubDetail(detailResponse(isCommentAvailable = true, commentIds = listOf(11L))) @@ -571,7 +666,8 @@ class CreatorChannelCommunityDetailViewModelTest { existOrdered: Boolean = true, creatorId: Long = 100L, commentIds: List = emptyList(), - commentsHasNext: Boolean = false + commentsHasNext: Boolean = false, + isPinned: Boolean = false ) = CreatorChannelCommunityPostDetailResponse( postId = POST_ID, creatorId = creatorId, @@ -587,7 +683,7 @@ class CreatorChannelCommunityDetailViewModelTest { likeCount = likeCount, commentCount = 2, isLiked = isLiked, - isPinned = false, + isPinned = isPinned, comments = commentsResponse(ids = commentIds, hasNext = commentsHasNext) ) diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragmentLayoutTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragmentLayoutTest.kt index 8f5d4c4a..cc3ca728 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragmentLayoutTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragmentLayoutTest.kt @@ -11,6 +11,7 @@ import androidx.test.core.app.ApplicationProvider import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.calculateCreatorChannelCommunityGridItemSize import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertSame import org.junit.Assert.assertTrue @@ -62,7 +63,7 @@ class CreatorChannelCommunityFragmentLayoutTest { } @Test - fun `커뮤니티 list item layout은 프로필 본문 이미지 잠금 재생 반응 owner 영역을 제공한다`() { + fun `커뮤니티 list item layout은 프로필 본문 이미지 잠금 반응과 작성자 금액을 제공한다`() { 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 imageContainer = requireNotNull(item.findViewById(R.id.layout_creator_channel_community_list_image_container)) @@ -77,24 +78,29 @@ class CreatorChannelCommunityFragmentLayoutTest { assertNotNull(item.findViewById(R.id.iv_creator_channel_community_list_image)) assertNotNull(item.findViewById(R.id.layout_creator_channel_community_list_locked_overlay)) assertNotNull(item.findViewById(R.id.tv_creator_channel_community_list_locked_price)) - assertNotNull(item.findViewById(R.id.iv_creator_channel_community_list_play)) assertNotNull(item.findViewById(R.id.tv_creator_channel_community_list_comment_count)) assertNotNull(item.findViewById(R.id.tv_creator_channel_community_list_like_count)) assertNotNull(item.findViewById(R.id.layout_creator_channel_community_list_top_actions)) assertNotNull(item.findViewById(R.id.layout_creator_channel_community_list_top_price)) - assertNotNull(item.findViewById(R.id.iv_creator_channel_community_list_owner_more)) assertNotNull(item.findViewById(R.id.tv_creator_channel_community_list_top_price)) + val moreButton = requireNotNull(item.findViewById(R.id.btn_creator_channel_community_list_more)) 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( item.findViewById(R.id.layout_creator_channel_community_list_top_actions), item.findViewById(R.id.layout_creator_channel_community_list_top_price).parent ) + assertSame( + item.findViewById(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/tv_creator_channel_community_list_locked_price\"")) assertTrue(itemLayout.contains("android:background=\"@drawable/bg_creator_channel_community_price\"")) - assertTrue(itemLayout.contains("android:drawableStart=\"@drawable/ic_bar_cash\"")) - assertTrue(itemLayout.contains("android:id=\"@+id/iv_creator_channel_community_list_play\"")) + assertFalse(itemLayout.contains("android:drawableStart=\"@drawable/ic_bar_cash\"")) + 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 @@ -138,7 +144,7 @@ class CreatorChannelCommunityFragmentLayoutTest { } @Test - fun `커뮤니티 fragment source는 pagination view mode owner padding stopContent 계약을 사용한다`() { + fun `커뮤니티 fragment source는 pagination view mode owner padding과 상세 이동 계약을 사용한다`() { val fragment = projectFile( "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt" ).readText() @@ -161,8 +167,10 @@ class CreatorChannelCommunityFragmentLayoutTest { assertTrue(fragment.contains("calculateCreatorChannelCommunityGridItemSize(")) assertTrue(fragment.contains("viewModel.consumePaginationErrorMessage()")) assertTrue(fragment.contains("applyOwnerCtaPadding")) - assertTrue(fragment.contains("pauseContent")) - assertTrue(fragment.contains("stopContent")) + assertFalse(fragment.contains("CreatorCommunityMediaPlayerManager")) + assertFalse(fragment.contains("toggleCommunityAudio")) + assertFalse(fragment.contains("onPlayClick")) + assertFalse(fragment.contains("onCreatorChannelCommunityOwnerMoreClicked")) } @Test @@ -181,19 +189,25 @@ class CreatorChannelCommunityFragmentLayoutTest { assertTrue(listAdapter.contains("showComment") && listAdapter.contains("isVisible = item.showComment")) assertTrue(listAdapter.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 onMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {}")) assertTrue(listAdapter.contains("root.setOnClickListener { onPostClick(item) }")) - assertTrue(listAdapter.contains("ivCreatorChannelCommunityListPlay.setOnClickListener { onPlayClick(item) }")) - assertTrue(listAdapter.contains("onOwnerMoreClick(item)")) - assertTrue(listAdapter.contains("ivCreatorChannelCommunityListOwnerMore.setOnClickListener { onOwnerMoreClick(item) }")) + assertTrue( + listAdapter.contains( + "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("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("item.isLocked || item.showOwnerTopPrice")) assertTrue(gridAdapter.contains(".asBitmap()")) @@ -211,8 +225,17 @@ class CreatorChannelCommunityFragmentLayoutTest { ).readText() 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("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 diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityMapperTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityMapperTest.kt index 0133f16f..f2579395 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityMapperTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityMapperTest.kt @@ -66,7 +66,7 @@ class CreatorChannelCommunityMapperTest { } @Test - fun `유료 미구매 타인 게시글은 잠금 상태이고 play button을 숨긴다`() { + fun `유료 미구매 타인 게시글은 잠금 상태이고 이미지를 숨긴다`() { val item = listOf( communityPost(price = 100, existOrdered = false, imageUrl = "image.png", audioUrl = "audio.mp3") ).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single() @@ -74,7 +74,6 @@ class CreatorChannelCommunityMapperTest { assertTrue(item.isLocked) assertNull(item.imageUrl) assertEquals(CreatorChannelCommunityImageMode.LockedGray, item.imageMode) - assertFalse(item.showPlayButton) } @Test @@ -93,7 +92,7 @@ class CreatorChannelCommunityMapperTest { } @Test - fun `고정 게시글 여부는 owner more 정책에서 사용할 수 있도록 UI 모델에 보존한다`() { + fun `고정 게시글 여부는 UI 모델에 보존한다`() { val pinnedItem = listOf(communityPost(isPinned = true)) .toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L) .single() @@ -108,33 +107,32 @@ class CreatorChannelCommunityMapperTest { } @Test - fun `본인 또는 구매한 사용자는 이미지와 오디오가 있으면 play button을 표시한다`() { - val ownerItem = listOf( - communityPost(price = 100, existOrdered = false, imageUrl = "image.png", audioUrl = "audio.mp3") - ).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L).single() - val orderedItem = listOf( - communityPost(price = 100, existOrdered = true, imageUrl = "image.png", audioUrl = "audio.mp3") - ).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single() + fun `메뉴 권한은 작성자 무료 구매 여부에 따라 계산하고 고정 상태를 보존한다`() { + val authorPaidNotOrdered = listOf(communityPost(price = 100, existOrdered = false, isPinned = true)) + .toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L) + .single() + val visitorFree = listOf(communityPost(price = 0, existOrdered = false)) + .toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L) + .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(ownerItem.showPlayButton) - assertFalse(orderedItem.isLocked) - assertTrue(orderedItem.showPlayButton) + assertTrue(authorPaidNotOrdered.menuItem.isOwner) + assertTrue(authorPaidNotOrdered.menuItem.showMore) + assertTrue(authorPaidNotOrdered.menuItem.isPinned) + assertTrue(authorPaidNotOrdered.isPinned) + assertTrue(visitorFree.menuItem.showMore) + assertTrue(visitorPaidOrdered.menuItem.showMore) + assertFalse(visitorPaidNotOrdered.menuItem.isOwner) + assertFalse(visitorPaidNotOrdered.menuItem.showMore) } @Test - fun `오디오 또는 이미지가 없으면 play button을 숨긴다`() { - 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를 표시한다`() { + fun `본인 채널에 본인이 쓴 유료 게시글에서만 top price를 표시한다`() { val ownerPaid = listOf(communityPost(price = 100, creatorId = 10L)) .toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L) .single() @@ -148,13 +146,9 @@ class CreatorChannelCommunityMapperTest { .toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L) .single() - assertTrue(ownerPaid.showOwnerMore) assertTrue(ownerPaid.showOwnerTopPrice) - assertTrue(ownerFree.showOwnerMore) assertFalse(ownerFree.showOwnerTopPrice) - assertFalse(otherCreator.showOwnerMore) assertFalse(otherCreator.showOwnerTopPrice) - assertFalse(otherChannel.showOwnerMore) assertFalse(otherChannel.showOwnerTopPrice) } diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt index 3f42eebf..95e90101 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt @@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.v2.main.home import android.app.Application import android.content.Context import android.content.res.Configuration -import android.graphics.drawable.ColorDrawable import android.text.Spanned import android.text.TextUtils import android.text.style.ClickableSpan @@ -659,6 +658,19 @@ class HomeMainFragmentLayoutTest { 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 fun `home relative time formatter converts active creator utc timestamp to relative time text`() { val context = ApplicationProvider.getApplicationContext() @@ -1509,7 +1521,9 @@ class HomeMainFragmentLayoutTest { private fun popularCommunityData( audioUrl: String?, - createdAt: String = "2분 전" + createdAt: String = "2분 전", + price: Int = 0, + existOrdered: Boolean = false ): HomePopularCommunityPostItem { return HomePopularCommunityPostItem( postId = 1L, @@ -1519,11 +1533,11 @@ class HomeMainFragmentLayoutTest { imageUrl = null, audioUrl = audioUrl, content = "본문", - price = 0, + price = price, createdAt = createdAt, likeCount = 6L, commentCount = 5L, - existOrdered = false + existOrdered = existOrdered ) } diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItemTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItemTest.kt index 8ace5c0d..d5c22957 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItemTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItemTest.kt @@ -1,6 +1,8 @@ package kr.co.vividnext.sodalive.v2.widget.feed import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import kr.co.vividnext.sodalive.R import org.junit.Test @@ -102,4 +104,71 @@ class FeedItemTest { assertEquals(5, item.commentCount) 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 + ) } diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedViewTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedViewTest.kt index 1a380e55..ec493fe3 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedViewTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedViewTest.kt @@ -63,6 +63,28 @@ class FeedViewTest { assertEquals(View.GONE, view.findViewById(R.id.tv_feed_community_body).visibility) } + @Test + fun `community more click은 현재 bind된 item만 전달한다`() { + val view = inflateView(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(R.id.btn_feed_community_more).performClick() + + assertSame(second, clickedItem) + assertEquals(null, rootClickedItem) + + view.performClick() + + assertSame(second, rootClickedItem) + } + @Test fun `community layout uses runtime image height instead of fixed crop height`() { val view = inflateView(R.layout.view_feed_community) @@ -201,7 +223,8 @@ class FeedViewTest { audioUrl: String? = null, price: Int = 0, existOrdered: Boolean = false, - showKeyword: Boolean = true + showKeyword: Boolean = true, + showMore: Boolean = false ) = FeedItem.Community( feedId = "feed-community-1", creatorId = "creator-1", @@ -217,7 +240,8 @@ class FeedViewTest { audioUrl = audioUrl, price = price, existOrdered = existOrdered, - showKeyword = showKeyword + showKeyword = showKeyword, + showMore = showMore ) private fun Int.dpToPx(): Int {