From d436efdc66b841d20ed474f1e60c93b2ced04f57 Mon Sep 17 00:00:00 2001 From: klaus Date: Sat, 11 Jul 2026 22:03:16 +0900 Subject: [PATCH] =?UTF-8?q?fix(dm):=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=EA=B3=BC=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=EB=A5=BC=20=EB=B3=B4=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/main/chat/dm/DmChatRoomActivity.kt | 28 +++--- .../chat/dm/DmChatRoomActivitySourceTest.kt | 85 +++++++++++++++++++ 2 files changed, 102 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt index b4b1e9ec..324266e9 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt @@ -4,15 +4,15 @@ import android.content.Context import android.content.Intent import android.text.Editable import android.text.TextWatcher -import android.view.View import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator import coil.transform.CircleCropTransformation import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.databinding.ActivityDmChatRoomBinding import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.v2.main.chat.dm.model.DmChatRoomUiState @@ -25,6 +25,7 @@ class DmChatRoomActivity : BaseActivity( private val viewModel: DmChatRoomViewModel by viewModel() private val messageAdapter = DmChatMessageAdapter { localId -> viewModel.retry(localId) } + private val loadingDialog: LoadingDialog by lazy { LoadingDialog(this, layoutInflater) } private lateinit var layoutManager: LinearLayoutManager private var roomId: Long = 0L private var creatorId: Long = 0L @@ -119,6 +120,9 @@ class DmChatRoomActivity : BaseActivity( viewModel.roomOpenedEventLiveData.observe(this) { if (it.consume() == true) connectRealtimeIfStarted() } + viewModel.isRealtimeConnectingLiveData.observe(this) { isConnecting -> + updateRealtimeLoadingDialog(isConnecting) + } } private fun bindContent(state: DmChatRoomUiState.Content) { @@ -129,7 +133,7 @@ class DmChatRoomActivity : BaseActivity( error(R.drawable.ic_placeholder_profile) transformations(CircleCropTransformation()) } - binding.ivBackgroundProfile.loadUrl(state.opponentProfileImageUrl) { + binding.ivBackgroundProfile.loadUrl(SharedPreferenceManager.profileImage) { placeholder(R.drawable.ic_placeholder_profile) error(R.drawable.ic_placeholder_profile) } @@ -143,10 +147,10 @@ class DmChatRoomActivity : BaseActivity( val text = binding.etMessage.text?.toString().orEmpty() if (text.isBlank()) return - viewModel.sendText(text) - binding.etMessage.setText("") - setSendButtonEnabled(false) - hideKeyboard(binding.etMessage) + if (viewModel.sendText(text)) { + binding.etMessage.setText("") + setSendButtonEnabled(false) + } } private fun setSendButtonEnabled(enabled: Boolean) { @@ -182,10 +186,12 @@ class DmChatRoomActivity : BaseActivity( viewModel.connectRealtime() } - private fun hideKeyboard(view: View) { - val imm = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.hideSoftInputFromWindow(view.windowToken, 0) - view.clearFocus() + private fun updateRealtimeLoadingDialog(isConnecting: Boolean) { + if (isConnecting) { + loadingDialog.show(screenWidth) + } else { + loadingDialog.dismiss() + } } companion object { diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivitySourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivitySourceTest.kt index f93b6644..a965a691 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivitySourceTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivitySourceTest.kt @@ -14,6 +14,8 @@ class DmChatRoomActivitySourceTest { ).readText() assertTrue(source.contains("BaseActivity")) + assertTrue(source.contains("import kr.co.vividnext.sodalive.common.LoadingDialog")) + assertTrue(source.contains("private val loadingDialog: LoadingDialog by lazy")) assertTrue(source.contains("const val EXTRA_ROOM_ID")) assertTrue(source.contains("const val EXTRA_CREATOR_ID")) assertTrue(source.contains("fun newIntentByRoomId")) @@ -26,6 +28,11 @@ class DmChatRoomActivitySourceTest { assertTrue(source.contains("EditorInfo.IME_ACTION_SEND")) assertTrue(source.contains("binding.ivSend.setOnClickListener")) assertTrue(source.contains("viewModel.sendText")) + assertTrue(source.contains("if (viewModel.sendText(text))")) + assertFalse(source.contains("viewModel.sendText(text)\n binding.etMessage.setText(\"\")")) + assertFalse(source.contains("hideKeyboard")) + assertFalse(source.contains("hideSoftInputFromWindow")) + assertFalse(source.contains("view.clearFocus()")) assertTrue(source.contains("findFirstVisibleItemPosition()")) assertTrue(source.contains("viewModel.loadOlderMessages()")) assertTrue(source.contains("prependedMessageCountLiveData.observe(this)")) @@ -35,6 +42,10 @@ class DmChatRoomActivitySourceTest { assertTrue(source.contains("viewModel.connectRealtime()")) assertTrue(source.contains("roomOpenedEventLiveData.observe(this)")) assertTrue(source.contains("if (it.consume() == true) connectRealtimeIfStarted()")) + assertTrue(source.contains("isRealtimeConnectingLiveData.observe(this)")) + assertTrue(source.contains("updateRealtimeLoadingDialog(isConnecting)")) + assertTrue(source.contains("loadingDialog.show(screenWidth)")) + assertTrue(source.contains("loadingDialog.dismiss()")) assertTrue(source.contains("connectRealtimeIfStarted()")) assertTrue(source.contains("viewModel.leaveRealtime()")) assertFalse(source.contains("if (isStarted) viewModel.connectRealtime()")) @@ -53,6 +64,80 @@ class DmChatRoomActivitySourceTest { assertTrue(source.contains("putExtra(EXTRA_CREATOR_ID, creatorId)")) } + @Test + fun `DM 메시지 UI는 좌우 정렬 기존 버블 텍스트 전송만 유지한다`() { + val myMessage = projectFile( + "app/src/main/res/layout/item_dm_chat_my_message.xml" + ).readText() + val opponentMessage = projectFile( + "app/src/main/res/layout/item_dm_chat_opponent_message.xml" + ).readText() + val roomLayout = projectFile( + "app/src/main/res/layout/activity_dm_chat_room.xml" + ).readText() + val activity = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt" + ).readText() + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/ui/DmChatMessageAdapter.kt" + ).readText() + + assertTrue(myMessage.contains("android:background=\"@drawable/bg_chat_user_message\"")) + assertTrue(myMessage.contains("android:id=\"@+id/tv_time\"")) + assertTrue(myMessage.contains("app:layout_constraintEnd_toStartOf=\"@id/message_container\"")) + assertTrue(myMessage.contains("app:layout_constraintEnd_toEndOf=\"parent\"")) + assertTrue(myMessage.contains("app:layout_constraintStart_toStartOf=\"parent\"")) + assertTrue(myMessage.contains("app:layout_constraintStart_toEndOf=\"@id/tv_time\"")) + assertTrue(myMessage.contains("app:layout_constraintHorizontal_bias=\"1\"")) + assertTrue(myMessage.contains("app:layout_constraintHorizontal_chainStyle=\"packed\"")) + assertTrue(myMessage.contains("app:layout_constrainedWidth=\"true\"")) + assertTrue(opponentMessage.contains("android:background=\"@drawable/bg_chat_ai_message\"")) + assertTrue(opponentMessage.contains("android:id=\"@+id/tv_time\"")) + assertTrue(opponentMessage.contains("app:layout_constraintStart_toStartOf=\"parent\"")) + assertTrue(opponentMessage.contains("app:layout_constraintStart_toEndOf=\"@id/iv_profile\"")) + assertTrue(opponentMessage.contains("app:layout_constraintStart_toEndOf=\"@id/message_group\"")) + assertTrue(adapter.contains("import kr.co.vividnext.sodalive.chat.talk.room.formatMessageTime")) + assertTrue(adapter.contains("tvTime.text = formatMessageTime(item.createdAt)")) + assertTrue(adapter.contains("MESSAGE_MAX_WIDTH_RATIO = 0.68f")) + + assertTrue(roomLayout.contains("@+id/et_message")) + assertTrue(roomLayout.contains("@+id/iv_send")) + assertFalse(roomLayout.contains("voice")) + assertFalse(activity.contains("sendVoiceMessage")) + assertFalse(activity.contains("record")) + assertFalse(activity.contains("playVoice")) + } + + @Test + fun `AppDI는 DM WebSocket raw JSON logger를 production 생성 경로에 주입한다`() { + val appDi = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt" + ).readText() + + assertTrue(appDi.contains("import com.orhanobut.logger.Logger")) + assertTrue(appDi.contains("rawJsonLogger = { rawJson ->")) + assertTrue(appDi.contains("Logger.d(\"DM WebSocket raw JSON: ${'$'}rawJson\")")) + assertFalse(appDi.contains("DmChatSocketClient(okHttpClient = get(), gson = get(), baseUrl = baseUrl)")) + } + + @Test + fun `DM 채팅방 Activity 프로필 배경은 내 프로필 이미지를 사용하고 헤더는 상대 프로필을 유지한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt" + ).readText() + + val foregroundProfileBinding = source.substringAfter("binding.ivProfile.loadUrl(") + .substringBefore("binding.ivBackgroundProfile.loadUrl(") + val backgroundProfileBinding = source.substringAfter("binding.ivBackgroundProfile.loadUrl(") + .substringBefore("messageAdapter.submitItems") + + assertTrue(source.contains("import kr.co.vividnext.sodalive.common.SharedPreferenceManager")) + assertTrue(foregroundProfileBinding.contains("state.opponentProfileImageUrl")) + assertTrue(foregroundProfileBinding.contains("transformations(CircleCropTransformation())")) + assertTrue(backgroundProfileBinding.contains("SharedPreferenceManager.profileImage")) + assertFalse(backgroundProfileBinding.contains("state.opponentProfileImageUrl")) + } + private fun projectFile(relativePath: String): File { val candidates = listOf(File(relativePath), File("../$relativePath")) return candidates.firstOrNull { it.exists() }