feat(creator-channel): 댓글 수정 입력 모드를 구현한다

This commit is contained in:
2026-07-09 01:28:29 +09:00
parent 255e6dc67d
commit a325a1bbe9
3 changed files with 135 additions and 44 deletions

View File

@@ -4,11 +4,9 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.InputType
import android.text.method.ScrollingMovementMethod
import android.widget.EditText
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
@@ -22,6 +20,7 @@ import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.Cr
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.moneyFormat
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyActivity
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui.CreatorChannelCommunityCommentAdapter
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -39,11 +38,12 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
}
private val commentAdapter = CreatorChannelCommunityCommentAdapter(
onReplyClick = { comment -> replyLauncher.launch(CreatorChannelCommunityReplyActivity.newIntent(this, postId, comment)) },
onModifyClick = { comment -> showModifyCommentDialog(comment.commentId, comment.comment) },
onModifyClick = { comment -> viewModel.startCommentEdit(comment.commentId, comment.comment) },
onDeleteClick = { comment -> showDeleteCommentDialog(comment.commentId) }
)
private lateinit var mediaPlayerManager: CreatorCommunityMediaPlayerManager
private var postId: Long = 0L
private var renderedEditingCommentId: Long? = null
override fun setupView() {
mediaPlayerManager = CreatorCommunityMediaPlayerManager(this) { }
@@ -70,18 +70,24 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
viewModel.updateCommentInput(editable?.toString().orEmpty())
}
binding.btnCreatorChannelCommunityDetailSend.setOnClickListener { viewModel.submitComment() }
binding.btnCreatorChannelCommunityDetailEditCancel.setOnClickListener { viewModel.cancelCommentEdit() }
binding.ivCreatorChannelCommunityDetailHeart.setOnClickListener { viewModel.toggleLike() }
viewModel.detailStateLiveData.observe(this) { state -> render(state) }
viewModel.commentWrittenEventLiveData.observe(this) { written ->
if (written == true) {
setResult(Activity.RESULT_OK)
hideCommentKeyboard()
viewModel.consumeCommentWrittenEvent()
}
}
viewModel.postChangedEventLiveData.observe(this) { changed ->
if (changed == true) {
setResult(Activity.RESULT_OK)
val current = viewModel.detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
if (current?.editingCommentId != null) {
hideCommentKeyboard()
}
viewModel.consumePostChangedEvent()
}
}
@@ -171,6 +177,11 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
etCreatorChannelCommunityDetailComment.setText(content.commentInput)
etCreatorChannelCommunityDetailComment.setSelection(content.commentInput.length)
}
btnCreatorChannelCommunityDetailEditCancel.isVisible = content.editingCommentId != null
if (content.editingCommentId != null && content.editingCommentId != renderedEditingCommentId) {
showCommentKeyboard()
}
renderedEditingCommentId = content.editingCommentId
updateSendButton(content.commentInput.trim().isNotEmpty() && !content.isSendingComment)
}
@@ -191,35 +202,32 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
mediaPlayerManager.toggleContent(CreatorCommunityContentItem(post.postId, audioUrl))
}
private fun showModifyCommentDialog(commentId: Long, comment: String) {
val editText = EditText(this).apply {
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
minLines = 3
setText(comment)
setSelection(comment.length)
}
AlertDialog.Builder(this)
.setTitle(R.string.menu_modify_action)
.setView(editText)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.confirm) { _, _ ->
val modifiedComment = editText.text?.toString().orEmpty()
if (modifiedComment.trim() == comment.trim()) {
Toast.makeText(this, R.string.audio_content_comment_no_change, Toast.LENGTH_SHORT).show()
} else {
viewModel.modifyComment(commentId, modifiedComment)
}
}
.show()
private fun showDeleteCommentDialog(commentId: Long) {
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.deleteComment(commentId) },
cancelButtonTitle = getString(R.string.cancel)
).show(screenWidth)
}
private fun showDeleteCommentDialog(commentId: Long) {
AlertDialog.Builder(this)
.setTitle(R.string.confirm_delete_title)
.setMessage(R.string.confirm_delete_message)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.confirm) { _, _ -> viewModel.deleteComment(commentId) }
.show()
private fun showCommentKeyboard() {
val editText = binding.etCreatorChannelCommunityDetailComment
editText.requestFocus()
editText.post {
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
}
}
private fun hideCommentKeyboard() {
val editText = binding.etCreatorChannelCommunityDetailComment
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(editText.windowToken, 0)
editText.clearFocus()
}
companion object {

View File

@@ -128,6 +128,11 @@ class CreatorChannelCommunityDetailViewModel(
fun submitComment() {
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
val comment = content.commentInput.trim()
val editingCommentId = content.editingCommentId
if (editingCommentId != null) {
submitCommentEdit(content, editingCommentId, comment)
return
}
if (!content.isCommentAvailable || comment.isBlank() || isSendingComment) return
isSendingComment = true
@@ -175,6 +180,26 @@ class CreatorChannelCommunityDetailViewModel(
_postChangedEventLiveData.value = false
}
fun startCommentEdit(commentId: Long, comment: String) {
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
if (commentId <= 0) return
_detailStateLiveData.value = content.copy(
commentInput = comment,
editingCommentId = commentId,
editingOriginalComment = comment
)
}
fun cancelCommentEdit() {
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
_detailStateLiveData.value = content.copy(
commentInput = "",
editingCommentId = null,
editingOriginalComment = ""
)
}
fun modifyComment(commentId: Long, comment: String) {
val trimmedComment = comment.trim()
if (commentId <= 0 || isModifyingComment) return
@@ -192,6 +217,57 @@ class CreatorChannelCommunityDetailViewModel(
modifyComment(ModifyCommentRequest(commentId = commentId, isActive = false))
}
private fun submitCommentEdit(
content: CreatorChannelCommunityDetailUiState.Content,
commentId: Long,
comment: String
) {
if (commentId <= 0 || isModifyingComment) return
if (comment.isBlank()) {
showBlankCommentToast()
return
}
if (comment == content.editingOriginalComment.trim()) {
showNoChangeToast()
return
}
isModifyingComment = true
_detailStateLiveData.value = content.copy(isSendingComment = true)
compositeDisposable.add(
legacyRepository.modifyComment(ModifyCommentRequest(commentId = commentId, comment = comment), authToken())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
isModifyingComment = false
val current = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
?: return@subscribe
if (response.success) {
_postChangedEventLiveData.value = true
_detailStateLiveData.value = current.copy(
commentInput = "",
isSendingComment = false,
editingCommentId = null,
editingOriginalComment = ""
)
loadDetail(postId)
} else {
showMutationFailureToast(response.message)
_detailStateLiveData.value = current.copy(isSendingComment = false)
}
},
{
isModifyingComment = false
val current = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
?: return@subscribe
showMutationFailureToast()
_detailStateLiveData.value = current.copy(isSendingComment = false)
}
)
)
}
private fun handleDetailResponse(response: ApiResponse<CreatorChannelCommunityPostDetailResponse>) {
val data = response.data
if (!response.success || data == null) {
@@ -313,6 +389,12 @@ class CreatorChannelCommunityDetailViewModel(
)
}
private fun showNoChangeToast() {
_toastLiveData.value = CreatorChannelEvent(
ToastMessage(resId = R.string.audio_content_comment_no_change)
)
}
private fun CreatorChannelCommunityPostDetailResponse.toUiModel() = CreatorChannelCommunityPostDetailUiModel(
postId = postId,
creatorId = creatorId,
@@ -392,7 +474,9 @@ sealed interface CreatorChannelCommunityDetailUiState {
val hasNextComment: Boolean,
val isLoadingComments: Boolean = false,
val commentInput: String = "",
val isSendingComment: Boolean = false
val isSendingComment: Boolean = false,
val editingCommentId: Long? = null,
val editingOriginalComment: String = ""
) : CreatorChannelCommunityDetailUiState
}

View File

@@ -25,18 +25,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_creator_channel_community_detail_title"
style="@style/Typography.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:text="@string/creator_channel_community_detail_title"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.core.widget.NestedScrollView
@@ -335,5 +323,16 @@
android:enabled="false"
android:src="@drawable/ic_new_arrow_up_gray"
tools:background="@drawable/bg_creator_channel_community_send_enabled" />
<ImageButton
android:id="@+id/btn_creator_channel_community_detail_edit_cancel"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="@dimen/spacing_8"
android:background="@android:color/transparent"
android:contentDescription="@string/cancel"
android:src="@drawable/ic_x_white"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>