fix(content): 상세 오디오와 구매 흐름을 보정한다
This commit is contained in:
@@ -16,6 +16,7 @@ import com.bumptech.glide.Glide
|
|||||||
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.databinding.ActivityCreatorChannelCommunityDetailBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding
|
||||||
|
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.extensions.loadUrl
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
@@ -46,7 +47,7 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
private var renderedEditingCommentId: Long? = null
|
private var renderedEditingCommentId: Long? = null
|
||||||
|
|
||||||
override fun setupView() {
|
override fun setupView() {
|
||||||
mediaPlayerManager = CreatorCommunityMediaPlayerManager(this) { }
|
mediaPlayerManager = CreatorCommunityMediaPlayerManager(this) { renderCurrentAudioButton() }
|
||||||
postId = intent.getLongExtra(EXTRA_POST_ID, 0L)
|
postId = intent.getLongExtra(EXTRA_POST_ID, 0L)
|
||||||
if (postId <= 0L) {
|
if (postId <= 0L) {
|
||||||
Toast.makeText(this, R.string.creator_channel_community_invalid_post, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.creator_channel_community_invalid_post, Toast.LENGTH_SHORT).show()
|
||||||
@@ -154,6 +155,12 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
layoutCreatorChannelCommunityDetailImageContainer.isVisible = post.imageUrl != null || post.showPaywall
|
layoutCreatorChannelCommunityDetailImageContainer.isVisible = post.imageUrl != null || post.showPaywall
|
||||||
layoutCreatorChannelCommunityDetailPaywall.isVisible = post.showPaywall
|
layoutCreatorChannelCommunityDetailPaywall.isVisible = post.showPaywall
|
||||||
tvCreatorChannelCommunityDetailPaywallPrice.text = post.price.moneyFormat()
|
tvCreatorChannelCommunityDetailPaywallPrice.text = post.price.moneyFormat()
|
||||||
|
layoutCreatorChannelCommunityDetailPaywall.setOnClickListener {
|
||||||
|
showPurchaseDialog(post)
|
||||||
|
}
|
||||||
|
tvCreatorChannelCommunityDetailPaywallPrice.setOnClickListener {
|
||||||
|
showPurchaseDialog(post)
|
||||||
|
}
|
||||||
if (post.imageUrl != null) {
|
if (post.imageUrl != null) {
|
||||||
Glide.with(ivCreatorChannelCommunityDetailImage)
|
Glide.with(ivCreatorChannelCommunityDetailImage)
|
||||||
.load(post.imageUrl)
|
.load(post.imageUrl)
|
||||||
@@ -162,11 +169,14 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
} else {
|
} else {
|
||||||
Glide.with(ivCreatorChannelCommunityDetailImage).clear(ivCreatorChannelCommunityDetailImage)
|
Glide.with(ivCreatorChannelCommunityDetailImage).clear(ivCreatorChannelCommunityDetailImage)
|
||||||
}
|
}
|
||||||
tvCreatorChannelCommunityDetailAudio.isVisible = post.audioUrl != null
|
ivCreatorChannelCommunityDetailPlayOrPause.isVisible = post.audioUrl != null && post.imageUrl != null
|
||||||
|
ivCreatorChannelCommunityDetailPlayOrPause.setOnClickListener { toggleAudio(post) }
|
||||||
|
tvCreatorChannelCommunityDetailAudio.isVisible = post.audioUrl != null && post.imageUrl == null
|
||||||
tvCreatorChannelCommunityDetailAudio.text = getString(R.string.creator_channel_tab_audio)
|
tvCreatorChannelCommunityDetailAudio.text = getString(R.string.creator_channel_tab_audio)
|
||||||
tvCreatorChannelCommunityDetailAudio.setOnClickListener {
|
tvCreatorChannelCommunityDetailAudio.setOnClickListener {
|
||||||
toggleAudio(post)
|
toggleAudio(post)
|
||||||
}
|
}
|
||||||
|
renderAudioButton(post)
|
||||||
rvCreatorChannelCommunityDetailComments.isVisible = content.isCommentAvailable
|
rvCreatorChannelCommunityDetailComments.isVisible = content.isCommentAvailable
|
||||||
tvCreatorChannelCommunityDetailCommentHeader.isVisible = content.isCommentAvailable
|
tvCreatorChannelCommunityDetailCommentHeader.isVisible = content.isCommentAvailable
|
||||||
viewCreatorChannelCommunityDetailCommentDivider.isVisible = content.isCommentAvailable
|
viewCreatorChannelCommunityDetailCommentDivider.isVisible = content.isCommentAvailable
|
||||||
@@ -201,6 +211,32 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
private fun toggleAudio(post: CreatorChannelCommunityPostDetailUiModel) {
|
private fun toggleAudio(post: CreatorChannelCommunityPostDetailUiModel) {
|
||||||
val audioUrl = post.audioUrl ?: return
|
val audioUrl = post.audioUrl ?: return
|
||||||
mediaPlayerManager.toggleContent(CreatorCommunityContentItem(post.postId, audioUrl))
|
mediaPlayerManager.toggleContent(CreatorCommunityContentItem(post.postId, audioUrl))
|
||||||
|
renderAudioButton(post)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderCurrentAudioButton() {
|
||||||
|
val content = viewModel.detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||||
|
renderAudioButton(content.post)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderAudioButton(post: CreatorChannelCommunityPostDetailUiModel) {
|
||||||
|
binding.ivCreatorChannelCommunityDetailPlayOrPause.setImageResource(
|
||||||
|
if (mediaPlayerManager.isPlayingContent(post.postId)) {
|
||||||
|
R.drawable.btn_audio_content_pause
|
||||||
|
} else {
|
||||||
|
R.drawable.btn_audio_content_play
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPurchaseDialog(post: CreatorChannelCommunityPostDetailUiModel) {
|
||||||
|
if (!post.isLocked) return
|
||||||
|
PurchaseCommunityPostDialog(
|
||||||
|
activity = this,
|
||||||
|
layoutInflater = layoutInflater,
|
||||||
|
can = post.price,
|
||||||
|
confirmButtonClick = { viewModel.purchaseCommunityPost() }
|
||||||
|
).show(screenWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showDeleteCommentDialog(commentId: Long) {
|
private fun showDeleteCommentDialog(commentId: Long) {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class CreatorChannelCommunityDetailViewModel(
|
|||||||
private var isTogglingLike = false
|
private var isTogglingLike = false
|
||||||
private var isSendingComment = false
|
private var isSendingComment = false
|
||||||
private var isModifyingComment = false
|
private var isModifyingComment = false
|
||||||
|
private var isPurchasingPost = false
|
||||||
|
|
||||||
fun loadDetail(postId: Long) {
|
fun loadDetail(postId: Long) {
|
||||||
if (postId <= 0) return
|
if (postId <= 0) return
|
||||||
@@ -217,6 +218,33 @@ class CreatorChannelCommunityDetailViewModel(
|
|||||||
modifyComment(ModifyCommentRequest(commentId = commentId, isActive = false))
|
modifyComment(ModifyCommentRequest(commentId = commentId, isActive = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun purchaseCommunityPost() {
|
||||||
|
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||||
|
if (!content.post.isLocked || isPurchasingPost) return
|
||||||
|
|
||||||
|
isPurchasingPost = true
|
||||||
|
compositeDisposable.add(
|
||||||
|
legacyRepository.purchaseCommunityPost(postId, authToken())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ response ->
|
||||||
|
isPurchasingPost = false
|
||||||
|
if (response.success) {
|
||||||
|
_postChangedEventLiveData.value = true
|
||||||
|
loadDetail(postId)
|
||||||
|
} else {
|
||||||
|
showMutationFailureToast(response.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isPurchasingPost = false
|
||||||
|
showMutationFailureToast()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun submitCommentEdit(
|
private fun submitCommentEdit(
|
||||||
content: CreatorChannelCommunityDetailUiState.Content,
|
content: CreatorChannelCommunityDetailUiState.Content,
|
||||||
commentId: Long,
|
commentId: Long,
|
||||||
|
|||||||
@@ -149,6 +149,16 @@
|
|||||||
android:contentDescription="@string/a11y_feed_content_image"
|
android:contentDescription="@string/a11y_feed_content_image"
|
||||||
android:scaleType="centerCrop" />
|
android:scaleType="centerCrop" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_creator_channel_community_detail_play_or_pause"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/btn_audio_content_play"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/layout_creator_channel_community_detail_paywall"
|
android:id="@+id/layout_creator_channel_community_detail_paywall"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
|
|||||||
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))
|
||||||
assertNotNull(detail.findViewById<TextView>(R.id.tv_creator_channel_community_detail_paywall_price))
|
assertNotNull(detail.findViewById<TextView>(R.id.tv_creator_channel_community_detail_paywall_price))
|
||||||
|
assertNotNull(detail.findViewById<ImageView>(R.id.iv_creator_channel_community_detail_play_or_pause))
|
||||||
assertNotNull(detail.findViewById<View>(R.id.layout_creator_channel_community_detail_input_bar))
|
assertNotNull(detail.findViewById<View>(R.id.layout_creator_channel_community_detail_input_bar))
|
||||||
assertNotNull(detail.findViewById<EditText>(R.id.et_creator_channel_community_detail_comment))
|
assertNotNull(detail.findViewById<EditText>(R.id.et_creator_channel_community_detail_comment))
|
||||||
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_send))
|
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_send))
|
||||||
@@ -138,6 +139,23 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
|
|||||||
assertTrue(!replyActivity.contains("AlertDialog"))
|
assertTrue(!replyActivity.contains("AlertDialog"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `상세 Activity는 레거시 구매 다이얼로그와 이미지 중앙 오디오 버튼을 사용한다`() {
|
||||||
|
val detailLayout = projectFile("app/src/main/res/layout/activity_creator_channel_community_detail.xml").readText()
|
||||||
|
val detailActivity = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/" +
|
||||||
|
"CreatorChannelCommunityDetailActivity.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(detailLayout.contains("@+id/iv_creator_channel_community_detail_play_or_pause"))
|
||||||
|
assertTrue(detailLayout.contains("android:layout_gravity=\"center\""))
|
||||||
|
assertTrue(detailLayout.contains("@drawable/btn_audio_content_play"))
|
||||||
|
assertTrue(detailActivity.contains("PurchaseCommunityPostDialog"))
|
||||||
|
assertTrue(detailActivity.contains("viewModel.purchaseCommunityPost()"))
|
||||||
|
assertTrue(detailActivity.contains("btn_audio_content_pause"))
|
||||||
|
assertTrue(detailActivity.contains("btn_audio_content_play"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `댓글 답글 Adapter는 PopupMenu 대신 regular font popup을 사용한다`() {
|
fun `댓글 답글 Adapter는 PopupMenu 대신 regular font popup을 사용한다`() {
|
||||||
val commentAdapter = projectFile(
|
val commentAdapter = projectFile(
|
||||||
|
|||||||
@@ -394,6 +394,51 @@ class CreatorChannelCommunityDetailViewModelTest {
|
|||||||
assertFalse(state.post.showPaywall)
|
assertFalse(state.post.showPaywall)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `게시물 구매 성공은 상세를 다시 조회해 잠금 상태를 해제한다`() {
|
||||||
|
whenever(repository.getCommunityPostDetail(POST_ID, AUTH_TOKEN))
|
||||||
|
.thenReturn(
|
||||||
|
Single.just(
|
||||||
|
ApiResponse(
|
||||||
|
true,
|
||||||
|
detailResponse(
|
||||||
|
isCommentAvailable = false,
|
||||||
|
price = 300,
|
||||||
|
existOrdered = false,
|
||||||
|
creatorId = 999L
|
||||||
|
),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Single.just(
|
||||||
|
ApiResponse(
|
||||||
|
true,
|
||||||
|
detailResponse(
|
||||||
|
isCommentAvailable = false,
|
||||||
|
price = 300,
|
||||||
|
existOrdered = true,
|
||||||
|
creatorId = 999L
|
||||||
|
),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
whenever(legacyRepository.purchaseCommunityPost(POST_ID, AUTH_TOKEN))
|
||||||
|
.thenReturn(Single.just(ApiResponse(true, org.mockito.kotlin.mock(), null)))
|
||||||
|
viewModel.loadDetail(POST_ID)
|
||||||
|
|
||||||
|
viewModel.purchaseCommunityPost()
|
||||||
|
|
||||||
|
val state = viewModel.detailStateLiveData.requireValue() as CreatorChannelCommunityDetailUiState.Content
|
||||||
|
assertFalse(state.post.isLocked)
|
||||||
|
assertEquals("image.png", state.post.imageUrl)
|
||||||
|
assertEquals("audio.mp3", state.post.audioUrl)
|
||||||
|
assertTrue(state.post.existOrdered)
|
||||||
|
assertTrue(viewModel.postChangedEventLiveData.requireValue() == true)
|
||||||
|
verify(legacyRepository).purchaseCommunityPost(POST_ID, AUTH_TOKEN)
|
||||||
|
verify(repository, times(2)).getCommunityPostDetail(POST_ID, AUTH_TOKEN)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `댓글 UI 모델은 답글 화면 재포맷을 위해 원본 createdAtUtc를 보존한다`() {
|
fun `댓글 UI 모델은 답글 화면 재포맷을 위해 원본 createdAtUtc를 보존한다`() {
|
||||||
stubDetail(detailResponse(isCommentAvailable = true, commentIds = listOf(11L)))
|
stubDetail(detailResponse(isCommentAvailable = true, commentIds = listOf(11L)))
|
||||||
|
|||||||
Reference in New Issue
Block a user