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

View File

@@ -128,6 +128,11 @@ class CreatorChannelCommunityDetailViewModel(
fun submitComment() { fun submitComment() {
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
val comment = content.commentInput.trim() val comment = content.commentInput.trim()
val editingCommentId = content.editingCommentId
if (editingCommentId != null) {
submitCommentEdit(content, editingCommentId, comment)
return
}
if (!content.isCommentAvailable || comment.isBlank() || isSendingComment) return if (!content.isCommentAvailable || comment.isBlank() || isSendingComment) return
isSendingComment = true isSendingComment = true
@@ -175,6 +180,26 @@ class CreatorChannelCommunityDetailViewModel(
_postChangedEventLiveData.value = false _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) { fun modifyComment(commentId: Long, comment: String) {
val trimmedComment = comment.trim() val trimmedComment = comment.trim()
if (commentId <= 0 || isModifyingComment) return if (commentId <= 0 || isModifyingComment) return
@@ -192,6 +217,57 @@ class CreatorChannelCommunityDetailViewModel(
modifyComment(ModifyCommentRequest(commentId = commentId, isActive = false)) 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>) { private fun handleDetailResponse(response: ApiResponse<CreatorChannelCommunityPostDetailResponse>) {
val data = response.data val data = response.data
if (!response.success || data == null) { 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( private fun CreatorChannelCommunityPostDetailResponse.toUiModel() = CreatorChannelCommunityPostDetailUiModel(
postId = postId, postId = postId,
creatorId = creatorId, creatorId = creatorId,
@@ -392,7 +474,9 @@ sealed interface CreatorChannelCommunityDetailUiState {
val hasNextComment: Boolean, val hasNextComment: Boolean,
val isLoadingComments: Boolean = false, val isLoadingComments: Boolean = false,
val commentInput: String = "", val commentInput: String = "",
val isSendingComment: Boolean = false val isSendingComment: Boolean = false,
val editingCommentId: Long? = null,
val editingOriginalComment: String = ""
) : CreatorChannelCommunityDetailUiState ) : CreatorChannelCommunityDetailUiState
} }

View File

@@ -25,18 +25,6 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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.constraintlayout.widget.ConstraintLayout>
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
@@ -335,5 +323,16 @@
android:enabled="false" android:enabled="false"
android:src="@drawable/ic_new_arrow_up_gray" android:src="@drawable/ic_new_arrow_up_gray"
tools:background="@drawable/bg_creator_channel_community_send_enabled" /> 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> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>