feat(creator-channel): FanTalk 상세 화면을 추가한다

This commit is contained in:
2026-07-09 07:44:06 +09:00
parent 04dcb3f09f
commit da26b65a73
5 changed files with 652 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail
import kr.co.vividnext.sodalive.R
import org.junit.Assert.assertFalse
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
class CreatorChannelFanTalkDetailUiContractTest {
@Test
fun `상세 Activity source는 payload intent와 ViewModel 이벤트를 연결한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/detail/CreatorChannelFanTalkDetailActivity.kt"
).readText()
assertTrue(source.contains("BaseActivity<ActivityCreatorChannelFantalkDetailBinding>"))
assertTrue(source.contains("private val viewModel: CreatorChannelFanTalkDetailViewModel by viewModel()"))
assertTrue(
source.contains(
"fun newIntent(context: Context, creatorId: Long, " +
"payload: CreatorChannelFanTalkDetailPayload): Intent"
)
)
assertTrue(source.contains("putExtra(EXTRA_PAYLOAD, payload)"))
assertTrue(source.contains("IntentCompat.getParcelableExtra"))
assertTrue(source.contains("if (creatorId <= 0L || payload.fanTalkId <= 0L)"))
assertTrue(source.contains("R.string.creator_channel_community_invalid_post"))
assertTrue(source.contains("viewModel.load(creatorId, payload)"))
assertTrue(source.contains("doAfterTextChanged"))
assertTrue(source.contains("viewModel.updateReplyInput"))
assertTrue(source.contains("viewModel.submitReply()"))
assertTrue(source.contains("viewModel.cancelReplyEdit()"))
assertTrue(source.contains("viewModel.replacementModalEventLiveData.observe"))
assertTrue(source.contains("viewModel.replyChangedEventLiveData.observe"))
assertTrue(source.contains("setResult(Activity.RESULT_OK)"))
assertTrue(source.contains("viewModel.consumeReplyChangedEvent()"))
assertTrue(source.contains("viewModel.startReplyEdit()"))
assertTrue(source.contains("showDeleteReplyDialog()"))
assertTrue(source.contains("ivCreatorChannelFantalkDetailReplyMore.isVisible = reply.fanTalkId > 0L"))
assertTrue(source.contains("V2ModalDialog("))
assertTrue(source.contains("getString(modal.titleResId)"))
assertTrue(source.contains("getString(modal.descriptionResId)"))
assertTrue(source.contains("getString(modal.confirmResId)"))
assertTrue(source.contains("getString(modal.cancelResId)"))
assertTrue(source.contains("R.drawable.bg_creator_channel_community_send_enabled"))
assertTrue(source.contains("R.drawable.bg_creator_channel_community_send_disabled"))
assertTrue(source.contains("R.drawable.ic_new_arrow_up_white"))
assertTrue(source.contains("R.drawable.ic_new_arrow_up_gray"))
}
@Test
fun `상세 layout은 커뮤니티 답글 구조와 FanTalk 답글 card 토큰을 사용한다`() {
val layout = projectFile("app/src/main/res/layout/activity_creator_channel_fantalk_detail.xml").readText()
assertTrue(layout.contains("""android:background="@color/black""""))
assertTrue(layout.contains("""android:id="@+id/btn_creator_channel_fantalk_detail_back""""))
assertTrue(layout.contains("""android:layout_height="60dp""""))
assertTrue(layout.contains("""android:id="@+id/iv_creator_channel_fantalk_detail_profile""""))
assertTrue(layout.contains("""android:layout_width="42dp""""))
assertTrue(layout.contains("""android:id="@+id/tv_creator_channel_fantalk_detail_nickname""""))
assertTrue(layout.contains("""android:id="@+id/tv_creator_channel_fantalk_detail_content""""))
assertTrue(layout.contains("""android:id="@+id/layout_creator_channel_fantalk_detail_reply""""))
assertTrue(layout.contains("@drawable/bg_creator_channel_fantalk_reply"))
assertTrue(layout.contains("""android:id="@+id/iv_creator_channel_fantalk_detail_reply_profile""""))
assertTrue(layout.contains("""android:layout_width="20dp""""))
assertTextViewUsesStyle(
layout,
"tv_creator_channel_fantalk_detail_reply_nickname",
"@style/Typography.Caption3"
)
assertTextViewUsesStyle(
layout,
"tv_creator_channel_fantalk_detail_reply_time",
"@style/Typography.Caption3"
)
assertTextViewContains(
layout,
"tv_creator_channel_fantalk_detail_reply_nickname",
"""android:layout_width="wrap_content"""
)
assertTextViewContains(
layout,
"tv_creator_channel_fantalk_detail_reply_time",
"""android:layout_marginStart="@dimen/spacing_4"""
)
assertTrue(layout.contains("""android:id="@+id/iv_creator_channel_fantalk_detail_reply_more""""))
assertTrue(layout.contains("""android:id="@+id/layout_creator_channel_fantalk_detail_input_bar""""))
assertTrue(layout.contains("view_creator_channel_fantalk_detail_reply_header_spacer"))
assertTrue(layout.contains("android:layout_width=\"0dp\""))
assertTrue(layout.contains("android:layout_weight=\"1\""))
assertTrue(layout.contains("@drawable/bg_creator_channel_community_input_field"))
assertTrue(layout.contains("@string/creator_channel_community_reply_placeholder"))
assertTrue(layout.contains("""android:maxLines="3""""))
assertTrue(layout.contains("@drawable/bg_creator_channel_community_send_disabled"))
assertTrue(layout.contains("@drawable/ic_new_arrow_up_gray"))
}
@Test
fun `상세 Manifest DI ViewModel source 계약을 등록한다`() {
val manifest = projectFile("app/src/main/AndroidManifest.xml").readText()
val appDi = projectFile("app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt").readText()
val viewModel = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/detail/CreatorChannelFanTalkDetailViewModel.kt"
).readText()
assertTrue(manifest.contains(".v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailActivity"))
assertTrue(manifest.contains("""android:windowSoftInputMode="stateAlwaysHidden|adjustResize""""))
assertTrue(
appDi.contains(
"import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail." +
"CreatorChannelFanTalkDetailViewModel"
)
)
assertTrue(appDi.contains("viewModel { CreatorChannelFanTalkDetailViewModel(get(), get()) }"))
assertTrue(viewModel.contains("fun cancelReplyEdit()"))
assertTrue(viewModel.contains("fun consumeReplyChangedEvent()"))
}
@Test
fun `답글 대체 모달 문자열은 리소스 id와 다국어 strings로 제공한다`() {
val uiModels = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/detail/CreatorChannelFanTalkDetailUiModels.kt"
).readText()
val koStrings = projectFile("app/src/main/res/values/strings.xml").readText()
val enStrings = projectFile("app/src/main/res/values-en/strings.xml").readText()
val jaStrings = projectFile("app/src/main/res/values-ja/strings.xml").readText()
assertTrue(uiModels.contains("R.string.screen_user_profile_reply_edit"))
assertTrue(uiModels.contains("R.string.creator_channel_fantalk_reply_replace_description"))
assertTrue(uiModels.contains("R.string.cancel"))
assertTrue(uiModels.contains("R.string.confirm"))
assertFalse(uiModels.contains("기존 답글이 신규 답글로 대체됩니다"))
assertTrue(koStrings.contains("creator_channel_fantalk_reply_replace_description"))
assertTrue(enStrings.contains("creator_channel_fantalk_reply_replace_description"))
assertTrue(jaStrings.contains("creator_channel_fantalk_reply_replace_description"))
assertEquals(R.string.screen_user_profile_reply_edit, CreatorChannelFanTalkReplacementModalUiModel().titleResId)
}
private fun assertTextViewUsesStyle(layout: String, id: String, style: String) {
val textViewPattern = Regex("<TextView[\\s\\S]*?android:id=\"@\\+id/$id\"[\\s\\S]*?style=\"$style\"")
assertTrue("Expected $id to use $style", textViewPattern.containsMatchIn(layout))
}
private fun assertTextViewContains(layout: String, id: String, expected: String) {
val textViewPattern = Regex("<TextView[\\s\\S]*?android:id=\"@\\+id/$id\"[\\s\\S]*?/>")
val textView = textViewPattern.find(layout)?.value.orEmpty()
assertTrue("Expected $id to contain $expected", textView.contains(expected))
}
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() }
}
}