feat(creator-channel): FanTalk 글 입력 화면을 연결한다

This commit is contained in:
2026-07-08 03:44:44 +09:00
parent b020b8096b
commit 25af312d91
4 changed files with 228 additions and 0 deletions

View File

@@ -114,6 +114,9 @@
<activity android:name=".v2.main.MainV2Activity" /> <activity android:name=".v2.main.MainV2Activity" />
<activity android:name=".v2.main.content.overview.ContentOverviewActivity" /> <activity android:name=".v2.main.content.overview.ContentOverviewActivity" />
<activity android:name=".v2.creator.channel.CreatorChannelActivity" /> <activity android:name=".v2.creator.channel.CreatorChannelActivity" />
<activity
android:name=".v2.creator.channel.fantalk.write.CreatorChannelFanTalkWriteActivity"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".v2.live.onair.HomeOnAirLiveActivity" /> <activity android:name=".v2.live.onair.HomeOnAirLiveActivity" />
<activity <activity
android:name=".v2.main.chat.dm.DmChatRoomActivity" android:name=".v2.main.chat.dm.DmChatRoomActivity"

View File

@@ -185,6 +185,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.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationViewModel 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.CreatorChannelFanTalkViewModel
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.live.CreatorChannelLiveViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesViewModel import kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesViewModel
import kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveViewModel import kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveViewModel
@@ -453,6 +454,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { CreatorChannelSeriesViewModel(get()) } viewModel { CreatorChannelSeriesViewModel(get()) }
viewModel { CreatorChannelCommunityViewModel(get(), get()) } viewModel { CreatorChannelCommunityViewModel(get(), get()) }
viewModel { CreatorChannelFanTalkViewModel(get(), get()) } viewModel { CreatorChannelFanTalkViewModel(get(), get()) }
viewModel { CreatorChannelFanTalkWriteViewModel(get()) }
viewModel { CreatorChannelDonationViewModel(get(), get()) } viewModel { CreatorChannelDonationViewModel(get(), get()) }
viewModel { PushNotificationListViewModel(get()) } viewModel { PushNotificationListViewModel(get()) }
viewModel { CharacterTabViewModel(get()) } viewModel { CharacterTabViewModel(get()) }

View File

@@ -0,0 +1,146 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.write
import android.content.Context
import android.content.Intent
import android.text.InputFilter
import android.view.Gravity
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.addCallback
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.widget.addTextChangedListener
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelFantalkWriteBinding
import org.koin.androidx.viewmodel.ext.android.viewModel
class CreatorChannelFanTalkWriteActivity : BaseActivity<ActivityCreatorChannelFantalkWriteBinding>(
ActivityCreatorChannelFantalkWriteBinding::inflate
) {
private val viewModel: CreatorChannelFanTalkWriteViewModel by viewModel()
private var creatorId: Long = 0L
override fun setupView() {
creatorId = intent.getLongExtra(EXTRA_CREATOR_ID, 0L)
if (creatorId <= 0L) {
finish()
return
}
setupContentInput()
setupClickListeners()
setupBackHandler()
observeViewModel()
focusContentInput()
}
private fun setupContentInput() = with(binding.etCreatorChannelFantalkWriteContent) {
filters = arrayOf(InputFilter.LengthFilter(CreatorChannelFanTalkWriteViewModel.MAX_CONTENT_LENGTH))
}
private fun setupClickListeners() = with(binding) {
tvCreatorChannelFantalkWriteCancel.setOnClickListener { handleCancel() }
tvCreatorChannelFantalkWriteSend.setOnClickListener { viewModel.submit(creatorId) }
etCreatorChannelFantalkWriteContent.addTextChangedListener { text ->
viewModel.onContentChanged(text?.toString().orEmpty())
}
}
private fun setupBackHandler() {
onBackPressedDispatcher.addCallback(this) {
handleCancel()
}
}
private fun observeViewModel() {
viewModel.isSendEnabledLiveData.observe(this) { isEnabled ->
bindSendEnabled(isEnabled && viewModel.isLoadingLiveData.value != true)
}
viewModel.contentLengthLiveData.observe(this) { length ->
binding.tvCreatorChannelFantalkWriteCount.text = length.toString()
}
viewModel.isLoadingLiveData.observe(this) { isLoading ->
bindLoading(isLoading)
}
viewModel.finishEventLiveData.observe(this) { shouldFinish ->
if (shouldFinish == true) {
setResult(RESULT_OK)
finish()
}
}
viewModel.toastEventLiveData.observe(this) { message ->
message?.let { Toast.makeText(this, it, Toast.LENGTH_SHORT).show() }
}
}
private fun bindSendEnabled(isEnabled: Boolean) = with(binding.tvCreatorChannelFantalkWriteSend) {
this.isEnabled = isEnabled
setTextColor(
ContextCompat.getColor(
context,
if (isEnabled) R.color.white else R.color.gray_500
)
)
setBackgroundResource(
if (isEnabled) {
R.drawable.bg_creator_channel_fantalk_write_send_enabled
} else {
R.drawable.bg_creator_channel_fantalk_write_send_disabled
}
)
}
private fun bindLoading(isLoading: Boolean) = with(binding) {
etCreatorChannelFantalkWriteContent.isEnabled = !isLoading
tvCreatorChannelFantalkWriteSend.text = getString(
if (isLoading) R.string.creator_channel_fantalk_write_sending else R.string.creator_channel_fantalk_write_send
)
bindSendEnabled(!isLoading && viewModel.isSendEnabledLiveData.value == true)
}
private fun focusContentInput() {
binding.etCreatorChannelFantalkWriteContent.post {
binding.etCreatorChannelFantalkWriteContent.requestFocus()
WindowCompat.getInsetsController(window, binding.etCreatorChannelFantalkWriteContent)
.show(WindowInsetsCompat.Type.ime())
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.etCreatorChannelFantalkWriteContent, InputMethodManager.SHOW_IMPLICIT)
}
}
private fun handleCancel() {
if (binding.etCreatorChannelFantalkWriteContent.text?.toString().orEmpty().trim().isEmpty()) {
finish()
} else {
showCancelConfirmDialog()
}
}
private fun showCancelConfirmDialog() {
SodaDialog(
activity = this,
layoutInflater = layoutInflater,
title = getString(R.string.creator_channel_fantalk_write_exit_title),
desc = getString(R.string.creator_channel_fantalk_write_exit_desc),
confirmButtonTitle = getString(R.string.creator_channel_fantalk_write_exit_confirm),
confirmButtonClick = { finish() },
cancelButtonTitle = getString(R.string.creator_channel_fantalk_write_exit_cancel),
cancelButtonClick = {},
descGravity = Gravity.CENTER
).show(screenWidth)
}
companion object {
const val EXTRA_CREATOR_ID: String = "extra_creator_id"
fun newIntent(context: Context, creatorId: Long): Intent {
return Intent(context, CreatorChannelFanTalkWriteActivity::class.java).apply {
putExtra(EXTRA_CREATOR_ID, creatorId)
}
}
}
}

View File

@@ -0,0 +1,77 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.write
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
class CreatorChannelFanTalkWriteSourceTest {
@Test
fun `글 입력 Activity source는 intent focus keyboard result cancel back 계약을 포함한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/write/CreatorChannelFanTalkWriteActivity.kt"
).readText()
val manifest = projectFile("app/src/main/AndroidManifest.xml").readText()
assertTrue(source.contains("fun newIntent(context: Context, creatorId: Long): Intent"))
assertTrue(source.contains("putExtra(EXTRA_CREATOR_ID, creatorId)"))
assertTrue(source.contains("if (creatorId <= 0L)"))
assertTrue(source.contains("finish()"))
assertTrue(source.contains("requestFocus()"))
assertTrue(source.contains("WindowCompat.getInsetsController(window, binding.etCreatorChannelFantalkWriteContent)"))
assertTrue(source.contains("WindowInsetsCompat.Type.ime()"))
assertTrue(source.contains("onBackPressedDispatcher.addCallback"))
assertTrue(source.contains("showCancelConfirmDialog()"))
assertTrue(source.contains("setResult(RESULT_OK)"))
assertTrue(source.contains("viewModel.submit(creatorId)"))
assertTrue(manifest.contains(".v2.creator.channel.fantalk.write.CreatorChannelFanTalkWriteActivity"))
}
@Test
fun `글 입력 layout과 문자열은 Figma 확정 계약을 포함한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/write/CreatorChannelFanTalkWriteActivity.kt"
).readText()
val layout = projectFile("app/src/main/res/layout/activity_creator_channel_fantalk_write.xml").readText()
val strings = projectFile("app/src/main/res/values/strings.xml").readText()
val stringsEn = projectFile("app/src/main/res/values-en/strings.xml").readText()
val stringsJa = projectFile("app/src/main/res/values-ja/strings.xml").readText()
assertTrue(layout.contains("@+id/tv_creator_channel_fantalk_write_cancel"))
assertTrue(layout.contains("@+id/et_creator_channel_fantalk_write_content"))
assertTrue(layout.contains("@+id/tv_creator_channel_fantalk_write_send"))
assertTrue(layout.contains("@+id/layout_creator_channel_fantalk_write_info_bar"))
assertTrue(layout.contains("@+id/tv_creator_channel_fantalk_write_count"))
assertTrue(layout.contains("@+id/tv_creator_channel_fantalk_write_count_limit"))
assertTrue(layout.contains("android:maxLength=\"500\""))
assertTrue(layout.contains("android:text=\"/500자\""))
assertFalse(layout.contains("tv_creator_channel_fantalk_write_title"))
assertTrue(layout.contains("@string/creator_channel_fantalk_write_cancel"))
assertTrue(layout.contains("@string/creator_channel_fantalk_write_hint"))
assertTrue(layout.contains("@string/creator_channel_fantalk_write_send"))
assertTrue(
projectFile("app/src/main/res/drawable/bg_creator_channel_fantalk_write_send_enabled.xml")
.readText()
.contains("@color/soda_400")
)
assertTrue(source.contains("if (isEnabled) R.color.white else R.color.gray_500"))
assertTrue(strings.contains("<string name=\"creator_channel_fantalk_write_exit_title\">입력 종료</string>"))
assertTrue(
strings.contains(
"<string name=\"creator_channel_fantalk_write_exit_desc\">입력을 종료할까요?\\n지금 나가면 입력한 내용이 저장되지 않아요.</string>"
)
)
assertTrue(strings.contains("<string name=\"creator_channel_fantalk_write_exit_cancel\">계속 작성</string>"))
assertTrue(strings.contains("<string name=\"creator_channel_fantalk_write_exit_confirm\">종료</string>"))
assertTrue(stringsEn.contains("creator_channel_fantalk_write_exit_title"))
assertTrue(stringsJa.contains("creator_channel_fantalk_write_exit_title"))
}
private fun projectFile(path: String): File = File(projectRoot(), path)
private fun projectRoot(): File {
return generateSequence(File(System.getProperty("user.dir") ?: ".").absoluteFile) { it.parentFile }
.first { File(it, "settings.gradle").exists() }
}
}