diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2bdde154..ba32e368 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -114,6 +114,9 @@
+
(
+ 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)
+ }
+ }
+ }
+}
diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/write/CreatorChannelFanTalkWriteSourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/write/CreatorChannelFanTalkWriteSourceTest.kt
new file mode 100644
index 00000000..9ad005f3
--- /dev/null
+++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/write/CreatorChannelFanTalkWriteSourceTest.kt
@@ -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("입력 종료"))
+ assertTrue(
+ strings.contains(
+ "입력을 종료할까요?\\n지금 나가면 입력한 내용이 저장되지 않아요."
+ )
+ )
+ assertTrue(strings.contains("계속 작성"))
+ assertTrue(strings.contains("종료"))
+ 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() }
+ }
+}