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() } + } +}