feat(creator-channel): FanTalk 상세 화면을 추가한다

This commit is contained in:
2026-07-09 07:44:06 +09:00
parent 04dcb3f09f
commit da26b65a73
5 changed files with 652 additions and 0 deletions
@@ -187,6 +187,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelApi
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.write.CreatorChannelFanTalkWriteViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesViewModel
@@ -458,6 +459,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { CreatorChannelCommunityDetailViewModel(get(), get(), get()) }
viewModel { CreatorChannelCommunityReplyViewModel(get(), get(), get()) }
viewModel { CreatorChannelFanTalkViewModel(get(), get()) }
viewModel { CreatorChannelFanTalkDetailViewModel(get(), get()) }
viewModel { CreatorChannelFanTalkWriteViewModel(get()) }
viewModel { CreatorChannelDonationViewModel(get(), get()) }
viewModel { PushNotificationListViewModel(get()) }
@@ -0,0 +1,238 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.text.method.ScrollingMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import android.widget.PopupWindow
import android.widget.Toast
import androidx.core.content.IntentCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelFantalkDetailBinding
import kr.co.vividnext.sodalive.databinding.ViewCreatorChannelFantalkMorePopupBinding
import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkReplyUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkUiModel
import org.koin.androidx.viewmodel.ext.android.viewModel
class CreatorChannelFanTalkDetailActivity : BaseActivity<ActivityCreatorChannelFantalkDetailBinding>(
ActivityCreatorChannelFantalkDetailBinding::inflate
) {
private val viewModel: CreatorChannelFanTalkDetailViewModel by viewModel()
private var creatorId: Long = 0L
private var replyMorePopup: PopupWindow? = null
private var renderedEditingTarget: CreatorChannelFanTalkDetailEditTarget? = null
override fun setupView() {
creatorId = intent.getLongExtra(EXTRA_CREATOR_ID, 0L)
val payload = payloadFromIntent() ?: run {
Toast.makeText(this, R.string.creator_channel_community_invalid_post, Toast.LENGTH_SHORT).show()
finish()
return
}
if (creatorId <= 0L || payload.fanTalkId <= 0L) {
Toast.makeText(this, R.string.creator_channel_community_invalid_post, Toast.LENGTH_SHORT).show()
finish()
return
}
binding.btnCreatorChannelFantalkDetailBack.setOnClickListener { finish() }
binding.etCreatorChannelFantalkDetailReply.movementMethod = ScrollingMovementMethod()
binding.etCreatorChannelFantalkDetailReply.doAfterTextChanged { editable ->
viewModel.updateReplyInput(editable?.toString().orEmpty())
}
binding.btnCreatorChannelFantalkDetailSend.setOnClickListener { viewModel.submitReply() }
binding.btnCreatorChannelFantalkDetailEditCancel.setOnClickListener { viewModel.cancelReplyEdit() }
viewModel.detailStateLiveData.observe(this) { state -> render(state) }
viewModel.replacementModalEventLiveData.observe(this) { event ->
event.consume()?.let(::showReplacementDialog)
}
viewModel.replyChangedEventLiveData.observe(this) { changed ->
if (changed == true) {
setResult(Activity.RESULT_OK)
hideReplyKeyboard()
viewModel.consumeReplyChangedEvent()
}
}
viewModel.toastLiveData.observe(this) { event ->
event.consume()?.let { toast ->
val message = toast.message ?: toast.resId?.let(::getString)
message?.let(::showToast)
}
}
viewModel.load(creatorId, payload)
}
override fun onDestroy() {
replyMorePopup?.dismiss()
replyMorePopup = null
super.onDestroy()
}
private fun render(state: CreatorChannelFanTalkDetailUiState) {
when (state) {
is CreatorChannelFanTalkDetailUiState.Content -> renderContent(state)
}
}
private fun renderContent(content: CreatorChannelFanTalkDetailUiState.Content) = with(binding) {
bindParentFanTalk(content.parentFanTalk)
bindReply(content.reply)
if (etCreatorChannelFantalkDetailReply.text.toString() != content.replyInput) {
etCreatorChannelFantalkDetailReply.setText(content.replyInput)
etCreatorChannelFantalkDetailReply.setSelection(content.replyInput.length)
}
btnCreatorChannelFantalkDetailEditCancel.isVisible = content.editingTarget != null
if (content.editingTarget != null && content.editingTarget != renderedEditingTarget) {
showReplyKeyboard()
}
renderedEditingTarget = content.editingTarget
updateSendButton(content.isSendEnabled)
}
private fun bindParentFanTalk(item: CreatorChannelFanTalkUiModel) = with(binding) {
ivCreatorChannelFantalkDetailProfile.loadProfile(item.writerProfileImageUrl)
tvCreatorChannelFantalkDetailNickname.text = item.writerNickname
tvCreatorChannelFantalkDetailTime.text = item.createdAtText
tvCreatorChannelFantalkDetailContent.text = item.content
ivCreatorChannelFantalkDetailMore.isVisible = false
ivCreatorChannelFantalkDetailMore.setOnClickListener(null)
}
private fun bindReply(reply: CreatorChannelFanTalkReplyUiModel?) = with(binding) {
viewCreatorChannelFantalkDetailReplyConnector.isVisible = reply != null
layoutCreatorChannelFantalkDetailReply.isVisible = reply != null
if (reply == null) {
replyMorePopup?.dismiss()
return@with
}
ivCreatorChannelFantalkDetailReplyProfile.loadProfile(reply.writerProfileImageUrl)
tvCreatorChannelFantalkDetailReplyNickname.text = reply.writerNickname
tvCreatorChannelFantalkDetailReplyTime.text = reply.createdAtText
tvCreatorChannelFantalkDetailReplyContent.text = reply.content
ivCreatorChannelFantalkDetailReplyMore.isVisible = reply.fanTalkId > 0L
ivCreatorChannelFantalkDetailReplyMore.setOnClickListener(
if (reply.fanTalkId > 0L) {
View.OnClickListener { anchor -> showReplyMorePopup(anchor) }
} else {
null
}
)
}
private fun showReplyMorePopup(anchor: View) {
replyMorePopup?.dismiss()
val popupBinding = ViewCreatorChannelFantalkMorePopupBinding.inflate(LayoutInflater.from(anchor.context))
replyMorePopup = PopupWindow(anchor.context).apply {
contentView = popupBinding.root
width = ViewGroup.LayoutParams.WRAP_CONTENT
height = ViewGroup.LayoutParams.WRAP_CONTENT
isOutsideTouchable = true
isFocusable = true
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
}
popupBinding.tvCreatorChannelFantalkMoreEdit.isVisible = true
popupBinding.tvCreatorChannelFantalkMoreDelete.isVisible = true
popupBinding.tvCreatorChannelFantalkMoreEdit.setOnClickListener {
replyMorePopup?.dismiss()
viewModel.startReplyEdit()
}
popupBinding.tvCreatorChannelFantalkMoreDelete.setOnClickListener {
replyMorePopup?.dismiss()
showDeleteReplyDialog()
}
replyMorePopup?.showAsDropDown(anchor)
}
private fun showReplacementDialog(modal: CreatorChannelFanTalkReplacementModalUiModel) {
V2ModalDialog(
activity = this,
layoutInflater = layoutInflater,
title = getString(modal.titleResId),
desc = getString(modal.descriptionResId),
confirmButtonTitle = getString(modal.confirmResId),
confirmButtonClick = { viewModel.confirmReplacement() },
cancelButtonTitle = getString(modal.cancelResId),
cancelButtonClick = { viewModel.cancelReplacement() }
).show(screenWidth)
}
private fun showDeleteReplyDialog() {
V2ModalDialog(
activity = this,
layoutInflater = layoutInflater,
title = getString(R.string.confirm_delete_title),
desc = getString(R.string.confirm_delete_message),
confirmButtonTitle = getString(R.string.confirm),
confirmButtonClick = { viewModel.deleteReply() },
cancelButtonTitle = getString(R.string.cancel)
).show(screenWidth)
}
private fun updateSendButton(enabled: Boolean) = with(binding.btnCreatorChannelFantalkDetailSend) {
isEnabled = enabled
setBackgroundResource(
if (enabled) {
R.drawable.bg_creator_channel_community_send_enabled
} else {
R.drawable.bg_creator_channel_community_send_disabled
}
)
setImageResource(if (enabled) R.drawable.ic_new_arrow_up_white else R.drawable.ic_new_arrow_up_gray)
}
private fun showReplyKeyboard() {
val editText = binding.etCreatorChannelFantalkDetailReply
editText.requestFocus()
editText.post {
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
}
}
private fun hideReplyKeyboard() {
val editText = binding.etCreatorChannelFantalkDetailReply
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(editText.windowToken, 0)
editText.clearFocus()
}
private fun payloadFromIntent(): CreatorChannelFanTalkDetailPayload? {
return IntentCompat.getParcelableExtra(intent, EXTRA_PAYLOAD, CreatorChannelFanTalkDetailPayload::class.java)
}
private fun ImageView.loadProfile(url: String) {
loadUrl(url) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
}
companion object {
private const val EXTRA_CREATOR_ID = "extra_creator_id"
private const val EXTRA_PAYLOAD = "extra_payload"
fun newIntent(context: Context, creatorId: Long, payload: CreatorChannelFanTalkDetailPayload): Intent {
return Intent(context, CreatorChannelFanTalkDetailActivity::class.java)
.putExtra(EXTRA_CREATOR_ID, creatorId)
.putExtra(EXTRA_PAYLOAD, payload)
}
}
}