fix(content): 상세 오디오와 구매 흐름을 보정한다

This commit is contained in:
2026-07-12 08:32:48 +09:00
parent 53cc96b84d
commit 39959c3f47
5 changed files with 139 additions and 2 deletions

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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"

View File

@@ -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(

View File

@@ -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)))