Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ec6c6c29b | ||
|
|
3d217197e7 | ||
|
|
8f942c676a | ||
|
|
388304dbaa | ||
|
|
16633b978e | ||
|
|
80a8cd1d2b | ||
|
|
60df86f899 | ||
|
|
5d43f367e3 | ||
|
|
2452a84395 | ||
|
|
7a21b37d8f | ||
|
|
cbce4af2f9 | ||
|
|
7f8388aa21 |
@@ -11,8 +11,8 @@ import kr.co.vividnext.sodalive.R
|
|||||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityFollowingCreatorBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityFollowingCreatorBinding
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.CreatorFollowNotifyFragment
|
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
|
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
|
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
@@ -43,24 +43,22 @@ class FollowingCreatorActivity : BaseActivity<ActivityFollowingCreatorBinding>(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClickFollow = { creatorId, isFollow ->
|
onClickFollow = { creatorId, isFollow ->
|
||||||
if (isFollow) {
|
if (!isFollow) {
|
||||||
val notifyFragment = CreatorFollowNotifyFragment(
|
|
||||||
onClickNotifyAll = {
|
|
||||||
viewModel.follow(creatorId, follow = true, notify = true)
|
|
||||||
},
|
|
||||||
onClickNotifyNone = {
|
|
||||||
viewModel.follow(creatorId, follow = true, notify = false)
|
|
||||||
},
|
|
||||||
onClickUnFollow = {
|
|
||||||
viewModel.follow(creatorId, follow = false, notify = false)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (notifyFragment.isAdded) return@FollowingCreatorAdapter
|
|
||||||
notifyFragment.show(supportFragmentManager, notifyFragment.tag)
|
|
||||||
} else {
|
|
||||||
viewModel.follow(creatorId)
|
viewModel.follow(creatorId)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onClickUnfollow = { creatorId, creatorName ->
|
||||||
|
V2ModalDialog(
|
||||||
|
activity = this,
|
||||||
|
layoutInflater = layoutInflater,
|
||||||
|
title = getString(R.string.creator_unfollow_confirm_title),
|
||||||
|
desc = getString(R.string.creator_unfollow_confirm_desc, creatorName),
|
||||||
|
confirmButtonTitle = getString(R.string.creator_unfollow_confirm_confirm),
|
||||||
|
confirmButtonClick = {
|
||||||
|
viewModel.follow(creatorId, follow = false, notify = false)
|
||||||
|
},
|
||||||
|
cancelButtonTitle = getString(R.string.creator_unfollow_confirm_cancel)
|
||||||
|
).show(screenWidth)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import coil.load
|
|||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.databinding.ItemFollowerListBinding
|
import kr.co.vividnext.sodalive.databinding.ItemFollowerListBinding
|
||||||
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
|
|
||||||
class FollowingCreatorAdapter(
|
class FollowingCreatorAdapter(
|
||||||
private val onClickItem: (Long) -> Unit,
|
private val onClickItem: (Long) -> Unit,
|
||||||
private val onClickFollow: (Long, Boolean) -> Unit
|
private val onClickFollow: (Long, Boolean) -> Unit,
|
||||||
|
private val onClickUnfollow: ((Long, String) -> Unit)? = null
|
||||||
) : RecyclerView.Adapter<FollowingCreatorAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<FollowingCreatorAdapter.ViewHolder>() {
|
||||||
|
|
||||||
private val items = mutableListOf<GetCreatorFollowingAllListItem>()
|
private val items = mutableListOf<GetCreatorFollowingAllListItem>()
|
||||||
@@ -31,20 +33,23 @@ class FollowingCreatorAdapter(
|
|||||||
|
|
||||||
binding.ivFollow.visibility = View.VISIBLE
|
binding.ivFollow.visibility = View.VISIBLE
|
||||||
if (item.isFollow) {
|
if (item.isFollow) {
|
||||||
binding.ivFollow.setImageResource(
|
val padding = 8.dpToPx().toInt()
|
||||||
if (item.isNotify) {
|
binding.ivFollow.setPadding(padding, padding, padding, padding)
|
||||||
R.drawable.btn_following_big
|
binding.ivFollow.setBackgroundResource(R.drawable.bg_following_circle_gray_800)
|
||||||
} else {
|
binding.ivFollow.setImageResource(R.drawable.ic_new_following_white)
|
||||||
R.drawable.btn_following_no_alarm_big
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
|
binding.ivFollow.setPadding(0, 0, 0, 0)
|
||||||
|
binding.ivFollow.background = null
|
||||||
binding.ivFollow.setImageResource(R.drawable.btn_follow_big)
|
binding.ivFollow.setImageResource(R.drawable.btn_follow_big)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.ivFollow.setOnClickListener {
|
binding.ivFollow.setOnClickListener {
|
||||||
|
if (item.isFollow && onClickUnfollow != null) {
|
||||||
|
onClickUnfollow.invoke(item.creatorId, item.nickname)
|
||||||
|
} else {
|
||||||
onClickFollow(item.creatorId, item.isFollow)
|
onClickFollow(item.creatorId, item.isFollow)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
binding.root.setOnClickListener { onClickItem(item.creatorId) }
|
binding.root.setOnClickListener { onClickItem(item.creatorId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesFr
|
|||||||
import kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinator
|
import kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinator
|
||||||
import kr.co.vividnext.sodalive.v2.live.action.LiveCreationResult
|
import kr.co.vividnext.sodalive.v2.live.action.LiveCreationResult
|
||||||
import kr.co.vividnext.sodalive.v2.live.action.resolveLiveCreationResult
|
import kr.co.vividnext.sodalive.v2.live.action.resolveLiveCreationResult
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
@@ -331,7 +332,17 @@ class CreatorChannelActivity :
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
V2ModalDialog(
|
||||||
|
activity = this,
|
||||||
|
layoutInflater = layoutInflater,
|
||||||
|
title = getString(R.string.creator_unfollow_confirm_title),
|
||||||
|
desc = getString(R.string.creator_unfollow_confirm_desc, header.nickname),
|
||||||
|
confirmButtonTitle = getString(R.string.creator_unfollow_confirm_confirm),
|
||||||
|
confirmButtonClick = {
|
||||||
homeActionDelegate?.follow(follow = false, notify = false)
|
homeActionDelegate?.follow(follow = false, notify = false)
|
||||||
|
},
|
||||||
|
cancelButtonTitle = getString(R.string.creator_unfollow_confirm_cancel)
|
||||||
|
).show(screenWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onBellClicked() {
|
private fun onBellClicked() {
|
||||||
@@ -557,6 +568,13 @@ class CreatorChannelActivity :
|
|||||||
handleChatAction(ChatActionCommand.AiRoom(chatRoomId))
|
handleChatAction(ChatActionCommand.AiRoom(chatRoomId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreatorChannelBlockSucceeded() {
|
||||||
|
if (isTaskRoot) {
|
||||||
|
startActivity(Intent(this, MainV2Activity::class.java))
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreatorChannelScheduleClicked(schedule: CreatorChannelScheduleResponse) {
|
override fun onCreatorChannelScheduleClicked(schedule: CreatorChannelScheduleResponse) {
|
||||||
onScheduleClicked(schedule)
|
onScheduleClicked(schedule)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
|
|||||||
viewModel.chatRoomIdLiveData.observe(viewLifecycleOwner) { event ->
|
viewModel.chatRoomIdLiveData.observe(viewLifecycleOwner) { event ->
|
||||||
event.consume()?.let(host::onCreatorChannelChatRoomCreated)
|
event.consume()?.let(host::onCreatorChannelChatRoomCreated)
|
||||||
}
|
}
|
||||||
|
viewModel.blockSuccessLiveData.observe(viewLifecycleOwner) { event ->
|
||||||
|
event.consume()?.let { host.onCreatorChannelBlockSucceeded() }
|
||||||
|
}
|
||||||
viewModel.toastLiveData.observe(viewLifecycleOwner) { event ->
|
viewModel.toastLiveData.observe(viewLifecycleOwner) { event ->
|
||||||
event.consume()?.let {
|
event.consume()?.let {
|
||||||
val message = it.message ?: it.resId?.let(::getString)
|
val message = it.message ?: it.resId?.let(::getString)
|
||||||
@@ -148,6 +151,7 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
|
|||||||
fun onCreatorChannelHeaderChanged(header: CreatorChannelHeaderUiModel)
|
fun onCreatorChannelHeaderChanged(header: CreatorChannelHeaderUiModel)
|
||||||
fun onCreatorChannelFollowProgressChanged(inProgress: Boolean)
|
fun onCreatorChannelFollowProgressChanged(inProgress: Boolean)
|
||||||
fun onCreatorChannelChatRoomCreated(chatRoomId: Long)
|
fun onCreatorChannelChatRoomCreated(chatRoomId: Long)
|
||||||
|
fun onCreatorChannelBlockSucceeded()
|
||||||
fun onCreatorChannelScheduleClicked(schedule: CreatorChannelScheduleResponse)
|
fun onCreatorChannelScheduleClicked(schedule: CreatorChannelScheduleResponse)
|
||||||
fun onCreatorChannelAudioContentClicked(audioContent: CreatorChannelAudioContentResponse)
|
fun onCreatorChannelAudioContentClicked(audioContent: CreatorChannelAudioContentResponse)
|
||||||
fun onCreatorChannelSeriesClicked(series: CreatorChannelSeriesResponse)
|
fun onCreatorChannelSeriesClicked(series: CreatorChannelSeriesResponse)
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ class CreatorChannelHomeViewModel(
|
|||||||
val chatRoomIdLiveData: LiveData<CreatorChannelEvent<Long>>
|
val chatRoomIdLiveData: LiveData<CreatorChannelEvent<Long>>
|
||||||
get() = _chatRoomIdLiveData
|
get() = _chatRoomIdLiveData
|
||||||
|
|
||||||
|
private val _blockSuccessLiveData = MutableLiveData<CreatorChannelEvent<Unit>>()
|
||||||
|
val blockSuccessLiveData: LiveData<CreatorChannelEvent<Unit>>
|
||||||
|
get() = _blockSuccessLiveData
|
||||||
|
|
||||||
private val _isFollowInProgressLiveData = MutableLiveData(false)
|
private val _isFollowInProgressLiveData = MutableLiveData(false)
|
||||||
val isFollowInProgressLiveData: LiveData<Boolean>
|
val isFollowInProgressLiveData: LiveData<Boolean>
|
||||||
get() = _isFollowInProgressLiveData
|
get() = _isFollowInProgressLiveData
|
||||||
@@ -179,6 +183,7 @@ class CreatorChannelHomeViewModel(
|
|||||||
_toastLiveData.value = CreatorChannelEvent(
|
_toastLiveData.value = CreatorChannelEvent(
|
||||||
ToastMessage(resId = R.string.creator_channel_block_success)
|
ToastMessage(resId = R.string.creator_channel_block_success)
|
||||||
)
|
)
|
||||||
|
_blockSuccessLiveData.value = CreatorChannelEvent(Unit)
|
||||||
} else {
|
} else {
|
||||||
showUnknownErrorToast()
|
showUnknownErrorToast()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import kr.co.vividnext.sodalive.base.BaseFragment
|
|||||||
import kr.co.vividnext.sodalive.databinding.FragmentCreatorChannelCommunityBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentCreatorChannelCommunityBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
|
import kr.co.vividnext.sodalive.settings.language.LanguageManager
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityViewMode
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityViewMode
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.CreatorChannelCommunityGridAdapter
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.CreatorChannelCommunityGridAdapter
|
||||||
@@ -134,6 +135,8 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
|
|||||||
layoutCreatorChannelCommunitySortBar.isVisible = true
|
layoutCreatorChannelCommunitySortBar.isVisible = true
|
||||||
tvCreatorChannelCommunityTotalCount.text = state.communityPostCount.moneyFormat()
|
tvCreatorChannelCommunityTotalCount.text = state.communityPostCount.moneyFormat()
|
||||||
tvCreatorChannelCommunityViewModeLabel.setText(state.viewMode.labelResId)
|
tvCreatorChannelCommunityViewModeLabel.setText(state.viewMode.labelResId)
|
||||||
|
tvCreatorChannelCommunityViewModeLabel.isVisible =
|
||||||
|
LanguageManager.getEffectiveLanguage(requireContext()) != LanguageManager.LANG_JA
|
||||||
ivCreatorChannelCommunityViewMode.setImageResource(state.viewMode.iconResId)
|
ivCreatorChannelCommunityViewMode.setImageResource(state.viewMode.iconResId)
|
||||||
rvCreatorChannelCommunity.isVisible = true
|
rvCreatorChannelCommunity.isVisible = true
|
||||||
bindCommunityAdapter(state)
|
bindCommunityAdapter(state)
|
||||||
|
|||||||
@@ -102,12 +102,11 @@ class CreatorChannelDonationAdapter(
|
|||||||
private val onEmptyDonationClick: () -> Unit
|
private val onEmptyDonationClick: () -> Unit
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
init {
|
|
||||||
binding.btnCreatorChannelDonationEmptyWrite.setOnClickListener { onEmptyDonationClick() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bind(isOwner: Boolean) = with(binding) {
|
fun bind(isOwner: Boolean) = with(binding) {
|
||||||
btnCreatorChannelDonationEmptyWrite.isVisible = !isOwner
|
btnCreatorChannelDonationEmptyWrite.isVisible = !isOwner
|
||||||
|
btnCreatorChannelDonationEmptyWrite.setOnClickListener(
|
||||||
|
if (!isOwner) View.OnClickListener { onEmptyDonationClick() } else null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ class CreatorChannelFanTalkFragment : BaseFragment<FragmentCreatorChannelFantalk
|
|||||||
viewModel.retryFanTalks()
|
viewModel.retryFanTalks()
|
||||||
}
|
}
|
||||||
btnCreatorChannelFantalkWrite.setOnClickListener { host.onCreatorChannelFanTalkWriteClicked() }
|
btnCreatorChannelFantalkWrite.setOnClickListener { host.onCreatorChannelFanTalkWriteClicked() }
|
||||||
layoutCreatorChannelFantalkEmptyWriteButton.setOnClickListener { host.onCreatorChannelFanTalkWriteClicked() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeViewModel() {
|
private fun observeViewModel() {
|
||||||
@@ -113,6 +112,10 @@ class CreatorChannelFanTalkFragment : BaseFragment<FragmentCreatorChannelFantalk
|
|||||||
tvCreatorChannelFantalkErrorMessage.isVisible = false
|
tvCreatorChannelFantalkErrorMessage.isVisible = false
|
||||||
btnCreatorChannelFantalkRetry.isVisible = false
|
btnCreatorChannelFantalkRetry.isVisible = false
|
||||||
btnCreatorChannelFantalkWrite.isVisible = false
|
btnCreatorChannelFantalkWrite.isVisible = false
|
||||||
|
layoutCreatorChannelFantalkEmptyWriteButton.isVisible = !host.isCreatorChannelOwner()
|
||||||
|
layoutCreatorChannelFantalkEmptyWriteButton.setOnClickListener(
|
||||||
|
if (!host.isCreatorChannelOwner()) View.OnClickListener { host.onCreatorChannelFanTalkWriteClicked() } else null
|
||||||
|
)
|
||||||
host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(false)
|
host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(false)
|
||||||
host.onCreatorChannelFanTalkContentChanged()
|
host.onCreatorChannelFanTalkContentChanged()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ fun CreatorChannelHomeResponse.toUiContent(currentMemberId: Long): CreatorChanne
|
|||||||
series.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Series(it)) }
|
series.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Series(it)) }
|
||||||
communities.takeIf { it.isNotEmpty() }
|
communities.takeIf { it.isNotEmpty() }
|
||||||
?.let { add(CreatorChannelHomeSection.Communities(it, currentMemberId)) }
|
?.let { add(CreatorChannelHomeSection.Communities(it, currentMemberId)) }
|
||||||
add(CreatorChannelHomeSection.FanTalk(fanTalk))
|
add(CreatorChannelHomeSection.FanTalk(fanTalk, isOwner))
|
||||||
introduce.takeIf { it.isNotBlank() }?.let { add(CreatorChannelHomeSection.Introduce(it)) }
|
introduce.takeIf { it.isNotBlank() }?.let { add(CreatorChannelHomeSection.Introduce(it)) }
|
||||||
add(CreatorChannelHomeSection.Activity(activity))
|
add(CreatorChannelHomeSection.Activity(activity))
|
||||||
sns.toUiItems().takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Sns(it)) }
|
sns.toUiItems().takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Sns(it)) }
|
||||||
|
|||||||
@@ -61,7 +61,10 @@ sealed interface CreatorChannelHomeSection {
|
|||||||
val communities: List<CreatorChannelCommunityPostResponse>,
|
val communities: List<CreatorChannelCommunityPostResponse>,
|
||||||
val currentMemberId: Long
|
val currentMemberId: Long
|
||||||
) : CreatorChannelHomeSection
|
) : CreatorChannelHomeSection
|
||||||
data class FanTalk(val fanTalk: CreatorChannelFanTalkSummaryResponse) : CreatorChannelHomeSection
|
data class FanTalk(
|
||||||
|
val fanTalk: CreatorChannelFanTalkSummaryResponse,
|
||||||
|
val isOwner: Boolean
|
||||||
|
) : CreatorChannelHomeSection
|
||||||
data class Introduce(val introduce: String) : CreatorChannelHomeSection
|
data class Introduce(val introduce: String) : CreatorChannelHomeSection
|
||||||
data class Activity(val activity: CreatorChannelActivityResponse) : CreatorChannelHomeSection
|
data class Activity(val activity: CreatorChannelActivityResponse) : CreatorChannelHomeSection
|
||||||
data class Sns(val items: List<CreatorChannelSnsUiItem>) : CreatorChannelHomeSection
|
data class Sns(val items: List<CreatorChannelSnsUiItem>) : CreatorChannelHomeSection
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
private val fanTalkTotalRow: View? = view.findViewById(R.id.layout_fantalk_total_row)
|
private val fanTalkTotalRow: View? = view.findViewById(R.id.layout_fantalk_total_row)
|
||||||
private val fanTalkLatestRow: View? = view.findViewById(R.id.layout_fantalk_latest_row)
|
private val fanTalkLatestRow: View? = view.findViewById(R.id.layout_fantalk_latest_row)
|
||||||
private val fanTalkEmpty: View? = view.findViewById(R.id.layout_fantalk_empty)
|
private val fanTalkEmpty: View? = view.findViewById(R.id.layout_fantalk_empty)
|
||||||
|
private val fanTalkSupportButton: View? = view.findViewById(R.id.layout_fantalk_support_button)
|
||||||
private val fanTalkTotalCount: TextView? = view.findViewById(R.id.tv_fantalk_total_count)
|
private val fanTalkTotalCount: TextView? = view.findViewById(R.id.tv_fantalk_total_count)
|
||||||
private val fanTalkProfile: ImageView? = view.findViewById(R.id.iv_fantalk_profile)
|
private val fanTalkProfile: ImageView? = view.findViewById(R.id.iv_fantalk_profile)
|
||||||
private val fanTalkContent: TextView? = view.findViewById(R.id.tv_fantalk_content)
|
private val fanTalkContent: TextView? = view.findViewById(R.id.tv_fantalk_content)
|
||||||
@@ -184,13 +185,7 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
val isDonationEmptyButtonVisible = !item.isOwner
|
val isDonationEmptyButtonVisible = !item.isOwner
|
||||||
donationButton?.isVisible = isDonationButtonVisible
|
donationButton?.isVisible = isDonationButtonVisible
|
||||||
donationEmptyButton?.isVisible = isDonationEmptyButtonVisible
|
donationEmptyButton?.isVisible = isDonationEmptyButtonVisible
|
||||||
donationEmptyTitle?.setText(
|
donationEmptyTitle?.setText(R.string.creator_channel_home_donation_empty_title)
|
||||||
if (item.isOwner) {
|
|
||||||
R.string.creator_channel_donation_empty_owner_title
|
|
||||||
} else {
|
|
||||||
R.string.creator_channel_donation_empty_title
|
|
||||||
}
|
|
||||||
)
|
|
||||||
donationButton?.setOnClickListener(
|
donationButton?.setOnClickListener(
|
||||||
if (isDonationButtonVisible) View.OnClickListener { onDonationClick() } else null
|
if (isDonationButtonVisible) View.OnClickListener { onDonationClick() } else null
|
||||||
)
|
)
|
||||||
@@ -417,6 +412,11 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
fanTalkTotalRow?.isVisible = fanTalk != null && item.fanTalk.totalCount > 0
|
fanTalkTotalRow?.isVisible = fanTalk != null && item.fanTalk.totalCount > 0
|
||||||
fanTalkLatestRow?.isVisible = fanTalk != null && item.fanTalk.totalCount > 0
|
fanTalkLatestRow?.isVisible = fanTalk != null && item.fanTalk.totalCount > 0
|
||||||
fanTalkEmpty?.isVisible = fanTalk == null || item.fanTalk.totalCount <= 0
|
fanTalkEmpty?.isVisible = fanTalk == null || item.fanTalk.totalCount <= 0
|
||||||
|
val showSupportButton = (fanTalk == null || item.fanTalk.totalCount <= 0) && !item.isOwner
|
||||||
|
fanTalkSupportButton?.isVisible = showSupportButton
|
||||||
|
fanTalkSupportButton?.setOnClickListener(
|
||||||
|
if (showSupportButton) View.OnClickListener { onSectionChevronClick(CreatorChannelTab.FanTalk) } else null
|
||||||
|
)
|
||||||
if (fanTalk != null) {
|
if (fanTalk != null) {
|
||||||
fanTalkContent?.text = fanTalk.content
|
fanTalkContent?.text = fanTalk.content
|
||||||
fanTalkProfile?.loadUrl(fanTalk.profileImageUrl) {
|
fanTalkProfile?.loadUrl(fanTalk.profileImageUrl) {
|
||||||
|
|||||||
@@ -115,8 +115,7 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
private val popularCommunityAdapter = HomePopularCommunityAdapter { openPopularCommunityPost(it) }
|
private val popularCommunityAdapter = HomePopularCommunityAdapter { openPopularCommunityPost(it) }
|
||||||
private val creatorRankingAdapter = CreatorRankingAdapter { openCreatorRankingProfile(it) }
|
private val creatorRankingAdapter = CreatorRankingAdapter { openCreatorRankingProfile(it) }
|
||||||
private val followingCreatorAdapter = HomeFollowingCreatorAdapter(
|
private val followingCreatorAdapter = HomeFollowingCreatorAdapter(
|
||||||
onClickItem = { openCreatorProfile(it.creatorId) },
|
onClickItem = { openCreatorProfile(it.creatorId) }
|
||||||
onClickAll = { openFollowingCreatorAll() }
|
|
||||||
)
|
)
|
||||||
private val followingLiveAdapter = HomeFollowingLiveAdapter { onFollowingLiveClick(it) }
|
private val followingLiveAdapter = HomeFollowingLiveAdapter { onFollowingLiveClick(it) }
|
||||||
private val followingChatAdapter = HomeFollowingChatAdapter { openFollowingChat(it) }
|
private val followingChatAdapter = HomeFollowingChatAdapter { openFollowingChat(it) }
|
||||||
@@ -264,6 +263,7 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpFollowingAdapters() {
|
private fun setUpFollowingAdapters() {
|
||||||
|
binding.tvHomeFollowingCreatorAll.setOnClickListener { openFollowingCreatorAll() }
|
||||||
binding.rvHomeFollowingCreators.apply {
|
binding.rvHomeFollowingCreators.apply {
|
||||||
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||||
adapter = followingCreatorAdapter
|
adapter = followingCreatorAdapter
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ import kr.co.vividnext.sodalive.R
|
|||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingCreatorUiItem
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingCreatorUiItem
|
||||||
|
|
||||||
class HomeFollowingCreatorAdapter(
|
class HomeFollowingCreatorAdapter(
|
||||||
private val onClickItem: (HomeFollowingCreatorUiItem) -> Unit = {},
|
private val onClickItem: (HomeFollowingCreatorUiItem) -> Unit = {}
|
||||||
private val onClickAll: () -> Unit = {}
|
) : RecyclerView.Adapter<HomeFollowingCreatorAdapter.CreatorViewHolder>() {
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|
||||||
private var items: List<HomeFollowingCreatorUiItem> = emptyList()
|
private var items: List<HomeFollowingCreatorUiItem> = emptyList()
|
||||||
|
|
||||||
fun submitItems(items: List<HomeFollowingCreatorUiItem>) {
|
fun submitItems(items: List<HomeFollowingCreatorUiItem>) {
|
||||||
@@ -20,29 +19,18 @@ class HomeFollowingCreatorAdapter(
|
|||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CreatorViewHolder {
|
||||||
val layoutResId = when (viewType) {
|
val view = LayoutInflater.from(parent.context)
|
||||||
VIEW_TYPE_ALL -> R.layout.item_home_following_creator_all
|
.inflate(R.layout.item_home_following_creator, parent, false)
|
||||||
else -> R.layout.item_home_following_creator
|
|
||||||
}
|
|
||||||
val view = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
|
|
||||||
view.layoutParams = recyclerItemLayoutParams(parent)
|
view.layoutParams = recyclerItemLayoutParams(parent)
|
||||||
return when (viewType) {
|
return CreatorViewHolder(view, onClickItem)
|
||||||
VIEW_TYPE_ALL -> AllViewHolder(view, onClickAll)
|
|
||||||
else -> CreatorViewHolder(view, onClickItem)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: CreatorViewHolder, position: Int) {
|
||||||
when (holder) {
|
holder.bind(items[position])
|
||||||
is AllViewHolder -> holder.bind()
|
|
||||||
is CreatorViewHolder -> holder.bind(items[position])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = items.size + if (items.isNotEmpty()) 1 else 0
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int = if (position < items.size) VIEW_TYPE_CREATOR else VIEW_TYPE_ALL
|
|
||||||
|
|
||||||
class CreatorViewHolder(
|
class CreatorViewHolder(
|
||||||
itemView: View,
|
itemView: View,
|
||||||
@@ -57,18 +45,4 @@ class HomeFollowingCreatorAdapter(
|
|||||||
itemView.setOnClickListener { onClickItem(item) }
|
itemView.setOnClickListener { onClickItem(item) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AllViewHolder(
|
|
||||||
itemView: View,
|
|
||||||
private val onClickAll: () -> Unit
|
|
||||||
) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
fun bind() {
|
|
||||||
itemView.setOnClickListener { onClickAll() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
private const val VIEW_TYPE_CREATOR = 0
|
|
||||||
private const val VIEW_TYPE_ALL = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 446 B After Width: | Height: | Size: 346 B |
|
Before Width: | Height: | Size: 408 B After Width: | Height: | Size: 394 B |
|
After Width: | Height: | Size: 505 B |
|
After Width: | Height: | Size: 381 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/gray_800" />
|
||||||
|
<corners android:radius="100dp" />
|
||||||
|
</shape>
|
||||||
|
Before Width: | Height: | Size: 556 B |
@@ -96,8 +96,8 @@
|
|||||||
android:layout_marginTop="15dp"
|
android:layout_marginTop="15dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:fontFamily="@font/medium"
|
android:fontFamily="@font/medium"
|
||||||
android:maxLines="3"
|
|
||||||
android:lineSpacingExtra="5dp"
|
android:lineSpacingExtra="5dp"
|
||||||
|
android:maxLines="3"
|
||||||
android:textColor="@color/color_777777"
|
android:textColor="@color/color_777777"
|
||||||
android:textSize="13.3sp"
|
android:textSize="13.3sp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
@@ -143,18 +143,11 @@
|
|||||||
android:id="@+id/tv_applicant_count"
|
android:id="@+id/tv_applicant_count"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/spacing_4"
|
||||||
android:fontFamily="@font/medium"
|
android:fontFamily="@font/medium"
|
||||||
android:textColor="@color/color_3bb9f1"
|
android:textColor="@color/color_3bb9f1"
|
||||||
android:textSize="10.7sp"
|
android:textSize="10.7sp"
|
||||||
tools:text=" 24" />
|
tools:text=" 24" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:fontFamily="@font/medium"
|
|
||||||
android:text="@string/screen_audition_role_applicant_suffix"
|
|
||||||
android:textColor="@color/color_bbbbbb"
|
|
||||||
android:textSize="10.7sp" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
@@ -36,14 +36,6 @@
|
|||||||
android:textColor="@color/color_dd4500"
|
android:textColor="@color/color_dd4500"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
tools:text=" 10 " />
|
tools:text=" 10 " />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:fontFamily="@font/medium"
|
|
||||||
android:text="@string/screen_block_member_total_suffix"
|
|
||||||
android:textColor="@color/color_eeeeee"
|
|
||||||
android:textSize="12sp" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
|||||||
@@ -32,18 +32,11 @@
|
|||||||
android:id="@+id/tv_total_count"
|
android:id="@+id/tv_total_count"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/spacing_4"
|
||||||
android:fontFamily="@font/medium"
|
android:fontFamily="@font/medium"
|
||||||
android:textColor="@color/color_dd4500"
|
android:textColor="@color/color_dd4500"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
tools:text=" 10" />
|
tools:text=" 10" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:fontFamily="@font/medium"
|
|
||||||
android:text="@string/following_creator_total_suffix"
|
|
||||||
android:textColor="@color/color_eeeeee"
|
|
||||||
android:textSize="12sp" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
|||||||
@@ -171,18 +171,11 @@
|
|||||||
android:id="@+id/tv_total_count"
|
android:id="@+id/tv_total_count"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/spacing_4"
|
||||||
android:fontFamily="@font/medium"
|
android:fontFamily="@font/medium"
|
||||||
android:textColor="@color/color_dd4500"
|
android:textColor="@color/color_dd4500"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
tools:text=" 10" />
|
tools:text=" 10" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:fontFamily="@font/medium"
|
|
||||||
android:text="@string/following_creator_total_suffix"
|
|
||||||
android:textColor="@color/color_eeeeee"
|
|
||||||
android:textSize="12sp" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
|||||||
@@ -188,15 +188,6 @@
|
|||||||
android:textColor="@color/color_80d8ff"
|
android:textColor="@color/color_80d8ff"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
tools:text="56" />
|
tools:text="56" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:fontFamily="@font/medium"
|
|
||||||
android:layout_marginStart="2dp"
|
|
||||||
android:text="@string/screen_live_room_people_unit"
|
|
||||||
android:textColor="@color/color_777777"
|
|
||||||
android:textSize="12sp" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
|||||||
@@ -102,15 +102,6 @@
|
|||||||
android:textColor="@color/color_80d8ff"
|
android:textColor="@color/color_80d8ff"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
tools:text="56" />
|
tools:text="56" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:fontFamily="@font/medium"
|
|
||||||
android:layout_marginStart="2dp"
|
|
||||||
android:text="@string/screen_live_room_people_unit"
|
|
||||||
android:textColor="@color/color_777777"
|
|
||||||
android:textSize="12sp" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
|||||||
@@ -276,17 +276,29 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/spacing_12"
|
android:layout_marginTop="@dimen/spacing_12"
|
||||||
android:orientation="vertical">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/rv_home_following_creators"
|
android:id="@+id/rv_home_following_creators"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingHorizontal="@dimen/spacing_14"
|
android:paddingHorizontal="@dimen/spacing_20"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:listitem="@layout/item_home_following_creator" />
|
tools:listitem="@layout/item_home_following_creator" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_home_following_creator_all"
|
||||||
|
style="@style/Typography.Body5"
|
||||||
|
android:layout_width="@dimen/home_live_more_width"
|
||||||
|
android:layout_height="@dimen/home_live_row_height"
|
||||||
|
android:layout_marginEnd="@dimen/spacing_20"
|
||||||
|
android:background="@color/black"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/screen_home_theme_all"
|
||||||
|
android:textColor="@color/soda_400" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:includeFontPadding="true"
|
android:includeFontPadding="true"
|
||||||
android:text="@string/creator_channel_donation_empty_title"
|
android:text="@string/creator_channel_home_donation_empty_title"
|
||||||
android:textColor="@color/white" />
|
android:textColor="@color/white" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="44dp"
|
android:layout_height="44dp"
|
||||||
android:layout_marginTop="42dp"
|
android:layout_marginTop="42dp"
|
||||||
android:background="@drawable/bg_creator_channel_donation_empty_button"
|
android:background="@drawable/bg_round_corner_999_white"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
|||||||
@@ -77,10 +77,11 @@
|
|||||||
tools:text="팬이 쓴 응원이 보이는 부분입니다. 두 줄까지만 보여주고 초과 시 말줄임 처리." />
|
tools:text="팬이 쓴 응원이 보이는 부분입니다. 두 줄까지만 보여주고 초과 시 말줄임 처리." />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<FrameLayout
|
<LinearLayout
|
||||||
android:id="@+id/layout_fantalk_empty"
|
android:id="@+id/layout_fantalk_empty"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="169dp"
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
@@ -89,7 +90,6 @@
|
|||||||
style="@style/Typography.Heading1"
|
style="@style/Typography.Heading1"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="top|start"
|
|
||||||
android:layout_marginStart="@dimen/spacing_6"
|
android:layout_marginStart="@dimen/spacing_6"
|
||||||
android:layout_marginTop="@dimen/spacing_6"
|
android:layout_marginTop="@dimen/spacing_6"
|
||||||
android:includeFontPadding="false"
|
android:includeFontPadding="false"
|
||||||
@@ -100,8 +100,7 @@
|
|||||||
android:id="@+id/layout_fantalk_support_button"
|
android:id="@+id/layout_fantalk_support_button"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom|center_horizontal"
|
android:layout_marginTop="@dimen/spacing_14"
|
||||||
android:layout_marginBottom="@dimen/spacing_6"
|
|
||||||
android:background="@drawable/bg_round_corner_999_white"
|
android:background="@drawable/bg_round_corner_999_white"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
@@ -122,6 +121,6 @@
|
|||||||
android:text="@string/creator_channel_fantalk_support_action"
|
android:text="@string/creator_channel_fantalk_support_action"
|
||||||
android:textColor="@color/black" />
|
android:textColor="@color/black" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</FrameLayout>
|
</LinearLayout>
|
||||||
</kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelFanTalkCardView>
|
</kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelFanTalkCardView>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingHorizontal="16dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_home_following_creator_all"
|
|
||||||
style="@style/Typography.Body5"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="75dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:includeFontPadding="false"
|
|
||||||
android:text="@string/screen_home_theme_all"
|
|
||||||
android:textColor="@color/soda_400" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
style="@style/Typography.Body5"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/spacing_6"
|
|
||||||
android:includeFontPadding="false"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:text="@string/screen_home_theme_all"
|
|
||||||
android:visibility="invisible" />
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -337,7 +337,7 @@
|
|||||||
<string name="creator_channel_tab_audio">Audio</string>
|
<string name="creator_channel_tab_audio">Audio</string>
|
||||||
<string name="creator_channel_tab_series">Series</string>
|
<string name="creator_channel_tab_series">Series</string>
|
||||||
<string name="creator_channel_tab_community">Community</string>
|
<string name="creator_channel_tab_community">Community</string>
|
||||||
<string name="creator_channel_tab_fantalk">FanTalk</string>
|
<string name="creator_channel_tab_fantalk">Fan Letter</string>
|
||||||
<string name="creator_channel_tab_donation">Support</string>
|
<string name="creator_channel_tab_donation">Support</string>
|
||||||
<string name="creator_channel_follower_count">%1$s followers</string>
|
<string name="creator_channel_follower_count">%1$s followers</string>
|
||||||
<string name="creator_channel_chat_button">Chat Now</string>
|
<string name="creator_channel_chat_button">Chat Now</string>
|
||||||
@@ -345,6 +345,10 @@
|
|||||||
<string name="creator_channel_dm_check_button">Check DMs</string>
|
<string name="creator_channel_dm_check_button">Check DMs</string>
|
||||||
<string name="creator_channel_follow_button">Follow</string>
|
<string name="creator_channel_follow_button">Follow</string>
|
||||||
<string name="creator_channel_unfollow_success">Following has been canceled.</string>
|
<string name="creator_channel_unfollow_success">Following has been canceled.</string>
|
||||||
|
<string name="creator_unfollow_confirm_title">Unfollow</string>
|
||||||
|
<string name="creator_unfollow_confirm_desc">You’ll no longer receive updates from %1$s.\nDo you want to unfollow?</string>
|
||||||
|
<string name="creator_unfollow_confirm_cancel">Cancel</string>
|
||||||
|
<string name="creator_unfollow_confirm_confirm">Unfollow</string>
|
||||||
<string name="creator_channel_block_success">Blocked.</string>
|
<string name="creator_channel_block_success">Blocked.</string>
|
||||||
<string name="creator_channel_section_live">Live now</string>
|
<string name="creator_channel_section_live">Live now</string>
|
||||||
<string name="creator_channel_section_latest_audio">Latest audio</string>
|
<string name="creator_channel_section_latest_audio">Latest audio</string>
|
||||||
@@ -354,7 +358,7 @@
|
|||||||
<string name="creator_channel_section_audio">Audio</string>
|
<string name="creator_channel_section_audio">Audio</string>
|
||||||
<string name="creator_channel_section_series">Series</string>
|
<string name="creator_channel_section_series">Series</string>
|
||||||
<string name="creator_channel_section_community">Community</string>
|
<string name="creator_channel_section_community">Community</string>
|
||||||
<string name="creator_channel_section_fantalk">FanTalk</string>
|
<string name="creator_channel_section_fantalk">Fan Letter</string>
|
||||||
<string name="creator_channel_section_introduce">About</string>
|
<string name="creator_channel_section_introduce">About</string>
|
||||||
<string name="creator_channel_section_activity">Activity</string>
|
<string name="creator_channel_section_activity">Activity</string>
|
||||||
<string name="creator_channel_section_sns">SNS</string>
|
<string name="creator_channel_section_sns">SNS</string>
|
||||||
@@ -375,10 +379,10 @@
|
|||||||
<string name="creator_channel_community_invalid_post">Could not load the post.</string>
|
<string name="creator_channel_community_invalid_post">Could not load the post.</string>
|
||||||
<string name="creator_channel_community_purchased_badge">Purchased</string>
|
<string name="creator_channel_community_purchased_badge">Purchased</string>
|
||||||
<string name="creator_channel_fantalk_summary">%1$s · Total %2$d</string>
|
<string name="creator_channel_fantalk_summary">%1$s · Total %2$d</string>
|
||||||
<string name="creator_channel_fantalk_empty_title">Be the first\nto cheer them on!</string>
|
<string name="creator_channel_fantalk_empty_title">We\'re waiting for your\nfirst fan letter!</string>
|
||||||
<string name="creator_channel_fantalk_support_action">Leave support</string>
|
<string name="creator_channel_fantalk_support_action">Send fan letter</string>
|
||||||
<string name="creator_channel_fantalk_empty_message">No cheers yet.\nBe the first to cheer for the creator!</string>
|
<string name="creator_channel_fantalk_empty_message">No fan letters yet.\nSend the first fan letter!</string>
|
||||||
<string name="creator_channel_fantalk_error_message">Could not load FanTalk.</string>
|
<string name="creator_channel_fantalk_error_message">Could not load Fan Letter.</string>
|
||||||
<string name="creator_channel_fantalk_retry">Retry</string>
|
<string name="creator_channel_fantalk_retry">Retry</string>
|
||||||
<string name="creator_channel_fantalk_all_label">All</string>
|
<string name="creator_channel_fantalk_all_label">All</string>
|
||||||
<string name="creator_channel_fantalk_report">Report</string>
|
<string name="creator_channel_fantalk_report">Report</string>
|
||||||
@@ -404,7 +408,8 @@
|
|||||||
<string name="creator_channel_donation_action">Support</string>
|
<string name="creator_channel_donation_action">Support</string>
|
||||||
<string name="creator_channel_donation_error_message">Could not load donation history.</string>
|
<string name="creator_channel_donation_error_message">Could not load donation history.</string>
|
||||||
<string name="creator_channel_donation_retry">Retry</string>
|
<string name="creator_channel_donation_retry">Retry</string>
|
||||||
<string name="creator_channel_donation_empty_title">Be the first to\nsupport this creator!</string>
|
<string name="creator_channel_home_donation_empty_title">Be the first to\nsupport this creator!</string>
|
||||||
|
<string name="creator_channel_donation_empty_title">No support yet.\nBe the first to support this creator!</string>
|
||||||
<string name="creator_channel_donation_empty_owner_title">No donation history yet.</string>
|
<string name="creator_channel_donation_empty_owner_title">No donation history yet.</string>
|
||||||
<string name="creator_channel_donation_can_format">%1$s cans</string>
|
<string name="creator_channel_donation_can_format">%1$s cans</string>
|
||||||
<string name="creator_channel_donation_fallback_message">Supported with %1$d cans.</string>
|
<string name="creator_channel_donation_fallback_message">Supported with %1$d cans.</string>
|
||||||
@@ -717,7 +722,6 @@
|
|||||||
<string name="screen_live_room_heart_ranking_title">Current live heart ranking</string>
|
<string name="screen_live_room_heart_ranking_title">Current live heart ranking</string>
|
||||||
<string name="screen_live_room_total_label">Total</string>
|
<string name="screen_live_room_total_label">Total</string>
|
||||||
<string name="screen_live_room_total_count_label">All</string>
|
<string name="screen_live_room_total_count_label">All</string>
|
||||||
<string name="screen_live_room_people_unit">users</string>
|
|
||||||
<string name="screen_live_room_donation_ranking_title">Current live donation ranking</string>
|
<string name="screen_live_room_donation_ranking_title">Current live donation ranking</string>
|
||||||
<string name="screen_live_room_donation_normal_label">Normal</string>
|
<string name="screen_live_room_donation_normal_label">Normal</string>
|
||||||
<string name="screen_live_room_donation_secret_label">Secret</string>
|
<string name="screen_live_room_donation_secret_label">Secret</string>
|
||||||
@@ -979,7 +983,6 @@
|
|||||||
<string name="screen_block_member_title">Blocked users</string>
|
<string name="screen_block_member_title">Blocked users</string>
|
||||||
<string name="screen_block_member_total_prefix">Total</string>
|
<string name="screen_block_member_total_prefix">Total</string>
|
||||||
<string name="screen_block_member_total_count"> %1$d </string>
|
<string name="screen_block_member_total_count"> %1$d </string>
|
||||||
<string name="screen_block_member_total_suffix">users</string>
|
|
||||||
<string name="screen_block_member_empty">No blocked users.</string>
|
<string name="screen_block_member_empty">No blocked users.</string>
|
||||||
<string name="screen_block_member_block">Block</string>
|
<string name="screen_block_member_block">Block</string>
|
||||||
<string name="screen_block_member_unblock">Unblock</string>
|
<string name="screen_block_member_unblock">Unblock</string>
|
||||||
@@ -1120,7 +1123,6 @@
|
|||||||
<string name="screen_following_creator_title">Following list</string>
|
<string name="screen_following_creator_title">Following list</string>
|
||||||
<string name="following_creator_total_prefix">Total</string>
|
<string name="following_creator_total_prefix">Total</string>
|
||||||
<string name="following_creator_total_count_value">%1$d</string>
|
<string name="following_creator_total_count_value">%1$d</string>
|
||||||
<string name="following_creator_total_suffix">creators</string>
|
|
||||||
<string name="following_creator_empty">You are not following any channels.</string>
|
<string name="following_creator_empty">You are not following any channels.</string>
|
||||||
|
|
||||||
<!-- Audition -->
|
<!-- Audition -->
|
||||||
@@ -1156,7 +1158,6 @@
|
|||||||
<string name="screen_audition_role_script">Check audition script</string>
|
<string name="screen_audition_role_script">Check audition script</string>
|
||||||
<string name="screen_audition_role_info_title">Audition character info</string>
|
<string name="screen_audition_role_info_title">Audition character info</string>
|
||||||
<string name="screen_audition_role_applicant_prefix">Applicants </string>
|
<string name="screen_audition_role_applicant_prefix">Applicants </string>
|
||||||
<string name="screen_audition_role_applicant_suffix">people</string>
|
|
||||||
<string name="screen_audition_sort_newest">Newest</string>
|
<string name="screen_audition_sort_newest">Newest</string>
|
||||||
<string name="screen_audition_sort_likes">Most liked</string>
|
<string name="screen_audition_sort_likes">Most liked</string>
|
||||||
<string name="screen_audition_no_applicant">No applicants yet.</string>
|
<string name="screen_audition_no_applicant">No applicants yet.</string>
|
||||||
|
|||||||
@@ -207,7 +207,7 @@
|
|||||||
<string name="creator_channel_series_status_proceeding">連載</string>
|
<string name="creator_channel_series_status_proceeding">連載</string>
|
||||||
<string name="creator_channel_series_status_completed">完結</string>
|
<string name="creator_channel_series_status_completed">完結</string>
|
||||||
<string name="creator_channel_series_total_content_count">すべて</string>
|
<string name="creator_channel_series_total_content_count">すべて</string>
|
||||||
<string name="creator_channel_series_subtitle_content_count">全%1$s話</string>
|
<string name="creator_channel_series_subtitle_content_count">すべて%1$s話</string>
|
||||||
<string name="creator_channel_series_progress_count">%1$s/%2$s話</string>
|
<string name="creator_channel_series_progress_count">%1$s/%2$s話</string>
|
||||||
<string name="creator_channel_series_progress_percent">%1$s%%</string>
|
<string name="creator_channel_series_progress_percent">%1$s%%</string>
|
||||||
<string name="auth_title">本人確認</string>
|
<string name="auth_title">本人確認</string>
|
||||||
@@ -235,7 +235,7 @@
|
|||||||
<string name="screen_home_sort_sales_count">販売数</string>
|
<string name="screen_home_sort_sales_count">販売数</string>
|
||||||
<string name="screen_home_sort_comment_count">コメント</string>
|
<string name="screen_home_sort_comment_count">コメント</string>
|
||||||
<string name="screen_home_sort_like_count">いいね</string>
|
<string name="screen_home_sort_like_count">いいね</string>
|
||||||
<string name="screen_home_series_episode_count">全%1$d話</string>
|
<string name="screen_home_series_episode_count">すべて%1$d話</string>
|
||||||
<string name="screen_home_series_popular">人気</string>
|
<string name="screen_home_series_popular">人気</string>
|
||||||
<string name="screen_home_series_new">新作</string>
|
<string name="screen_home_series_new">新作</string>
|
||||||
<string name="screen_home_series_complete">完結</string>
|
<string name="screen_home_series_complete">完結</string>
|
||||||
@@ -317,7 +317,7 @@
|
|||||||
<string name="screen_home_following_ranking_rank_format">%1$d位</string>
|
<string name="screen_home_following_ranking_rank_format">%1$d位</string>
|
||||||
<string name="screen_home_following_creator_ranking_news_message">%1$sが今週のランキングで%2$sにランクイン!</string>
|
<string name="screen_home_following_creator_ranking_news_message">%1$sが今週のランキングで%2$sにランクイン!</string>
|
||||||
<string name="screen_home_following_content_ranking_news_message">%1$sが今週のランキング%2$sに入りました!</string>
|
<string name="screen_home_following_content_ranking_news_message">%1$sが今週のランキング%2$sに入りました!</string>
|
||||||
<string name="screen_home_following_empty">フォロー中のお知らせはまだありません。\n気になるクリエイターをフォローしてみましょう。</string>
|
<string name="screen_home_following_empty">まだフォローしているクリエイターがいません。\n気になるクリエイターをフォローしてみましょう。</string>
|
||||||
<string name="home_recommendation_activity_live">ライブ</string>
|
<string name="home_recommendation_activity_live">ライブ</string>
|
||||||
<string name="home_recommendation_activity_audio">オーディオ</string>
|
<string name="home_recommendation_activity_audio">オーディオ</string>
|
||||||
<string name="home_recommendation_activity_community">コミュニティ</string>
|
<string name="home_recommendation_activity_community">コミュニティ</string>
|
||||||
@@ -337,7 +337,7 @@
|
|||||||
<string name="creator_channel_tab_audio">オーディオ</string>
|
<string name="creator_channel_tab_audio">オーディオ</string>
|
||||||
<string name="creator_channel_tab_series">シリーズ</string>
|
<string name="creator_channel_tab_series">シリーズ</string>
|
||||||
<string name="creator_channel_tab_community">コミュニティ</string>
|
<string name="creator_channel_tab_community">コミュニティ</string>
|
||||||
<string name="creator_channel_tab_fantalk">ファントーク</string>
|
<string name="creator_channel_tab_fantalk">ファンレター</string>
|
||||||
<string name="creator_channel_tab_donation">サポート</string>
|
<string name="creator_channel_tab_donation">サポート</string>
|
||||||
<string name="creator_channel_follower_count">フォロワー %1$s人</string>
|
<string name="creator_channel_follower_count">フォロワー %1$s人</string>
|
||||||
<string name="creator_channel_chat_button">トークする</string>
|
<string name="creator_channel_chat_button">トークする</string>
|
||||||
@@ -345,6 +345,10 @@
|
|||||||
<string name="creator_channel_dm_check_button">DMを確認</string>
|
<string name="creator_channel_dm_check_button">DMを確認</string>
|
||||||
<string name="creator_channel_follow_button">フォロー</string>
|
<string name="creator_channel_follow_button">フォロー</string>
|
||||||
<string name="creator_channel_unfollow_success">フォローを解除しました。</string>
|
<string name="creator_channel_unfollow_success">フォローを解除しました。</string>
|
||||||
|
<string name="creator_unfollow_confirm_title">フォロー解除</string>
|
||||||
|
<string name="creator_unfollow_confirm_desc">%1$sからの最新情報を受け取れなくなります。\nフォローを解除しますか?</string>
|
||||||
|
<string name="creator_unfollow_confirm_cancel">キャンセル</string>
|
||||||
|
<string name="creator_unfollow_confirm_confirm">解除する</string>
|
||||||
<string name="creator_channel_block_success">ブロックしました。</string>
|
<string name="creator_channel_block_success">ブロックしました。</string>
|
||||||
<string name="creator_channel_section_live">配信中</string>
|
<string name="creator_channel_section_live">配信中</string>
|
||||||
<string name="creator_channel_section_latest_audio">最新オーディオ</string>
|
<string name="creator_channel_section_latest_audio">最新オーディオ</string>
|
||||||
@@ -354,7 +358,7 @@
|
|||||||
<string name="creator_channel_section_audio">オーディオ</string>
|
<string name="creator_channel_section_audio">オーディオ</string>
|
||||||
<string name="creator_channel_section_series">シリーズ</string>
|
<string name="creator_channel_section_series">シリーズ</string>
|
||||||
<string name="creator_channel_section_community">コミュニティ</string>
|
<string name="creator_channel_section_community">コミュニティ</string>
|
||||||
<string name="creator_channel_section_fantalk">ファントーク</string>
|
<string name="creator_channel_section_fantalk">ファンレター</string>
|
||||||
<string name="creator_channel_section_introduce">紹介</string>
|
<string name="creator_channel_section_introduce">紹介</string>
|
||||||
<string name="creator_channel_section_activity">アクティビティ</string>
|
<string name="creator_channel_section_activity">アクティビティ</string>
|
||||||
<string name="creator_channel_section_sns">SNS</string>
|
<string name="creator_channel_section_sns">SNS</string>
|
||||||
@@ -375,10 +379,10 @@
|
|||||||
<string name="creator_channel_community_invalid_post">投稿を読み込めません。</string>
|
<string name="creator_channel_community_invalid_post">投稿を読み込めません。</string>
|
||||||
<string name="creator_channel_community_purchased_badge">購入済み</string>
|
<string name="creator_channel_community_purchased_badge">購入済み</string>
|
||||||
<string name="creator_channel_fantalk_summary">%1$s · 全体 %2$d</string>
|
<string name="creator_channel_fantalk_summary">%1$s · 全体 %2$d</string>
|
||||||
<string name="creator_channel_fantalk_empty_title">最初の応援を\n待っています!</string>
|
<string name="creator_channel_fantalk_empty_title">あなたの最初のファンレターを待っています!</string>
|
||||||
<string name="creator_channel_fantalk_support_action">ファンレターを送る</string>
|
<string name="creator_channel_fantalk_support_action">ファンレターを送る</string>
|
||||||
<string name="creator_channel_fantalk_empty_message">まだファンレターがありません。\n最初のファンレターを送ってみましょう!</string>
|
<string name="creator_channel_fantalk_empty_message">まだファンレターがありません。\n最初のファンレターを送ってみましょう!</string>
|
||||||
<string name="creator_channel_fantalk_error_message">ファントークを読み込めませんでした。</string>
|
<string name="creator_channel_fantalk_error_message">ファンレターを読み込めませんでした。</string>
|
||||||
<string name="creator_channel_fantalk_retry">再試行</string>
|
<string name="creator_channel_fantalk_retry">再試行</string>
|
||||||
<string name="creator_channel_fantalk_all_label">すべて</string>
|
<string name="creator_channel_fantalk_all_label">すべて</string>
|
||||||
<string name="creator_channel_fantalk_report">通報</string>
|
<string name="creator_channel_fantalk_report">通報</string>
|
||||||
@@ -404,6 +408,7 @@
|
|||||||
<string name="creator_channel_donation_action">サポートする</string>
|
<string name="creator_channel_donation_action">サポートする</string>
|
||||||
<string name="creator_channel_donation_error_message">応援履歴を読み込めませんでした。</string>
|
<string name="creator_channel_donation_error_message">応援履歴を読み込めませんでした。</string>
|
||||||
<string name="creator_channel_donation_retry">再試行</string>
|
<string name="creator_channel_donation_retry">再試行</string>
|
||||||
|
<string name="creator_channel_home_donation_empty_title">最初のサポートをしてみましょう!</string>
|
||||||
<string name="creator_channel_donation_empty_title">まだサポートがありません。\n最初のサポートをしてみましょう!</string>
|
<string name="creator_channel_donation_empty_title">まだサポートがありません。\n最初のサポートをしてみましょう!</string>
|
||||||
<string name="creator_channel_donation_empty_owner_title">応援履歴がありません</string>
|
<string name="creator_channel_donation_empty_owner_title">応援履歴がありません</string>
|
||||||
<string name="creator_channel_donation_can_format">%1$sCAN</string>
|
<string name="creator_channel_donation_can_format">%1$sCAN</string>
|
||||||
@@ -717,7 +722,6 @@
|
|||||||
<string name="screen_live_room_heart_ranking_title">現在のライブ・ハートランキング</string>
|
<string name="screen_live_room_heart_ranking_title">現在のライブ・ハートランキング</string>
|
||||||
<string name="screen_live_room_total_label">合計</string>
|
<string name="screen_live_room_total_label">合計</string>
|
||||||
<string name="screen_live_room_total_count_label">すべて</string>
|
<string name="screen_live_room_total_count_label">すべて</string>
|
||||||
<string name="screen_live_room_people_unit">名</string>
|
|
||||||
<string name="screen_live_room_donation_ranking_title">現在のライブ・ギフトランキング</string>
|
<string name="screen_live_room_donation_ranking_title">現在のライブ・ギフトランキング</string>
|
||||||
<string name="screen_live_room_donation_normal_label">一般</string>
|
<string name="screen_live_room_donation_normal_label">一般</string>
|
||||||
<string name="screen_live_room_donation_secret_label">シークレット</string>
|
<string name="screen_live_room_donation_secret_label">シークレット</string>
|
||||||
@@ -977,9 +981,8 @@
|
|||||||
<string name="screen_service_center_kakao">TALK問い合わせ</string>
|
<string name="screen_service_center_kakao">TALK問い合わせ</string>
|
||||||
<!-- Block Member -->
|
<!-- Block Member -->
|
||||||
<string name="screen_block_member_title">ブロックリスト</string>
|
<string name="screen_block_member_title">ブロックリスト</string>
|
||||||
<string name="screen_block_member_total_prefix">全</string>
|
<string name="screen_block_member_total_prefix">すべて</string>
|
||||||
<string name="screen_block_member_total_count"> %1$d </string>
|
<string name="screen_block_member_total_count"> %1$d </string>
|
||||||
<string name="screen_block_member_total_suffix">名</string>
|
|
||||||
<string name="screen_block_member_empty">ブロックしたユーザーがいません。</string>
|
<string name="screen_block_member_empty">ブロックしたユーザーがいません。</string>
|
||||||
<string name="screen_block_member_block">ブロック</string>
|
<string name="screen_block_member_block">ブロック</string>
|
||||||
<string name="screen_block_member_unblock">ブロック解除</string>
|
<string name="screen_block_member_unblock">ブロック解除</string>
|
||||||
@@ -1028,7 +1031,7 @@
|
|||||||
<string name="screen_user_follower_list_total_label">すべて</string>
|
<string name="screen_user_follower_list_total_label">すべて</string>
|
||||||
<string name="screen_user_follower_list_error_invalid_request">無効なリクエストです。\n恐れ入りますが、もう一度お試しください。</string>
|
<string name="screen_user_follower_list_error_invalid_request">無効なリクエストです。\n恐れ入りますが、もう一度お試しください。</string>
|
||||||
<string name="screen_user_profile_series_title">シリーズ</string>
|
<string name="screen_user_profile_series_title">シリーズ</string>
|
||||||
<string name="screen_user_profile_series_total_count">全%1$d話</string>
|
<string name="screen_user_profile_series_total_count">すべて%1$d話</string>
|
||||||
<string name="screen_user_profile_donation_title">ギフトランキング</string>
|
<string name="screen_user_profile_donation_title">ギフトランキング</string>
|
||||||
<string name="screen_user_profile_donation_all_title">ギフトランキングをすべて見る</string>
|
<string name="screen_user_profile_donation_all_title">ギフトランキングをすべて見る</string>
|
||||||
<string name="screen_user_profile_donation_toggle_title">チャンネルにギフトランキングを表示</string>
|
<string name="screen_user_profile_donation_toggle_title">チャンネルにギフトランキングを表示</string>
|
||||||
@@ -1118,16 +1121,15 @@
|
|||||||
|
|
||||||
<!-- Following Creator -->
|
<!-- Following Creator -->
|
||||||
<string name="screen_following_creator_title">フォロー中リスト</string>
|
<string name="screen_following_creator_title">フォロー中リスト</string>
|
||||||
<string name="following_creator_total_prefix">全</string>
|
<string name="following_creator_total_prefix">すべて</string>
|
||||||
<string name="following_creator_total_count_value">%1$d</string>
|
<string name="following_creator_total_count_value">%1$d</string>
|
||||||
<string name="following_creator_total_suffix">名</string>
|
|
||||||
<string name="following_creator_empty">フォロー中のチャンネルがありません。</string>
|
<string name="following_creator_empty">フォロー中のチャンネルがありません。</string>
|
||||||
|
|
||||||
<!-- Audition -->
|
<!-- Audition -->
|
||||||
<string name="screen_audition_title">オーディション</string>
|
<string name="screen_audition_title">オーディション</string>
|
||||||
<string name="screen_audition_on">ON</string>
|
<string name="screen_audition_on">ON</string>
|
||||||
<string name="screen_audition_off">OFF</string>
|
<string name="screen_audition_off">OFF</string>
|
||||||
<string name="screen_audition_total_count">全 %1$d個</string>
|
<string name="screen_audition_total_count">すべて %1$d個</string>
|
||||||
<string name="screen_audition_how_to_use">ボイスオンオーディション利用方法</string>
|
<string name="screen_audition_how_to_use">ボイスオンオーディション利用方法</string>
|
||||||
<string name="screen_audition_how_to_use_more">詳しく></string>
|
<string name="screen_audition_how_to_use_more">詳しく></string>
|
||||||
<string name="screen_audition_error_invalid_request">無効なリクエストです。\n恐れ入りますが、もう一度お試しください。</string>
|
<string name="screen_audition_error_invalid_request">無効なリクエストです。\n恐れ入りますが、もう一度お試しください。</string>
|
||||||
@@ -1156,7 +1158,6 @@
|
|||||||
<string name="screen_audition_role_script">オーディション台本確認</string>
|
<string name="screen_audition_role_script">オーディション台本確認</string>
|
||||||
<string name="screen_audition_role_info_title">オーディションキャラ情報</string>
|
<string name="screen_audition_role_info_title">オーディションキャラ情報</string>
|
||||||
<string name="screen_audition_role_applicant_prefix">リスナー</string>
|
<string name="screen_audition_role_applicant_prefix">リスナー</string>
|
||||||
<string name="screen_audition_role_applicant_suffix">名</string>
|
|
||||||
<string name="screen_audition_sort_newest">最新順</string>
|
<string name="screen_audition_sort_newest">最新順</string>
|
||||||
<string name="screen_audition_sort_likes">いいね順</string>
|
<string name="screen_audition_sort_likes">いいね順</string>
|
||||||
<string name="screen_audition_no_applicant">応募者がいません。</string>
|
<string name="screen_audition_no_applicant">応募者がいません。</string>
|
||||||
@@ -1322,7 +1323,7 @@
|
|||||||
<string name="screen_audio_content_box_tab_orders">購入リスト</string>
|
<string name="screen_audio_content_box_tab_orders">購入リスト</string>
|
||||||
<string name="screen_audio_content_box_tab_playlists">プレイリスト</string>
|
<string name="screen_audio_content_box_tab_playlists">プレイリスト</string>
|
||||||
<string name="screen_audio_content_order_title">コンテンツ保管箱</string>
|
<string name="screen_audio_content_order_title">コンテンツ保管箱</string>
|
||||||
<string name="screen_audio_content_order_total_prefix">全</string>
|
<string name="screen_audio_content_order_total_prefix">すべて</string>
|
||||||
<string name="audio_content_order_label_rental">レンタル</string>
|
<string name="audio_content_order_label_rental">レンタル</string>
|
||||||
<string name="audio_content_order_label_rental_period">(利用期間5日)</string>
|
<string name="audio_content_order_label_rental_period">(利用期間5日)</string>
|
||||||
<string name="audio_content_order_label_keep">購入</string>
|
<string name="audio_content_order_label_keep">購入</string>
|
||||||
@@ -1356,7 +1357,7 @@
|
|||||||
<string name="audio_content_playlist_create_title_min_length_error">タイトルを%1$d文字以上入力してください</string>
|
<string name="audio_content_playlist_create_title_min_length_error">タイトルを%1$d文字以上入力してください</string>
|
||||||
<string name="audio_content_playlist_create_content_min_count_error">コンテンツを%1$d個以上追加してください</string>
|
<string name="audio_content_playlist_create_content_min_count_error">コンテンツを%1$d個以上追加してください</string>
|
||||||
<string name="audio_content_playlist_total_count">%1$d個</string>
|
<string name="audio_content_playlist_total_count">%1$d個</string>
|
||||||
<string name="audio_content_playlist_content_count">全 %1$d個</string>
|
<string name="audio_content_playlist_content_count">すべて %1$d個</string>
|
||||||
<string name="audio_content_playlist_empty_title">プレイリストが空です。</string>
|
<string name="audio_content_playlist_empty_title">プレイリストが空です。</string>
|
||||||
<string name="audio_content_playlist_empty_desc">よく聴くコンテンツで\nプレイリストを作ってみましょう。</string>
|
<string name="audio_content_playlist_empty_desc">よく聴くコンテンツで\nプレイリストを作ってみましょう。</string>
|
||||||
<string name="audio_content_playlist_modify_title">プレイリスト編集</string>
|
<string name="audio_content_playlist_modify_title">プレイリスト編集</string>
|
||||||
|
|||||||
@@ -206,7 +206,7 @@
|
|||||||
<string name="creator_channel_series_status_proceeding">연재</string>
|
<string name="creator_channel_series_status_proceeding">연재</string>
|
||||||
<string name="creator_channel_series_status_completed">완결</string>
|
<string name="creator_channel_series_status_completed">완결</string>
|
||||||
<string name="creator_channel_series_total_content_count">전체</string>
|
<string name="creator_channel_series_total_content_count">전체</string>
|
||||||
<string name="creator_channel_series_subtitle_content_count">총 %1$s화</string>
|
<string name="creator_channel_series_subtitle_content_count">전체 %1$s화</string>
|
||||||
<string name="creator_channel_series_progress_count">%1$s/%2$s화</string>
|
<string name="creator_channel_series_progress_count">%1$s/%2$s화</string>
|
||||||
<string name="creator_channel_series_progress_percent">%1$s%%</string>
|
<string name="creator_channel_series_progress_percent">%1$s%%</string>
|
||||||
<string name="auth_title">본인인증</string>
|
<string name="auth_title">본인인증</string>
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
<string name="screen_home_sort_sales_count">판매량</string>
|
<string name="screen_home_sort_sales_count">판매량</string>
|
||||||
<string name="screen_home_sort_comment_count">댓글</string>
|
<string name="screen_home_sort_comment_count">댓글</string>
|
||||||
<string name="screen_home_sort_like_count">좋아요</string>
|
<string name="screen_home_sort_like_count">좋아요</string>
|
||||||
<string name="screen_home_series_episode_count">총 %1$d화</string>
|
<string name="screen_home_series_episode_count">전체 %1$d화</string>
|
||||||
<string name="screen_home_series_popular">인기</string>
|
<string name="screen_home_series_popular">인기</string>
|
||||||
<string name="screen_home_series_new">신작</string>
|
<string name="screen_home_series_new">신작</string>
|
||||||
<string name="screen_home_series_complete">완결</string>
|
<string name="screen_home_series_complete">완결</string>
|
||||||
@@ -316,7 +316,7 @@
|
|||||||
<string name="screen_home_following_ranking_rank_format">%1$d위</string>
|
<string name="screen_home_following_ranking_rank_format">%1$d위</string>
|
||||||
<string name="screen_home_following_creator_ranking_news_message">%1$s님이 이번 주 랭킹 %2$s에 올랐어요!</string>
|
<string name="screen_home_following_creator_ranking_news_message">%1$s님이 이번 주 랭킹 %2$s에 올랐어요!</string>
|
||||||
<string name="screen_home_following_content_ranking_news_message">%1$s 콘텐츠가 이번 주 랭킹 %2$s에 올랐어요!</string>
|
<string name="screen_home_following_content_ranking_news_message">%1$s 콘텐츠가 이번 주 랭킹 %2$s에 올랐어요!</string>
|
||||||
<string name="screen_home_following_empty">아직 팔로잉 소식이 없어요.\n관심 있는 크리에이터를 팔로우해 보세요.</string>
|
<string name="screen_home_following_empty">아직 팔로우한 크리에이터가 없어요.\n관심 있는 크리에이터를 팔로우해 보세요.</string>
|
||||||
<string name="home_recommendation_activity_live">라이브</string>
|
<string name="home_recommendation_activity_live">라이브</string>
|
||||||
<string name="home_recommendation_activity_audio">오디오</string>
|
<string name="home_recommendation_activity_audio">오디오</string>
|
||||||
<string name="home_recommendation_activity_community">커뮤니티</string>
|
<string name="home_recommendation_activity_community">커뮤니티</string>
|
||||||
@@ -336,7 +336,7 @@
|
|||||||
<string name="creator_channel_tab_audio">오디오</string>
|
<string name="creator_channel_tab_audio">오디오</string>
|
||||||
<string name="creator_channel_tab_series">시리즈</string>
|
<string name="creator_channel_tab_series">시리즈</string>
|
||||||
<string name="creator_channel_tab_community">커뮤니티</string>
|
<string name="creator_channel_tab_community">커뮤니티</string>
|
||||||
<string name="creator_channel_tab_fantalk">팬Talk</string>
|
<string name="creator_channel_tab_fantalk">팬레터</string>
|
||||||
<string name="creator_channel_tab_donation">후원</string>
|
<string name="creator_channel_tab_donation">후원</string>
|
||||||
<string name="creator_channel_follower_count">팔로워 %1$s명</string>
|
<string name="creator_channel_follower_count">팔로워 %1$s명</string>
|
||||||
<string name="creator_channel_chat_button">대화하기</string>
|
<string name="creator_channel_chat_button">대화하기</string>
|
||||||
@@ -344,6 +344,10 @@
|
|||||||
<string name="creator_channel_dm_check_button">DM 확인하기</string>
|
<string name="creator_channel_dm_check_button">DM 확인하기</string>
|
||||||
<string name="creator_channel_follow_button">팔로우</string>
|
<string name="creator_channel_follow_button">팔로우</string>
|
||||||
<string name="creator_channel_unfollow_success">팔로잉이 취소되었습니다.</string>
|
<string name="creator_channel_unfollow_success">팔로잉이 취소되었습니다.</string>
|
||||||
|
<string name="creator_unfollow_confirm_title">팔로우 해제</string>
|
||||||
|
<string name="creator_unfollow_confirm_desc">팔로우를 해제할까요?\n%1$s의 소식을 받을 수 없어요.</string>
|
||||||
|
<string name="creator_unfollow_confirm_cancel">취소</string>
|
||||||
|
<string name="creator_unfollow_confirm_confirm">해제하기</string>
|
||||||
<string name="creator_channel_block_success">차단하였습니다.</string>
|
<string name="creator_channel_block_success">차단하였습니다.</string>
|
||||||
<string name="creator_channel_section_live">현재 라이브</string>
|
<string name="creator_channel_section_live">현재 라이브</string>
|
||||||
<string name="creator_channel_section_latest_audio">최신 오디오</string>
|
<string name="creator_channel_section_latest_audio">최신 오디오</string>
|
||||||
@@ -353,7 +357,7 @@
|
|||||||
<string name="creator_channel_section_audio">오디오</string>
|
<string name="creator_channel_section_audio">오디오</string>
|
||||||
<string name="creator_channel_section_series">시리즈</string>
|
<string name="creator_channel_section_series">시리즈</string>
|
||||||
<string name="creator_channel_section_community">커뮤니티</string>
|
<string name="creator_channel_section_community">커뮤니티</string>
|
||||||
<string name="creator_channel_section_fantalk">팬Talk</string>
|
<string name="creator_channel_section_fantalk">팬레터</string>
|
||||||
<string name="creator_channel_section_introduce">소개</string>
|
<string name="creator_channel_section_introduce">소개</string>
|
||||||
<string name="creator_channel_section_activity">활동</string>
|
<string name="creator_channel_section_activity">활동</string>
|
||||||
<string name="creator_channel_section_sns">SNS</string>
|
<string name="creator_channel_section_sns">SNS</string>
|
||||||
@@ -377,7 +381,7 @@
|
|||||||
<string name="creator_channel_fantalk_empty_title">당신의 첫 응원을\n기다리고 있어요!</string>
|
<string name="creator_channel_fantalk_empty_title">당신의 첫 응원을\n기다리고 있어요!</string>
|
||||||
<string name="creator_channel_fantalk_support_action">응원남기기</string>
|
<string name="creator_channel_fantalk_support_action">응원남기기</string>
|
||||||
<string name="creator_channel_fantalk_empty_message">아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!</string>
|
<string name="creator_channel_fantalk_empty_message">아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!</string>
|
||||||
<string name="creator_channel_fantalk_error_message">팬톡을 불러오지 못했습니다.</string>
|
<string name="creator_channel_fantalk_error_message">팬레터를 불러오지 못했습니다.</string>
|
||||||
<string name="creator_channel_fantalk_retry">다시 시도</string>
|
<string name="creator_channel_fantalk_retry">다시 시도</string>
|
||||||
<string name="creator_channel_fantalk_all_label">전체</string>
|
<string name="creator_channel_fantalk_all_label">전체</string>
|
||||||
<string name="creator_channel_fantalk_report">신고</string>
|
<string name="creator_channel_fantalk_report">신고</string>
|
||||||
@@ -403,6 +407,7 @@
|
|||||||
<string name="creator_channel_donation_action">후원하기</string>
|
<string name="creator_channel_donation_action">후원하기</string>
|
||||||
<string name="creator_channel_donation_error_message">후원 내역을 불러오지 못했습니다.</string>
|
<string name="creator_channel_donation_error_message">후원 내역을 불러오지 못했습니다.</string>
|
||||||
<string name="creator_channel_donation_retry">다시 시도</string>
|
<string name="creator_channel_donation_retry">다시 시도</string>
|
||||||
|
<string name="creator_channel_home_donation_empty_title">처음으로 크리에이터를\n후원해 보세요!</string>
|
||||||
<string name="creator_channel_donation_empty_title">아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!</string>
|
<string name="creator_channel_donation_empty_title">아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!</string>
|
||||||
<string name="creator_channel_donation_empty_owner_title">후원 내역이 없습니다</string>
|
<string name="creator_channel_donation_empty_owner_title">후원 내역이 없습니다</string>
|
||||||
<string name="creator_channel_donation_can_format">%1$s캔</string>
|
<string name="creator_channel_donation_can_format">%1$s캔</string>
|
||||||
@@ -716,7 +721,6 @@
|
|||||||
<string name="screen_live_room_heart_ranking_title">현재 라이브 하트랭킹</string>
|
<string name="screen_live_room_heart_ranking_title">현재 라이브 하트랭킹</string>
|
||||||
<string name="screen_live_room_total_label">합계</string>
|
<string name="screen_live_room_total_label">합계</string>
|
||||||
<string name="screen_live_room_total_count_label">전체</string>
|
<string name="screen_live_room_total_count_label">전체</string>
|
||||||
<string name="screen_live_room_people_unit">명</string>
|
|
||||||
<string name="screen_live_room_donation_ranking_title">현재 라이브 후원랭킹</string>
|
<string name="screen_live_room_donation_ranking_title">현재 라이브 후원랭킹</string>
|
||||||
<string name="screen_live_room_donation_normal_label">일반</string>
|
<string name="screen_live_room_donation_normal_label">일반</string>
|
||||||
<string name="screen_live_room_donation_secret_label">비밀</string>
|
<string name="screen_live_room_donation_secret_label">비밀</string>
|
||||||
@@ -976,9 +980,8 @@
|
|||||||
<string name="screen_service_center_kakao">TALK 문의</string>
|
<string name="screen_service_center_kakao">TALK 문의</string>
|
||||||
<!-- Block Member -->
|
<!-- Block Member -->
|
||||||
<string name="screen_block_member_title">차단 리스트</string>
|
<string name="screen_block_member_title">차단 리스트</string>
|
||||||
<string name="screen_block_member_total_prefix">총</string>
|
<string name="screen_block_member_total_prefix">전체</string>
|
||||||
<string name="screen_block_member_total_count"> %1$d </string>
|
<string name="screen_block_member_total_count"> %1$d </string>
|
||||||
<string name="screen_block_member_total_suffix">명</string>
|
|
||||||
<string name="screen_block_member_empty">차단한 유저가 없습니다.</string>
|
<string name="screen_block_member_empty">차단한 유저가 없습니다.</string>
|
||||||
<string name="screen_block_member_block">차단</string>
|
<string name="screen_block_member_block">차단</string>
|
||||||
<string name="screen_block_member_unblock">차단해제</string>
|
<string name="screen_block_member_unblock">차단해제</string>
|
||||||
@@ -1027,7 +1030,7 @@
|
|||||||
<string name="screen_user_follower_list_total_label">전체</string>
|
<string name="screen_user_follower_list_total_label">전체</string>
|
||||||
<string name="screen_user_follower_list_error_invalid_request">잘못된 요청입니다.\n다시 시도해 주세요.</string>
|
<string name="screen_user_follower_list_error_invalid_request">잘못된 요청입니다.\n다시 시도해 주세요.</string>
|
||||||
<string name="screen_user_profile_series_title">시리즈</string>
|
<string name="screen_user_profile_series_title">시리즈</string>
|
||||||
<string name="screen_user_profile_series_total_count">총 %1$d화</string>
|
<string name="screen_user_profile_series_total_count">전체 %1$d화</string>
|
||||||
<string name="screen_user_profile_donation_title">후원랭킹</string>
|
<string name="screen_user_profile_donation_title">후원랭킹</string>
|
||||||
<string name="screen_user_profile_donation_all_title">후원랭킹 전체보기</string>
|
<string name="screen_user_profile_donation_all_title">후원랭킹 전체보기</string>
|
||||||
<string name="screen_user_profile_donation_toggle_title">채널에 후원랭킹 활성화</string>
|
<string name="screen_user_profile_donation_toggle_title">채널에 후원랭킹 활성화</string>
|
||||||
@@ -1117,16 +1120,15 @@
|
|||||||
|
|
||||||
<!-- Following Creator -->
|
<!-- Following Creator -->
|
||||||
<string name="screen_following_creator_title">팔로잉 리스트</string>
|
<string name="screen_following_creator_title">팔로잉 리스트</string>
|
||||||
<string name="following_creator_total_prefix">총</string>
|
<string name="following_creator_total_prefix">전체</string>
|
||||||
<string name="following_creator_total_count_value">%1$d</string>
|
<string name="following_creator_total_count_value">%1$d</string>
|
||||||
<string name="following_creator_total_suffix">명</string>
|
|
||||||
<string name="following_creator_empty">팔로우 중인 채널이 없습니다.</string>
|
<string name="following_creator_empty">팔로우 중인 채널이 없습니다.</string>
|
||||||
|
|
||||||
<!-- Audition -->
|
<!-- Audition -->
|
||||||
<string name="screen_audition_title">오디션</string>
|
<string name="screen_audition_title">오디션</string>
|
||||||
<string name="screen_audition_on">ON</string>
|
<string name="screen_audition_on">ON</string>
|
||||||
<string name="screen_audition_off">OFF</string>
|
<string name="screen_audition_off">OFF</string>
|
||||||
<string name="screen_audition_total_count">총 %1$d개</string>
|
<string name="screen_audition_total_count">전체 %1$d개</string>
|
||||||
<string name="screen_audition_how_to_use">보이스온 오디션 이용방법</string>
|
<string name="screen_audition_how_to_use">보이스온 오디션 이용방법</string>
|
||||||
<string name="screen_audition_how_to_use_more">자세히></string>
|
<string name="screen_audition_how_to_use_more">자세히></string>
|
||||||
<string name="screen_audition_error_invalid_request">잘못된 요청입니다.\n다시 시도해 주세요.</string>
|
<string name="screen_audition_error_invalid_request">잘못된 요청입니다.\n다시 시도해 주세요.</string>
|
||||||
@@ -1155,7 +1157,6 @@
|
|||||||
<string name="screen_audition_role_script">오디션 대본 확인</string>
|
<string name="screen_audition_role_script">오디션 대본 확인</string>
|
||||||
<string name="screen_audition_role_info_title">오디션 캐릭터 정보</string>
|
<string name="screen_audition_role_info_title">오디션 캐릭터 정보</string>
|
||||||
<string name="screen_audition_role_applicant_prefix">참여자</string>
|
<string name="screen_audition_role_applicant_prefix">참여자</string>
|
||||||
<string name="screen_audition_role_applicant_suffix">명</string>
|
|
||||||
<string name="screen_audition_sort_newest">최신순</string>
|
<string name="screen_audition_sort_newest">최신순</string>
|
||||||
<string name="screen_audition_sort_likes">좋아요순</string>
|
<string name="screen_audition_sort_likes">좋아요순</string>
|
||||||
<string name="screen_audition_no_applicant">지원자가 없습니다.</string>
|
<string name="screen_audition_no_applicant">지원자가 없습니다.</string>
|
||||||
@@ -1321,7 +1322,7 @@
|
|||||||
<string name="screen_audio_content_box_tab_orders">구매목록</string>
|
<string name="screen_audio_content_box_tab_orders">구매목록</string>
|
||||||
<string name="screen_audio_content_box_tab_playlists">재생목록</string>
|
<string name="screen_audio_content_box_tab_playlists">재생목록</string>
|
||||||
<string name="screen_audio_content_order_title">콘텐츠 보관함</string>
|
<string name="screen_audio_content_order_title">콘텐츠 보관함</string>
|
||||||
<string name="screen_audio_content_order_total_prefix">총</string>
|
<string name="screen_audio_content_order_total_prefix">전체</string>
|
||||||
<string name="audio_content_order_label_rental">대여</string>
|
<string name="audio_content_order_label_rental">대여</string>
|
||||||
<string name="audio_content_order_label_rental_period">(이용기간 5일)</string>
|
<string name="audio_content_order_label_rental_period">(이용기간 5일)</string>
|
||||||
<string name="audio_content_order_label_keep">소장</string>
|
<string name="audio_content_order_label_keep">소장</string>
|
||||||
@@ -1355,7 +1356,7 @@
|
|||||||
<string name="audio_content_playlist_create_title_min_length_error">제목을 %1$d자 이상 입력하세요</string>
|
<string name="audio_content_playlist_create_title_min_length_error">제목을 %1$d자 이상 입력하세요</string>
|
||||||
<string name="audio_content_playlist_create_content_min_count_error">콘텐츠를 %1$d개 이상 추가하세요</string>
|
<string name="audio_content_playlist_create_content_min_count_error">콘텐츠를 %1$d개 이상 추가하세요</string>
|
||||||
<string name="audio_content_playlist_total_count">%1$d개</string>
|
<string name="audio_content_playlist_total_count">%1$d개</string>
|
||||||
<string name="audio_content_playlist_content_count">총 %1$d개</string>
|
<string name="audio_content_playlist_content_count">전체 %1$d개</string>
|
||||||
<string name="audio_content_playlist_empty_title">재생목록이 비어있습니다.</string>
|
<string name="audio_content_playlist_empty_title">재생목록이 비어있습니다.</string>
|
||||||
<string name="audio_content_playlist_empty_desc">자주 듣는 콘텐츠를\n재생목록으로 만들어 보세요.</string>
|
<string name="audio_content_playlist_empty_desc">자주 듣는 콘텐츠를\n재생목록으로 만들어 보세요.</string>
|
||||||
<string name="audio_content_playlist_modify_title">재생목록 수정</string>
|
<string name="audio_content_playlist_modify_title">재생목록 수정</string>
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package kr.co.vividnext.sodalive.following
|
||||||
|
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class FollowingCreatorActivitySourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `팔로잉 리스트는 Bottom Sheet 대신 해제 확인 Dialog를 표시한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/following/FollowingCreatorActivity.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertFalse(source.contains("CreatorFollowNotifyFragment"))
|
||||||
|
assertTrue(source.contains("onClickUnfollow = { creatorId, creatorName ->"))
|
||||||
|
assertTrue(source.contains("V2ModalDialog("))
|
||||||
|
assertTrue(source.contains("R.string.creator_unfollow_confirm_desc, creatorName"))
|
||||||
|
assertTrue(source.contains("confirmButtonClick = {"))
|
||||||
|
assertTrue(source.contains("viewModel.follow(creatorId, follow = false, notify = false)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `팔로우 해제 확인 문구는 세 locale에서 같은 placeholder 계약을 사용한다`() {
|
||||||
|
val korean = projectFile("app/src/main/res/values/strings.xml").readText()
|
||||||
|
val english = projectFile("app/src/main/res/values-en/strings.xml").readText()
|
||||||
|
val japanese = projectFile("app/src/main/res/values-ja/strings.xml").readText()
|
||||||
|
|
||||||
|
listOf(korean, english, japanese).forEach { strings ->
|
||||||
|
assertTrue(strings.contains("name=\"creator_unfollow_confirm_title\""))
|
||||||
|
assertTrue(strings.contains("name=\"creator_unfollow_confirm_desc\""))
|
||||||
|
assertTrue(strings.contains("name=\"creator_unfollow_confirm_cancel\""))
|
||||||
|
assertTrue(strings.contains("name=\"creator_unfollow_confirm_confirm\""))
|
||||||
|
assertTrue(strings.contains("%1\$s"))
|
||||||
|
}
|
||||||
|
assertTrue(
|
||||||
|
english.contains(
|
||||||
|
"You’ll no longer receive updates from %1\$s.\\nDo you want to unfollow?"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package kr.co.vividnext.sodalive.following
|
||||||
|
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class FollowingCreatorAdapterSourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `팔로잉 중 버튼은 흰색 아이콘과 gray 800 원형 배경을 사용한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/following/FollowingCreatorAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
val background = projectFile(
|
||||||
|
"app/src/main/res/drawable/bg_following_circle_gray_800.xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(source.contains("R.drawable.ic_new_following_white"))
|
||||||
|
assertTrue(source.contains("R.drawable.bg_following_circle_gray_800"))
|
||||||
|
assertTrue(source.contains("8.dpToPx().toInt()"))
|
||||||
|
assertTrue(background.exists())
|
||||||
|
assertTrue(background.readText().contains("@color/gray_800"))
|
||||||
|
assertTrue(background.readText().contains("android:radius=\"100dp\""))
|
||||||
|
}
|
||||||
|
|
||||||
|
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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -283,6 +283,11 @@ class CreatorChannelActivitySourceTest {
|
|||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||||
).readText()
|
).readText()
|
||||||
val layout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText()
|
val layout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText()
|
||||||
|
val followClickSource = sourceSection(
|
||||||
|
source = source,
|
||||||
|
startMarker = "private fun onFollowCapsuleClicked()",
|
||||||
|
endMarker = "private fun onBellClicked()"
|
||||||
|
)
|
||||||
|
|
||||||
assertFalse(source.contains("CreatorFollowNotifyFragment"))
|
assertFalse(source.contains("CreatorFollowNotifyFragment"))
|
||||||
assertTrue(layout.contains("android:id=\"@+id/layout_follow_capsule\""))
|
assertTrue(layout.contains("android:id=\"@+id/layout_follow_capsule\""))
|
||||||
@@ -294,7 +299,11 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(source.contains("if (!header.isFollow)"))
|
assertTrue(source.contains("if (!header.isFollow)"))
|
||||||
assertTrue(source.contains("homeActionDelegate?.follow(follow = true, notify = true)"))
|
assertTrue(source.contains("homeActionDelegate?.follow(follow = true, notify = true)"))
|
||||||
assertTrue(source.contains("homeActionDelegate?.follow(follow = true, notify = !header.isNotify)"))
|
assertTrue(source.contains("homeActionDelegate?.follow(follow = true, notify = !header.isNotify)"))
|
||||||
assertTrue(source.contains("homeActionDelegate?.follow(follow = false, notify = false)"))
|
assertTrue(followClickSource.contains("V2ModalDialog("))
|
||||||
|
assertTrue(followClickSource.contains("R.string.creator_unfollow_confirm_title"))
|
||||||
|
assertTrue(followClickSource.contains("R.string.creator_unfollow_confirm_desc, header.nickname"))
|
||||||
|
assertTrue(followClickSource.contains("confirmButtonClick = {"))
|
||||||
|
assertTrue(followClickSource.contains("homeActionDelegate?.follow(follow = false, notify = false)"))
|
||||||
assertFalse(source.contains("showFollowNotifyFragment"))
|
assertFalse(source.contains("showFollowNotifyFragment"))
|
||||||
assertTrue(source.contains("onCreatorChannelFollowProgressChanged"))
|
assertTrue(source.contains("onCreatorChannelFollowProgressChanged"))
|
||||||
assertTrue(source.contains("binding.layoutFollowCapsule.isEnabled = titleBarState.isActionEnabled"))
|
assertTrue(source.contains("binding.layoutFollowCapsule.isEnabled = titleBarState.isActionEnabled"))
|
||||||
@@ -1113,7 +1122,12 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(donationLayout.contains("@+id/ll_donation_items"))
|
assertTrue(donationLayout.contains("@+id/ll_donation_items"))
|
||||||
assertTrue(donationLayout.contains("@+id/layout_donation_empty"))
|
assertTrue(donationLayout.contains("@+id/layout_donation_empty"))
|
||||||
assertTrue(donationLayout.contains("@+id/tv_donation_empty_title"))
|
assertTrue(donationLayout.contains("@+id/tv_donation_empty_title"))
|
||||||
assertTrue(donationLayout.contains("@string/creator_channel_donation_empty_title"))
|
assertTrue(donationLayout.contains("@string/creator_channel_home_donation_empty_title"))
|
||||||
|
val donationEmptyButtonBlock = donationLayout
|
||||||
|
.substringAfter("android:id=\"@+id/layout_donation_empty_button\"")
|
||||||
|
.substringBefore("</LinearLayout>")
|
||||||
|
assertTrue(donationEmptyButtonBlock.contains("android:background=\"@drawable/bg_round_corner_999_white\""))
|
||||||
|
assertTrue(donationEmptyButtonBlock.contains("android:textColor=\"@color/black\""))
|
||||||
assertTrue(donationLayout.contains("android:layout_width=\"match_parent\""))
|
assertTrue(donationLayout.contains("android:layout_width=\"match_parent\""))
|
||||||
assertTrue(donationLayout.contains("android:layout_height=\"wrap_content\""))
|
assertTrue(donationLayout.contains("android:layout_height=\"wrap_content\""))
|
||||||
assertTrue(donationLayout.contains("@+id/layout_donation_button"))
|
assertTrue(donationLayout.contains("@+id/layout_donation_button"))
|
||||||
@@ -1308,13 +1322,20 @@ class CreatorChannelActivitySourceTest {
|
|||||||
val en = projectFile("app/src/main/res/values-en/strings.xml").readText()
|
val en = projectFile("app/src/main/res/values-en/strings.xml").readText()
|
||||||
val ja = projectFile("app/src/main/res/values-ja/strings.xml").readText()
|
val ja = projectFile("app/src/main/res/values-ja/strings.xml").readText()
|
||||||
|
|
||||||
|
assertTrue(ko.contains("name=\"creator_channel_home_donation_empty_title\">처음으로 크리에이터를\\n후원해 보세요!"))
|
||||||
|
assertTrue(en.contains("name=\"creator_channel_home_donation_empty_title\">Be the first to\\nsupport this creator!"))
|
||||||
|
assertTrue(ja.contains("name=\"creator_channel_home_donation_empty_title\">最初のサポートをしてみましょう!"))
|
||||||
|
assertTrue(ko.contains("name=\"creator_channel_donation_empty_title\">아직 후원이 없습니다.\\n처음으로 크리에이터를 후원해 보세요!"))
|
||||||
assertTrue(
|
assertTrue(
|
||||||
ko.contains(
|
en.contains(
|
||||||
"name=\"creator_channel_donation_empty_title\">아직 후원이 없습니다.\\n처음으로 크리에이터를 후원해 보세요!"
|
"name=\"creator_channel_donation_empty_title\">No support yet.\\n" +
|
||||||
|
"Be the first to support this creator!"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertTrue(en.contains("creator_channel_donation_empty_title"))
|
assertTrue(ja.contains("name=\"creator_channel_donation_empty_title\">まだサポートがありません。\\n最初のサポートをしてみましょう!"))
|
||||||
assertTrue(ja.contains("creator_channel_donation_empty_title"))
|
assertTrue(ko.contains("name=\"creator_channel_donation_action\">후원하기"))
|
||||||
|
assertTrue(en.contains("name=\"creator_channel_donation_action\">Support"))
|
||||||
|
assertTrue(ja.contains("name=\"creator_channel_donation_action\">サポートする"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -2136,10 +2157,17 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(fanTalkLayout.contains("@+id/layout_fantalk_support_button"))
|
assertTrue(fanTalkLayout.contains("@+id/layout_fantalk_support_button"))
|
||||||
assertTrue(fanTalkLayout.contains("@string/creator_channel_fantalk_empty_title"))
|
assertTrue(fanTalkLayout.contains("@string/creator_channel_fantalk_empty_title"))
|
||||||
assertTrue(fanTalkLayout.contains("@string/creator_channel_fantalk_support_action"))
|
assertTrue(fanTalkLayout.contains("@string/creator_channel_fantalk_support_action"))
|
||||||
|
val fanTalkSupportButtonBlock = fanTalkLayout
|
||||||
|
.substringAfter("android:id=\"@+id/layout_fantalk_support_button\"")
|
||||||
|
.substringBefore("</LinearLayout>")
|
||||||
|
assertTrue(fanTalkSupportButtonBlock.contains("android:background=\"@drawable/bg_round_corner_999_white\""))
|
||||||
|
assertTrue(fanTalkSupportButtonBlock.contains("android:textColor=\"@color/black\""))
|
||||||
assertTrue(fanTalkLayout.contains("@drawable/ic_new_fantalk_plus"))
|
assertTrue(fanTalkLayout.contains("@drawable/ic_new_fantalk_plus"))
|
||||||
assertTrue(fanTalkLayout.contains("android:layout_width=\"match_parent\""))
|
assertTrue(fanTalkLayout.contains("android:layout_width=\"match_parent\""))
|
||||||
assertTrue(fanTalkLayout.contains("android:layout_height=\"169dp\""))
|
assertTrue(fanTalkLayout.contains("android:layout_height=\"wrap_content\""))
|
||||||
assertTrue(fanTalkLayout.contains("android:layout_marginBottom=\"@dimen/spacing_6\""))
|
assertTrue(fanTalkLayout.contains("android:layout_marginTop=\"@dimen/spacing_14\""))
|
||||||
|
assertFalse(fanTalkLayout.contains("android:layout_height=\"169dp\""))
|
||||||
|
assertFalse(fanTalkLayout.contains("android:layout_marginBottom=\"@dimen/spacing_6\""))
|
||||||
assertFalse(fanTalkLayout.contains("@+id/ll_section_items"))
|
assertFalse(fanTalkLayout.contains("@+id/ll_section_items"))
|
||||||
assertFalse(fanTalkLayout.contains("android:clipToOutline"))
|
assertFalse(fanTalkLayout.contains("android:clipToOutline"))
|
||||||
assertTrue(fanTalkCardView.contains("clipToOutline = true"))
|
assertTrue(fanTalkCardView.contains("clipToOutline = true"))
|
||||||
@@ -2153,10 +2181,14 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(adapter.contains("private val fanTalkTotalRow: View?"))
|
assertTrue(adapter.contains("private val fanTalkTotalRow: View?"))
|
||||||
assertTrue(adapter.contains("private val fanTalkLatestRow: View?"))
|
assertTrue(adapter.contains("private val fanTalkLatestRow: View?"))
|
||||||
assertTrue(adapter.contains("private val fanTalkEmpty: View?"))
|
assertTrue(adapter.contains("private val fanTalkEmpty: View?"))
|
||||||
|
assertTrue(adapter.contains("private val fanTalkSupportButton: View?"))
|
||||||
assertTrue(adapter.contains("fanTalkTotalCount?.text = item.fanTalk.totalCount.toString()"))
|
assertTrue(adapter.contains("fanTalkTotalCount?.text = item.fanTalk.totalCount.toString()"))
|
||||||
assertTrue(adapter.contains("fanTalkTotalRow?.isVisible = fanTalk != null"))
|
assertTrue(adapter.contains("fanTalkTotalRow?.isVisible = fanTalk != null"))
|
||||||
assertTrue(adapter.contains("fanTalkLatestRow?.isVisible = fanTalk != null"))
|
assertTrue(adapter.contains("fanTalkLatestRow?.isVisible = fanTalk != null"))
|
||||||
assertTrue(adapter.contains("fanTalkEmpty?.isVisible = fanTalk == null"))
|
assertTrue(adapter.contains("fanTalkEmpty?.isVisible = fanTalk == null"))
|
||||||
|
assertTrue(adapter.contains("val showSupportButton = (fanTalk == null || item.fanTalk.totalCount <= 0) && !item.isOwner"))
|
||||||
|
assertTrue(adapter.contains("fanTalkSupportButton?.isVisible = showSupportButton"))
|
||||||
|
assertTrue(adapter.contains("fanTalkSupportButton?.setOnClickListener"))
|
||||||
assertTrue(adapter.contains("fanTalkContent?.text = fanTalk.content"))
|
assertTrue(adapter.contains("fanTalkContent?.text = fanTalk.content"))
|
||||||
assertTrue(adapter.contains("fanTalkProfile?.loadUrl(fanTalk.profileImageUrl)"))
|
assertTrue(adapter.contains("fanTalkProfile?.loadUrl(fanTalk.profileImageUrl)"))
|
||||||
assertTrue(adapter.contains("fanTalkContent?.text = \"\""))
|
assertTrue(adapter.contains("fanTalkContent?.text = \"\""))
|
||||||
@@ -2457,6 +2489,43 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(source.contains("homeActionDelegate?.refreshHome()"))
|
assertTrue(source.contains("homeActionDelegate?.refreshHome()"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `차단 성공은 이전 화면으로 돌아가고 task root이면 MainV2Activity로 이동한다`() {
|
||||||
|
val fragmentSource = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt"
|
||||||
|
).readText()
|
||||||
|
val activitySource = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||||
|
).readText()
|
||||||
|
val observerSource = sourceSection(
|
||||||
|
source = fragmentSource,
|
||||||
|
startMarker = "private fun observeViewModel()",
|
||||||
|
endMarker = "private fun onScheduleClicked"
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(observerSource.contains("viewModel.blockSuccessLiveData.observe(viewLifecycleOwner)"))
|
||||||
|
assertTrue(observerSource.contains("event.consume()?.let { host.onCreatorChannelBlockSucceeded() }"))
|
||||||
|
assertTrue(fragmentSource.contains("fun onCreatorChannelBlockSucceeded()"))
|
||||||
|
|
||||||
|
val blockDialogSource = sourceSection(
|
||||||
|
source = activitySource,
|
||||||
|
startMarker = "private fun showUserBlockDialog()",
|
||||||
|
endMarker = "private fun showUserReportDialog()"
|
||||||
|
)
|
||||||
|
assertTrue(blockDialogSource.contains("homeActionDelegate?.blockUser()"))
|
||||||
|
assertFalse(blockDialogSource.contains("finish()"))
|
||||||
|
assertFalse(blockDialogSource.contains("MainV2Activity"))
|
||||||
|
|
||||||
|
val callbackSource = sourceSection(
|
||||||
|
source = activitySource,
|
||||||
|
startMarker = "override fun onCreatorChannelBlockSucceeded()",
|
||||||
|
endMarker = "override fun onCreatorChannelScheduleClicked"
|
||||||
|
)
|
||||||
|
assertTrue(callbackSource.contains("if (isTaskRoot)"))
|
||||||
|
assertTrue(callbackSource.contains("startActivity(Intent(this, MainV2Activity::class.java))"))
|
||||||
|
assertTrue(callbackSource.contains("finish()"))
|
||||||
|
}
|
||||||
|
|
||||||
private fun sourceSection(source: String, startMarker: String, endMarker: String): String {
|
private fun sourceSection(source: String, startMarker: String, endMarker: String): String {
|
||||||
val start = source.indexOf(startMarker)
|
val start = source.indexOf(startMarker)
|
||||||
val end = source.indexOf(endMarker, start)
|
val end = source.indexOf(endMarker, start)
|
||||||
|
|||||||
@@ -53,12 +53,14 @@ class CreatorChannelHomeMapperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `본인 채널 여부를 후원 section에도 매핑한다`() {
|
fun `본인 채널 여부를 후원과 팬Talk section에도 매핑한다`() {
|
||||||
val ownerContent = response().toUiContent(currentMemberId = 100L)
|
val ownerContent = response().toUiContent(currentMemberId = 100L)
|
||||||
val visitorContent = response().toUiContent(currentMemberId = 1L)
|
val visitorContent = response().toUiContent(currentMemberId = 1L)
|
||||||
|
|
||||||
assertTrue(ownerContent.sections.filterIsInstance<CreatorChannelHomeSection.Donations>().single().isOwner)
|
assertTrue(ownerContent.sections.filterIsInstance<CreatorChannelHomeSection.Donations>().single().isOwner)
|
||||||
assertFalse(visitorContent.sections.filterIsInstance<CreatorChannelHomeSection.Donations>().single().isOwner)
|
assertFalse(visitorContent.sections.filterIsInstance<CreatorChannelHomeSection.Donations>().single().isOwner)
|
||||||
|
assertTrue(ownerContent.sections.filterIsInstance<CreatorChannelHomeSection.FanTalk>().single().isOwner)
|
||||||
|
assertFalse(visitorContent.sections.filterIsInstance<CreatorChannelHomeSection.FanTalk>().single().isOwner)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ class CreatorChannelHomeViewModelTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `차단 성공은 block API를 호출하고 차단 완료 토스트를 emit한다`() {
|
fun `차단 성공은 block API를 호출하고 차단 완료 토스트와 성공 이벤트를 emit한다`() {
|
||||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||||
whenever(repository.blockUser(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, Any(), null)))
|
whenever(repository.blockUser(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, Any(), null)))
|
||||||
viewModel.loadHome(100L)
|
viewModel.loadHome(100L)
|
||||||
@@ -347,6 +347,35 @@ class CreatorChannelHomeViewModelTest {
|
|||||||
verify(repository).blockUser(100L, "Bearer test-token")
|
verify(repository).blockUser(100L, "Bearer test-token")
|
||||||
val toastEvent = viewModel.toastLiveData.requireValue()
|
val toastEvent = viewModel.toastLiveData.requireValue()
|
||||||
assertEquals(R.string.creator_channel_block_success, toastEvent?.consume()?.resId)
|
assertEquals(R.string.creator_channel_block_success, toastEvent?.consume()?.resId)
|
||||||
|
val blockSuccessEvent = viewModel.blockSuccessLiveData.requireValue()
|
||||||
|
assertEquals(Unit, blockSuccessEvent?.consume())
|
||||||
|
assertEquals(null, blockSuccessEvent?.consume())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `차단 실패는 unknown toast를 emit하고 성공 이벤트를 emit하지 않는다`() {
|
||||||
|
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||||
|
whenever(repository.blockUser(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(false, null, "failed")))
|
||||||
|
viewModel.loadHome(100L)
|
||||||
|
|
||||||
|
viewModel.blockUser()
|
||||||
|
|
||||||
|
val toastEvent = viewModel.toastLiveData.requireValue()
|
||||||
|
assertEquals(R.string.common_error_unknown, toastEvent?.consume()?.resId)
|
||||||
|
assertEquals(null, viewModel.blockSuccessLiveData.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `차단 예외는 unknown toast를 emit하고 성공 이벤트를 emit하지 않는다`() {
|
||||||
|
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||||
|
whenever(repository.blockUser(100L, "Bearer test-token")).thenReturn(Single.error(IllegalStateException("network")))
|
||||||
|
viewModel.loadHome(100L)
|
||||||
|
|
||||||
|
viewModel.blockUser()
|
||||||
|
|
||||||
|
val toastEvent = viewModel.toastLiveData.requireValue()
|
||||||
|
assertEquals(R.string.common_error_unknown, toastEvent?.consume()?.resId)
|
||||||
|
assertEquals(null, viewModel.blockSuccessLiveData.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -156,6 +156,9 @@ class CreatorChannelCommunityFragmentLayoutTest {
|
|||||||
assertTrue(fragment.contains("viewModel.loadMore()"))
|
assertTrue(fragment.contains("viewModel.loadMore()"))
|
||||||
assertTrue(fragment.contains("viewModel.retryCommunity()"))
|
assertTrue(fragment.contains("viewModel.retryCommunity()"))
|
||||||
assertTrue(fragment.contains("viewModel.toggleViewMode()"))
|
assertTrue(fragment.contains("viewModel.toggleViewMode()"))
|
||||||
|
assertTrue(fragment.contains("LanguageManager.getEffectiveLanguage(requireContext())"))
|
||||||
|
assertTrue(fragment.contains("LanguageManager.LANG_JA"))
|
||||||
|
assertTrue(fragment.contains("tvCreatorChannelCommunityViewModeLabel.isVisible ="))
|
||||||
assertTrue(fragment.contains("LinearLayoutManager"))
|
assertTrue(fragment.contains("LinearLayoutManager"))
|
||||||
assertTrue(
|
assertTrue(
|
||||||
fragment.contains("GridLayoutManager(requireContext(), 3") ||
|
fragment.contains("GridLayoutManager(requireContext(), 3") ||
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class CreatorChannelDonationActionTest {
|
|||||||
assertTrue(adapter.contains("onEmptyDonationClick"))
|
assertTrue(adapter.contains("onEmptyDonationClick"))
|
||||||
assertTrue(adapter.contains("btnCreatorChannelDonationRankingAll.setOnClickListener"))
|
assertTrue(adapter.contains("btnCreatorChannelDonationRankingAll.setOnClickListener"))
|
||||||
assertTrue(adapter.contains("btnCreatorChannelDonationEmptyWrite.isVisible = !isOwner"))
|
assertTrue(adapter.contains("btnCreatorChannelDonationEmptyWrite.isVisible = !isOwner"))
|
||||||
assertTrue(adapter.contains("btnCreatorChannelDonationEmptyWrite.setOnClickListener"))
|
assertTrue(adapter.contains("if (!isOwner) View.OnClickListener { onEmptyDonationClick() } else null"))
|
||||||
assertTrue(adapter.contains("bindCreatorChannelDonationCard("))
|
assertTrue(adapter.contains("bindCreatorChannelDonationCard("))
|
||||||
assertTrue(adapter.contains("headerColorResId = item.headerColorResId"))
|
assertTrue(adapter.contains("headerColorResId = item.headerColorResId"))
|
||||||
assertTrue(adapter.contains("createdAtText = item.createdAtText"))
|
assertTrue(adapter.contains("createdAtText = item.createdAtText"))
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class CreatorChannelDonationFragmentLayoutTest {
|
|||||||
assertTrue(layout.contains("android:text=\"@string/creator_channel_donation_empty_title\""))
|
assertTrue(layout.contains("android:text=\"@string/creator_channel_donation_empty_title\""))
|
||||||
assertTrue(layout.contains("android:text=\"@string/creator_channel_donation_action\""))
|
assertTrue(layout.contains("android:text=\"@string/creator_channel_donation_action\""))
|
||||||
assertTrue(layout.contains("android:background=\"@drawable/bg_creator_channel_donation_empty_button\""))
|
assertTrue(layout.contains("android:background=\"@drawable/bg_creator_channel_donation_empty_button\""))
|
||||||
|
assertTrue(layout.contains("android:textColor=\"@color/white\""))
|
||||||
assertTrue(layout.contains("android:layout_marginTop=\"@dimen/spacing_14\""))
|
assertTrue(layout.contains("android:layout_marginTop=\"@dimen/spacing_14\""))
|
||||||
assertTrue(layout.contains("android:src=\"@drawable/ic_new_donation\""))
|
assertTrue(layout.contains("android:src=\"@drawable/ic_new_donation\""))
|
||||||
assertTrue(layout.contains("android:paddingTop=\"@dimen/spacing_48\""))
|
assertTrue(layout.contains("android:paddingTop=\"@dimen/spacing_48\""))
|
||||||
|
|||||||
@@ -81,9 +81,15 @@ class CreatorChannelFanTalkFragmentLayoutTest {
|
|||||||
.readText()
|
.readText()
|
||||||
.contains("@color/gray_800")
|
.contains("@color/gray_800")
|
||||||
)
|
)
|
||||||
assertLocaleString("values/strings.xml", "아직 응원이 없습니다.\\n처음으로 크리에이터를 응원해 보세요!")
|
assertLocaleString("values/strings.xml", "creator_channel_fantalk_empty_title", "당신의 첫 응원을\\n기다리고 있어요!")
|
||||||
assertLocaleString("values-en/strings.xml", "No cheers yet.\\nBe the first to cheer for the creator!")
|
assertLocaleString("values-en/strings.xml", "creator_channel_fantalk_empty_title", "We\\'re waiting for your\\nfirst fan letter!")
|
||||||
assertLocaleString("values-ja/strings.xml", "まだ応援がありません。\\n最初にクリエイターを応援してみましょう!")
|
assertLocaleString("values-ja/strings.xml", "creator_channel_fantalk_empty_title", "あなたの最初のファンレターを待っています!")
|
||||||
|
assertLocaleString("values/strings.xml", "creator_channel_fantalk_empty_message", "아직 응원이 없습니다.\\n처음으로 크리에이터를 응원해 보세요!")
|
||||||
|
assertLocaleString("values-en/strings.xml", "creator_channel_fantalk_empty_message", "No fan letters yet.\\nSend the first fan letter!")
|
||||||
|
assertLocaleString("values-ja/strings.xml", "creator_channel_fantalk_empty_message", "まだファンレターがありません。\\n最初のファンレターを送ってみましょう!")
|
||||||
|
assertLocaleString("values/strings.xml", "creator_channel_fantalk_support_action", "응원남기기")
|
||||||
|
assertLocaleString("values-en/strings.xml", "creator_channel_fantalk_support_action", "Send fan letter")
|
||||||
|
assertLocaleString("values-ja/strings.xml", "creator_channel_fantalk_support_action", "ファンレターを送る")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -209,10 +215,12 @@ class CreatorChannelFanTalkFragmentLayoutTest {
|
|||||||
assertTrue(fragment.contains("fun onCreatorChannelFanTalkWriteClicked()"))
|
assertTrue(fragment.contains("fun onCreatorChannelFanTalkWriteClicked()"))
|
||||||
assertTrue(fragment.contains("fun onCreatorChannelFanTalkFloatingButtonVisibilityChanged(isVisible: Boolean)"))
|
assertTrue(fragment.contains("fun onCreatorChannelFanTalkFloatingButtonVisibilityChanged(isVisible: Boolean)"))
|
||||||
assertTrue(fragment.contains("layoutCreatorChannelFantalkEmptyWriteButton.setOnClickListener"))
|
assertTrue(fragment.contains("layoutCreatorChannelFantalkEmptyWriteButton.setOnClickListener"))
|
||||||
|
assertTrue(fragment.contains("layoutCreatorChannelFantalkEmptyWriteButton.isVisible = !host.isCreatorChannelOwner()"))
|
||||||
assertTrue(fragment.contains("host.onCreatorChannelFanTalkWriteClicked()"))
|
assertTrue(fragment.contains("host.onCreatorChannelFanTalkWriteClicked()"))
|
||||||
assertTrue(fragment.contains("host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(true)"))
|
assertTrue(fragment.contains("host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(true)"))
|
||||||
assertTrue(fragment.contains("host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(false)"))
|
assertTrue(fragment.contains("host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(false)"))
|
||||||
assertFalse(fragment.contains("btnCreatorChannelFantalkWrite.setOnClickListener { }"))
|
assertFalse(fragment.contains("btnCreatorChannelFantalkWrite.setOnClickListener { }"))
|
||||||
|
assertFalse(fragment.contains("layoutCreatorChannelFantalkEmptyWriteButton.setOnClickListener { host.onCreatorChannelFanTalkWriteClicked() }"))
|
||||||
assertFalse(fragment.contains("btnCreatorChannelFantalkWrite.isVisible = true"))
|
assertFalse(fragment.contains("btnCreatorChannelFantalkWrite.isVisible = true"))
|
||||||
assertTrue(layout.contains("@+id/layout_creator_channel_fantalk_empty_write_button"))
|
assertTrue(layout.contains("@+id/layout_creator_channel_fantalk_empty_write_button"))
|
||||||
assertTrue(layout.contains("android:id=\"@+id/btn_creator_channel_fantalk_write\""))
|
assertTrue(layout.contains("android:id=\"@+id/btn_creator_channel_fantalk_write\""))
|
||||||
@@ -227,9 +235,9 @@ class CreatorChannelFanTalkFragmentLayoutTest {
|
|||||||
|
|
||||||
private fun projectFile(path: String): File = File(projectRoot(), path)
|
private fun projectFile(path: String): File = File(projectRoot(), path)
|
||||||
|
|
||||||
private fun assertLocaleString(path: String, expectedValue: String) {
|
private fun assertLocaleString(path: String, name: String, expectedValue: String) {
|
||||||
val strings = projectFile("app/src/main/res/$path").readText()
|
val strings = projectFile("app/src/main/res/$path").readText()
|
||||||
assertTrue(strings.contains("<string name=\"creator_channel_fantalk_empty_message\">$expectedValue</string>"))
|
assertTrue(strings.contains("<string name=\"$name\">$expectedValue</string>"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun projectRoot(): File {
|
private fun projectRoot(): File {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
|
|
||||||
assertTrue(
|
assertTrue(
|
||||||
strings.contains(
|
strings.contains(
|
||||||
"<string name=\"screen_home_following_empty\">아직 팔로잉 소식이 없어요.\\n" +
|
"<string name=\"screen_home_following_empty\">아직 팔로우한 크리에이터가 없어요.\\n" +
|
||||||
"관심 있는 크리에이터를 팔로우해 보세요.</string>"
|
"관심 있는 크리에이터를 팔로우해 보세요.</string>"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -131,7 +131,7 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
)
|
)
|
||||||
assertTrue(
|
assertTrue(
|
||||||
stringsJa.contains(
|
stringsJa.contains(
|
||||||
"<string name=\"screen_home_following_empty\">フォロー中のお知らせはまだありません。\\n" +
|
"<string name=\"screen_home_following_empty\">まだフォローしているクリエイターがいません。\\n" +
|
||||||
"気になるクリエイターをフォローしてみましょう。</string>"
|
"気になるクリエイターをフォローしてみましょう。</string>"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -316,29 +316,28 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `팔로잉 크리에이터 목록은 전체 item을 마지막에 추가하고 이동을 연결한다`() {
|
fun `팔로잉 크리에이터 전체 버튼은 목록 밖에 고정하고 이동을 연결한다`() {
|
||||||
|
val layout = homeMainLayoutSource()
|
||||||
|
val section = layout.substringAfter("@+id/ll_home_following_creators_section")
|
||||||
|
.substringBefore("@+id/ll_home_following_on_air_section")
|
||||||
val adapter = projectFile(
|
val adapter = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingCreatorAdapter.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingCreatorAdapter.kt"
|
||||||
).readText()
|
).readText()
|
||||||
val fragment = homeMainFragmentSource()
|
val fragment = homeMainFragmentSource()
|
||||||
val allLayoutFile = projectFileOrNull("app/src/main/res/layout/item_home_following_creator_all.xml")
|
|
||||||
|
|
||||||
assertTrue(allLayoutFile != null)
|
assertTrue(section.contains("@+id/rv_home_following_creators"))
|
||||||
val allLayout = allLayoutFile?.readText().orEmpty()
|
assertTrue(section.contains("@+id/tv_home_following_creator_all"))
|
||||||
|
assertTrue(adapter.contains("override fun getItemCount(): Int = items.size"))
|
||||||
assertTrue(adapter.contains("VIEW_TYPE_CREATOR"))
|
assertFalse(adapter.contains("VIEW_TYPE_ALL"))
|
||||||
assertTrue(adapter.contains("VIEW_TYPE_ALL"))
|
assertFalse(adapter.contains("AllViewHolder"))
|
||||||
assertTrue(adapter.contains("getItemViewType(position: Int)"))
|
assertFalse(adapter.contains("item_home_following_creator_all"))
|
||||||
assertTrue(adapter.contains("if (position < items.size) VIEW_TYPE_CREATOR else VIEW_TYPE_ALL"))
|
assertTrue(projectFileOrNull("app/src/main/res/layout/item_home_following_creator_all.xml") == null)
|
||||||
assertTrue(adapter.contains("override fun getItemCount(): Int = items.size + if (items.isNotEmpty()) 1 else 0"))
|
|
||||||
assertTrue(adapter.contains("R.layout.item_home_following_creator_all"))
|
|
||||||
assertTrue(adapter.contains("AllViewHolder"))
|
|
||||||
assertTrue(fragment.contains("import kr.co.vividnext.sodalive.following.FollowingCreatorActivity"))
|
assertTrue(fragment.contains("import kr.co.vividnext.sodalive.following.FollowingCreatorActivity"))
|
||||||
assertTrue(fragment.contains("HomeFollowingCreatorAdapter("))
|
assertTrue(fragment.contains("HomeFollowingCreatorAdapter("))
|
||||||
assertTrue(fragment.contains("onClickAll = { openFollowingCreatorAll() }"))
|
assertTrue(fragment.contains("binding.tvHomeFollowingCreatorAll.setOnClickListener"))
|
||||||
|
assertTrue(fragment.contains("openFollowingCreatorAll()"))
|
||||||
assertTrue(fragment.contains("private fun openFollowingCreatorAll()"))
|
assertTrue(fragment.contains("private fun openFollowingCreatorAll()"))
|
||||||
assertTrue(fragment.contains("startActivity(Intent(requireContext(), FollowingCreatorActivity::class.java))"))
|
assertTrue(fragment.contains("startActivity(Intent(requireContext(), FollowingCreatorActivity::class.java))"))
|
||||||
assertTrue(allLayout.contains("@string/screen_home_theme_all"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -572,7 +572,35 @@
|
|||||||
- 검증 기록:
|
- 검증 기록:
|
||||||
- 2026-07-12: RED로 `CreatorChannelCommunityMapperTest.유료 미구매 타인 게시글은 reaction 현황과 댓글을 숨긴다`를 추가했고, production 변경 전 `showReaction` 미구현 컴파일 실패를 확인했다. 이후 UI 모델/mapper/list adapter에 `showReaction`과 잠금 조건을 반영했다.
|
- 2026-07-12: RED로 `CreatorChannelCommunityMapperTest.유료 미구매 타인 게시글은 reaction 현황과 댓글을 숨긴다`를 추가했고, production 변경 전 `showReaction` 미구현 컴파일 실패를 확인했다. 이후 UI 모델/mapper/list adapter에 `showReaction`과 잠금 조건을 반영했다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 8: 일본어 보기 방식 label 숨김 후속
|
||||||
|
|
||||||
|
- [x] **Task 8.1: 일본어 커뮤니티 보기 방식 label 숨김**
|
||||||
|
- 대상 파일:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragmentLayoutTest.kt`
|
||||||
|
- 작업:
|
||||||
|
- `LanguageManager.getEffectiveLanguage(requireContext()) == LanguageManager.LANG_JA`이면 `tvCreatorChannelCommunityViewModeLabel`을 숨긴다.
|
||||||
|
- 일본어가 아니면 기존처럼 보기 방식 label을 표시한다.
|
||||||
|
- 전체 count, 보기 방식 icon, 토글 버튼 동작은 유지한다.
|
||||||
|
- 검증 계획:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"`
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`
|
||||||
|
- `./gradlew :app:mergeDebugResources :app:compileDebugKotlin`
|
||||||
|
- `git diff --check`
|
||||||
|
- 수동 확인:
|
||||||
|
- 일본어 설정에서 커뮤니티 탭 전체 개수 우측 끝의 `リスト`/`グリッド` label이 보이지 않는지 사용자가 직접 확인한다.
|
||||||
|
- 한국어/영어 설정에서는 기존 label 표시가 유지되는지 사용자가 직접 확인한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 2026-08-14: RED로 `CreatorChannelCommunityFragmentLayoutTest`에 `LanguageManager.getEffectiveLanguage(requireContext())`, `LanguageManager.LANG_JA`, `tvCreatorChannelCommunityViewModeLabel.isVisible =` source 계약을 추가했고, production 변경 전 `커뮤니티 fragment source는 pagination view mode owner padding과 상세 이동 계약을 사용한다`가 `CreatorChannelCommunityFragmentLayoutTest.kt:159` assertion으로 실패함을 확인했다.
|
||||||
|
- 2026-08-14: `CreatorChannelCommunityFragment.bindContent()`에서 `LanguageManager.getEffectiveLanguage(requireContext()) != LanguageManager.LANG_JA`일 때만 보기 방식 label을 표시하도록 변경했다. 전체 count, 보기 방식 icon, 토글 버튼 동작은 변경하지 않았다.
|
||||||
|
- 2026-08-14: GREEN으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"` PASS, 회귀로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"` PASS를 확인했다. 최초 GREEN 실행 1회는 테스트 결과 출력 전 120초 timeout으로 중단되어 300초 제한으로 동일 명령을 재실행했다.
|
||||||
|
- 2026-08-14: `./gradlew :app:mergeDebugResources :app:compileDebugKotlin` PASS, `git diff --check` PASS를 확인했다. `./gradlew :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`는 기존 변경 범위 밖의 `CreatorChannelActivitySourceTest.kt:1320`, `CreatorChannelFanTalkFragmentLayoutTest.kt:85/88/89/223` line length/argument formatting 위반으로 FAIL했고, 이번 변경 파일의 ktlint 위반은 보고되지 않았다. `.editorconfig disabled_rules` deprecation 경고는 기존 설정 경고다.
|
||||||
|
- 2026-08-14: 리뷰 보고서 `docs/20260621_크리에이터_채널_커뮤니티_탭/reviews/phase8-japanese-view-mode-label-review.md`를 작성했고, 차단 이슈와 확정 발견 사항 없음으로 판정했다.
|
||||||
|
|
||||||
## Verification Log
|
## Verification Log
|
||||||
|
- 2026-08-14: Phase 8 일본어 커뮤니티 보기 방식 label 숨김을 완료했다. RED는 `CreatorChannelCommunityFragmentLayoutTest` source 계약 실패로 확인했고, 구현 후 focused layout test와 community 패키지 회귀 테스트가 `BUILD SUCCESSFUL`로 PASS했다. `mergeDebugResources`와 `compileDebugKotlin`, `git diff --check`도 PASS했다. visual-qa와 실제 화면 조작은 사용자 요청에 따라 실행하지 않았으며, 일본어/한국어/영어 설정의 실제 화면 표시는 사용자 수동 확인 항목으로 남겼다. `ktlintCheck`는 기존 변경 범위 밖 테스트 파일의 line length/argument formatting 위반으로 FAIL했다. 리뷰 보고서 `reviews/phase8-japanese-view-mode-label-review.md`는 차단 이슈 없음으로 종료했다.
|
||||||
- 2026-07-12: Phase 7 유료 미구매 리스트형 게시글 reaction/comment 숨김 변경을 완료했다. RED는 `CreatorChannelCommunityMapperTest.유료 미구매 타인 게시글은 reaction 현황과 댓글을 숨긴다`의 `showReaction` 미구현 컴파일 실패로 확인했고, 구현 후 focused 테스트, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`, `./gradlew :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`가 모두 `BUILD SUCCESSFUL`로 PASS했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation 경고는 기존 설정 경고다.
|
- 2026-07-12: Phase 7 유료 미구매 리스트형 게시글 reaction/comment 숨김 변경을 완료했다. RED는 `CreatorChannelCommunityMapperTest.유료 미구매 타인 게시글은 reaction 현황과 댓글을 숨긴다`의 `showReaction` 미구현 컴파일 실패로 확인했고, 구현 후 focused 테스트, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`, `./gradlew :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`가 모두 `BUILD SUCCESSFUL`로 PASS했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation 경고는 기존 설정 경고다.
|
||||||
- 계획 문서 생성 단계에서는 코드 변경을 수행하지 않았다. 구현 후 통합 검증, 회귀 검증, 최종 수동 확인 기록을 이 섹션에 누적한다.
|
- 계획 문서 생성 단계에서는 코드 변경을 수행하지 않았다. 구현 후 통합 검증, 회귀 검증, 최종 수동 확인 기록을 이 섹션에 누적한다.
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ Sort-bar는 전체 커뮤니티 게시글 수와 현재 보기 방식을 표시
|
|||||||
- 썸네일형 label은 `썸네일형`이며 icon은 `ic_new_grid`를 사용한다.
|
- 썸네일형 label은 `썸네일형`이며 icon은 `ic_new_grid`를 사용한다.
|
||||||
- 우측 보기 방식 영역을 터치하면 `리스트형 -> 썸네일형 -> 리스트형` 순서로 토글한다.
|
- 우측 보기 방식 영역을 터치하면 `리스트형 -> 썸네일형 -> 리스트형` 순서로 토글한다.
|
||||||
- 보기 방식 label은 한국어/영어/일본어 다국어 문자열 리소스로 관리한다.
|
- 보기 방식 label은 한국어/영어/일본어 다국어 문자열 리소스로 관리한다.
|
||||||
|
- 일본어로 설정된 경우에는 우측 보기 방식 label(`リスト`/`グリッド`)을 표시하지 않고 icon과 토글 동작은 유지한다.
|
||||||
- 보기 방식 변경 시 스크롤 위치 유지 여부는 구현 계획 단계에서 기존 탭 목록 상태 정책을 확인해 결정한다.
|
- 보기 방식 변경 시 스크롤 위치 유지 여부는 구현 계획 단계에서 기존 탭 목록 상태 정책을 확인해 결정한다.
|
||||||
|
|
||||||
#### Edge Cases
|
#### Edge Cases
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Phase 8 일본어 보기 방식 Label 숨김 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 8 / Task 8.1 |
|
||||||
|
| 기준 commit 또는 working tree | working tree 변경분 |
|
||||||
|
| 리뷰 일자 | 2026-08-14 |
|
||||||
|
| 리뷰어 | Sisyphus-Junior |
|
||||||
|
| 기준 문서 | `docs/20260621_크리에이터_채널_커뮤니티_탭/prd.md`, `docs/20260621_크리에이터_채널_커뮤니티_탭/plan-task.md` |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
### 목적
|
||||||
|
|
||||||
|
- 일본어 설정에서 커뮤니티 탭 Sort-bar 우측 보기 방식 label만 숨기는지 확인한다.
|
||||||
|
- 전체 count, 보기 방식 icon, 토글 동작이 유지되는지 확인한다.
|
||||||
|
- 기존 문서와 검증 기록이 변경 범위를 정확히 반영하는지 확인한다.
|
||||||
|
|
||||||
|
### 포함 범위
|
||||||
|
|
||||||
|
- 코드: `CreatorChannelCommunityFragment.kt`
|
||||||
|
- 테스트: `CreatorChannelCommunityFragmentLayoutTest.kt`
|
||||||
|
- 문서: Phase 8 요구사항, Task, Verification Log
|
||||||
|
|
||||||
|
### 제외 범위
|
||||||
|
|
||||||
|
- visual-qa와 실제 기기 화면 조작. 사용자가 수동으로 진행한다.
|
||||||
|
- 커뮤니티 탭 레이아웃, icon, 토글 로직의 시각적 재설계.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
| 심각도 | 기준 |
|
||||||
|
|---|---|
|
||||||
|
| Blocker | 일본어 label이 계속 표시되거나 비일본어 label이 숨겨지는 문제 |
|
||||||
|
| High | count, icon, toggle 동작 회귀 |
|
||||||
|
| Medium | 문서·검증 기록 누락으로 완료 판단이 어려운 문제 |
|
||||||
|
| Low | 비차단 유지보수성 또는 테스트 강도 개선 의견 |
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
### 문서와 코드
|
||||||
|
|
||||||
|
- 요구사항: `prd.md`의 일본어 `リスト`/`グリッド` label 숨김 요구.
|
||||||
|
- 계획: `plan-task.md` Phase 8 / Task 8.1.
|
||||||
|
- 코드: `CreatorChannelCommunityFragment.bindContent()`의 `LanguageManager.getEffectiveLanguage(requireContext()) != LanguageManager.LANG_JA` visibility 조건.
|
||||||
|
- 테스트: `CreatorChannelCommunityFragmentLayoutTest`의 source 계약.
|
||||||
|
|
||||||
|
### 실행한 검증
|
||||||
|
|
||||||
|
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|
||||||
|
|---|---|---|
|
||||||
|
| `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"` | 성공 | GREEN 재실행 `BUILD SUCCESSFUL` |
|
||||||
|
| `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"` | 성공 | community 패키지 회귀 `BUILD SUCCESSFUL` |
|
||||||
|
| `./gradlew :app:mergeDebugResources :app:compileDebugKotlin` | 성공 | resource merge, Kotlin compile `BUILD SUCCESSFUL` |
|
||||||
|
| `git diff --check` | 성공 | 출력 없음 |
|
||||||
|
| `./gradlew :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` | 실패 | 변경 범위 밖 기존 테스트 파일 line length/argument formatting 위반 |
|
||||||
|
| visual-qa / 실제 화면 조작 | 제외 | 사용자 수동 진행 요청 |
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| - | - | - | 확정 발견 사항 없음 | - | 전환 항목 없음 |
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
비차단 관찰: 테스트는 source 문자열 계약이므로 조건 연산자 자체를 깊게 실행 검증하지는 않는다. 다만 실제 diff의 `!= LanguageManager.LANG_JA` 조건이 요구사항을 충족하고, 기존 테스트 파일이 같은 source-contract 방식을 사용하므로 이번 변경의 차단 이슈는 아니다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 코드, 테스트, 문서 변경분 확인 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 확정 발견 사항 없음 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 전환 항목 없음 |
|
||||||
|
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 항목 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | Phase 8 검증 기록에 RED/GREEN/회귀/빌드/공백/ktlint 결과 기록 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음.
|
||||||
|
|
||||||
|
**남은 항목:** visual-qa와 실제 일본어/한국어/영어 화면 확인은 사용자 수동 QA로 진행한다.
|
||||||
@@ -1441,7 +1441,183 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Phase 14: 팔로잉 크리에이터 전체 버튼 우측 고정
|
||||||
|
|
||||||
|
**Phase 결과:** 팔로잉 탭 최상단 팔로잉 크리에이터 섹션에서 프로필 목록만 수평 스크롤되고, `전체` 버튼은 추천 탭 라이브 섹션과 동일하게 우측 끝에 고정된다.
|
||||||
|
|
||||||
|
**선행조건:** Phase 8의 기존 팔로잉 크리에이터 `전체` 버튼 클릭 라우팅과 `FollowingCreatorActivity` 진입 동작이 유지되어야 한다.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P14-T1`~`P14-GATE` 완료, 팔로잉 크리에이터 `전체` 버튼이 RecyclerView item이 아니라 별도 TextView로 분리됐음을 source/local unit test와 resource merge로 검증하고, 우측 고정 위치는 수동 확인으로 판정한다.
|
||||||
|
|
||||||
|
#### Task 14.1 팔로잉 전체 버튼 고정 구조 RED 테스트 추가
|
||||||
|
|
||||||
|
**Goal 실행 `P14-T1`:** 현재 RecyclerView 마지막 item 방식의 `전체` 버튼 구조를 실패 test로 고정하고, 추천 탭과 같은 우측 고정 구조가 없음을 재현한다.
|
||||||
|
|
||||||
|
- **시작 조건:** PRD의 `2026-08-18 팔로잉 크리에이터 전체 버튼 고정 Requirements` 확정.
|
||||||
|
- **완료 증거:** 구현 전 focused source/local test가 `tv_home_following_creator_all` 고정 TextView 부재 또는 adapter의 `VIEW_TYPE_ALL` 잔존으로 실패한다.
|
||||||
|
- **범위 밖:** 팔로잉 크리에이터 API/mapper 변경, 다른 팔로잉 섹션 UI 변경, 추천 탭 코드 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
|
||||||
|
|
||||||
|
- [x] **RED:** `fragment_v2_main_home.xml`에서 `rv_home_following_creators`와 sibling `TextView` `@+id/tv_home_following_creator_all`이 같은 팔로잉 크리에이터 section 아래에 존재하는지 검증한다. 레이아웃 폭, margin, gravity, visibility 같은 UI 표현 속성은 source test로 직접 검증하지 않는다.
|
||||||
|
- [x] **RED:** `HomeFollowingCreatorAdapter`가 `VIEW_TYPE_ALL`, `AllViewHolder`, `item_home_following_creator_all`을 더 이상 사용하지 않고 `getItemCount()`가 `items.size`를 반환하는지 검증한다.
|
||||||
|
- [x] **RED 확인:** `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"`를 실행해 현재 구현의 우측 고정 TextView 부재와 adapter `전체` item 잔존으로 인한 assertion 실패를 확인한다.
|
||||||
|
- [x] **GREEN:** Task 14.2와 Task 14.3의 최소 구현으로 RED를 통과시킨다.
|
||||||
|
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
|
||||||
|
- [x] **REFACTOR:** 구조와 라우팅 계약 검증만 남기고, 우측 고정 위치는 Task 14.4 수동 확인 항목으로 유지한다.
|
||||||
|
|
||||||
|
검증 기록:
|
||||||
|
|
||||||
|
- 2026-08-18 RED: focused test 17개 중 `팔로잉 크리에이터 전체 버튼은 목록 밖에 고정하고 이동을 연결한다` 1개가 `HomeFollowingFragmentSourceTest.kt:329` assertion으로 실패했다. 현재 layout에 sibling `tv_home_following_creator_all`이 없어 요청 구조가 구현되지 않은 것이 실패 원인이다.
|
||||||
|
- 2026-08-18 GREEN: `--rerun-tasks`로 focused test를 재실행해 17개 모두 PASS했다. 테스트는 RecyclerView와 고정 TextView의 sibling 구조, creator-only adapter, obsolete 전체 item layout 제거, 기존 `FollowingCreatorActivity` 이동 연결을 검증한다.
|
||||||
|
|
||||||
|
#### Task 14.2 팔로잉 크리에이터 섹션 레이아웃을 추천 탭 구조로 변경
|
||||||
|
|
||||||
|
**Goal 실행 `P14-T2`:** 팔로잉 크리에이터 RecyclerView 밖 우측 끝에 고정 `전체` TextView를 추가한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P14-T1` RED 확인.
|
||||||
|
- **완료 증거:** 구조 source test, resource merge 통과와 수동 확인 가능 상태 기록.
|
||||||
|
- **범위 밖:** 팔로잉 크리에이터 item 디자인 변경, 다른 섹션 spacing 변경, 새 drawable/string 추가.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/res/layout/fragment_v2_main_home.xml`
|
||||||
|
|
||||||
|
- [x] **RED:** Task 14.1의 실패 test를 기준으로 한다.
|
||||||
|
- [x] **RED 확인:** Task 14.1 실행 결과를 확인한다.
|
||||||
|
- [x] **GREEN:** `ll_home_following_creators_section`의 orientation을 `horizontal`로 바꾸고, `rv_home_following_creators`는 `layout_width="0dp"`, `layout_weight="1"`, `paddingStart="@dimen/spacing_20"`, `clipToPadding="false"`로 둔다.
|
||||||
|
- [x] **GREEN:** `rv_home_following_creators` 뒤에 `TextView` `@+id/tv_home_following_creator_all`을 추가하고, 추천 탭의 `tv_home_live_all`처럼 `@string/screen_home_theme_all`, `@color/soda_400`, `@color/black`, center gravity, 우측 margin을 사용한다.
|
||||||
|
- [x] **GREEN 확인:** `./gradlew --no-daemon --rerun-tasks :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"` 실행 과정의 resource merge와 focused test 성공을 확인한다.
|
||||||
|
- [x] **REFACTOR:** 레이아웃 변경이 만든 중복만 점검하고, 추천 탭 live 섹션과 팔로잉 섹션 구조 차이가 의도된 범위인지 기록한다.
|
||||||
|
|
||||||
|
검증 기록:
|
||||||
|
|
||||||
|
- 2026-08-18: 추천 탭 `ll_home_live_section`과 동일한 horizontal sibling 구조를 적용했다. RecyclerView는 남은 폭을 사용하고 `tv_home_following_creator_all`은 우측 고정 영역에 배치했다. 기존 팔로잉 크리에이터 item과 다른 섹션은 변경하지 않았으며, resource merge와 focused test가 PASS했다.
|
||||||
|
|
||||||
|
#### Task 14.3 adapter의 전체 item 제거와 클릭 연결 유지
|
||||||
|
|
||||||
|
**Goal 실행 `P14-T3`:** `HomeFollowingCreatorAdapter`는 크리에이터 item만 표시하고, `전체` 클릭은 Fragment의 고정 TextView에서 처리한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P14-T2` 완료.
|
||||||
|
- **완료 증거:** adapter source/local test, Kotlin compile 통과.
|
||||||
|
- **범위 밖:** `openFollowingCreatorAll()` 목적지 변경, `HomeFollowingCreatorUiItem` 모델 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingCreatorAdapter.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`
|
||||||
|
- Delete: `app/src/main/res/layout/item_home_following_creator_all.xml`
|
||||||
|
|
||||||
|
- [x] **RED:** Task 14.1의 adapter 실패 test를 기준으로 한다.
|
||||||
|
- [x] **RED 확인:** Task 14.1 실행 결과를 확인한다.
|
||||||
|
- [x] **GREEN:** `HomeFollowingCreatorAdapter`에서 `onClickAll`, `VIEW_TYPE_ALL`, `AllViewHolder`, `item_home_following_creator_all`, `items.size + 1` item count 로직을 제거한다.
|
||||||
|
- [x] **GREEN:** `HomeMainFragment`에서 `HomeFollowingCreatorAdapter` 생성자는 `onClickItem`만 전달하고, `binding.tvHomeFollowingCreatorAll.setOnClickListener { openFollowingCreatorAll() }`로 기존 전체 목록 이동을 연결한다.
|
||||||
|
- [x] **GREEN:** 팔로잉 크리에이터 섹션 visibility가 `followingCreators` empty 여부로 제어될 때 고정 `전체` TextView도 같은 section 안에서 함께 숨겨지도록 기존 section visibility 로직을 유지한다.
|
||||||
|
- [x] **GREEN 확인:** `--rerun-tasks` focused test 실행 과정의 Kotlin compile과 test 성공을 확인한다.
|
||||||
|
- [x] **REFACTOR:** 삭제한 `item_home_following_creator_all.xml` 참조가 남지 않았는지 `rg -n "item_home_following_creator_all|VIEW_TYPE_ALL|AllViewHolder|tv_home_following_creator_all" app/src/main`로 확인한다.
|
||||||
|
|
||||||
|
검증 기록:
|
||||||
|
|
||||||
|
- 2026-08-18: adapter를 creator-only 구조로 단순화하고 Fragment의 고정 TextView에 기존 `openFollowingCreatorAll()`을 연결했다. obsolete item layout을 삭제했으며, `rg` 결과 production에는 신규 고정 TextView id 참조만 남아 있다.
|
||||||
|
|
||||||
|
#### Task 14.4 팔로잉 전체 버튼 고정 회귀 검증
|
||||||
|
|
||||||
|
**Goal 실행 `P14-GATE`:** 팔로잉 크리에이터 전체 버튼 고정 구조와 기존 팔로잉 탭 동작에 회귀가 없는지 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P14-T1`, `P14-T2`, `P14-T3` 완료.
|
||||||
|
- **완료 증거:** 아래 자동 검증 결과와 수동 확인 가능 여부를 기록한다.
|
||||||
|
- **범위 밖:** 연결 기기 없는 환경에서 수동 검증을 성공으로 추정하는 행위, 사용자가 요청하지 않은 androidTest 작성.
|
||||||
|
|
||||||
|
- [x] `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"` PASS.
|
||||||
|
- [x] `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"` PASS.
|
||||||
|
- [x] `./gradlew --no-daemon :app:mergeDebugResources` PASS.
|
||||||
|
- [x] `./gradlew --no-daemon :app:compileDebugKotlin` PASS.
|
||||||
|
- [x] `./gradlew --no-daemon :app:ktlintCheck` PASS. 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다.
|
||||||
|
- [x] `git diff --check` PASS.
|
||||||
|
- [x] 사용자 요청에 따라 visual QA와 기기 조작은 실행하지 않았다. 사용자가 `app/build/outputs/apk/debug/app-debug.apk`로 팔로잉 크리에이터 목록 스크롤 중 `전체` 우측 고정과 전체 팔로잉 목록 이동을 수동 확인했다.
|
||||||
|
|
||||||
|
검증 기록:
|
||||||
|
|
||||||
|
- 2026-08-18: focused RED 1건을 확인한 뒤 최소 구현으로 GREEN 전환했다. 팔로잉 회귀 테스트, resource merge, Kotlin compile, ktlint, `git diff --check`, `:app:assembleDebug`가 모두 PASS했다. 첫 통합 검증에서 이번 변경의 adapter 마지막 빈 줄 1건이 ktlint 실패했으나 해당 줄만 제거한 뒤 전체 게이트를 재실행해 PASS했다. Oracle 읽기 전용 리뷰는 Critical/Important finding 없이 승인했다. visual QA와 기기 조작은 사용자 요청으로 생략했으며, 사용자가 debug APK로 `전체` 우측 고정과 전체 팔로잉 목록 이동을 수동 확인했다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 15: 팔로잉 empty 안내 문구 변경
|
||||||
|
|
||||||
|
**Phase 결과:** 팔로잉 탭의 5개 섹션이 모두 비면 기존 중앙 empty UI에 확정된 한국어·일본어 안내 문구가 표시되고, 안내 상태에서도 당겨서 새로고침할 수 있다.
|
||||||
|
|
||||||
|
**선행조건:** Phase 12의 `HomeFollowingUiState.Empty` 중앙 표시와 홈 탭 `SwipeRefreshLayout` 새로고침 경로가 유지되어야 한다.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P15-T1`과 `P15-GATE` 완료, 한국어·일본어 문구 변경 및 영어·empty 판정·중앙 표시·새로고침 회귀 검증 기록 누적.
|
||||||
|
|
||||||
|
#### Task 15.1 한국어·일본어 empty 안내 문구 교체
|
||||||
|
|
||||||
|
**Goal 실행 `P15-T1`:** 기존 empty UI와 string key를 재사용해 한국어·일본어 문구의 첫 문장만 확정 문구로 교체한다.
|
||||||
|
|
||||||
|
- **시작 조건:** PRD의 `2026-08-19 팔로잉 empty 안내 문구 변경 Requirements`와 `DEC-20260819-001` 확정.
|
||||||
|
- **완료 증거:** 기존 source test의 한국어·일본어 기대값 RED → 두 locale string resource 최소 변경 → focused test GREEN.
|
||||||
|
- **범위 밖:** 영어 문구, API/DTO/mapper, `HomeFollowingUiState`, `HomeMainFragment`, `fragment_v2_main_home.xml`, 다른 화면의 empty 문구 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
|
||||||
|
- Modify: `app/src/main/res/values/strings.xml`
|
||||||
|
- Modify: `app/src/main/res/values-ja/strings.xml`
|
||||||
|
- Confirm unchanged: `app/src/main/res/values-en/strings.xml`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `HomeFollowingUiState.Empty`, `R.string.screen_home_following_empty`, `tv_home_following_empty`의 기존 표시 경로.
|
||||||
|
- Produces: 한국어 `아직 팔로우한 크리에이터가 없어요.\n관심 있는 크리에이터를 팔로우해 보세요.`, 일본어 `まだフォローしているクリエイターがいません。\n気になるクリエイターをフォローしてみましょう。` resource 값.
|
||||||
|
|
||||||
|
- [x] **RED:** `HomeFollowingFragmentSourceTest`의 한국어·일본어 `screen_home_following_empty` 기대값을 확정 문구로 변경하고 영어 기대값은 유지한다.
|
||||||
|
- [x] **RED 확인:** `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"`를 실행해 기존 한국어 첫 문장 불일치로 focused test가 실패하는지 확인한다.
|
||||||
|
- [x] **GREEN:** `values/strings.xml`과 `values-ja/strings.xml`의 기존 `screen_home_following_empty` 첫 문장만 확정 문구로 교체한다.
|
||||||
|
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 한국어·일본어 새 문구와 기존 영어 문구 검증이 통과하는지 확인한다.
|
||||||
|
- [x] **REFACTOR:** 추가 abstraction이나 새 resource key를 만들지 않고 이번 변경이 만든 불필요한 수정만 정리한 뒤 focused test와 `git diff --check`를 다시 실행한다.
|
||||||
|
|
||||||
|
검증 기록:
|
||||||
|
|
||||||
|
- 2026-08-19 RED: `HomeFollowingFragmentSourceTest`의 한국어·일본어 기대값만 확정 문구로 변경하고 영어 기대값은 유지한 뒤 focused test를 실행했다. 기존 한국어 resource 값 불일치로 `HomeFollowingFragmentSourceTest.kt:120` assertion 실패를 확인했다.
|
||||||
|
- 2026-08-19 GREEN: `values/strings.xml`, `values-ja/strings.xml`의 기존 `screen_home_following_empty` 첫 문장만 교체했다. 같은 focused test가 `BUILD SUCCESSFUL`로 통과했고 `git diff --check`도 PASS했다. 새 resource key나 abstraction은 추가하지 않았다.
|
||||||
|
|
||||||
|
#### Task 15.2 empty 표시와 당겨서 새로고침 회귀 검증
|
||||||
|
|
||||||
|
**Goal 실행 `P15-GATE`:** 문구 변경 후에도 기존 5개 섹션 전체 empty 판정, 중앙 empty UI 연결, 팔로잉 당겨서 새로고침 경로가 유지되는지 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P15-T1` 완료.
|
||||||
|
- **완료 증거:** 아래 local unit/source test, resource merge, Kotlin compile, ktlint와 diff 검증 결과 기록.
|
||||||
|
- **범위 밖:** `app/src/androidTest` 작성·실행, 기기·에뮬레이터 UI 조작, 스크린샷·시각 QA, layout 속성 직접 테스트.
|
||||||
|
- **TDD 예외 사유:** 신규 production 로직이 없는 회귀 검증 Gate이므로 별도 RED/GREEN 구현 단계를 만들지 않는다.
|
||||||
|
- **대체 검증 방법:** 기존 mapper test로 5개 섹션 전체 empty 판정을, source test로 empty string 연결·상태 분기와 홈 탭 refresh routing을 검증하고, Phase 12에서 확정된 layout 파일이 이번 Phase에서 변경되지 않았는지 diff로 확인한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingMapperTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentSourceTest.kt`
|
||||||
|
- Confirm unchanged: `app/src/main/res/layout/fragment_v2_main_home.xml`
|
||||||
|
- Confirm unchanged: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`
|
||||||
|
|
||||||
|
- [x] `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingMapperTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentSourceTest"` PASS.
|
||||||
|
- [x] `./gradlew --no-daemon :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS.
|
||||||
|
- [x] `git diff --check` PASS.
|
||||||
|
- [x] 이번 Phase의 production 변경이 `values/strings.xml`, `values-ja/strings.xml`의 `screen_home_following_empty` 값으로 한정되고 영어 resource, layout, Kotlin production 파일이 변경되지 않았음을 확인한다.
|
||||||
|
- [x] Task별 검증 결과와 실행 중 확인된 기존 경고를 이 Phase의 검증 기록에 한국어로 누적한다.
|
||||||
|
|
||||||
|
검증 기록:
|
||||||
|
|
||||||
|
- 2026-08-19: mapper·Fragment·홈 refresh routing focused test가 `BUILD SUCCESSFUL`로 통과해 기존 5개 섹션 전체 empty 판정, 중앙 empty string 연결·상태 분기, 팔로잉 당겨서 새로고침 경로를 확인했다. `:app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`와 `git diff --check`도 PASS했다. ktlint에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다. 영어 resource, layout, Kotlin production 파일은 변경하지 않았고, 계획에 따라 androidTest·기기 조작·스크린샷·시각 QA는 실행하지 않았다. 명세 및 코드 품질 리뷰는 blocking finding 없이 승인됐다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Verification Log
|
## Verification Log
|
||||||
|
- 2026-08-19 Phase 15 구현 완료: `screen_home_following_empty`의 한국어·일본어 첫 문장만 확정 문구로 변경하고 영어 문구와 기존 empty 판정·중앙 표시·당겨서 새로고침 경로를 유지했다. focused RED는 기존 한국어 resource 불일치로 확인했고 최소 resource 수정 후 GREEN으로 전환했다. mapper·Fragment·refresh routing 회귀, resource merge, Kotlin compile, ktlint, `git diff --check`가 PASS했으며 명세·품질 리뷰에서 확정 발견 사항은 없었다.
|
||||||
|
- 2026-08-19 문서 전용 계획 추가: 사용자 인터뷰를 통해 기존 5개 섹션 전체 empty 판정과 영어 문구를 유지하고, 한국어·일본어 `screen_home_following_empty` 첫 문장만 변경하는 요구사항을 확정했다. 기존 중앙 empty UI와 당겨서 새로고침 경로를 재사용하는 최소 범위로 Phase 15를 추가했으며, 코드·resource·test 구현은 진행하지 않았다. 문서 검증에서 요구사항·Decision·Goal 추적, `git diff --check`, production/test 경로 변경 0건을 확인했다. `./gradlew --no-daemon tasks --all`은 sandbox의 Gradle lock 접근 제한으로 첫 실행이 실패했으나 승인된 동일 명령 재실행은 `BUILD SUCCESSFUL`로 통과했다.
|
||||||
|
- 2026-08-18 Phase 14 구현 완료: 팔로잉 크리에이터 `전체`를 RecyclerView 마지막 item에서 추천 탭 라이브 섹션과 같은 우측 고정 TextView로 변경했다. adapter는 creator item만 표시하고, 기존 로그인 가드와 `FollowingCreatorActivity` 이동을 고정 TextView 클릭에 연결했다. TDD RED/GREEN, 팔로잉 회귀, resource merge, Kotlin compile, ktlint, `git diff --check`, debug APK assemble가 PASS했다. visual QA와 기기 조작은 사용자 요청으로 실행하지 않았고, 사용자가 `app/build/outputs/apk/debug/app-debug.apk`로 팔로잉 크리에이터 목록 스크롤 중 `전체` 고정과 클릭 이동을 수동 확인했다. Phase 14 리뷰에서 Critical/Important finding은 없었다.
|
||||||
|
- 2026-08-18 문서 전용 계획 추가: 팔로잉 크리에이터 `전체` 버튼을 RecyclerView 마지막 item에서 추천 탭 라이브 섹션과 같은 우측 고정 TextView 구조로 변경하는 요구사항을 PRD에 추가하고, 구현은 진행하지 않은 상태로 Phase 14 계획/TASK만 작성했다.
|
||||||
- 2026-07-31 이번 Phase 1~13 리뷰 재검증: 현재 working tree의 PRD·계획·production/test 코드와 Phase별 리뷰 보고서 13개를 대조했다. 새 확정 발견 사항은 없고 기존 확정 항목은 모두 각 회귀 수정 Task에서 수정 완료 상태다. Phase 9 보고서 범위를 Task 9.7까지 확장해 production/test `FeedAdapter|FeedImageViews` 호출자 0건과 현재 Feed view/model 직접 재사용을 기록했다. `./gradlew --no-daemon :app:testDebugUnitTest ... :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS(`BUILD SUCCESSFUL`, 16 suites·120 tests, failures/errors/skipped 0, 46 actionable tasks), `./gradlew --no-daemon tasks --all`, staged/unstaged `git diff --check` PASS. `adb devices`에서 기기 `2cec640c34017ece` 연결은 확인했지만 테스트 계정·API 데이터 기반 팔로잉 화면 조작과 Figma 대조는 수행하지 않아 `REV-P5-001` / Task 5.3 보류를 유지한다. 이번 리뷰에서는 production/test/resource 코드를 수정하지 않았다.
|
- 2026-07-31 이번 Phase 1~13 리뷰 재검증: 현재 working tree의 PRD·계획·production/test 코드와 Phase별 리뷰 보고서 13개를 대조했다. 새 확정 발견 사항은 없고 기존 확정 항목은 모두 각 회귀 수정 Task에서 수정 완료 상태다. Phase 9 보고서 범위를 Task 9.7까지 확장해 production/test `FeedAdapter|FeedImageViews` 호출자 0건과 현재 Feed view/model 직접 재사용을 기록했다. `./gradlew --no-daemon :app:testDebugUnitTest ... :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS(`BUILD SUCCESSFUL`, 16 suites·120 tests, failures/errors/skipped 0, 46 actionable tasks), `./gradlew --no-daemon tasks --all`, staged/unstaged `git diff --check` PASS. `adb devices`에서 기기 `2cec640c34017ece` 연결은 확인했지만 테스트 계정·API 데이터 기반 팔로잉 화면 조작과 Figma 대조는 수행하지 않아 `REV-P5-001` / Task 5.3 보류를 유지한다. 이번 리뷰에서는 production/test/resource 코드를 수정하지 않았다.
|
||||||
- 2026-07-31 Phase 1~13 최종 리뷰 검증: 현재 working tree의 PRD·계획·production/test 코드와 Phase별 리뷰 보고서 13개를 다시 대조했다. 신규 확정 항목은 `REV-P4-004`(공용 source test의 영문 문장형 테스트명 11개)와 `REV-P9-002`(API가 제공하지 않는 creator ID 자리에 content/post ID를 합성)이며, 코드 수정 전에 Phase 4 Task 4.9 / `P4-R4`, Phase 9 Task 9.6 / `P9-R2`로 전환했다. 이번 리뷰에서는 production/test/resource 코드를 수정하지 않았다. `./gradlew --no-daemon :app:testDebugUnitTest ... :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS(`BUILD SUCCESSFUL`, 20 suites·306 tests, failures/errors/skipped 0, 46 actionable tasks), `./gradlew --no-daemon tasks --all`, staged/unstaged `git diff --check`도 PASS했다. `adb devices`는 정상 실행됐으나 연결 기기가 없어 `REV-P5-001` / Task 5.3 실기기 수동 확인 보류를 유지한다.
|
- 2026-07-31 Phase 1~13 최종 리뷰 검증: 현재 working tree의 PRD·계획·production/test 코드와 Phase별 리뷰 보고서 13개를 다시 대조했다. 신규 확정 항목은 `REV-P4-004`(공용 source test의 영문 문장형 테스트명 11개)와 `REV-P9-002`(API가 제공하지 않는 creator ID 자리에 content/post ID를 합성)이며, 코드 수정 전에 Phase 4 Task 4.9 / `P4-R4`, Phase 9 Task 9.6 / `P9-R2`로 전환했다. 이번 리뷰에서는 production/test/resource 코드를 수정하지 않았다. `./gradlew --no-daemon :app:testDebugUnitTest ... :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS(`BUILD SUCCESSFUL`, 20 suites·306 tests, failures/errors/skipped 0, 46 actionable tasks), `./gradlew --no-daemon tasks --all`, staged/unstaged `git diff --check`도 PASS했다. `adb devices`는 정상 실행됐으나 연결 기기가 없어 `REV-P5-001` / Task 5.3 실기기 수동 확인 보류를 유지한다.
|
||||||
- 2026-07-31 리뷰 보완 Task 2.8, 6.8 완료 검증: `REV-P2-002`에 따라 미사용 `HomeFollowingNewsUiItem.Content.labelResId`와 전용 `FollowingNewsType.toLabelResId()`를 제거하고, 실제 표시 경로인 `FeedContentCategory.Photo` label 회귀를 `HomeFollowingNewsAdapterTest`에 유지했다. `REV-P6-004`에 따라 creator·최근 대화 source test의 UI 표현 속성 문자열 assertion만 제거하고 구조·필수 field·Direct badge·상대 시간 formatter·click 계약 검증은 유지했다. focused test 3개, 팔로잉 전체 회귀, `./gradlew --no-daemon :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다. `adb devices`에는 연결 기기가 없어 Task 5.3 실기기 수동 확인 보류를 유지한다.
|
- 2026-07-31 리뷰 보완 Task 2.8, 6.8 완료 검증: `REV-P2-002`에 따라 미사용 `HomeFollowingNewsUiItem.Content.labelResId`와 전용 `FollowingNewsType.toLabelResId()`를 제거하고, 실제 표시 경로인 `FeedContentCategory.Photo` label 회귀를 `HomeFollowingNewsAdapterTest`에 유지했다. `REV-P6-004`에 따라 creator·최근 대화 source test의 UI 표현 속성 문자열 assertion만 제거하고 구조·필수 field·Direct badge·상대 시간 formatter·click 계약 검증은 유지했다. focused test 3개, 팔로잉 전체 회귀, `./gradlew --no-daemon :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다. `adb devices`에는 연결 기기가 없어 Task 5.3 실기기 수동 확인 보류를 유지한다.
|
||||||
|
|||||||
@@ -215,8 +215,9 @@ Figma `24:5682` 기준 상단 title bar와 tab bar는 기존 홈 화면 구조
|
|||||||
#### Requirements
|
#### Requirements
|
||||||
- profile image와 creator nickname을 표시한다.
|
- profile image와 creator nickname을 표시한다.
|
||||||
- item 터치 시 해당 크리에이터 채널로 이동한다.
|
- item 터치 시 해당 크리에이터 채널로 이동한다.
|
||||||
- `followingCreators`가 1명 이상이면 가로 RecyclerView의 마지막 item으로 `전체` 버튼을 항상 표시한다.
|
- `followingCreators`가 1명 이상이면 추천 탭 라이브 섹션의 `전체`와 동일하게 크리에이터 가로 RecyclerView 우측 끝에 고정 `TextView`로 `전체` 버튼을 항상 표시한다.
|
||||||
- `전체` 버튼 item은 `width=wrap_content`, creator item 전체 높이와 동일한 높이, `paddingHorizontal=16dp`, `textColor=@color/soda_400`로 표시한다.
|
- `전체` 버튼은 RecyclerView의 마지막 item으로 포함하지 않는다. 팔로잉 크리에이터 목록만 좌측 가로 스크롤 영역에 남고, `전체` 버튼은 스크롤되지 않아야 한다.
|
||||||
|
- `전체` 버튼은 추천 탭 라이브 섹션의 `tv_home_live_all` 구조처럼 section root를 horizontal container로 두고, `RecyclerView`는 `layout_width="0dp"`, `layout_weight="1"`, `전체` TextView는 우측 고정 폭과 end margin을 가진다.
|
||||||
- `전체` 버튼 터치 시 기존 전체 팔로잉 크리에이터 목록 화면으로 이동한다.
|
- `전체` 버튼 터치 시 기존 전체 팔로잉 크리에이터 목록 화면으로 이동한다.
|
||||||
- 리스트가 비어 있으면 섹션을 숨긴다.
|
- 리스트가 비어 있으면 섹션을 숨긴다.
|
||||||
- 이미지 URL이 비어 있거나 로드 실패하면 기존 홈 creator profile placeholder 정책을 따른다.
|
- 이미지 URL이 비어 있거나 로드 실패하면 기존 홈 creator profile placeholder 정책을 따른다.
|
||||||
@@ -306,6 +307,26 @@ Figma `24:5682` 기준 상단 title bar와 tab bar는 기존 홈 화면 구조
|
|||||||
- 기존 `COMMUNITY_POST` 상세 이동은 유지하고, `CONTENT_RANKING`과 `PHOTO_CONTENT`의 터치 동작은 이번 범위에서 추가하지 않는다.
|
- 기존 `COMMUNITY_POST` 상세 이동은 유지하고, `CONTENT_RANKING`과 `PHOTO_CONTENT`의 터치 동작은 이번 범위에서 추가하지 않는다.
|
||||||
- API/DTO/mapper, 최근 소식 표시 UI와 다른 팔로잉 섹션의 동작은 변경하지 않는다.
|
- API/DTO/mapper, 최근 소식 표시 UI와 다른 팔로잉 섹션의 동작은 변경하지 않는다.
|
||||||
|
|
||||||
|
#### 2026-08-18 팔로잉 크리에이터 전체 버튼 고정 Requirements
|
||||||
|
- 팔로잉 탭 최상단 팔로잉 크리에이터 섹션의 `전체` 버튼은 추천 탭 라이브 섹션과 동일하게 RecyclerView 밖 우측 끝에 고정한다.
|
||||||
|
- 기존 RecyclerView 마지막 item 방식의 `전체` 노출은 더 이상 사용하지 않는다.
|
||||||
|
- 팔로잉 크리에이터 프로필 item만 수평 스크롤되며, `전체` 버튼은 사용자가 리스트를 좌우로 스크롤해도 화면 우측에 남아 있어야 한다.
|
||||||
|
- `followingCreators`가 비어 있으면 기존처럼 팔로잉 크리에이터 섹션 전체를 숨기며, 고정 `전체` 버튼도 보이지 않아야 한다.
|
||||||
|
- `전체` 버튼 클릭 동작은 기존 `FollowingCreatorActivity` 진입 동작을 유지한다.
|
||||||
|
- 추천 탭과 동일한 구조 기준은 `fragment_v2_main_home.xml`의 `ll_home_live_section`, `rv_home_lives`, `tv_home_live_all` 배치 방식이다.
|
||||||
|
- 이번 요구사항은 팔로잉 크리에이터 섹션의 배치 방식만 변경하며 API/DTO/mapper, 다른 팔로잉 섹션, 최근 소식 라우팅, empty/login/error 정책은 변경하지 않는다.
|
||||||
|
|
||||||
|
#### 2026-08-19 팔로잉 empty 안내 문구 변경 Requirements
|
||||||
|
- `isLoginRequired = false`이고 `followingCreators`, `onAirLives`, `recentChats`, `monthlySchedules`, `recentNews`가 모두 비어 있는 기존 `HomeFollowingUiState.Empty` 판정 조건을 유지한다.
|
||||||
|
- `followingCreators`만 비어 있는 상태를 별도 empty 조건으로 추가하지 않는다. 팔로우한 크리에이터가 없으면 나머지 네 섹션도 비어 있는 서버 동작을 전제로 한다.
|
||||||
|
- 기존 `screen_home_following_empty` string resource와 `tv_home_following_empty`를 재사용한다.
|
||||||
|
- 한국어 문구는 `아직 팔로우한 크리에이터가 없어요.\n관심 있는 크리에이터를 팔로우해 보세요.`로 표시한다.
|
||||||
|
- 일본어 문구는 `まだフォローしているクリエイターがいません。\n気になるクリエイターをフォローしてみましょう。`로 표시한다.
|
||||||
|
- 영어 문구 `No following updates yet.\nFollow creators you are interested in.`은 변경하지 않는다.
|
||||||
|
- 안내 문구는 기존처럼 팔로잉 탭 본문 가운데에 표시하고 텍스트를 가운데 정렬한다.
|
||||||
|
- 안내 문구가 표시된 상태에서도 기존 `SwipeRefreshLayout` 경로로 당겨서 새로고침할 수 있어야 하며, 새 `Empty` 응답을 받으면 refresh 상태를 종료하고 안내 문구를 계속 표시한다.
|
||||||
|
- API/DTO/mapper, `HomeFollowingUiState`, 팔로잉 section visibility, layout 구조, `Content`·`LoginRequired`·`Error` 처리는 변경하지 않는다.
|
||||||
|
|
||||||
#### 2026-06-30 후속 수정 Requirements
|
#### 2026-06-30 후속 수정 Requirements
|
||||||
- `CREATOR_RANKING` 최근 소식 본문은 순위 숫자만 단독 표시하지 않고, Figma `24:5717` 기준으로 크리에이터 이름과 순위를 포함한 문장형 메시지를 표시한다.
|
- `CREATOR_RANKING` 최근 소식 본문은 순위 숫자만 단독 표시하지 않고, Figma `24:5717` 기준으로 크리에이터 이름과 순위를 포함한 문장형 메시지를 표시한다.
|
||||||
- ranking 문장 안의 순위 텍스트는 기존 `FeedRankTextStyler` highlight range로 강조한다.
|
- ranking 문장 안의 순위 텍스트는 기존 `FeedRankTextStyler` highlight range로 강조한다.
|
||||||
@@ -379,3 +400,11 @@ PRD 작성 전 확인한 `v2` 패키지 하위 재사용 후보는 다음과 같
|
|||||||
- 로그인 유도 화면의 정확한 디자인, 문구, CTA, 로그인 완료 후 복귀 정책은 아직 정해지지 않았다.
|
- 로그인 유도 화면의 정확한 디자인, 문구, CTA, 로그인 완료 후 복귀 정책은 아직 정해지지 않았다.
|
||||||
- `PHOTO_CONTENT`의 `화보` label은 임시 정책이며 이후 기획 변경 가능성이 있다.
|
- `PHOTO_CONTENT`의 `화보` label은 임시 정책이며 이후 기획 변경 가능성이 있다.
|
||||||
- 각 섹션의 “더보기” chevron 실제 이동 목적지는 아직 만들지 않는다. 이번 범위에서는 터치 액션 연결까지만 지정한다.
|
- 각 섹션의 “더보기” chevron 실제 이동 목적지는 아직 만들지 않는다. 이번 범위에서는 터치 액션 연결까지만 지정한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계획 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-08-19 | `DEC-20260819-001` | 확정 | 기존 5개 섹션 전체 empty 판정을 유지하고 한국어·일본어 안내 문구의 첫 문장만 변경하며 영어 문구는 유지한다. | 사용자 인터뷰에서 서버가 팔로우한 크리에이터가 없을 때 5개 섹션을 모두 빈 배열로 반환하는 동작을 전제로 확정했다. | `2026-08-19 팔로잉 empty 안내 문구 변경 Requirements`, `P15-T1`, `P15-GATE` |
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Phase 14 팔로잉 크리에이터 전체 버튼 우측 고정 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 14 / Task 14.1~14.4 |
|
||||||
|
| 기준 commit 또는 working tree | `388304dbaae06eeb7204fe8d0c697d58ce721f00` 기준 working tree |
|
||||||
|
| 리뷰 일자 | 2026-08-18 |
|
||||||
|
| 리뷰어 | Oracle, Sisyphus |
|
||||||
|
| 기준 문서 | `docs/20260625_메인_홈_팔로잉_탭/prd.md`, `docs/20260625_메인_홈_팔로잉_탭/plan-task.md` Phase 14 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
### 목적
|
||||||
|
|
||||||
|
- 팔로잉 크리에이터 `전체` 버튼이 RecyclerView item에서 우측 고정 sibling TextView로 변경됐는지 확인한다.
|
||||||
|
- 빈 목록 visibility와 기존 로그인 가드·전체 목록 이동에 회귀가 없는지 확인한다.
|
||||||
|
- Phase 14 체크박스와 실제 코드·테스트·검증 기록이 일치하는지 확인한다.
|
||||||
|
|
||||||
|
### 포함 범위
|
||||||
|
|
||||||
|
- 코드: `HomeMainFragment.kt`, `HomeFollowingCreatorAdapter.kt`, `fragment_v2_main_home.xml`, 삭제된 `item_home_following_creator_all.xml`.
|
||||||
|
- 테스트: `HomeFollowingFragmentSourceTest.kt`와 `v2.main.home.*Following*` local unit/source test.
|
||||||
|
- 문서: PRD의 2026-08-18 요구사항과 `plan-task.md` Phase 14.
|
||||||
|
- 수동 검증: 사용자 요청에 따라 visual QA와 기기 조작을 제외하고 debug APK 확인 절차만 인계한다.
|
||||||
|
|
||||||
|
### 제외 범위
|
||||||
|
|
||||||
|
- 다른 홈 섹션 UI, API/DTO/mapper, androidTest, 스크린샷·시각 비교.
|
||||||
|
|
||||||
|
## 3. 검토한 근거
|
||||||
|
|
||||||
|
- `fragment_v2_main_home.xml:274`: RecyclerView와 `tv_home_following_creator_all` sibling 구조.
|
||||||
|
- `HomeFollowingCreatorAdapter.kt:12`: creator-only adapter와 `items.size` item count.
|
||||||
|
- `HomeMainFragment.kt:265`: 고정 TextView 클릭 연결.
|
||||||
|
- `HomeMainFragment.kt:547`: 빈 creator 목록의 부모 section visibility 처리.
|
||||||
|
- `HomeMainFragment.kt:724`: 기존 로그인 가드와 `FollowingCreatorActivity` 이동.
|
||||||
|
- `HomeFollowingFragmentSourceTest.kt:318`: 구조·adapter·route 회귀 테스트.
|
||||||
|
|
||||||
|
## 4. 실행한 검증
|
||||||
|
|
||||||
|
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|
||||||
|
|---|---|---|
|
||||||
|
| focused test RED | 성공 | 17개 중 신규 시나리오 1개가 고정 TextView 부재로 실패 |
|
||||||
|
| focused test GREEN `--rerun-tasks` | 성공 | `BUILD SUCCESSFUL`, 39개 task 실제 실행 |
|
||||||
|
| 팔로잉 회귀 + resource + compile + ktlint | 성공 | 첫 실행의 신규 빈 줄 위반을 제거한 뒤 `BUILD SUCCESSFUL` |
|
||||||
|
| `git diff --check` | 성공 | 출력 없음 |
|
||||||
|
| `./gradlew --no-daemon :app:assembleDebug` | 성공 | `app/build/outputs/apk/debug/app-debug.apk` 생성 |
|
||||||
|
| visual QA·기기 조작 | 제외 | 사용자 명시 요청에 따라 미실행 |
|
||||||
|
| 사용자 수동 확인 | 성공 | debug APK에서 팔로잉 크리에이터 목록 스크롤 중 `전체` 우측 고정과 전체 팔로잉 목록 이동 확인 |
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음. Oracle 리뷰에서 Critical/Important finding 없이 승인했다.
|
||||||
|
|
||||||
|
## 6. plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 7. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | Phase 14 코드·테스트·문서 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | blocking finding 없음 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 보류 항목 기록 | 해당 없음 | visual QA는 사용자 요청에 따른 제외, 사용자 수동 확인 완료 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | Phase 14 검증 기록과 Verification Log 갱신 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 없음
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Phase 15 팔로잉 empty 안내 문구 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 15 / Task 15.1~15.2 |
|
||||||
|
| 기준 commit 또는 working tree | 현재 working tree |
|
||||||
|
| 리뷰 일자 | 2026-08-19 |
|
||||||
|
| 리뷰어 | Sisyphus, Oracle |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md`의 Phase 15 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 한국어·일본어 empty 문구가 확정 요구사항과 일치하는지 확인한다.
|
||||||
|
- 영어 문구와 기존 empty 판정·중앙 표시·refresh 경로가 유지되는지 확인한다.
|
||||||
|
- `HomeFollowingFragmentSourceTest.kt`, `values/strings.xml`, `values-ja/strings.xml` 변경만 검토한다.
|
||||||
|
- androidTest, 기기·에뮬레이터 조작, 스크린샷·시각 QA는 Phase 15 계획에 따라 제외한다.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
- PRD `2026-08-19 팔로잉 empty 안내 문구 변경 Requirements`
|
||||||
|
- `DEC-20260819-001`, `P15-T1`, `P15-GATE`
|
||||||
|
- 범위 밖 Kotlin production, layout, 영어 resource 변경 여부
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 한국어: `app/src/main/res/values/strings.xml:319`
|
||||||
|
- 일본어: `app/src/main/res/values-ja/strings.xml:320`
|
||||||
|
- 영어 유지: `app/src/main/res/values-en/strings.xml:320`
|
||||||
|
- 다국어 및 상태 분기 테스트: `HomeFollowingFragmentSourceTest.kt:107`
|
||||||
|
- 전체 empty 판정: `HomeFollowingMapperTest`
|
||||||
|
- refresh routing: `HomeMainFragmentSourceTest`
|
||||||
|
|
||||||
|
| 명령 | 결과 | 핵심 증거 |
|
||||||
|
|---|---|---|
|
||||||
|
| Phase 15 focused test RED | 실패 | 기존 한국어 resource 불일치 assertion |
|
||||||
|
| Phase 15 focused test GREEN | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| mapper·Fragment·refresh focused tests | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| `:app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` | 성공 | 기존 `disabled_rules` deprecation warning만 출력 |
|
||||||
|
| `git diff --check` | 성공 | whitespace 오류 없음 |
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
명세 적합성 및 코드 품질 검토에서 blocking finding이 확인되지 않았다. `contains` 기반 resource 검증은 XML 재포맷에 민감하지만 기존 테스트 방식이며 Phase 15에서 새로 발생한 위험은 아니다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | Phase 15 변경 파일과 요구사항 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 확정 발견 사항 없음 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 전환할 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | focused test, 회귀, build, lint, diff PASS |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 없음
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
추가 수정 항목 없음.
|
||||||
@@ -0,0 +1,577 @@
|
|||||||
|
# 크리에이터 채널 Empty State 다국어 문구 정리 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 크리에이터 채널 홈/팬레터/후원 empty state의 한국어/영어/일본어 문구와 내 채널 action 버튼 노출 정책을 일관되게 적용한다.
|
||||||
|
|
||||||
|
**Architecture:** API와 화면 구조는 유지하고 Android string resource, 기존 v2 크리에이터 채널 XML/ViewBinding 바인딩, Fragment/adapter 표시 분기만 최소 변경한다. 홈 후원 empty와 후원 탭 empty는 서로 다른 문구를 요구하므로 홈 전용 후원 empty string을 추가하고 상세 탭 기존 string은 유지한다.
|
||||||
|
|
||||||
|
**Tech Stack:** Android XML string resources, XML Views, ViewBinding, Kotlin Fragment/RecyclerView Adapter, Gradle `:app:mergeDebugResources`, `app/src/test` local unit/source/layout tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| 문서 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 상태 | 후속 색상 수정 완료 |
|
||||||
|
| 작성일 | `2026-08-14` |
|
||||||
|
| 요구사항 기준 | `docs/20260814_크리에이터_채널_empty_state_다국어_문구_정리/prd.md` |
|
||||||
|
| API 기준 | 없음 |
|
||||||
|
| 현재 Phase | Phase 1 후속 색상 수정 완료 |
|
||||||
|
| 현재 활성 Goal | 없음 |
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
크리에이터 채널에서 데이터가 없는 홈 후원, 홈 팬레터, 팬레터 탭, 후원 탭 empty state가 locale별 확정 문구를 표시하고 내 채널에서는 작성/후원 action 버튼을 숨긴다.
|
||||||
|
|
||||||
|
## 현재 상태
|
||||||
|
|
||||||
|
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|
||||||
|
|---:|---|---:|---|---|
|
||||||
|
| 1 | 완료 | `6/6` | 없음 | 없음 |
|
||||||
|
|
||||||
|
## 범위
|
||||||
|
|
||||||
|
### 포함
|
||||||
|
|
||||||
|
- 한국어/영어/일본어 string resource 값 정리
|
||||||
|
- 홈 전용 후원 empty title resource 추가
|
||||||
|
- 홈 후원 empty, 홈 팬레터 empty, 팬레터 탭 empty, 후원 탭 empty의 내 채널 action 버튼 숨김 정책
|
||||||
|
- 기존 홈 탭 팬레터 empty UI의 문구와 내 채널 action 버튼 정책 보강
|
||||||
|
- `app/src/test` 기반 resource/source/layout 회귀 테스트
|
||||||
|
|
||||||
|
### 제외
|
||||||
|
|
||||||
|
- 후원 dialog, 팬레터 작성 화면 내부 UX 변경
|
||||||
|
- API, DTO, repository, pagination, ranking 변경
|
||||||
|
- 크리에이터 채널 header/tab-bar/sticky scroll 구조 변경
|
||||||
|
- 신규 dependency 추가
|
||||||
|
- `app/src/androidTest`, 기기·에뮬레이터 UI 조작, 스크린샷·시각 QA
|
||||||
|
|
||||||
|
## 기술적 제약
|
||||||
|
|
||||||
|
- 기존 resource 파일은 `app/src/main/res/values/strings.xml`, `app/src/main/res/values-en/strings.xml`, `app/src/main/res/values-ja/strings.xml`을 수정한다.
|
||||||
|
- 홈 후원 empty는 `creator_channel_home_donation_empty_title` 신규 key를 사용한다.
|
||||||
|
- 후원 탭 empty는 기존 `creator_channel_donation_empty_title` key를 유지한다.
|
||||||
|
- 후원 action은 기존 `creator_channel_donation_action` key를 재사용한다.
|
||||||
|
- 팬레터 action은 기존 `creator_channel_fantalk_support_action` key를 유지하고 값만 확정 문구로 맞춘다.
|
||||||
|
- 레거시 후원 구현 파일은 직접 수정하지 않는다.
|
||||||
|
- 구현 Task는 RED -> GREEN -> REFACTOR 순서를 따른다.
|
||||||
|
|
||||||
|
## Phase 1
|
||||||
|
|
||||||
|
**Phase 결과:** 모든 대상 empty state가 확정 문구와 내 채널 버튼 숨김 정책을 만족한다.
|
||||||
|
|
||||||
|
**선행조건:** PRD `CREATOR-EMPTY-I18N-001~005`, `CREATOR-EMPTY-UI-001~005` 확정.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P1-T1`~`P1-T5`와 `P1-GATE` 완료, 검증 기록 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 1.1 Resource 문구 정리
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T1`:** 한국어/영어/일본어 string resource를 PRD 문구 표와 일치시킨다.
|
||||||
|
|
||||||
|
- **시작 조건:** PRD `DEC-002`, `DEC-003` 확정.
|
||||||
|
- **완료 증거:** 세 locale의 대상 key 값이 PRD 표와 일치하고 XML resource merge가 통과한다.
|
||||||
|
- **범위 밖:** Kotlin/UI 표시 분기 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/res/values/strings.xml`
|
||||||
|
- Modify: `app/src/main/res/values-en/strings.xml`
|
||||||
|
- Modify: `app/src/main/res/values-ja/strings.xml`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragmentLayoutTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationFragmentLayoutTest.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: 기존 `R.string.creator_channel_fantalk_empty_title`, `R.string.creator_channel_fantalk_empty_message`, `R.string.creator_channel_fantalk_support_action`, `R.string.creator_channel_donation_empty_title`, `R.string.creator_channel_donation_action`
|
||||||
|
- Produces: 신규 `R.string.creator_channel_home_donation_empty_title`
|
||||||
|
|
||||||
|
- [x] **RED:** resource 값 검증 테스트에 아래 기대값을 추가하거나 갱신한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val expectedKo = mapOf(
|
||||||
|
"creator_channel_home_donation_empty_title" to "처음으로 크리에이터를\\n후원해 보세요!",
|
||||||
|
"creator_channel_fantalk_empty_title" to "당신의 첫 응원을\\n기다리고 있어요!",
|
||||||
|
"creator_channel_fantalk_empty_message" to "아직 응원이 없습니다.\\n처음으로 크리에이터를 응원해 보세요!",
|
||||||
|
"creator_channel_fantalk_support_action" to "응원남기기",
|
||||||
|
"creator_channel_donation_empty_title" to "아직 후원이 없습니다.\\n처음으로 크리에이터를 후원해 보세요!",
|
||||||
|
"creator_channel_donation_action" to "후원하기"
|
||||||
|
)
|
||||||
|
val expectedEn = mapOf(
|
||||||
|
"creator_channel_home_donation_empty_title" to "Be the first to\\nsupport this creator!",
|
||||||
|
"creator_channel_fantalk_empty_title" to "We're waiting for your\\nfirst fan letter!",
|
||||||
|
"creator_channel_fantalk_empty_message" to "No fan letters yet.\\nSend the first fan letter!",
|
||||||
|
"creator_channel_fantalk_support_action" to "Send fan letter",
|
||||||
|
"creator_channel_donation_empty_title" to "No support yet.\\nBe the first to support this creator!",
|
||||||
|
"creator_channel_donation_action" to "Support"
|
||||||
|
)
|
||||||
|
val expectedJa = mapOf(
|
||||||
|
"creator_channel_home_donation_empty_title" to "最初のサポートをしてみましょう!",
|
||||||
|
"creator_channel_fantalk_empty_title" to "あなたの最初のファンレターを待っています!",
|
||||||
|
"creator_channel_fantalk_empty_message" to "まだファンレターがありません。\\n最初のファンレターを送ってみましょう!",
|
||||||
|
"creator_channel_fantalk_support_action" to "ファンレターを送る",
|
||||||
|
"creator_channel_donation_empty_title" to "まだサポートがありません。\\n最初のサポートをしてみましょう!",
|
||||||
|
"creator_channel_donation_action" to "サポートする"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **RED 확인:** focused resource/source test를 실행해 신규 홈 후원 key 누락 또는 기존 값 불일치로 실패함을 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `creator_channel_home_donation_empty_title` 누락 또는 기존 팬레터/후원 문구 불일치 assertion 실패.
|
||||||
|
|
||||||
|
- [x] **GREEN:** 세 locale resource에 아래 값을 반영한다.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<string name="creator_channel_home_donation_empty_title">처음으로 크리에이터를\n후원해 보세요!</string>
|
||||||
|
<string name="creator_channel_fantalk_empty_title">당신의 첫 응원을\n기다리고 있어요!</string>
|
||||||
|
<string name="creator_channel_fantalk_empty_message">아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!</string>
|
||||||
|
<string name="creator_channel_fantalk_support_action">응원남기기</string>
|
||||||
|
<string name="creator_channel_donation_empty_title">아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!</string>
|
||||||
|
<string name="creator_channel_donation_action">후원하기</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<string name="creator_channel_home_donation_empty_title">Be the first to\nsupport this creator!</string>
|
||||||
|
<string name="creator_channel_fantalk_empty_title">We\'re waiting for your\nfirst fan letter!</string>
|
||||||
|
<string name="creator_channel_fantalk_empty_message">No fan letters yet.\nSend the first fan letter!</string>
|
||||||
|
<string name="creator_channel_fantalk_support_action">Send fan letter</string>
|
||||||
|
<string name="creator_channel_donation_empty_title">No support yet.\nBe the first to support this creator!</string>
|
||||||
|
<string name="creator_channel_donation_action">Support</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<string name="creator_channel_home_donation_empty_title">最初のサポートをしてみましょう!</string>
|
||||||
|
<string name="creator_channel_fantalk_empty_title">あなたの最初のファンレターを待っています!</string>
|
||||||
|
<string name="creator_channel_fantalk_empty_message">まだファンレターがありません。\n最初のファンレターを送ってみましょう!</string>
|
||||||
|
<string name="creator_channel_fantalk_support_action">ファンレターを送る</string>
|
||||||
|
<string name="creator_channel_donation_empty_title">まだサポートがありません。\n最初のサポートをしてみましょう!</string>
|
||||||
|
<string name="creator_channel_donation_action">サポートする</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN 확인:** focused resource/source test와 resource merge를 실행한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
|
||||||
|
./gradlew :app:mergeDebugResources
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 두 명령 모두 `BUILD SUCCESSFUL`.
|
||||||
|
|
||||||
|
- [x] **REFACTOR:** resource key 순서와 테스트 중복만 정리하고 같은 명령을 재실행해 결과를 Progress에 기록한다.
|
||||||
|
|
||||||
|
#### Task 1.2 홈 탭 empty UI와 내 채널 버튼 정책
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T2`:** 홈 후원/팬레터 empty UI가 화면별 문구를 사용하고 내 채널에서는 action 버튼을 숨긴다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T1` 완료.
|
||||||
|
- **완료 증거:** 홈 후원 empty가 홈 전용 string을 사용하고, 기존 홈 팬레터 empty 표시 분기는 유지되며, `isOwner == true`에서 두 empty action 버튼이 숨겨진다.
|
||||||
|
- **범위 밖:** 상세 팬레터/후원 탭 Fragment 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/res/layout/item_creator_channel_home_donation.xml`
|
||||||
|
- Modify: `app/src/main/res/layout/item_creator_channel_home_fantalk.xml`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeMapperTest.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `CreatorChannelHomeSection.Donations.isOwner`, `CreatorChannelHomeSection.FanTalk`, 신규 `R.string.creator_channel_home_donation_empty_title`
|
||||||
|
- Produces: 홈 empty button visibility와 click listener owner 분기
|
||||||
|
|
||||||
|
- [x] **RED:** 홈 후원 layout이 홈 전용 string을 참조하는지 테스트한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val donationLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_donation.xml").readText()
|
||||||
|
assertTrue(donationLayout.contains("@string/creator_channel_home_donation_empty_title"))
|
||||||
|
assertFalse(donationLayout.contains("@string/creator_channel_donation_empty_title"))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **RED:** 홈 팬레터 empty 버튼이 owner일 때 숨겨지고 click listener가 제거되는 source test를 추가한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val adapterSource = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
assertTrue(adapterSource.contains("private val fanTalkSupportButton"))
|
||||||
|
assertTrue(adapterSource.contains("fanTalkSupportButton?.isVisible ="))
|
||||||
|
assertTrue(adapterSource.contains("!item.isOwner"))
|
||||||
|
assertTrue(adapterSource.contains("fanTalkSupportButton?.setOnClickListener"))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **RED 확인:** focused tests를 실행해 아직 홈 전용 string 참조와 팬레터 owner 분기가 없어 실패함을 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 홈 후원 layout string 또는 홈 팬레터 owner 분기 assertion 실패.
|
||||||
|
|
||||||
|
- [x] **GREEN:** `item_creator_channel_home_donation.xml`의 empty title 참조를 홈 전용 key로 변경한다.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
android:text="@string/creator_channel_home_donation_empty_title"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** `CreatorChannelHomeSectionAdapter.bindDonations()`에서 owner용 별도 empty title 변경을 제거하고 홈 전용 문구를 유지한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
donationEmptyTitle?.setText(R.string.creator_channel_home_donation_empty_title)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** 홈 팬레터 empty action button을 ViewHolder에 연결한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private val fanTalkSupportButton: View? = view.findViewById(R.id.layout_fantalk_support_button)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** 홈 팬레터 empty 바인딩에서 기존 empty 표시 분기를 유지하고 owner 버튼 숨김을 추가한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val fanTalk = item.fanTalk.latestFanTalk
|
||||||
|
fanTalkTotalRow?.isVisible = fanTalk != null && item.fanTalk.totalCount > 0
|
||||||
|
fanTalkLatestRow?.isVisible = fanTalk != null && item.fanTalk.totalCount > 0
|
||||||
|
fanTalkEmpty?.isVisible = fanTalk == null || item.fanTalk.totalCount <= 0
|
||||||
|
val showSupportButton = (fanTalk == null || item.fanTalk.totalCount <= 0) && !item.isOwner
|
||||||
|
fanTalkSupportButton?.isVisible = showSupportButton
|
||||||
|
fantalkSupportButton?.setOnClickListener(
|
||||||
|
if (showSupportButton) View.OnClickListener { onSectionChevronClick(CreatorChannelTab.FanTalk) } else null
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN 확인:** 홈 관련 focused tests를 실행한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeMapperTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `BUILD SUCCESSFUL`.
|
||||||
|
|
||||||
|
- [x] **REFACTOR:** 이번 Task에서 만든 중복만 정리하고 focused tests와 `:app:mergeDebugResources` 결과를 Progress에 기록한다.
|
||||||
|
|
||||||
|
#### Task 1.3 팬레터/후원 탭 empty action owner 정책
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T3`:** 팬레터 탭과 후원 탭 empty 상태에서 내 채널 action 버튼을 숨긴다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T1` 완료.
|
||||||
|
- **완료 증거:** `isOwner == true`일 때 팬레터 탭 `응원남기기`, 후원 탭 `후원하기` empty action 버튼이 숨겨지고 click listener가 제거된다.
|
||||||
|
- **범위 밖:** 홈 탭 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragment.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/ui/CreatorChannelDonationAdapter.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkViewModelTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragmentLayoutTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationFragmentLayoutTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationViewModelTest.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `host.isCreatorChannelOwner()`, `CreatorChannelDonationUiState.Empty.isOwner`
|
||||||
|
- Produces: 팬레터 empty state owner 정보 또는 Fragment owner 분기, 후원 empty adapter owner visibility 분기
|
||||||
|
|
||||||
|
- [x] **RED:** 팬레터 empty state가 owner 여부를 전달하거나 Fragment가 host owner 값을 사용해 empty write button을 숨기는 테스트를 추가한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val fragmentSource = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragment.kt"
|
||||||
|
).readText()
|
||||||
|
assertTrue(fragmentSource.contains("layoutCreatorChannelFantalkEmptyWriteButton.isVisible = !host.isCreatorChannelOwner()"))
|
||||||
|
assertTrue(fragmentSource.contains("layoutCreatorChannelFantalkEmptyWriteButton.setOnClickListener"))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **RED:** 후원 empty adapter가 owner일 때 `btn_creator_channel_donation_empty_write`를 숨기는 테스트를 추가한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val adapterSource = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/ui/CreatorChannelDonationAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
assertTrue(adapterSource.contains("if (!isOwner) View.OnClickListener { onEmptyDonationClick() } else null"))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **RED 확인:** focused layout/source tests를 실행해 owner empty action 분기 누락으로 실패함을 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: owner empty action visibility 관련 assertion 실패.
|
||||||
|
|
||||||
|
- [x] **GREEN:** `CreatorChannelFanTalkFragment.bindEmpty()`에서 내 채널 action button을 숨기고 listener를 제거한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val showWriteButton = !host.isCreatorChannelOwner()
|
||||||
|
layoutCreatorChannelFantalkEmptyWriteButton.isVisible = showWriteButton
|
||||||
|
layoutCreatorChannelFantalkEmptyWriteButton.setOnClickListener(
|
||||||
|
if (showWriteButton) View.OnClickListener { host.onCreatorChannelFanTalkWriteClicked() } else null
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** `setupClickListeners()`에서 empty write button의 고정 click listener를 제거하고 일반 floating write button listener만 유지한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
btnCreatorChannelFantalkWrite.setOnClickListener { host.onCreatorChannelFanTalkWriteClicked() }
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** `CreatorChannelDonationAdapter` empty ViewHolder에서 `isOwner`에 따라 버튼 visibility와 click listener를 설정한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
binding.btnCreatorChannelDonationEmptyWrite.isVisible = !isOwner
|
||||||
|
binding.btnCreatorChannelDonationEmptyWrite.setOnClickListener(
|
||||||
|
if (!isOwner) View.OnClickListener { onEmptyDonationClick() } else null
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN 확인:** 팬레터/후원 탭 focused tests를 실행한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkViewModelTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationViewModelTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `BUILD SUCCESSFUL`.
|
||||||
|
|
||||||
|
- [x] **REFACTOR:** owner 버튼 분기 중복만 정리하고 focused tests 결과를 Progress에 기록한다.
|
||||||
|
|
||||||
|
#### Task 1.4 홈 팬레터 empty 버튼 영역 회귀 수정
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T4`:** 홈 팬레터 empty에서 내 채널이라 action 버튼이 숨겨질 때 버튼 영역도 레이아웃 높이에 남지 않게 한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T2` 완료.
|
||||||
|
- **완료 증거:** 홈 팬레터 empty root가 fixed height를 쓰지 않고, action button이 숨겨지면 해당 button view/margin이 레이아웃 높이에 기여하지 않는다.
|
||||||
|
- **범위 밖:** 상세 팬레터/후원 탭, API, adapter 데이터 정책 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/res/layout/item_creator_channel_home_fantalk.xml`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `CreatorChannelHomeSectionAdapter.bindFanTalk()`의 기존 `fanTalkSupportButton?.isVisible = showSupportButton`
|
||||||
|
- Produces: button `GONE` 시 button 영역 제거가 가능한 홈 팬레터 empty layout 구조
|
||||||
|
|
||||||
|
- [x] **RED:** 홈 팬레터 empty layout이 fixed `169dp` 높이와 button bottom margin을 쓰지 않도록 source/layout test를 추가한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
assertTrue(fanTalkLayout.contains("android:layout_height=\"wrap_content\""))
|
||||||
|
assertTrue(fanTalkLayout.contains("android:layout_marginTop=\"@dimen/spacing_14\""))
|
||||||
|
assertFalse(fanTalkLayout.contains("android:layout_height=\"169dp\""))
|
||||||
|
assertFalse(fanTalkLayout.contains("android:layout_marginBottom=\"@dimen/spacing_6\""))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **RED 확인:** focused source/layout test를 실행해 기존 fixed height로 실패함을 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** `layout_fantalk_empty`를 `LinearLayout` `wrap_content` 구조로 변경하고, support button은 `marginTop`만 갖는 sibling으로 둔다.
|
||||||
|
|
||||||
|
- [x] **GREEN 확인:** focused source/layout test와 resource merge를 실행한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
|
||||||
|
./gradlew :app:mergeDebugResources
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **REFACTOR:** 불필요한 변경이 없음을 확인하고 공백 검사를 실행해 Progress에 기록한다.
|
||||||
|
|
||||||
|
#### Task 1.5 Empty action 버튼 색상 정리
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T5`:** 홈/팬레터/후원 empty action 버튼 색상을 화면별 확정값과 일치시킨다.
|
||||||
|
|
||||||
|
- **시작 조건:** 사용자 색상 요구사항 확정.
|
||||||
|
- **완료 증거:** 홈 후원/홈 팬레터 버튼은 white 배경과 black 글자색, 팬레터 탭 버튼은 gray 800 배경과 white 글자색, 후원 탭 버튼은 soda 400 배경과 white 글자색을 source/layout test로 검증한다.
|
||||||
|
- **범위 밖:** 문구, 노출 정책, click listener, API, 신규 drawable/dependency 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/res/layout/item_creator_channel_home_donation.xml`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragmentLayoutTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationFragmentLayoutTest.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: 기존 `bg_round_corner_999_white`, `bg_creator_channel_fantalk_empty_button`, `bg_creator_channel_donation_empty_button`, `@color/black`, `@color/white`
|
||||||
|
- Produces: 화면별 empty action button 색상 계약
|
||||||
|
|
||||||
|
- [x] **RED:** 홈/팬레터/후원 empty action button 색상 source/layout test를 추가한다.
|
||||||
|
|
||||||
|
- [x] **RED 확인:** focused tests를 실행해 홈 후원 empty button 배경 mismatch로 실패함을 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** 홈 후원 empty `layout_donation_empty_button` 배경을 기존 white capsule drawable로 변경한다.
|
||||||
|
|
||||||
|
- [x] **GREEN 확인:** focused tests와 `:app:mergeDebugResources`를 실행한다.
|
||||||
|
|
||||||
|
- [x] **REFACTOR:** 신규 drawable 없이 기존 resource 재사용만 남았는지 확인하고 공백 검사를 실행한다.
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Phase 1 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P1-GATE`:** resource, 홈 empty UI, 탭 empty owner 정책을 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T1`, `P1-T2`, `P1-T3` 완료.
|
||||||
|
- **완료 증거:** 아래 명령 통과와 Progress 기록.
|
||||||
|
- **범위 밖:** 실패와 무관한 리팩터링, UI/계측 테스트 추가, 기기·에뮬레이터 조작.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeMapperTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkViewModelTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationViewModelTest"
|
||||||
|
./gradlew :app:mergeDebugResources
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** 모든 명령이 성공한다. 기존 전체 `:app:testDebugUnitTest`에 이번 범위와 무관한 실패가 있으면 focused 통과 결과와 전체 실패 항목을 분리해 Progress에 기록한다.
|
||||||
|
|
||||||
|
## 실행 순서와 의존성
|
||||||
|
|
||||||
|
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|
||||||
|
|---:|---|---|---|---|
|
||||||
|
| 1 | `P1-T1` | PRD 확정 | 아니요 | 문구 표와 기존 resource key 대조 |
|
||||||
|
| 2 | `P1-T2` | `P1-T1` | 예, `P1-T3`와 파일 비중복 시 가능 | 홈 section adapter/layout 범위 재확인 |
|
||||||
|
| 3 | `P1-T3` | `P1-T1` | 예, `P1-T2`와 파일 비중복 시 가능 | Fragment/adapter owner 전달 경로 재확인 |
|
||||||
|
| 4 | `P1-GATE` | `P1-T2`, `P1-T3` | 아니요 | 실패 소유 Task에 회귀 수정 Task 추가 |
|
||||||
|
| 5 | `P1-T4` | `P1-T2` | 아니요 | 홈 팬레터 empty layout 구조 재확인 |
|
||||||
|
| 6 | `P1-T5` | 사용자 색상 요구사항 확정 | 아니요 | 기존 drawable/color resource 재확인 |
|
||||||
|
|
||||||
|
```text
|
||||||
|
P1-T1 -> P1-T2 -> P1-GATE
|
||||||
|
-> P1-T3 -> P1-GATE
|
||||||
|
P1-T2 -> P1-T4
|
||||||
|
색상 요구사항 -> P1-T5
|
||||||
|
```
|
||||||
|
|
||||||
|
## 변경 금지 항목
|
||||||
|
|
||||||
|
- API, DTO, repository, pagination, ranking 동작을 변경하지 않는다.
|
||||||
|
- 레거시 후원 파일을 직접 수정하지 않는다.
|
||||||
|
- 기존 resource key를 불필요하게 rename하지 않는다.
|
||||||
|
- 신규 dependency를 추가하지 않는다.
|
||||||
|
- `app/src/androidTest`, 기기·에뮬레이터 UI 조작, 스크린샷·시각 QA를 추가하지 않는다.
|
||||||
|
- 테스트를 삭제·skip·완화하지 않는다.
|
||||||
|
|
||||||
|
## Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-08-14 | `DEC-001` | 확정 | 내 채널에서는 대상 empty state action 버튼을 모두 숨긴다. | 사용자 A안 선택 | `P1-T2`, `P1-T3`, `prd.md` |
|
||||||
|
| 2026-08-14 | `DEC-002` | 확정 | 영어 resource도 이번 범위에 포함하고 제공되지 않은 영문은 번역 초안을 사용한다. | 사용자 직접 지시 | `P1-T1`, `prd.md` |
|
||||||
|
| 2026-08-14 | `DEC-003` | 확정 | 홈 후원 empty 전용 string key를 추가한다. | 홈/후원 탭 문구가 서로 다름 | `P1-T1`, `P1-T2`, `prd.md` |
|
||||||
|
| 2026-08-14 | `DEC-004` | 확정 | empty action 버튼 색상은 홈 탭 white/black, 팬레터 탭 gray 800/white, 후원 탭 soda 400/white로 구분한다. | 사용자 직접 지시 | `P1-T5`, `prd.md` |
|
||||||
|
|
||||||
|
## 발견된 문제
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `ISSUE-001` | Medium | 확정 | 현재 홈 후원 empty와 후원 탭 empty가 `creator_channel_donation_empty_title` 단일 key를 공유해 서로 다른 문구 요구를 동시에 만족할 수 없다. | `P1-T1`, `P1-T2` | 홈 전용 `creator_channel_home_donation_empty_title` 추가 |
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
기존 기록을 삭제하거나 덮어쓰지 않고 실제 실행 결과를 차수별로 누적한다.
|
||||||
|
|
||||||
|
### 문서 작성 1차 실행 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: PRD와 구현 계획/TASK 문서를 생성했다.
|
||||||
|
- 왜: 저장소 규칙상 구현 전에 요구사항과 실행 계획을 확정해야 하기 때문이다.
|
||||||
|
- 어떻게:
|
||||||
|
- `rg --files docs | rg 'sample|creator|channel|fanletter|support|후원|응원|팬레터|크리에이터'` — 기존 관련 문서와 샘플 문서를 확인했다.
|
||||||
|
- `rg -n "후원하기|응원남기기|creator_channel.*donation.*empty|creator_channel.*fantalk.*empty|support|donat|fan.?letter|ファンレター|サポート|応援|送る" app/src/main/res/values app/src/main/res/values-en app/src/main/res/values-ja` — 현재 resource 값을 확인했다.
|
||||||
|
- `rg -n "creator_channel_(fantalk|donation).*empty|creator_channel_fantalk_support_action|creator_channel_donation_(button|action)" app/src/main/java app/src/main/res/layout app/src/test/java -g '!**/build/**'` — layout/source/test 영향 범위를 확인했다.
|
||||||
|
- 남은 항목: 사용자 승인 후 `P1-T1` 구현 시작.
|
||||||
|
- 다음 행동: 구현 요청이 들어오면 `P1-T1`부터 실행한다.
|
||||||
|
|
||||||
|
### `P1-T1` 1차 실행 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: 한국어/영어/일본어 resource 테스트를 먼저 갱신하고, 홈 전용 후원 empty key와 팬레터/후원 empty 문구를 반영했다.
|
||||||
|
- 왜: 홈/팬레터/후원 empty state 문구가 PRD 문구 표와 일치해야 하기 때문이다.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest"` — RED에서 `CreatorChannelActivitySourceTest.kt:1311`, `CreatorChannelFanTalkFragmentLayoutTest.kt:238` assertion 실패를 확인했다.
|
||||||
|
- 같은 focused test 재실행 — `BUILD SUCCESSFUL`.
|
||||||
|
- `./gradlew :app:mergeDebugResources` — `BUILD SUCCESSFUL`.
|
||||||
|
- 남은 항목: 없음.
|
||||||
|
- 다음 행동: `P1-T2` 실행.
|
||||||
|
|
||||||
|
### `P1-T2` 1차 실행 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: 홈 후원 empty가 홈 전용 string을 사용하도록 변경하고, `FanTalk` section에 기존 owner 판정값을 전달해 홈 팬레터 empty action 버튼을 owner일 때 숨기도록 했다.
|
||||||
|
- 왜: 사용자가 지적한 대로 이미 알고 있는 내 채널 여부를 홈 팬레터 empty 버튼 정책에 활용하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeMapperTest"` — RED에서 `CreatorChannelHomeMapperTest.kt:62`, `:63`의 `FanTalk.isOwner` 미존재 컴파일 실패를 확인했다.
|
||||||
|
- 같은 focused test 재실행 — `BUILD SUCCESSFUL`.
|
||||||
|
- 남은 항목: 없음.
|
||||||
|
- 다음 행동: `P1-T3` 실행.
|
||||||
|
|
||||||
|
### `P1-T3` 1차 실행 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: 팬레터 탭 empty 버튼은 내 채널에서 숨기고 click listener를 제거했다. 후원 탭 empty 버튼은 기존 visibility 분기를 유지하면서 owner일 때 listener도 제거하도록 보강했다.
|
||||||
|
- 왜: 내 채널에서는 본인에게 후원하거나 팬레터를 남기는 action을 표시하거나 실행하지 않아야 하기 때문이다.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationActionTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest"` — RED에서 `CreatorChannelDonationActionTest.kt:94`, `CreatorChannelFanTalkFragmentLayoutTest.kt:218` assertion 실패를 확인했다.
|
||||||
|
- 같은 focused test 재실행 — `BUILD SUCCESSFUL`.
|
||||||
|
- 남은 항목: 없음.
|
||||||
|
- 다음 행동: `P1-GATE` 실행.
|
||||||
|
|
||||||
|
### `P1-GATE` 1차 실행 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: focused regression, resource merge, 전체 unit suite, 공백 검사와 reviewer gate를 완료했다.
|
||||||
|
- 왜: 문구/resource merge, owner 버튼 정책, 인접 회귀를 최종 판정하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeMapperTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkViewModelTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkActionTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationViewModelTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationActionTest"` — `BUILD SUCCESSFUL`.
|
||||||
|
- `./gradlew :app:mergeDebugResources` — `BUILD SUCCESSFUL`.
|
||||||
|
- `./gradlew :app:testDebugUnitTest` — 최초 전체 실행은 `CreatorChannelHomeViewModelTest`의 can 차감 assertion 1건이 실패했으나, 해당 테스트 단독 재실행은 `BUILD SUCCESSFUL`이었고 전체 suite 재실행도 `BUILD SUCCESSFUL`이었다.
|
||||||
|
- `git diff --check` — 출력 없음.
|
||||||
|
- reviewer gate — `APPROVE`, 차단 발견 없음.
|
||||||
|
- 남은 항목: 없음.
|
||||||
|
- 다음 행동: 없음.
|
||||||
|
|
||||||
|
### `P1-T4` 1차 실행 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: 홈 팬레터 empty root의 fixed `169dp` 높이를 제거하고 `LinearLayout` `wrap_content` 구조로 바꿨다. `응원남기기` 버튼은 title 아래 sibling으로 두고 `marginTop`만 갖게 해 `GONE`일 때 버튼과 간격이 함께 제거되도록 했다.
|
||||||
|
- 왜: 내 채널에서 버튼을 숨겨도 기존 `FrameLayout` fixed height 때문에 버튼 영역이 남았기 때문이다.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` — RED에서 `CreatorChannelActivitySourceTest.kt:2145` assertion 실패를 확인했다.
|
||||||
|
- 같은 focused test 재실행 — `BUILD SUCCESSFUL`.
|
||||||
|
- `./gradlew :app:mergeDebugResources` — `BUILD SUCCESSFUL`.
|
||||||
|
- `git diff --check` — 출력 없음.
|
||||||
|
- 남은 항목: 없음.
|
||||||
|
- 다음 행동: 없음.
|
||||||
|
|
||||||
|
### `P1-T5` 1차 실행 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: 홈 후원/홈 팬레터/팬레터 탭/후원 탭 empty action button 색상 계약을 source/layout test로 고정하고, 홈 후원 empty button 배경만 기존 white capsule drawable로 변경했다.
|
||||||
|
- 왜: 홈 탭 action button은 white 배경과 black 글자색이어야 하고, 후원 탭 action button은 기존 soda 400 배경과 white 글자색을 유지해야 하기 때문이다.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest"` — RED에서 `CreatorChannelActivitySourceTest.kt:1120` assertion 실패를 확인했다.
|
||||||
|
- 같은 focused test 재실행 — `BUILD SUCCESSFUL`.
|
||||||
|
- `./gradlew :app:mergeDebugResources` — `BUILD SUCCESSFUL`.
|
||||||
|
- `git diff --check` — 출력 없음.
|
||||||
|
- 남은 항목: 없음.
|
||||||
|
- 다음 행동: 없음.
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
|
||||||
|
- 2026-08-14: 문서 생성 단계에서는 production resource, Kotlin, XML layout을 수정하지 않았다.
|
||||||
|
- 2026-08-14: Phase 1 구현 후 focused regression, resource merge, 전체 unit suite 재실행, 공백 검사와 reviewer gate가 통과했다.
|
||||||
|
- 2026-08-14: `P1-T4` 후속 회귀 수정에서 홈 팬레터 empty fixed height 제거 후 focused source/layout test, resource merge, 공백 검사가 통과했다.
|
||||||
|
- 2026-08-14: `P1-T5` 후속 색상 수정에서 홈 후원 empty button 배경을 white capsule로 변경한 뒤 focused source/layout test, resource merge, 공백 검사가 통과했다.
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# PRD: 크리에이터 채널 Empty State 다국어 문구 정리
|
||||||
|
|
||||||
|
## 문서 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 문서 상태 | 후속 색상 수정 완료 |
|
||||||
|
| 작성일 | `2026-08-14` |
|
||||||
|
| 최종 수정일 | `2026-08-14` |
|
||||||
|
| 대상 제품 | 크리에이터 채널 홈/팬레터/후원 empty state |
|
||||||
|
| 작성자·결정권자 | 사용자 요청 기준 |
|
||||||
|
| 관련 API Contract | 없음 |
|
||||||
|
| 관련 구현 계획 | `docs/20260814_크리에이터_채널_empty_state_다국어_문구_정리/plan-task.md` |
|
||||||
|
| 관련 review | 없음 |
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
크리에이터 채널의 홈, 팬레터, 후원 영역에서 후원 또는 팬레터 데이터가 없을 때 표시되는 empty state 문구와 action 버튼 노출 정책을 정리한다. 이번 문서는 구현하지 않고, 한국어/영어/일본어 Android string resource와 화면별 empty state 요구사항을 확정한다.
|
||||||
|
|
||||||
|
## 2. Problem Statement
|
||||||
|
|
||||||
|
- 크리에이터 채널 일부 empty state 문구가 화면별 의도와 다르게 재사용되어 홈 탭과 상세 탭의 안내 문구를 동시에 만족하지 못한다.
|
||||||
|
- 팬레터/후원 empty state 일부 문구와 action label이 한국어, 영어, 일본어에서 일관되게 다국어 처리되어야 한다.
|
||||||
|
- 홈 탭 팬레터 empty state는 레이아웃과 표시 분기가 이미 존재하므로, 확정 문구와 내 채널 action 버튼 숨김 정책을 보강해야 한다.
|
||||||
|
- 내 채널에서는 사용자가 본인에게 후원하거나 본인 채널에 팬레터를 남기는 action 버튼을 보지 않아야 한다.
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
|
||||||
|
- 홈 탭 후원 empty 안내 문구를 한국어/영어/일본어 resource로 분리해 표시한다.
|
||||||
|
- 홈 탭 팬레터 empty 안내 문구를 한국어/영어/일본어 resource로 표시하고, 기존 카드형 empty UI의 action 버튼 노출 정책을 확정한다.
|
||||||
|
- 팬레터 탭 empty 안내 문구와 `응원남기기` action label을 한국어/영어/일본어 resource로 표시한다.
|
||||||
|
- 후원 탭 empty 안내 문구와 `후원하기` action label을 한국어/영어/일본어 resource로 표시한다.
|
||||||
|
- 내 채널의 홈/팬레터/후원 empty state에서 `후원하기`와 `응원남기기` 버튼을 모두 숨기는 정책을 확정한다.
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
|
||||||
|
- 이번 작업에서는 Kotlin, XML, resource를 수정하지 않는다.
|
||||||
|
- 후원 dialog, 팬레터 작성 화면, API, DTO, pagination, ranking 동작은 변경하지 않는다.
|
||||||
|
- 크리에이터 채널 전체 UI 구조, tab-bar, header, sticky scroll 동작은 변경하지 않는다.
|
||||||
|
- 영어 외 신규 locale을 추가하지 않는다.
|
||||||
|
- 기존 class, layout id, drawable id, resource key rename은 구현 계획에서 필요한 최소 범위로만 다루며, 문서 작성 단계에서는 수행하지 않는다.
|
||||||
|
- UI/계측 테스트, 기기·에뮬레이터 조작, 스크린샷·시각 QA는 사용자가 별도 요청하기 전까지 계획의 필수 실행 범위에 포함하지 않는다.
|
||||||
|
|
||||||
|
## 5. Target Users and Permissions
|
||||||
|
|
||||||
|
| 사용자 | 목표 | 주요 작업 | 사용 환경 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 타인 크리에이터 채널 방문자 | 데이터가 없을 때 다음 action을 이해 | 홈/팬레터/후원 empty state 확인, 후원 또는 팬레터 작성 진입 | Android 앱 |
|
||||||
|
| 본인 채널을 보는 크리에이터 | 본인에게 부적절한 action을 보지 않음 | 내 채널 empty state 확인 | Android 앱 |
|
||||||
|
|
||||||
|
권한과 소유자 판정은 기존 `CreatorChannelActivity`/탭 Fragment의 `isOwner` 흐름을 따른다.
|
||||||
|
|
||||||
|
## 6. 핵심 사용자 흐름
|
||||||
|
|
||||||
|
1. 사용자가 크리에이터 채널 홈 탭에 진입한다.
|
||||||
|
2. 후원 목록이 비어 있으면 홈 후원 empty 카드가 표시된다.
|
||||||
|
3. 팬레터 요약이 비어 있으면 홈 팬레터 empty 카드가 표시된다.
|
||||||
|
4. 사용자가 팬레터 탭에 진입하고 팬레터 목록이 비어 있으면 팬레터 탭 empty 상태가 표시된다.
|
||||||
|
5. 사용자가 후원 탭에 진입하고 후원 목록이 비어 있으면 후원 탭 empty 상태가 표시된다.
|
||||||
|
6. 타인 채널에서는 empty action 버튼을 표시하고, 내 채널에서는 해당 버튼을 숨긴다.
|
||||||
|
|
||||||
|
## 7. 기능 요구사항
|
||||||
|
|
||||||
|
### 7.1 다국어 문구
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `CREATOR-EMPTY-I18N-001` | 확정 | 홈 탭 후원 empty 문구를 상세 후원 탭 문구와 분리한다. | 홈 후원 empty는 아래 `creator_channel_home_donation_empty_title` 값으로 표시되고, 후원 탭은 `creator_channel_donation_empty_title` 값을 유지한다. | `P1-T1` |
|
||||||
|
| `CREATOR-EMPTY-I18N-002` | 확정 | 홈 탭 팬레터 empty 문구를 한국어/영어/일본어 resource로 표시한다. | `creator_channel_fantalk_empty_title` 값이 아래 문구 표와 일치한다. | `P1-T1` |
|
||||||
|
| `CREATOR-EMPTY-I18N-003` | 확정 | 팬레터 탭 empty 문구와 action label을 한국어/영어/일본어 resource로 표시한다. | `creator_channel_fantalk_empty_message`, `creator_channel_fantalk_support_action` 값이 아래 문구 표와 일치한다. | `P1-T1` |
|
||||||
|
| `CREATOR-EMPTY-I18N-004` | 확정 | 후원 탭 empty 문구와 action label을 한국어/영어/일본어 resource로 표시한다. | `creator_channel_donation_empty_title`, `creator_channel_donation_action` 값이 아래 문구 표와 일치한다. | `P1-T1` |
|
||||||
|
| `CREATOR-EMPTY-I18N-005` | 확정 | 후원 탭의 `후원하기`는 기존 다국어 처리된 `creator_channel_donation_action` 값을 우선 사용한다. | 신규 후원 action string을 만들지 않고 기존 key가 한국어/영어/일본어에서 사용된다. | `P1-T1` |
|
||||||
|
|
||||||
|
### 7.2 Empty State UI와 소유자 정책
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `CREATOR-EMPTY-UI-001` | 확정 | 홈 탭 팬레터 empty UI의 기존 표시 분기를 보존하고 확정 문구를 적용한다. | `item_creator_channel_home_fantalk.xml`의 `layout_fantalk_empty`와 `CreatorChannelHomeSectionAdapter.bindFanTalk()` empty 표시 분기를 유지하며, 문구는 팬레터 전용 값으로 표시한다. | `P1-T2` |
|
||||||
|
| `CREATOR-EMPTY-UI-002` | 확정 | 내 채널 홈 후원 empty에서는 `후원하기` 버튼을 숨긴다. | `isOwner == true`이면 `layout_donation_empty_button`이 보이지 않고 click listener가 연결되지 않는다. | `P1-T2` |
|
||||||
|
| `CREATOR-EMPTY-UI-003` | 확정 | 내 채널 홈 팬레터 empty에서는 `응원남기기` 버튼을 숨긴다. | `isOwner == true`이면 `layout_fantalk_support_button`이 보이지 않고 click listener가 연결되지 않는다. | `P1-T2` |
|
||||||
|
| `CREATOR-EMPTY-UI-004` | 확정 | 내 채널 팬레터 탭 empty에서는 `응원남기기` 버튼을 숨긴다. | `isOwner == true`이면 `layout_creator_channel_fantalk_empty_write_button`이 보이지 않고 click listener가 연결되지 않는다. | `P1-T3` |
|
||||||
|
| `CREATOR-EMPTY-UI-005` | 확정 | 내 채널 후원 탭 empty에서는 `후원하기` 버튼을 숨긴다. | `CreatorChannelDonationUiState.Empty.isOwner == true`이면 `btn_creator_channel_donation_empty_write`가 보이지 않고 click listener가 연결되지 않는다. | `P1-T3` |
|
||||||
|
| `CREATOR-EMPTY-UI-006` | 확정 | 홈 탭 후원 empty와 홈 탭 팬레터 empty action 버튼은 white 배경과 black 글자색을 사용한다. | `layout_donation_empty_button`, `layout_fantalk_support_button`은 white capsule 배경을 사용하고 label text color는 black이다. | `P1-T5` |
|
||||||
|
| `CREATOR-EMPTY-UI-007` | 확정 | 팬레터 탭 empty `응원남기기` 버튼은 gray 800 배경과 white 글자색을 사용한다. | `layout_creator_channel_fantalk_empty_write_button`은 `gray_800` 배경 drawable을 사용하고 label text color는 white이다. | `P1-T5` |
|
||||||
|
| `CREATOR-EMPTY-UI-008` | 확정 | 후원 탭 empty `후원하기` 버튼은 soda 400 배경과 white 글자색을 사용한다. | `btn_creator_channel_donation_empty_write`는 `soda_400` 배경 drawable을 사용하고 label text color는 white이다. | `P1-T5` |
|
||||||
|
|
||||||
|
## 8. 문구 표
|
||||||
|
|
||||||
|
### 8.1 홈 탭
|
||||||
|
|
||||||
|
| 화면 상태 | string name | 한국어 | 영어 | 일본어 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 후원 없음 | `creator_channel_home_donation_empty_title` | `처음으로 크리에이터를\n후원해 보세요!` | `Be the first to\nsupport this creator!` | `最初のサポートをしてみましょう!` |
|
||||||
|
| 팬레터 없음 | `creator_channel_fantalk_empty_title` | `당신의 첫 응원을\n기다리고 있어요!` | `We're waiting for your\nfirst fan letter!` | `あなたの最初のファンレターを待っています!` |
|
||||||
|
|
||||||
|
### 8.2 팬레터 탭
|
||||||
|
|
||||||
|
| 화면 상태 | string name | 한국어 | 영어 | 일본어 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 팬레터 없음 | `creator_channel_fantalk_empty_message` | `아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!` | `No fan letters yet.\nSend the first fan letter!` | `まだファンレターがありません。\n最初のファンレターを送ってみましょう!` |
|
||||||
|
| 팬레터 작성 action | `creator_channel_fantalk_support_action` | `응원남기기` | `Send fan letter` | `ファンレターを送る` |
|
||||||
|
|
||||||
|
### 8.3 후원 탭
|
||||||
|
|
||||||
|
| 화면 상태 | string name | 한국어 | 영어 | 일본어 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 후원 없음 | `creator_channel_donation_empty_title` | `아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!` | `No support yet.\nBe the first to support this creator!` | `まだサポートがありません。\n最初のサポートをしてみましょう!` |
|
||||||
|
| 후원 action | `creator_channel_donation_action` | `후원하기` | `Support` | `サポートする` |
|
||||||
|
|
||||||
|
## 9. UI/UX Expectations
|
||||||
|
|
||||||
|
- 홈 탭 팬레터 empty UI는 기존 `layout_fantalk_empty` 카드형 구조를 유지한다.
|
||||||
|
- 홈 탭 후원 empty와 팬레터 empty는 타인 채널에서 action 버튼을 표시하고 내 채널에서 action 버튼을 숨긴다.
|
||||||
|
- 팬레터 탭과 후원 탭 empty 상태는 타인 채널에서 문구 아래 action 버튼을 표시하고 내 채널에서 action 버튼을 숨긴다.
|
||||||
|
- action 버튼이 숨겨져도 empty 문구 정렬과 상하 여백이 어색하게 무너지면 안 된다.
|
||||||
|
- 홈 탭 empty action 버튼은 white 배경/black 글자색, 팬레터 탭 empty action 버튼은 gray 800 배경/white 글자색, 후원 탭 empty action 버튼은 soda 400 배경/white 글자색을 사용한다.
|
||||||
|
- 후원 탭 floating `후원하기` 버튼은 기존처럼 내 채널에서 숨긴다.
|
||||||
|
|
||||||
|
## 10. 기술적 제약
|
||||||
|
|
||||||
|
- Android XML string resource는 `app/src/main/res/values/strings.xml`, `values-en/strings.xml`, `values-ja/strings.xml`에서 관리한다.
|
||||||
|
- 홈 후원 empty와 후원 탭 empty의 문구가 다르므로 홈 전용 `creator_channel_home_donation_empty_title` resource를 추가해 기존 상세 탭 key와 분리한다.
|
||||||
|
- 후원 action은 기존 `creator_channel_donation_action` resource를 재사용한다.
|
||||||
|
- 팬레터 action은 기존 `creator_channel_fantalk_support_action` key를 유지하고 값만 조정한다.
|
||||||
|
- 기존 legacy 파일은 직접 수정하지 않고, 크리에이터 채널 v2 하위 구현과 resource/layout만 최소 변경한다.
|
||||||
|
- 자동 테스트는 `app/src/test`의 local unit/source/layout test만 계획한다.
|
||||||
|
|
||||||
|
## 11. 성공 기준
|
||||||
|
|
||||||
|
- [x] 문구 표의 모든 한국어/영어/일본어 resource 값이 실제 resource와 일치한다.
|
||||||
|
- [x] 홈 후원 empty는 `creator_channel_home_donation_empty_title`, 후원 탭 empty는 `creator_channel_donation_empty_title`을 사용한다.
|
||||||
|
- [x] 홈 팬레터 empty UI가 팬레터 데이터 없음 상태에서 표시된다.
|
||||||
|
- [x] 내 채널의 홈 후원, 홈 팬레터, 팬레터 탭, 후원 탭 empty action 버튼이 모두 숨겨진다.
|
||||||
|
- [x] 타인 채널의 empty action 버튼은 기존 후원/팬레터 진입 동작을 유지한다.
|
||||||
|
- [x] 홈/팬레터/후원 empty action 버튼 색상이 화면별 확정 색상과 일치한다.
|
||||||
|
- [x] `./gradlew :app:mergeDebugResources`와 관련 local unit/source/layout test가 통과한다.
|
||||||
|
|
||||||
|
## 12. Open Questions
|
||||||
|
|
||||||
|
| ID | 상태 | 결정 필요 사항 | 현재 권고 | 결정 주체 | 결정 기한/시점 | 영향 Goal |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 없음 | 확정 | 추가 질문 없음 | 확정 범위대로 진행 | 사용자 | 2026-08-14 | `P1-T1~P1-GATE` |
|
||||||
|
|
||||||
|
## 13. 요구사항 추적표
|
||||||
|
|
||||||
|
| 요구사항 범위 | API Contract | 계획 Phase | Goal | 자동 검증 | 수동 검증 |
|
||||||
|
|---|---|---:|---|---|---|
|
||||||
|
| `CREATOR-EMPTY-I18N-001~005` | 없음 | 1 | `P1-T1` | resource source test, `mergeDebugResources` | 해당 없음 |
|
||||||
|
| `CREATOR-EMPTY-UI-001~003` | 없음 | 1 | `P1-T2` | home mapper/source/layout test | 구현자가 필요 시 로컬 화면 확인 |
|
||||||
|
| `CREATOR-EMPTY-UI-004~005` | 없음 | 1 | `P1-T3` | tab Fragment/adapter layout test | 구현자가 필요 시 로컬 화면 확인 |
|
||||||
|
|
||||||
|
## 14. Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-08-14 | `DEC-001` | 확정 | 내 채널에서는 홈 탭 후원 empty, 홈 탭 팬레터 empty, 팬레터 탭 empty, 후원 탭 empty의 `후원하기`/`응원남기기` 버튼을 모두 숨긴다. | 사용자 A안 선택 | `CREATOR-EMPTY-UI-002~005`, `P1-T2`, `P1-T3` |
|
||||||
|
| 2026-08-14 | `DEC-002` | 확정 | 영어 resource도 이번 범위에 포함하고, 제공되지 않은 영문 문구는 한국어/일본어 의미에 맞춰 번역 초안을 사용한다. | 사용자 A안 선택 및 직접 지시 | `CREATOR-EMPTY-I18N-001~004`, `P1-T1` |
|
||||||
|
| 2026-08-14 | `DEC-003` | 확정 | 홈 후원 empty와 후원 탭 empty 문구가 다르므로 홈 전용 후원 empty string을 추가한다. | 기존 `creator_channel_donation_empty_title` 단일 key로 두 화면 문구를 동시에 만족할 수 없음 | `CREATOR-EMPTY-I18N-001`, `P1-T1`, `P1-T2` |
|
||||||
|
| 2026-08-14 | `DEC-004` | 확정 | empty action 버튼 색상은 홈 탭 white/black, 팬레터 탭 gray 800/white, 후원 탭 soda 400/white로 구분한다. | 사용자 직접 지시 | `CREATOR-EMPTY-UI-006~008`, `P1-T5` |
|
||||||
|
|
||||||
|
## 15. Verification Log
|
||||||
|
|
||||||
|
- 2026-08-14: `docs/sample/sample-prd.md`, `docs/sample/sample-plan-task.md`를 확인해 신규 문서 구조를 맞췄다.
|
||||||
|
- 2026-08-14: 기존 관련 문서 `docs/20260611_크리에이터_채널_홈_탭/prd.md`, `docs/20260622_크리에이터_채널_후원_탭/prd.md`, `docs/20260814_팬Talk_팬레터_resource_문구_변경/prd.md`를 확인했다.
|
||||||
|
- 2026-08-14: 현재 resource와 layout 사용처를 확인해 홈 후원 empty와 후원 탭 empty가 같은 string key를 공유한다는 제약을 문서화했다.
|
||||||
|
- 2026-08-14: 홈 팬레터 empty UI는 `item_creator_channel_home_fantalk.xml`의 `layout_fantalk_empty`와 `CreatorChannelHomeSectionAdapter.bindFanTalk()` 표시 분기가 이미 있어, 신규 UI 추가가 아니라 문구와 내 채널 action 버튼 정책 보강 범위로 정정했다.
|
||||||
|
- 2026-08-14: 구현 후 focused regression, `:app:mergeDebugResources`, 전체 `:app:testDebugUnitTest`, `git diff --check`가 통과했고 reviewer gate가 `APPROVE`했다.
|
||||||
|
- 2026-08-14: 후속 색상 수정에서 홈 후원 empty button 배경만 white capsule로 변경하고, 홈/팬레터/후원 empty action button 색상 source/layout test와 `:app:mergeDebugResources`가 통과했다.
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
# 크리에이터 채널 차단 후 이탈 구현 계획/TASK
|
||||||
|
|
||||||
|
> **For agentic workers:** 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다.
|
||||||
|
|
||||||
|
**Goal:** 차단 성공 후 사용자가 현재 크리에이터 채널에 머물지 않고 이전 화면 또는 `MainV2Activity` 홈 fallback으로 이동한다.
|
||||||
|
|
||||||
|
**Architecture:** `CreatorChannelHomeViewModel`은 기존 차단 API 성공 흐름에서 one-shot 성공 이벤트를 추가로 emit한다. `CreatorChannelHomeFragment`는 이벤트를 consume해 `Host` callback으로 넘기고, `CreatorChannelActivity`는 `isTaskRoot` 여부에 따라 `finish()` 또는 `MainV2Activity` fallback 후 `finish()`를 수행한다.
|
||||||
|
|
||||||
|
**Tech Stack:** Kotlin, AndroidX LiveData, RxJava3, JUnit4 local unit test, source contract test.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 전제와 성공 기준
|
||||||
|
|
||||||
|
- PRD: `docs/20260814_크리에이터_채널_차단_후_이탈/prd.md`
|
||||||
|
- 구현은 차단 API 성공 후에만 화면 이탈을 수행한다.
|
||||||
|
- 기존 차단 확인 모달과 성공/실패 toast는 유지한다.
|
||||||
|
- 홈 fallback Activity는 `kr.co.vividnext.sodalive.v2.main.MainV2Activity`다.
|
||||||
|
- 테스트 범위는 `app/src/test` local unit/source contract test로 한정한다.
|
||||||
|
- 사용자가 명시적으로 요청하지 않았으므로 `app/src/androidTest`, `connectedDebugAndroidTest`, 기기·에뮬레이터 조작, 스크린샷 검증은 포함하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 파일 구조
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModel.kt`
|
||||||
|
- 차단 성공 one-shot event를 제공한다.
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt`
|
||||||
|
- 차단 성공 event를 observe/consume하고 host callback으로 전달한다.
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- host callback을 구현하고 `isTaskRoot` fallback을 처리한다.
|
||||||
|
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModelTest.kt`
|
||||||
|
- 차단 성공/실패 event contract를 검증한다.
|
||||||
|
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
- Fragment/Activity wiring과 `MainV2Activity` fallback source contract를 검증한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: 차단 성공 후 채널 이탈
|
||||||
|
|
||||||
|
**Phase 결과:** 사용자가 크리에이터 채널에서 현재 크리에이터 차단에 성공하면 현재 채널에서 빠져나가고, 이전 화면이 없으면 `MainV2Activity`로 이동한다.
|
||||||
|
|
||||||
|
**선행조건:** `docs/20260814_크리에이터_채널_차단_후_이탈/prd.md` 확정.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P1-T1`, `P1-T2`, `P1-GATE` 완료와 검증 기록 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 1.1 차단 성공 이벤트 추가
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T1`:** `CreatorChannelHomeViewModel.blockUser()`가 API 성공 시 기존 성공 toast와 함께 차단 성공 event를 emit한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `CCB-001`, `CCB-005` 확정.
|
||||||
|
- **완료 증거:** RED/GREEN 체크박스 전체 완료, focused `CreatorChannelHomeViewModelTest` 통과.
|
||||||
|
- **범위 밖:** Fragment/Activity navigation wiring은 `P1-T2`에서 처리한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModelTest.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModel.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: 기존 `CreatorChannelHomeUiState.Content`, `CreatorChannelRepository.blockUser(userId, token)`, `CreatorChannelEvent<T>`.
|
||||||
|
- Produces: `blockSuccessLiveData: LiveData<CreatorChannelEvent<Unit>>`.
|
||||||
|
|
||||||
|
- [x] **RED:** 기존 `차단 성공은 block API를 호출하고 차단 완료 토스트를 emit한다` test에 `blockSuccessLiveData` event 검증을 추가한다. 실패 케이스 test에는 `ApiResponse.success == false`일 때 `blockSuccessLiveData.value == null`을 검증한다.
|
||||||
|
- [x] **RED 확인:** 아래 focused test를 실행해 `blockSuccessLiveData` 미정의 또는 event 미발행 assertion 실패를 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest" --no-parallel --no-daemon -Pkotlin.incremental=false
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** `CreatorChannelHomeViewModel`에 `MutableLiveData<CreatorChannelEvent<Unit>>` 기반 `blockSuccessLiveData`를 추가하고, `blockUser()` 성공 분기에서 기존 `creator_channel_block_success` toast emit 후 `CreatorChannelEvent(Unit)`을 emit한다. 실패·예외 분기에서는 emit하지 않는다.
|
||||||
|
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
|
||||||
|
- [x] **REFACTOR:** event naming과 observe 순서를 기존 `toastLiveData`, `chatRoomIdLiveData` 스타일과 맞추고 focused test를 재실행해 결과를 이 Task 아래 검증 기록에 남긴다.
|
||||||
|
|
||||||
|
**검증 기록:**
|
||||||
|
|
||||||
|
- 2026-08-14 RED: `CreatorChannelHomeViewModelTest` 실행 시 `blockSuccessLiveData` 미정의로 `compileDebugUnitTestKotlin`이 실패했다.
|
||||||
|
- 2026-08-14 GREEN: `blockSuccessLiveData`와 성공 분기 emit을 최소 추가한 뒤 같은 focused test가 `BUILD SUCCESSFUL`로 통과했다. 성공 event는 한 번만 consume되고, API 실패·예외에서는 emit되지 않음을 확인했다.
|
||||||
|
|
||||||
|
#### Task 1.2 Fragment/Activity 이탈 연결
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T2`:** 차단 성공 event를 `CreatorChannelActivity`까지 전달하고, 이전 화면 또는 `MainV2Activity` fallback으로 이동한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T1` 완료.
|
||||||
|
- **완료 증거:** RED/GREEN 체크박스 전체 완료, focused `CreatorChannelActivitySourceTest` 통과.
|
||||||
|
- **범위 밖:** 차단 API 호출 방식, 기존 modal UI, `MainV2Activity` 내부 동작 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `CreatorChannelHomeViewModel.blockSuccessLiveData`.
|
||||||
|
- Produces: `CreatorChannelHomeFragment.Host.onCreatorChannelBlockSucceeded()`.
|
||||||
|
|
||||||
|
- [x] **RED:** `CreatorChannelActivitySourceTest`에 source contract test를 추가한다. 검증 조건은 `CreatorChannelHomeFragment`가 `blockSuccessLiveData.observe`와 `event.consume()?.let { host.onCreatorChannelBlockSucceeded() }` 흐름을 갖는지, `CreatorChannelActivity`가 `onCreatorChannelBlockSucceeded()`를 구현하는지, 구현 내부에서 `isTaskRoot`, `MainV2Activity::class.java`, `finish()`를 사용하는지 확인한다. 또한 `showUserBlockDialog()` 구간에는 `homeActionDelegate?.blockUser()`만 있고 직접 `finish()`나 `MainV2Activity` 이동이 없음을 확인한다.
|
||||||
|
- [x] **RED 확인:** 아래 focused test를 실행해 observer, host callback, fallback 구현 부재로 인한 assertion 실패를 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --no-parallel --no-daemon -Pkotlin.incremental=false
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** `CreatorChannelHomeFragment.observeViewModel()`에서 `blockSuccessLiveData`를 observe하고 consume 후 host callback을 호출한다. `CreatorChannelHomeFragment.Host`에 `onCreatorChannelBlockSucceeded()`를 추가한다. `CreatorChannelActivity`는 `MainV2Activity` import를 추가하고, `onCreatorChannelBlockSucceeded()`에서 `isTaskRoot`이면 `Intent(this, MainV2Activity::class.java)`를 `startActivity`한 뒤 `finish()`를 호출한다. `isTaskRoot`가 아니면 `finish()`만 호출한다.
|
||||||
|
- [x] **GREEN 확인:** 같은 focused source test를 다시 실행해 성공을 확인한다.
|
||||||
|
- [x] **REFACTOR:** 추가한 source contract가 과도하게 넓지 않은지 확인하고, 기존 `CreatorChannelActivitySourceTest.sourceSection()` helper를 재사용한다. focused test를 재실행해 결과를 이 Task 아래 검증 기록에 남긴다.
|
||||||
|
|
||||||
|
**검증 기록:**
|
||||||
|
|
||||||
|
- 2026-08-14 RED: `CreatorChannelActivitySourceTest` 100개 중 신규 차단 이탈 contract만 observer 부재 assertion으로 실패했다.
|
||||||
|
- 2026-08-14 GREEN: Fragment observer/Host callback과 Activity `isTaskRoot` 분기를 최소 추가한 뒤 같은 source test 100개가 모두 통과했다.
|
||||||
|
|
||||||
|
### 완료 조건
|
||||||
|
|
||||||
|
- [x] `P1-T1`, `P1-T2`의 체크박스와 완료 증거가 모두 충족됐다.
|
||||||
|
- [x] `CCB-001~005`가 구현 또는 명시 검증으로 추적된다.
|
||||||
|
- [x] API 성공 전 navigation이 발생하지 않는다는 source contract가 남아 있다.
|
||||||
|
- [x] task root fallback 대상이 `MainV2Activity`임을 source contract가 검증한다.
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Phase 1 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P1-GATE`:** 차단 성공 후 채널 이탈과 홈 fallback 동작을 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T1`, `P1-T2` 완료.
|
||||||
|
- **완료 증거:** 아래 자동 검증 통과와 Progress 기록.
|
||||||
|
- **범위 밖:** Gate 통과를 위한 test 삭제·완화, unrelated refactor, instrumentation test 추가.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" \
|
||||||
|
--no-parallel --no-daemon -Pkotlin.incremental=false
|
||||||
|
|
||||||
|
./gradlew :app:compileDebugKotlin --no-parallel --no-daemon -Pkotlin.incremental=false
|
||||||
|
|
||||||
|
./gradlew :app:ktlintCheck --no-parallel --no-daemon
|
||||||
|
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** focused tests와 compile은 `BUILD SUCCESSFUL`, `git diff --check`는 출력 없음. `ktlintCheck`가 기존 전역 위반으로 실패하면 이번 변경 파일 포함 여부를 확인해 Progress에 구분 기록한다.
|
||||||
|
|
||||||
|
- [x] ViewModel과 Activity source focused test 통합 실행이 통과했다.
|
||||||
|
- [x] `compileDebugKotlin`과 `ktlintCheck`가 통과했다.
|
||||||
|
- [x] `git diff --check`와 `git diff --cached --check` 결과 출력이 없었다.
|
||||||
|
- [x] 전체 local unit suite의 범위 밖 실패를 분리하고 아래 Progress에 기록했다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 실행 순서와 의존성
|
||||||
|
|
||||||
|
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|
||||||
|
|---:|---|---|---|---|
|
||||||
|
| 1 | `P1-T1` | PRD 확정 | 아니요 | ViewModel event contract 재검토 |
|
||||||
|
| 2 | `P1-T2` | `P1-T1` | 아니요 | Host callback 또는 Activity fallback contract 재검토 |
|
||||||
|
| 3 | `P1-GATE` | `P1-T1`, `P1-T2` | 아니요 | 실패 소유 Task의 회귀 수정 Task 추가 |
|
||||||
|
|
||||||
|
```text
|
||||||
|
P1-T1 -> P1-T2 -> P1-GATE
|
||||||
|
```
|
||||||
|
|
||||||
|
## 변경 금지 항목
|
||||||
|
|
||||||
|
- 차단 API request/response 계약을 변경하지 않는다.
|
||||||
|
- `MainV2Activity` 내부 구현을 변경하지 않는다.
|
||||||
|
- API 성공 전 `CreatorChannelActivity`를 종료하거나 홈으로 이동하지 않는다.
|
||||||
|
- 기존 modal UI와 string을 변경하지 않는다.
|
||||||
|
- `app/src/androidTest`, 기기·에뮬레이터 UI 조작, 스크린샷 QA를 추가하지 않는다.
|
||||||
|
- 관련 없는 creator channel tab, community, fan talk, live routing 코드를 리팩터링하지 않는다.
|
||||||
|
|
||||||
|
## 의사결정 및 중단 규칙
|
||||||
|
|
||||||
|
- 실패 event 또는 예외 event에서도 화면 이탈이 필요하다는 요구가 생기면 PRD `Decision Log`와 이 문서 범위를 먼저 갱신한다.
|
||||||
|
- `MainV2Activity` fallback에 flag 정책이 필요하다는 증거가 나오면 source/test로 먼저 실패를 고정하고, 기존 홈 진입점의 intent flag 관례를 확인한 뒤 최소 변경한다.
|
||||||
|
- 검증 중 기존 전역 test/ktlint 실패가 나오면 이번 변경 파일 포함 여부를 분리해 기록하고, 범위 밖 실패는 임의 수정하지 않는다.
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
기존 기록을 삭제하거나 덮어쓰지 않고 실제 실행 결과를 차수별로 누적한다.
|
||||||
|
|
||||||
|
### 문서 작성 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: PRD와 구현 계획/TASK 문서를 생성했다. production code와 test code는 변경하지 않았다.
|
||||||
|
- 왜: 사용자가 “문서만 생성 또는 수정하고 아직 구현은 하지 않는다”고 지시했다.
|
||||||
|
- 어떻게:
|
||||||
|
- `docs/sample/sample-prd.md` — 샘플 구조 확인.
|
||||||
|
- `docs/sample/sample-plan-task.md` — TDD Task 구조 확인.
|
||||||
|
- `docs/agent-guides/work-plan-docs.md` — 신규 문서 배치와 검증 기록 규칙 확인.
|
||||||
|
- `rg -n "차단|block|CreatorChannel|크리에이터 채널|MainV2Activity|뒤로가기" "docs"` — 기존 관련 문서 범위 확인.
|
||||||
|
- 남은 항목: `P1-T1`, `P1-T2`, `P1-GATE` 구현과 검증.
|
||||||
|
- 다음 행동: 사용자가 구현 진행을 승인하면 `P1-T1` RED 단계부터 시작한다.
|
||||||
|
|
||||||
|
### P1-GATE 1차 실행 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: `P1-T1`, `P1-T2`를 RED/GREEN으로 구현하고 focused test, compile, ktlint, diff 검증을 완료했다.
|
||||||
|
- 왜: 차단 성공 후 현재 크리에이터 채널에서 이탈하고 task root에서는 `MainV2Activity`로 fallback해야 한다.
|
||||||
|
- 어떻게:
|
||||||
|
- ViewModel RED — `blockSuccessLiveData` 미정의 compile failure 확인.
|
||||||
|
- ViewModel GREEN — focused `CreatorChannelHomeViewModelTest` `BUILD SUCCESSFUL`.
|
||||||
|
- Activity RED — source test 100개 중 신규 차단 이탈 contract 1건 실패 확인.
|
||||||
|
- Activity GREEN — source test 100개 전체 통과.
|
||||||
|
- focused 통합 test — `BUILD SUCCESSFUL`.
|
||||||
|
- `./gradlew :app:compileDebugKotlin --no-parallel --no-daemon -Pkotlin.incremental=false` — `BUILD SUCCESSFUL`.
|
||||||
|
- `./gradlew :app:ktlintCheck --no-parallel --no-daemon` — 기존 `disabled_rules` deprecation warning만 출력하고 `BUILD SUCCESSFUL`.
|
||||||
|
- `git diff --check`, `git diff --cached --check` — 출력 없음.
|
||||||
|
- 전체 `testDebugUnitTest` — 1,221개 중 범위 밖 `CreatorChannelFanTalkFragmentLayoutTest` 1건 실패. 일본어 리소스는 팬레터 문구이나 테스트는 기존 응원 문구를 기대하며, 관련 파일은 현재 diff에 없다.
|
||||||
|
- 남은 항목: Phase 1 리뷰 blocker 해소 확인.
|
||||||
|
- 다음 행동: 문서 완료 상태를 반영한 diff를 재리뷰한다.
|
||||||
|
|
||||||
|
### P1-GATE 재리뷰 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: PRD와 계획 문서의 체크박스·검증 기록을 현재 구현 증거와 재대조했다.
|
||||||
|
- 왜: 초기 리뷰의 유일한 blocker였던 문서 완료 상태 누락을 해소하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- Oracle 재리뷰 — 이전 blocker 해소 및 코드 신규 blocker 없음, 최종 `APPROVE`.
|
||||||
|
- `rg -n -- "- \\[ \\]" prd.md plan-task.md` — 실제 미완료 체크박스 없음. 템플릿 안내문에 포함된 리터럴 표기만 검색됨.
|
||||||
|
- `git diff --check`, `git diff --cached --check` — 출력 없음.
|
||||||
|
- 남은 항목: 없음.
|
||||||
|
- 다음 행동: 완료 결과를 보고한다.
|
||||||
|
|
||||||
|
### 범위 밖 테스트 정정 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: `CreatorChannelFanTalkFragmentLayoutTest`의 일본어 empty message 기대값을 현재 `values-ja/strings.xml`과 맞췄다.
|
||||||
|
- 왜: 전체 unit suite에서 남아 있던 유일한 실패가 테스트 기대값의 오래된 “응원” 문구 때문이었고, 사용자가 테스트 수정 지시를 했다.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest" --no-parallel --no-daemon -Pkotlin.incremental=false` — `BUILD SUCCESSFUL`.
|
||||||
|
- `./gradlew :app:testDebugUnitTest --no-parallel --no-daemon -Pkotlin.incremental=false` — `BUILD SUCCESSFUL`.
|
||||||
|
- 남은 항목: 없음.
|
||||||
|
- 다음 행동: 완료 결과를 보고한다.
|
||||||
|
|
||||||
|
## Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `2026-08-14` | `DEC-001` | 확정 | 차단 성공 후 현재 크리에이터 채널에서 이탈한다. | 사용자 요청과 후속 확인 | `P1-T1`, `P1-T2`, PRD `CCB-001~005` |
|
||||||
|
| `2026-08-14` | `DEC-002` | 확정 | 이전 화면이 없으면 홈인 `MainV2Activity`로 fallback 이동한다. | 사용자 명시 | `P1-T2`, PRD `CCB-004` |
|
||||||
|
| `2026-08-14` | `DEC-003` | 확정 | 이번 턴에서는 문서만 생성하고 구현하지 않는다. | 사용자 명시 | 이 문서 `Progress` |
|
||||||
|
|
||||||
|
## 발견된 문제
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `ISSUE-001` | Medium | 확정 | 현재 차단 성공 흐름은 성공 토스트만 emit하고 채널 화면에 머문다. | `P1-T1`, `P1-T2` | 구현 승인 후 RED test부터 수정한다. |
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
|
||||||
|
- 2026-08-14: 문서 작성만 수행했다. 구현 파일과 테스트 파일은 수정하지 않았으므로 Gradle 테스트·compile은 실행하지 않았다. 문서 샘플과 작업 문서 규칙, 기존 관련 문서 검색 결과를 기준으로 새 작업 디렉터리를 생성했다.
|
||||||
|
- 2026-08-14: `P1-T1`, `P1-T2`를 RED/GREEN으로 완료했다. focused test 통합 실행, `compileDebugKotlin`, `ktlintCheck`, diff check는 통과했다. 전체 unit suite는 현재 diff 밖 일본어 팬레터 문구와 기존 테스트 기대값 불일치 1건으로 실패했으며 해당 실패를 범위 밖 blocker로 분리 기록했다.
|
||||||
|
- 2026-08-14: 문서 완료 상태를 갱신한 뒤 Oracle 재리뷰에서 최종 `APPROVE`를 받았다. PRD와 계획 문서에 실제 미완료 체크박스가 없음을 확인했다.
|
||||||
|
- 2026-08-14: 후속 테스트 정정으로 Fan Talk 일본어 empty message 기대값을 현재 리소스와 맞췄고, focused Fan Talk test와 전체 local unit suite가 통과했다.
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# PRD: 크리에이터 채널 차단 후 이탈
|
||||||
|
|
||||||
|
## 문서 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 문서 상태 | 구현 완료 |
|
||||||
|
| 작성일 | `2026-08-14` |
|
||||||
|
| 최종 수정일 | `2026-08-14` |
|
||||||
|
| 대상 제품 | 크리에이터 채널 사용자 차단 흐름 |
|
||||||
|
| 작성자·결정권자 | 사용자, Sisyphus |
|
||||||
|
| 관련 API Contract | 기존 `CreatorChannelRepository.blockUser(userId, token)` 계약 사용 |
|
||||||
|
| 관련 구현 계획 | `docs/20260814_크리에이터_채널_차단_후_이탈/plan-task.md` |
|
||||||
|
| 관련 review | 없음 |
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
사용자가 크리에이터 채널에서 현재 보고 있는 크리에이터를 차단하면, 차단 성공 후 더 이상 해당 크리에이터 채널 화면에 머물지 않는다. 일반 진입에서는 현재 채널 화면을 종료해 이전 화면으로 돌아가고, 푸시 또는 딥링크처럼 이전 화면이 없는 진입에서는 홈 화면인 `MainV2Activity`로 이동한다.
|
||||||
|
|
||||||
|
## 2. Problem Statement
|
||||||
|
|
||||||
|
- 현재 `CreatorChannelHomeViewModel.blockUser()`는 차단 성공 시 성공 토스트만 emit한다.
|
||||||
|
- 그 결과 사용자가 차단한 크리에이터 채널 화면에 그대로 남아, 차단 이후에도 차단 대상의 채널 콘텐츠를 보는 부자연스러운 상태가 된다.
|
||||||
|
- 딥링크·푸시 진입처럼 이전 화면이 없는 경우 단순 종료만 하면 앱 내 안전한 목적지가 부족할 수 있다.
|
||||||
|
|
||||||
|
문제를 해결했다는 판단은 차단 API 성공 후 현재 `CreatorChannelActivity`가 종료되고, task root 상태에서는 `MainV2Activity`가 fallback으로 시작되는 것으로 한다.
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
|
||||||
|
### 3.1 제품 목표
|
||||||
|
|
||||||
|
- 사용자는 차단 성공 후 현재 크리에이터 채널에서 즉시 빠져나간다.
|
||||||
|
- 일반 진입에서는 Android 뒤로가기와 같은 의미로 이전 화면으로 돌아간다.
|
||||||
|
- 이전 화면이 없는 푸시·딥링크 진입에서는 홈인 `MainV2Activity`로 이동한다.
|
||||||
|
|
||||||
|
### 3.2 UX 목표
|
||||||
|
|
||||||
|
- 기존 차단 확인 모달과 성공 토스트는 유지한다.
|
||||||
|
- API 성공 전에는 화면을 이탈하지 않는다.
|
||||||
|
- API 실패 시에는 현재처럼 오류 토스트만 표시하고 화면에 머문다.
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
|
||||||
|
- 차단 API 계약, 차단 대상 식별자, 서버 응답 형식을 변경하지 않는다.
|
||||||
|
- `MainV2Activity` 자체의 홈 탭 구조나 초기 탭 정책을 변경하지 않는다.
|
||||||
|
- 차단 목록, 차단 해제, 차단된 크리에이터 콘텐츠 필터링 정책은 이번 범위에 포함하지 않는다.
|
||||||
|
- `app/src/androidTest`, 기기·에뮬레이터 조작, 스크린샷 기반 검증은 이번 계획에 포함하지 않는다.
|
||||||
|
|
||||||
|
## 5. Target Users and Permissions
|
||||||
|
|
||||||
|
| 사용자 | 목표 | 주요 작업 | 사용 환경 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 로그인 사용자 | 원하지 않는 크리에이터를 차단하고 해당 채널에서 벗어난다 | 크리에이터 채널 더보기 > 차단 확인 | Android 앱 |
|
||||||
|
|
||||||
|
- 인증 주체: 기존 `SharedPreferenceManager.token` 기반 로그인 사용자.
|
||||||
|
- 허용 조건: 기존 크리에이터 채널 차단 액션을 수행할 수 있는 상태.
|
||||||
|
- 거부 조건: API 실패, 네트워크 오류, `CreatorChannelHomeUiState.Content` 미준비 상태.
|
||||||
|
|
||||||
|
## 6. 핵심 사용자 흐름
|
||||||
|
|
||||||
|
1. 사용자가 `CreatorChannelActivity`에서 더보기 메뉴를 연다.
|
||||||
|
2. 사용자가 차단을 선택하고 기존 확인 모달에서 확인한다.
|
||||||
|
3. 앱은 기존 `homeActionDelegate?.blockUser()` 경로로 차단 API를 호출한다.
|
||||||
|
4. API 성공 시 기존 차단 성공 토스트를 표시하고 현재 크리에이터 채널에서 이탈한다.
|
||||||
|
5. `CreatorChannelActivity.isTaskRoot == false`이면 `finish()`로 이전 화면에 돌아간다.
|
||||||
|
6. `CreatorChannelActivity.isTaskRoot == true`이면 `MainV2Activity`를 시작한 뒤 현재 `CreatorChannelActivity`를 종료한다.
|
||||||
|
7. API 실패 또는 예외 발생 시 기존 unknown error toast를 표시하고 현재 화면에 머문다.
|
||||||
|
|
||||||
|
## 7. 정보 구조와 라우팅
|
||||||
|
|
||||||
|
```text
|
||||||
|
CreatorChannelActivity
|
||||||
|
block success, isTaskRoot=false -> finish() -> previous Activity
|
||||||
|
block success, isTaskRoot=true -> MainV2Activity -> finish()
|
||||||
|
```
|
||||||
|
|
||||||
|
- 홈 fallback 대상은 `kr.co.vividnext.sodalive.v2.main.MainV2Activity`다.
|
||||||
|
- fallback은 차단 성공 후 현재 Activity가 task root일 때만 수행한다.
|
||||||
|
|
||||||
|
## 8. 기능 요구사항
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `CCB-001` | 확정 | 차단 성공은 화면 이탈 이벤트를 발생시킨다. | `CreatorChannelHomeViewModel.blockUser()`가 `ApiResponse.success == true`일 때만 one-shot 차단 성공 이벤트를 emit한다. | `P1-T1` |
|
||||||
|
| `CCB-002` | 확정 | API 성공 전에는 화면 이탈을 시작하지 않는다. | `showUserBlockDialog()`의 확인 콜백은 기존처럼 `homeActionDelegate?.blockUser()`만 호출하며 직접 `finish()` 또는 홈 이동을 하지 않는다. | `P1-T2` |
|
||||||
|
| `CCB-003` | 확정 | 일반 진입에서는 이전 화면으로 돌아간다. | `CreatorChannelActivity.isTaskRoot == false`이면 `finish()`만 호출한다. | `P1-T2` |
|
||||||
|
| `CCB-004` | 확정 | 이전 화면이 없는 진입에서는 홈으로 이동한다. | `CreatorChannelActivity.isTaskRoot == true`이면 `MainV2Activity` intent를 시작하고 현재 Activity를 종료한다. | `P1-T2` |
|
||||||
|
| `CCB-005` | 확정 | 기존 성공/실패 feedback은 유지한다. | 성공 시 `creator_channel_block_success`, 실패·예외 시 `common_error_unknown` toast 흐름을 유지한다. | `P1-T1` |
|
||||||
|
|
||||||
|
## 9. UI/UX Expectations
|
||||||
|
|
||||||
|
- 신규 UI는 없다.
|
||||||
|
- 기존 차단 확인 모달 문구와 버튼은 변경하지 않는다.
|
||||||
|
- 성공 후 이동은 추가 확인 없이 즉시 수행한다.
|
||||||
|
|
||||||
|
## 10. API 계약
|
||||||
|
|
||||||
|
- 신규 API는 없다.
|
||||||
|
- 기존 호출 경로: `CreatorChannelRepository.blockUser(userId, token)`.
|
||||||
|
- 성공 판단은 기존 `ApiResponse.success`를 따른다.
|
||||||
|
|
||||||
|
## 11. 보안과 데이터 취급
|
||||||
|
|
||||||
|
- token, URL, 사용자 식별자 등 민감 정보는 로그·Toast·문서 fixture에 추가 노출하지 않는다.
|
||||||
|
- 실패 시 서버 오류 상세를 사용자에게 직접 노출하지 않고 기존 unknown error toast를 유지한다.
|
||||||
|
|
||||||
|
## 12. 성공 기준
|
||||||
|
|
||||||
|
### 12.1 기능 수용 기준
|
||||||
|
|
||||||
|
- [x] 차단 성공 시 `CreatorChannelHomeViewModel`이 성공 토스트와 차단 성공 이벤트를 emit한다. (`CCB-001`, `CCB-005`)
|
||||||
|
- [x] 차단 실패 또는 예외 시 차단 성공 이벤트가 emit되지 않는다. (`CCB-001`, `CCB-005`)
|
||||||
|
- [x] `CreatorChannelHomeFragment`가 차단 성공 이벤트를 consume한 뒤 `Host`에 전달한다. (`CCB-001`)
|
||||||
|
- [x] `CreatorChannelActivity`가 `isTaskRoot`에 따라 이전 화면 복귀 또는 `MainV2Activity` fallback을 수행한다. (`CCB-003`, `CCB-004`)
|
||||||
|
- [x] 확인 모달의 confirm callback은 API 성공 전 직접 이동하지 않는다. (`CCB-002`)
|
||||||
|
|
||||||
|
### 12.2 검증 기준
|
||||||
|
|
||||||
|
- [x] `CreatorChannelHomeViewModelTest` focused unit test가 통과한다.
|
||||||
|
- [x] `CreatorChannelActivitySourceTest` focused source contract test가 통과한다.
|
||||||
|
- [x] 변경 파일 compile, ktlint 범위 검증이 통과하거나 기존 전역 blocker를 문서에 기록한다.
|
||||||
|
|
||||||
|
## 13. Open Questions
|
||||||
|
|
||||||
|
| ID | 상태 | 결정 필요 사항 | 현재 결정 | 결정 주체 | 영향 Goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `OQ-001` | 확정 | 뒤로가기 의미 | 현재 `CreatorChannelActivity`에서 빠져나가 이전 화면으로 돌아가는 의미 | 사용자 | `P1-T2` |
|
||||||
|
| `OQ-002` | 확정 | 이전 화면이 없는 경우 fallback | `MainV2Activity`로 이동 | 사용자 | `P1-T2` |
|
||||||
|
|
||||||
|
## 14. 요구사항 추적표
|
||||||
|
|
||||||
|
| 요구사항 범위 | API Contract | 계획 Phase | Goal | 자동 검증 | 수동 검증 |
|
||||||
|
|---|---|---:|---|---|---|
|
||||||
|
| `CCB-001`, `CCB-005` | 기존 `blockUser` | 1 | `P1-T1` | `CreatorChannelHomeViewModelTest` | 없음 |
|
||||||
|
| `CCB-002~004` | 없음 | 1 | `P1-T2` | `CreatorChannelActivitySourceTest` | 없음 |
|
||||||
|
|
||||||
|
## 15. Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `2026-08-14` | `DEC-001` | 확정 | 차단 성공 후 현재 크리에이터 채널에서 이탈한다. | 사용자 요청 | `CCB-001~005`, `P1-T1`, `P1-T2` |
|
||||||
|
| `2026-08-14` | `DEC-002` | 확정 | 일반 진입은 `finish()`로 이전 화면에 돌아가고, task root는 `MainV2Activity`로 fallback한다. | 사용자 확인 | `CCB-003`, `CCB-004`, `P1-T2` |
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
|
||||||
|
- 2026-08-14: 현재 구현 맥락을 확인했다. `CreatorChannelHomeViewModel.blockUser()`는 성공 토스트만 emit하고, `CreatorChannelActivity.showUserBlockDialog()`는 확인 시 `homeActionDelegate?.blockUser()`를 호출한다. 이번 문서 작성 요청에 따라 production code와 test code는 변경하지 않았다.
|
||||||
|
- 2026-08-14: 승인 후 `CreatorChannelHomeViewModel`의 차단 성공 one-shot event, `CreatorChannelHomeFragment.Host` 전달, `CreatorChannelActivity`의 `isTaskRoot` 기반 `MainV2Activity` fallback을 구현했다. ViewModel/source contract RED를 각각 확인한 뒤 최소 구현으로 GREEN 전환했다.
|
||||||
|
- 2026-08-14: focused test 통합 실행, `compileDebugKotlin`, `ktlintCheck`, `git diff --check`가 성공했다. 전체 `testDebugUnitTest`는 1,221개 중 이번 변경과 무관한 `CreatorChannelFanTalkFragmentLayoutTest` 일본어 문구 기대값 불일치 1건으로 실패했으며, 실패 test와 locale string 파일은 현재 diff에 포함되지 않는다.
|
||||||
|
- 2026-08-14: Phase 1 재리뷰에서 이전 문서 정합성 blocker 해소를 확인하고 최종 `APPROVE` 판정을 받았다.
|
||||||
|
- 2026-08-14: 후속으로 `CreatorChannelFanTalkFragmentLayoutTest`의 일본어 empty message 기대값을 현재 `values-ja/strings.xml`의 팬레터 문구와 맞췄다. focused Fan Talk layout test와 전체 `testDebugUnitTest`가 `BUILD SUCCESSFUL`로 통과했다.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Phase 1 크리에이터 차단 후 이탈 코드 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 1 / `P1-T1`, `P1-T2`, `P1-GATE` |
|
||||||
|
| 기준 commit 또는 working tree | 2026-08-14 working tree |
|
||||||
|
| 리뷰 일자 | `2026-08-14` |
|
||||||
|
| 리뷰어 | Oracle, Sisyphus |
|
||||||
|
| 기준 문서 | `docs/20260814_크리에이터_채널_차단_후_이탈/prd.md`, `plan-task.md` |
|
||||||
|
| 리뷰 상태 | 수정 검증 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 차단 성공 후에만 현재 채널에서 이탈하는지 확인한다.
|
||||||
|
- 일반 진입은 이전 화면으로 돌아가고 task root는 `MainV2Activity`로 fallback하는지 확인한다.
|
||||||
|
- ViewModel event, Fragment Host 전달, Activity navigation과 local test·문서 완료 증거를 검토한다.
|
||||||
|
- `app/src/androidTest`, 기기·에뮬레이터 조작, 범위 밖 i18n 실패 수정은 제외한다.
|
||||||
|
|
||||||
|
## 3. 검토한 근거
|
||||||
|
|
||||||
|
- 요구사항: `CCB-001~005`.
|
||||||
|
- 계획: `P1-T1`, `P1-T2`, `P1-GATE`.
|
||||||
|
- 코드: `CreatorChannelHomeViewModel.kt`, `CreatorChannelHomeFragment.kt`, `CreatorChannelActivity.kt`.
|
||||||
|
- 테스트: `CreatorChannelHomeViewModelTest`, `CreatorChannelActivitySourceTest`.
|
||||||
|
|
||||||
|
## 4. 실행한 검증
|
||||||
|
|
||||||
|
| 명령 | 결과 | 핵심 증거 |
|
||||||
|
|---|---|---|
|
||||||
|
| ViewModel focused RED/GREEN | 성공 | 미정의 compile failure 후 `BUILD SUCCESSFUL` |
|
||||||
|
| Activity source RED/GREEN | 성공 | 신규 contract 1건 실패 후 source test 100개 통과 |
|
||||||
|
| focused test 통합 실행 | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| `compileDebugKotlin` | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| `ktlintCheck` | 성공 | 기존 deprecation warning 외 오류 없음 |
|
||||||
|
| `git diff --check`, `git diff --cached --check` | 성공 | 출력 없음 |
|
||||||
|
| 전체 `testDebugUnitTest` | 범위 밖 실패 | 1,221개 중 기존 Fan Talk 일본어 문구 불일치 1건 |
|
||||||
|
|
||||||
|
## 5. 발견 사항
|
||||||
|
|
||||||
|
### REV-P1-001 — 계획 문서 완료 상태와 검증 기록 누락
|
||||||
|
|
||||||
|
- **심각도:** Blocker
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** Phase 완료 조건
|
||||||
|
- **소유 Task:** `P1-GATE`
|
||||||
|
|
||||||
|
초기 리뷰에서 구현과 테스트는 `CCB-001~005`를 충족했지만, `plan-task.md` 체크박스와 검증 기록이 미구현 상태로 남아 완료 판정을 막았다. 실제 RED/GREEN/focused/compile/ktlint/diff/전체 suite 결과를 기존 기록을 삭제하지 않고 누적하고, 증거가 충족된 체크박스를 완료 처리했다.
|
||||||
|
|
||||||
|
## 6. plan·goal 전환
|
||||||
|
|
||||||
|
별도 코드 회귀 수정 Task는 없다. `REV-P1-001`은 코드 결함이 아닌 현재 Phase 문서 정합성 문제로, 기존 `P1-GATE` 기록 갱신으로 처리했다.
|
||||||
|
|
||||||
|
## 7. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | `CCB-001~005` 코드·test 대조 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | `REV-P1-001` 수정 완료 |
|
||||||
|
| 확정 항목 plan 반영 | 충족 | `P1-GATE` 체크박스와 Progress 갱신 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | Task별 검증 기록과 Verification Log |
|
||||||
|
|
||||||
|
**최종 결론:** 수정 검증 완료
|
||||||
|
|
||||||
|
**남은 항목:** 없음.
|
||||||
|
|
||||||
|
## 8. 수정 후 검증 기록
|
||||||
|
|
||||||
|
### 1차 수정 검증 — 2026-08-14
|
||||||
|
|
||||||
|
- 무엇을: `REV-P1-001`의 문서 체크박스와 RED/GREEN/Gate 검증 기록을 현재 증거에 맞게 갱신했다.
|
||||||
|
- 왜: 구현과 테스트가 완료됐지만 계획 문서가 미구현 상태로 남아 Phase 완료 판정을 막았기 때문이다.
|
||||||
|
- 어떻게:
|
||||||
|
- Oracle 재리뷰 — 이전 blocker 해소, 코드 신규 blocker 없음, 최종 `APPROVE`.
|
||||||
|
- PRD/plan 미완료 체크박스 검색 — 실제 미완료 항목 없음.
|
||||||
|
- `git diff --check`, `git diff --cached --check` — 출력 없음.
|
||||||
|
- 남은 항목: 없음.
|
||||||
|
|
||||||
|
### 2차 수정 검증 — 2026-08-14
|
||||||
|
|
||||||
|
- 무엇을: 이전 전체 suite 실패 원인이던 Fan Talk 일본어 empty message 테스트 기대값을 현재 리소스와 맞췄다.
|
||||||
|
- 왜: 테스트가 오래된 “응원” 문구를 기대했고, 현재 일본어 리소스는 “팬레터” 문구가 맞기 때문이다.
|
||||||
|
- 어떻게:
|
||||||
|
- focused `CreatorChannelFanTalkFragmentLayoutTest` — `BUILD SUCCESSFUL`.
|
||||||
|
- 전체 `:app:testDebugUnitTest` — `BUILD SUCCESSFUL`.
|
||||||
|
- 남은 항목: 없음.
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
# 팬Talk 팬레터 Resource 문구 변경 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 크리에이터 채널 FanTalk 사용자 노출 문구를 기본/영어/일본어 resource에서 팬레터 계열 표현으로 통일한다.
|
||||||
|
|
||||||
|
**Architecture:** Android string resource 값만 수정한다. Resource key와 Kotlin/layout/drawable 식별자는 호환성 유지를 위해 그대로 둔다.
|
||||||
|
|
||||||
|
**Tech Stack:** Android XML string resources, Gradle `:app:mergeDebugResources`, repository docs workflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| 문서 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 상태 | 구현 완료 |
|
||||||
|
| 작성일 | `2026-08-14` |
|
||||||
|
| 요구사항 기준 | `docs/20260814_팬Talk_팬레터_resource_문구_변경/prd.md` |
|
||||||
|
| API 기준 | 없음 |
|
||||||
|
| 현재 Phase | Phase 1 완료 |
|
||||||
|
| 현재 활성 Goal | 없음 |
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
크리에이터 채널의 FanTalk 탭/섹션/오류 문구가 한국어 `팬레터`, 영어 `Fan Letter`, 일본어 `ファンレター`로 표시된다.
|
||||||
|
|
||||||
|
## 현재 상태
|
||||||
|
|
||||||
|
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|
||||||
|
|---:|---|---:|---|---|
|
||||||
|
| 1 | 완료 | `2/2` | 없음 | 없음 |
|
||||||
|
|
||||||
|
## 범위
|
||||||
|
|
||||||
|
### 포함
|
||||||
|
|
||||||
|
- `app/src/main/res/values/strings.xml`의 변경 대상 string 값 수정
|
||||||
|
- `app/src/main/res/values-en/strings.xml`의 변경 대상 string 값 수정
|
||||||
|
- `app/src/main/res/values-ja/strings.xml`의 변경 대상 string 값 수정
|
||||||
|
- resource merge와 diff whitespace 검증
|
||||||
|
|
||||||
|
### 제외
|
||||||
|
|
||||||
|
- Resource key rename
|
||||||
|
- Kotlin class/package/method rename
|
||||||
|
- Layout id, drawable name, API, DTO, repository, ViewModel 변경
|
||||||
|
- 과거 PRD/plan-task 이력 문구 일괄 치환
|
||||||
|
- UI/계측 테스트와 기기·에뮬레이터 수동 조작
|
||||||
|
|
||||||
|
## 기술적 제약
|
||||||
|
|
||||||
|
- 기술 스택: Android XML string resources
|
||||||
|
- 아키텍처: 기존 `creator_channel_*_fantalk` key를 유지하고 값만 변경한다.
|
||||||
|
- 데이터·보안: 민감정보와 외부 API 변경 없음
|
||||||
|
- 호환성: 기존 resource 참조 경로를 유지한다.
|
||||||
|
- 테스트 범위: resource-only 변경이므로 신규 unit test를 작성하지 않는다.
|
||||||
|
- 검증: `rg`, `:app:mergeDebugResources`, `git diff --check`로 실제 변경과 XML merge를 확인한다.
|
||||||
|
|
||||||
|
## Phase 1
|
||||||
|
|
||||||
|
**Phase 결과:** 크리에이터 채널 FanTalk 관련 사용자 노출 resource 값이 팬레터 표현으로 바뀐다.
|
||||||
|
|
||||||
|
**선행조건:** PRD `I18N-FANLETTER-001~004` 확정.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P1-T1`, `P1-GATE` 완료와 검증 기록 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 1.1 Resource 값 변경
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T1`:** 기본/영어/일본어 FanTalk 사용자 노출 string 값을 팬레터 표현으로 변경한다.
|
||||||
|
|
||||||
|
- **시작 조건:** PRD `DEC-001`, `DEC-002` 확정.
|
||||||
|
- **완료 증거:** 변경 대상 string 값이 PRD 표와 일치하고 이전 기능명 표현이 대상 resource 값에 남지 않는다.
|
||||||
|
- **범위 밖:** resource key rename, 코드 식별자 rename, 과거 문서 이력 수정.
|
||||||
|
- **TDD 예외 사유:** XML resource 값만 바꾸는 작업이라 실패 unit test를 작성할 로직 단위가 없다.
|
||||||
|
- **대체 검증 방법:** 변경 전후 `rg` 대조와 resource merge로 확인한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/res/values/strings.xml`
|
||||||
|
- Modify: `app/src/main/res/values-en/strings.xml`
|
||||||
|
- Modify: `app/src/main/res/values-ja/strings.xml`
|
||||||
|
- Test: 없음
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: 기존 `R.string.creator_channel_tab_fantalk`, `R.string.creator_channel_section_fantalk`, `R.string.creator_channel_fantalk_error_message` 참조
|
||||||
|
- Produces: 같은 resource key의 새 표시 값
|
||||||
|
|
||||||
|
- [x] **RED:** 변경 전 대상 resource에 이전 기능명 표현이 존재함을 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "팬Talk|팬톡|FanTalk|ファントーク" "app/src/main/res/values" "app/src/main/res/values-en" "app/src/main/res/values-ja"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `creator_channel_tab_fantalk`, `creator_channel_section_fantalk`, `creator_channel_fantalk_error_message` 중 하나 이상에서 이전 표현이 출력된다.
|
||||||
|
|
||||||
|
- [x] **RED 확인:** 변경 대상 key를 재확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "creator_channel_(tab|section)_fantalk|creator_channel_fantalk_error_message" "app/src/main/res/values" "app/src/main/res/values-en" "app/src/main/res/values-ja"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 기본/영어/일본어 resource에서 세 key가 모두 출력된다.
|
||||||
|
|
||||||
|
- [x] **GREEN:** 아래 값으로 XML string 값을 수정한다.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<string name="creator_channel_tab_fantalk">팬레터</string>
|
||||||
|
<string name="creator_channel_section_fantalk">팬레터</string>
|
||||||
|
<string name="creator_channel_fantalk_error_message">팬레터를 불러오지 못했습니다.</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<string name="creator_channel_tab_fantalk">Fan Letter</string>
|
||||||
|
<string name="creator_channel_section_fantalk">Fan Letter</string>
|
||||||
|
<string name="creator_channel_fantalk_error_message">Could not load Fan Letter.</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<string name="creator_channel_tab_fantalk">ファンレター</string>
|
||||||
|
<string name="creator_channel_section_fantalk">ファンレター</string>
|
||||||
|
<string name="creator_channel_fantalk_error_message">ファンレターを読み込めませんでした。</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN 확인:** 대상 resource 범위에서 이전 기능명 표현이 사라졌는지 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "팬Talk|팬톡|FanTalk|ファントーク" "app/src/main/res/values" "app/src/main/res/values-en" "app/src/main/res/values-ja"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 출력 없음. 코드 식별자나 layout id 검색은 이 확인 범위에 포함하지 않는다.
|
||||||
|
|
||||||
|
- [x] **REFACTOR:** 추가 정리 없이 변경 파일을 유지하고 resource merge를 실행한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:mergeDebugResources
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 두 명령 모두 성공한다.
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Phase 1 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P1-GATE`:** resource-only 변경과 제외 범위 준수를 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T1` 완료.
|
||||||
|
- **완료 증거:** 아래 명령 통과와 Progress 기록.
|
||||||
|
- **범위 밖:** Gate 통과를 위한 코드 식별자 rename, 테스트 삭제·완화, 관련 없는 문구 수정.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "creator_channel_(tab|section)_fantalk|creator_channel_fantalk_error_message" "app/src/main/res/values" "app/src/main/res/values-en" "app/src/main/res/values-ja"
|
||||||
|
rg -n "팬Talk|팬톡|FanTalk|ファントーク" "app/src/main/res/values" "app/src/main/res/values-en" "app/src/main/res/values-ja"
|
||||||
|
./gradlew :app:mergeDebugResources
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** 첫 번째 명령은 변경된 팬레터 값만 출력한다. 두 번째 명령은 출력이 없다. Gradle resource merge와 whitespace check는 성공한다.
|
||||||
|
|
||||||
|
- [x] 변경된 세 locale의 세 key가 PRD 값과 일치한다.
|
||||||
|
- [x] 대상 resource에서 이전 기능명 표현이 출력되지 않는다.
|
||||||
|
- [x] `./gradlew :app:mergeDebugResources`가 `BUILD SUCCESSFUL`로 통과한다.
|
||||||
|
- [x] `git diff --check`가 출력 없이 통과한다.
|
||||||
|
|
||||||
|
## 실행 순서와 의존성
|
||||||
|
|
||||||
|
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|
||||||
|
|---:|---|---|---|---|
|
||||||
|
| 1 | `P1-T1` | PRD 확정 | 아니요 | 변경 대상 key 재검색 후 계획 보정 |
|
||||||
|
| 2 | `P1-GATE` | `P1-T1` | 아니요 | 실패한 검증의 소유 Task로 회귀 수정 Task 추가 |
|
||||||
|
|
||||||
|
```text
|
||||||
|
P1-T1 -> P1-GATE
|
||||||
|
```
|
||||||
|
|
||||||
|
## 변경 금지 항목
|
||||||
|
|
||||||
|
- Resource key를 변경하지 않는다.
|
||||||
|
- Kotlin, layout, drawable, API 관련 식별자를 변경하지 않는다.
|
||||||
|
- 과거 문서의 이력 문구를 일괄 치환하지 않는다.
|
||||||
|
- 신규 dependency를 추가하지 않는다.
|
||||||
|
- UI/계측 테스트나 기기·에뮬레이터 조작을 실행하지 않는다.
|
||||||
|
|
||||||
|
## Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-08-14 | `DEC-001` | 확정 | 관련 기능명 표현 전체를 팬레터 계열로 변경한다. | 사용자 A안 선택 | `P1-T1`, `prd.md` |
|
||||||
|
| 2026-08-14 | `DEC-002` | 확정 | 구현 범위는 string resource 값 변경으로 한정한다. | 사용자 요청과 최소 변경 원칙 | `P1-T1`, `P1-GATE` |
|
||||||
|
|
||||||
|
## 발견된 문제
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 없음 | Low | 해결 | 문서 작성 시점에 별도 문제 없음 | 없음 | 없음 |
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
기존 기록을 삭제하거나 덮어쓰지 않고 실제 실행 결과를 차수별로 누적한다.
|
||||||
|
|
||||||
|
### 문서 작성 1차 실행 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: PRD와 구현 계획/TASK 문서를 생성했다.
|
||||||
|
- 왜: 저장소 규칙상 구현 전에 요구사항과 실행 계획을 확정해야 하기 때문이다.
|
||||||
|
- 어떻게:
|
||||||
|
- `rg -n "팬Talk|FanTalk" "app/src/main/res/values" "app/src/main/res/values-en" "app/src/main/res/values-ja"` — 변경 후보 확인.
|
||||||
|
- `rg -n "creator_channel_(tab|section)_fantalk|creator_channel_fantalk_error_message" "app/src/main/res/values" "app/src/main/res/values-en" "app/src/main/res/values-ja"` — 기본/영어/일본어의 세 key 확인.
|
||||||
|
- 남은 항목: `P1-T1`, `P1-GATE` 실행.
|
||||||
|
- 다음 행동: 사용자 승인 후 계획의 `P1-T1`부터 resource 값을 수정한다.
|
||||||
|
|
||||||
|
### `P1-T1` 1차 실행 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: 기본/영어/일본어의 세 FanTalk 표시 resource 값을 PRD의 팬레터 표현으로 변경했다.
|
||||||
|
- 왜: 사용자에게 노출되는 같은 기능명을 locale별로 일관되게 표시하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- 변경 전 이전 표현 검색 — 한국어 3건, 영어 3건, 일본어 3건을 확인했다.
|
||||||
|
- 변경 후 대상 key 검색 — 한국어 `팬레터`, 영어 `Fan Letter`, 일본어 `ファンレター`와 각 오류 문구가 PRD 표와 일치했다.
|
||||||
|
- 변경 후 이전 표현 검색 — 출력 없음으로 통과했다.
|
||||||
|
- 남은 항목: `P1-GATE`.
|
||||||
|
- 다음 행동: resource merge와 공백 검증을 실행한다.
|
||||||
|
|
||||||
|
### `P1-GATE` 1차 실행 — 2026-08-14
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: resource merge, 공백 검사와 인접 debug unit test 상태를 확인했다.
|
||||||
|
- 왜: XML resource 유효성과 직접 영향 범위의 회귀 여부를 판정하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew :app:mergeDebugResources` — `BUILD SUCCESSFUL in 8s`, 7개 task 중 2개 실행·5개 up-to-date.
|
||||||
|
- `git diff --check` — 출력 없음으로 통과했다.
|
||||||
|
- `./gradlew :app:testDebugUnitTest` — 1,218개 중 기존 범위의 2개 테스트가 실패했다. `CreatorChannelHomeViewModelTest` 후원 재로딩 실패와 `CreatorChannelFanTalkFragmentLayoutTest`의 일본어 empty 문구 기대값 불일치다.
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest"` — 6개 중 1개가 같은 기존 불일치로 실패했다. 실패 지점은 변경 대상 세 key가 아닌 `creator_channel_fantalk_empty_message` 기대값이다.
|
||||||
|
- 남은 항목: 없음.
|
||||||
|
- 다음 행동: 없음.
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
|
||||||
|
- 2026-08-14: 문서 생성 단계에서는 production resource를 수정하지 않았다. 구현 검증은 `P1-T1`과 `P1-GATE` 실행 시 누적한다.
|
||||||
|
- 2026-08-14: 문서 placeholder 검증은 샘플 placeholder와 미완성 표시 패턴을 대상으로 실행했고 출력 없음으로 통과했다.
|
||||||
|
- 2026-08-14: 문서 변경 후 `git diff --check`는 출력 없음으로 통과했고, `./gradlew tasks --all`은 `BUILD SUCCESSFUL`로 통과했다.
|
||||||
|
- 2026-08-14: `P1-T1` 변경 후 세 locale의 대상 값 일치와 이전 표현 0건을 확인했다. `:app:mergeDebugResources`와 `git diff --check`는 통과했다.
|
||||||
|
- 2026-08-14: debug unit suite는 1,218개 중 기존 범위의 2개가 실패했다. 이번 변경 대상과 직접 관련된 resource merge는 통과했고, FanTalk focused 실패 assertion은 변경하지 않은 일본어 empty 문구 기대값 불일치로 확인했다.
|
||||||
|
- 2026-08-14: Phase 1 독립 리뷰는 승인된 9개 resource 값, 범위 준수, PRD/plan 완료 증거를 확인하고 blocking 없음으로 `APPROVE`했다. 결과는 `reviews/phase1-fan-letter-resource-review.md`에 기록했다.
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# PRD: 팬Talk 팬레터 Resource 문구 변경
|
||||||
|
|
||||||
|
## 문서 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 문서 상태 | 구현 완료 |
|
||||||
|
| 작성일 | `2026-08-14` |
|
||||||
|
| 최종 수정일 | `2026-08-14` |
|
||||||
|
| 대상 제품 | 크리에이터 채널 FanTalk 표시 문구 |
|
||||||
|
| 작성자·결정권자 | 사용자 요청 기준 |
|
||||||
|
| 관련 API Contract | 없음 |
|
||||||
|
| 관련 구현 계획 | `docs/20260814_팬Talk_팬레터_resource_문구_변경/plan-task.md` |
|
||||||
|
| 관련 review | `docs/20260814_팬Talk_팬레터_resource_문구_변경/reviews/phase1-fan-letter-resource-review.md` |
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
크리에이터 채널의 FanTalk 기능명 중 사용자에게 노출되는 Android string resource 값을 `팬레터` 계열 표현으로 변경한다. 기능 구조, resource key, Kotlin class/package, layout id는 유지하고 표시 문구만 교체한다.
|
||||||
|
|
||||||
|
## 2. Problem Statement
|
||||||
|
|
||||||
|
현재 크리에이터 채널 일부 resource가 `팬Talk`, `팬톡`, `FanTalk`, `ファントーク` 표현을 사용한다. 서비스 명칭을 `팬레터`로 바꾸려면 한국어, 영어, 일본어 사용자에게 보이는 같은 기능명이 일관되게 표시되어야 한다.
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
|
||||||
|
- 한국어 기본 resource에서 FanTalk 기능명 표현을 `팬레터`로 통일한다.
|
||||||
|
- 영어 resource에서 FanTalk 기능명 표현을 `Fan Letter`로 통일한다.
|
||||||
|
- 일본어 resource에서 FanTalk 기능명 표현을 `ファンレター`로 통일한다.
|
||||||
|
- resource merge로 XML 문법과 placeholder 보존 여부를 확인한다.
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
|
||||||
|
- `creator_channel_*_fantalk` resource key를 rename하지 않는다.
|
||||||
|
- `CreatorChannelFanTalk*` Kotlin class, package, method, layout id, drawable name을 rename하지 않는다.
|
||||||
|
- 기존 PRD/plan-task의 과거 이력 문구를 일괄 치환하지 않는다.
|
||||||
|
- API, DTO, repository, ViewModel 동작을 변경하지 않는다.
|
||||||
|
- UI/계측 테스트, 기기·에뮬레이터 조작, 스크린샷 검증은 수행하지 않는다.
|
||||||
|
|
||||||
|
## 5. Target Users and Permissions
|
||||||
|
|
||||||
|
| 사용자 | 목표 | 주요 작업 | 사용 환경 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 크리에이터 채널 방문자 | FanTalk 영역을 새 명칭으로 인지 | 탭/홈 섹션/오류 문구 확인 | Android 앱 |
|
||||||
|
|
||||||
|
권한 정책은 변경하지 않는다. 기존 FanTalk 조회 권한과 오류 처리는 그대로 따른다.
|
||||||
|
|
||||||
|
## 6. 기능 요구사항
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `I18N-FANLETTER-001` | 확정 | 기본 한국어 resource의 FanTalk 기능명 노출 문구를 `팬레터`로 변경한다. | `app/src/main/res/values/strings.xml`에서 관련 값에 `팬Talk` 또는 `팬톡`이 남지 않는다. | `P1-T1` |
|
||||||
|
| `I18N-FANLETTER-002` | 확정 | 영어 resource의 FanTalk 기능명 노출 문구를 `Fan Letter`로 변경한다. | `app/src/main/res/values-en/strings.xml`에서 관련 값에 `FanTalk`가 남지 않는다. | `P1-T1` |
|
||||||
|
| `I18N-FANLETTER-003` | 확정 | 일본어 resource의 FanTalk 기능명 노출 문구를 `ファンレター`로 변경한다. | `app/src/main/res/values-ja/strings.xml`에서 관련 값에 `ファントーク`가 남지 않는다. | `P1-T1` |
|
||||||
|
| `I18N-FANLETTER-004` | 확정 | resource 값 변경 외 식별자와 동작을 보존한다. | Kotlin, layout id, drawable, API 관련 파일은 수정하지 않는다. | `P1-GATE` |
|
||||||
|
|
||||||
|
## 7. 변경 대상 Resource
|
||||||
|
|
||||||
|
현재 확인된 변경 대상은 다음 string name으로 한정한다.
|
||||||
|
|
||||||
|
| string name | 한국어 값 | 영어 값 | 일본어 값 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `creator_channel_tab_fantalk` | `팬레터` | `Fan Letter` | `ファンレター` |
|
||||||
|
| `creator_channel_section_fantalk` | `팬레터` | `Fan Letter` | `ファンレター` |
|
||||||
|
| `creator_channel_fantalk_error_message` | `팬레터를 불러오지 못했습니다.` | `Could not load Fan Letter.` | `ファンレターを読み込めませんでした。` |
|
||||||
|
|
||||||
|
## 8. 성공 기준
|
||||||
|
|
||||||
|
- [x] 기본/영어/일본어 resource의 변경 대상 값이 위 표와 일치한다.
|
||||||
|
- [x] `app/src/main/res/values*`의 변경 대상 범위에 이전 기능명 표현이 남지 않는다.
|
||||||
|
- [x] `./gradlew :app:mergeDebugResources`가 `BUILD SUCCESSFUL`로 통과한다.
|
||||||
|
- [x] `git diff --check`가 공백 오류 없이 통과한다.
|
||||||
|
|
||||||
|
## 9. Open Questions
|
||||||
|
|
||||||
|
| ID | 상태 | 결정 필요 사항 | 현재 권고 | 결정 주체 | 결정 기한/시점 | 영향 Goal |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 없음 | 확정 | 추가 질문 없음 | 확정 범위대로 진행 | 사용자 | 2026-08-14 | `P1-T1` |
|
||||||
|
|
||||||
|
## 10. Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-08-14 | `DEC-001` | 확정 | `팬Talk`, `팬톡`, `FanTalk`, `ファントーク`를 같은 기능명 표현으로 보고 각 언어의 팬레터 표현으로 통일한다. | 사용자 A안 선택 | `I18N-FANLETTER-001~003`, `P1-T1` |
|
||||||
|
| 2026-08-14 | `DEC-002` | 확정 | 변경 범위는 string resource 값으로 한정하고 코드 식별자와 과거 문서 이력은 변경하지 않는다. | 사용자 요청이 `string resource` 변경이며 최소 변경 원칙을 적용 | `I18N-FANLETTER-004`, `P1-GATE` |
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Phase 1 Fan Letter Resource 코드 리뷰 보고서
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 1 / `P1-T1`, `P1-GATE` |
|
||||||
|
| 기준 commit 또는 working tree | `HEAD` 대비 working tree 전체 변경 |
|
||||||
|
| 리뷰 일자 | `2026-08-14` |
|
||||||
|
| 리뷰어 | Oracle read-only reviewer |
|
||||||
|
| 기준 문서 | `docs/20260814_팬Talk_팬레터_resource_문구_변경/prd.md`, `docs/20260814_팬Talk_팬레터_resource_문구_변경/plan-task.md` |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
### 목적
|
||||||
|
|
||||||
|
- 기본/영어/일본어의 지정된 세 resource key가 승인된 팬레터 표현과 일치하는지 확인한다.
|
||||||
|
- resource-only 변경 범위와 PRD/plan 완료 기록이 실제 검증 증거와 일치하는지 확인한다.
|
||||||
|
|
||||||
|
### 포함 범위
|
||||||
|
|
||||||
|
- 코드: `app/src/main/res/values*/strings.xml`의 지정된 세 key
|
||||||
|
- 테스트: `:app:mergeDebugResources`, debug unit suite와 FanTalk focused test 결과
|
||||||
|
- 문서: PRD 성공 기준, `P1-T1`, `P1-GATE`, Progress와 Verification Log
|
||||||
|
|
||||||
|
### 제외 범위
|
||||||
|
|
||||||
|
- Resource key와 Kotlin/layout/drawable 식별자 rename
|
||||||
|
- API, DTO, repository, ViewModel 동작
|
||||||
|
- UI/계측 테스트, 기기·에뮬레이터 조작, 스크린샷 검증
|
||||||
|
|
||||||
|
## 3. 검토한 근거
|
||||||
|
|
||||||
|
### 문서와 코드
|
||||||
|
|
||||||
|
- 요구사항: `I18N-FANLETTER-001~004`
|
||||||
|
- 계획: `P1-T1`, `P1-GATE`
|
||||||
|
- 코드: `app/src/main/res/values/strings.xml`, `values-en/strings.xml`, `values-ja/strings.xml`
|
||||||
|
- 인접 테스트: `CreatorChannelFanTalkFragmentLayoutTest`
|
||||||
|
|
||||||
|
### 실행한 검증
|
||||||
|
|
||||||
|
| 명령 또는 검증 | 결과 | 핵심 증거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 대상 세 key `rg` 검색 | 성공 | 세 locale의 9개 값이 PRD 표와 일치 |
|
||||||
|
| 이전 표현 guarded `rg` 검색 | 성공 | 출력 없음 |
|
||||||
|
| `./gradlew :app:mergeDebugResources` | 성공 | `BUILD SUCCESSFUL in 1s` |
|
||||||
|
| `git diff --check HEAD` | 성공 | 출력 없음 |
|
||||||
|
| `./gradlew :app:testDebugUnitTest` | 실패 | 1,218개 중 이번 변경 범위 밖 2개 실패 |
|
||||||
|
| FanTalk focused test | 실패 | 6개 중 변경하지 않은 `creator_channel_fantalk_empty_message` 기대값 1개 실패 |
|
||||||
|
|
||||||
|
## 4. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `REV-P1-001` | Low | 오탐 | FanTalk focused test의 일본어 empty 문구 기대값 불일치 | 범위 밖 | 없음 |
|
||||||
|
| `REV-P1-002` | Low | 오탐 | Creator Channel 후원 재로딩 test 실패 | 범위 밖 | 없음 |
|
||||||
|
|
||||||
|
## 5. 발견 사항 상세
|
||||||
|
|
||||||
|
### REV-P1-001 — FanTalk focused test의 일본어 empty 문구 기대값 불일치
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 오탐
|
||||||
|
- **관련 요구사항:** 없음
|
||||||
|
- **소유 Task:** 없음
|
||||||
|
|
||||||
|
`CreatorChannelFanTalkFragmentLayoutTest.kt:232`는 `creator_channel_fantalk_empty_message`를 검사한다. 이 resource는 `HEAD`와 현재 working tree가 동일하며, 이번 변경 대상인 tab/section/error 세 key에 포함되지 않는다. 따라서 이번 변경의 회귀나 `P1-GATE` 차단 사유가 아니다.
|
||||||
|
|
||||||
|
### REV-P1-002 — Creator Channel 후원 재로딩 test 실패
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 오탐
|
||||||
|
- **관련 요구사항:** 없음
|
||||||
|
- **소유 Task:** 없음
|
||||||
|
|
||||||
|
실패 영역은 ViewModel 후원 재로딩 동작이며 이번 diff에는 Kotlin/ViewModel 변경이 없다. PRD가 API, repository, ViewModel 동작을 제외하므로 이번 Phase의 차단 사유가 아니다.
|
||||||
|
|
||||||
|
## 6. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 7. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 세 locale의 resource 값과 전체 diff 확인 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | test 실패 2건을 범위 밖 기존 불일치로 판정 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 항목 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | resource merge, rg, diff와 test 결과 기록 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음. `APPROVE`.
|
||||||
|
|
||||||
|
**남은 항목:** 없음.
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
# 팔로잉 해제 확인과 버튼 UI 구현 계획
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use `subagent-driven-development` or `executing-plans` to implement this plan task-by-task. 이 문서는 구현 계획이며, 현재 요청에서는 코드 구현을 수행하지 않는다.
|
||||||
|
|
||||||
|
**Goal:** 팔로잉 리스트와 크리에이터 채널에서 팔로우 해제 전 `V2ModalDialog` 확인을 거치고, 팔로잉 리스트의 팔로잉 중 버튼을 크리에이터 채널과 같은 원형 스타일로 맞춘다.
|
||||||
|
|
||||||
|
**Architecture:** 기존 `V2ModalDialog`와 기존 follow/unfollow delegate를 재사용한다. 팔로잉 리스트는 legacy 화면이지만 요청 동작을 위해 `FollowingCreatorActivity`, `FollowingCreatorAdapter`, `item_follower_list.xml`에만 최소 변경을 계획한다.
|
||||||
|
|
||||||
|
**Tech Stack:** Android Kotlin, XML layout/drawable/string resources, RxJava 기반 기존 ViewModel 흐름, `app/src/test` local/source tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| 문서 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 상태 | 구현 완료 |
|
||||||
|
| 작성일 | `2026-08-18` |
|
||||||
|
| 요구사항 기준 | `docs/20260818_팔로잉_해제_확인과_버튼_UI/prd.md` |
|
||||||
|
| API 기준 | 변경 없음 |
|
||||||
|
| 현재 Phase | 완료 |
|
||||||
|
| 현재 활성 Goal | 없음 |
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
사용자가 팔로잉 리스트 또는 크리에이터 채널에서 팔로우 해제를 시도하면 확인 Dialog를 본 뒤 명시적으로 확인한 경우에만 기존 팔로우 해제 프로세스가 실행된다.
|
||||||
|
|
||||||
|
## 현재 상태
|
||||||
|
|
||||||
|
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|
||||||
|
|---:|---|---:|---|---|
|
||||||
|
| 1 | 완료 | `3/3` | 없음 | 없음 |
|
||||||
|
| 2 | 완료 | `1/1` | 없음 | 없음 |
|
||||||
|
| 3 | 완료 | `2/2` | 없음 | 없음 |
|
||||||
|
| 4 | 완료 | `1/1` | 없음 | 없음 |
|
||||||
|
| 5 | 완료 | `1/1` | 없음 | 없음 |
|
||||||
|
|
||||||
|
## 범위
|
||||||
|
|
||||||
|
### 포함
|
||||||
|
|
||||||
|
- 팔로잉 리스트 팔로잉 중 버튼의 icon/background/shape 변경 계획.
|
||||||
|
- 팔로잉 리스트 팔로잉 중 버튼의 Bottom Sheet route 제거 계획.
|
||||||
|
- 팔로잉 리스트와 크리에이터 채널의 팔로우 해제 확인 Dialog 적용 계획.
|
||||||
|
- 한국어, 일본어, 영어 string resource 추가 계획.
|
||||||
|
- `app/src/test` 기반 local/source test 계획.
|
||||||
|
|
||||||
|
### 제외
|
||||||
|
|
||||||
|
- 이 문서 작성 작업에서의 코드 구현.
|
||||||
|
- `CreatorFollowNotifyFragment` 자체 수정 또는 삭제.
|
||||||
|
- API, Repository, ViewModel 계약 변경.
|
||||||
|
- 신규 dependency 또는 공통 abstraction 추가.
|
||||||
|
- `app/src/androidTest`, `connectedDebugAndroidTest`, 기기·에뮬레이터 UI 조작, 스크린샷·시각 QA.
|
||||||
|
- 요청 범위 밖 레거시 리팩터링.
|
||||||
|
|
||||||
|
## 기술적 제약
|
||||||
|
|
||||||
|
- 기존 `V2ModalDialog` 생성자와 `show(screenWidth)` 패턴을 재사용한다.
|
||||||
|
- Dialog description은 string resource의 `%1$s`에 크리에이터 이름을 넣어 만든다.
|
||||||
|
- 팔로잉 리스트 legacy 파일 변경은 요청 동작에 필요한 최소 범위로 제한한다.
|
||||||
|
- 테스트는 source/local test로 route, resource 선택, callback 호출 여부를 검증한다.
|
||||||
|
- layout 크기, constraint, pixel-perfect 결과는 자동 테스트 대상으로 삼지 않는다.
|
||||||
|
|
||||||
|
## 파일 구조
|
||||||
|
|
||||||
|
| File | 계획된 역할 |
|
||||||
|
|---|---|
|
||||||
|
| `app/src/main/java/kr/co/vividnext/sodalive/following/FollowingCreatorActivity.kt` | 팔로잉 중 클릭 route를 Bottom Sheet에서 `V2ModalDialog` 확인 후 기존 unfollow 호출로 변경한다. |
|
||||||
|
| `app/src/main/java/kr/co/vividnext/sodalive/following/FollowingCreatorAdapter.kt` | 팔로잉 중 상태의 아이콘/배경 리소스 선택을 변경한다. |
|
||||||
|
| `app/src/main/res/layout/item_follower_list.xml` | 팔로잉 중 버튼에 원형 배경을 적용할 수 있는 구조를 최소 반영한다. |
|
||||||
|
| `app/src/main/res/drawable/bg_following_circle_gray_800.xml` | 팔로잉 리스트 팔로잉 중 버튼의 `gray_800` 원형 배경을 제공한다. |
|
||||||
|
| `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` | 팔로잉 중 캡슐 클릭 시 즉시 unfollow하지 않고 Dialog 확인 후 기존 delegate를 호출한다. |
|
||||||
|
| `app/src/main/java/kr/co/vividnext/sodalive/v2/components/modal/V2ModalDialog.kt` | 수정하지 않고 재사용한다. 기존 API 한계가 증명될 때만 계획 갱신 후 수정한다. |
|
||||||
|
| `app/src/main/res/values/strings.xml` | 한국어 Dialog 문구를 추가한다. |
|
||||||
|
| `app/src/main/res/values-en/strings.xml` | 영어 Dialog 문구를 추가한다. |
|
||||||
|
| `app/src/main/res/values-ja/strings.xml` | 일본어 Dialog 문구를 추가한다. |
|
||||||
|
| `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorAdapterSourceTest.kt` | 팔로잉 중 버튼 리소스 계약을 검증한다. 없으면 생성한다. |
|
||||||
|
| `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorActivitySourceTest.kt` | 팔로잉 리스트 route 계약을 검증한다. 없으면 생성한다. |
|
||||||
|
| `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` | 크리에이터 채널 즉시 unfollow 계약을 Dialog 확인 계약으로 갱신한다. |
|
||||||
|
| `app/src/test/java/kr/co/vividnext/sodalive/v2/components/modal/V2ModalDialogTest.kt` | Dialog callback, 버튼, description 회귀를 확인한다. |
|
||||||
|
| `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModelTest.kt` | 기존 unfollow 성공/실패 흐름 회귀를 확인한다. |
|
||||||
|
|
||||||
|
## Phase 1. 테스트로 현재 동작 고정
|
||||||
|
|
||||||
|
**Phase 결과:** 기존 구현이 요청 동작과 다른 지점을 local/source test로 먼저 드러낸다.
|
||||||
|
|
||||||
|
**선행조건:** `prd.md`의 `UNFOLLOW-001~006` 확정.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P1-T1`~`P1-T3` 완료, 각 RED 확인 기록 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 1.1 팔로잉 리스트 버튼 표현 계약
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T1`:** 팔로잉 리스트 팔로잉 중 버튼이 요청 리소스를 사용해야 함을 source/local test로 고정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `UNFOLLOW-001`, `UNFOLLOW-002` 확정.
|
||||||
|
- **완료 증거:** test가 기존 `btn_following_big`, `btn_following_no_alarm_big` 사용 때문에 실패한다.
|
||||||
|
- **범위 밖:** 실제 adapter/layout 구현 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorAdapterSourceTest.kt` if no equivalent exists
|
||||||
|
- Modify: equivalent existing following adapter source/local test if present
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorAdapterSourceTest.kt`
|
||||||
|
|
||||||
|
- [x] **RED:** `FollowingCreatorAdapter.kt`가 팔로잉 중 branch에서 `R.drawable.ic_new_following_white`와 `R.drawable.bg_following_circle_gray_800`을 참조하는지 검증하는 source/local test를 작성한다.
|
||||||
|
- [x] **RED 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorAdapterSourceTest"`를 실행해 기존 이미지 리소스 사용 때문에 실패함을 확인했다.
|
||||||
|
- [x] **GREEN:** `P3-T1`에서 production 구현을 완료했다.
|
||||||
|
- [x] **GREEN 확인:** 같은 focused test 성공을 확인했다.
|
||||||
|
- [x] **REFACTOR:** 요청 리소스 계약만 남기고 과도한 검증은 추가하지 않았다.
|
||||||
|
|
||||||
|
#### Task 1.2 팔로잉 리스트 팔로우 해제 route 계약
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T2`:** 팔로잉 리스트 팔로잉 중 클릭이 Bottom Sheet가 아니라 `V2ModalDialog`를 거쳐야 함을 source/local test로 고정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `UNFOLLOW-003`, `UNFOLLOW-004` 확정.
|
||||||
|
- **완료 증거:** test가 `CreatorFollowNotifyFragment` route 잔존과 Dialog 미사용 때문에 실패한다.
|
||||||
|
- **범위 밖:** 실제 Activity 구현 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorActivitySourceTest.kt` if no equivalent exists
|
||||||
|
- Modify: equivalent existing following activity source/local test if present
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorActivitySourceTest.kt`
|
||||||
|
|
||||||
|
- [x] **RED:** `FollowingCreatorActivity.kt`가 팔로잉 중 branch에서 `CreatorFollowNotifyFragment`를 생성하지 않고 `V2ModalDialog`를 생성하는지 검증했다.
|
||||||
|
- [x] **RED:** 확인 callback 안에서만 `viewModel.follow(creatorId, follow = false, notify = false)`가 호출되는지 source/local test로 검증했다.
|
||||||
|
- [x] **RED 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorActivitySourceTest"`를 실행해 현재 Bottom Sheet route 때문에 실패함을 확인했다.
|
||||||
|
- [x] **GREEN:** `P3-T2`에서 production 구현을 완료했다.
|
||||||
|
- [x] **GREEN 확인:** 같은 focused test 성공을 확인했다.
|
||||||
|
- [x] **REFACTOR:** Dialog route와 unfollow callback 검증 외의 과도한 검증은 추가하지 않았다.
|
||||||
|
|
||||||
|
#### Task 1.3 크리에이터 채널 팔로우 해제 확인 계약
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T3`:** 크리에이터 채널 팔로잉 중 캡슐이 즉시 unfollow하지 않고 Dialog 확인을 거쳐야 함을 source/local test로 고정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `UNFOLLOW-005`, `UNFOLLOW-006` 확정.
|
||||||
|
- **완료 증거:** 기존 `onFollowCapsuleClicked()`가 즉시 `homeActionDelegate?.follow(false, false)`를 호출하므로 test가 실패한다.
|
||||||
|
- **범위 밖:** 실제 Activity 구현 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
|
||||||
|
- [x] **RED:** `onFollowCapsuleClicked()`의 팔로잉 중 branch가 `V2ModalDialog`를 생성하고 description에 현재 header nickname을 전달하도록 기대값을 갱신했다.
|
||||||
|
- [x] **RED:** `homeActionDelegate?.follow(follow = false, notify = false)` 호출이 Dialog confirm callback 안에만 있어야 함을 검증했다.
|
||||||
|
- [x] **RED 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.follow notify source는 Phase 11 직접 팔로우 알림 액션을 연결한다"`를 실행해 즉시 unfollow 구현 때문에 실패함을 확인했다.
|
||||||
|
- [x] **GREEN:** `P4-T1`에서 production 구현을 완료했다.
|
||||||
|
- [x] **GREEN 확인:** focused test와 전체 `CreatorChannelActivitySourceTest` 성공을 확인했다.
|
||||||
|
- [x] **REFACTOR:** 기존 title bar, owner, bell 동작 test가 깨지지 않도록 변경 범위를 팔로잉 해제 branch로 제한했다.
|
||||||
|
|
||||||
|
## Phase 2. 공통 Dialog 문구 리소스 추가
|
||||||
|
|
||||||
|
**Phase 결과:** 두 화면이 같은 string resource 계약으로 Dialog 문구를 사용할 수 있다.
|
||||||
|
|
||||||
|
**선행조건:** Phase 1 RED 확인 완료.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P2-T1` 완료와 locale resource 검증 기록 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 2.1 다국어 string resource 추가
|
||||||
|
|
||||||
|
**Goal 실행 `P2-T1`:** 팔로우 해제 확인 Dialog 문구를 한국어, 영어, 일본어 resource로 추가한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `DEC-001` 한국어 문구 확정.
|
||||||
|
- **완료 증거:** 세 locale 파일 모두 같은 resource name과 `%1$s` placeholder를 가진다.
|
||||||
|
- **범위 밖:** Activity에서 resource를 소비하는 구현.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/res/values/strings.xml`
|
||||||
|
- Modify: `app/src/main/res/values-en/strings.xml`
|
||||||
|
- Modify: `app/src/main/res/values-ja/strings.xml`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorActivitySourceTest.kt` or equivalent resource/source test
|
||||||
|
|
||||||
|
- [x] **RED:** 세 locale에 `creator_unfollow_confirm_title`, `creator_unfollow_confirm_desc`, `creator_unfollow_confirm_cancel`, `creator_unfollow_confirm_confirm`가 없어서 실패하는 source/local test를 작성했다.
|
||||||
|
- [x] **RED 확인:** `FollowingCreatorActivitySourceTest`에서 resource 누락 실패를 확인했다.
|
||||||
|
- [x] **GREEN:** 한국어 `팔로우 해제`, `팔로우를 해제할까요?\n%1$s의 소식을 받을 수 없어요.`, `취소`, `해제하기`를 추가했다.
|
||||||
|
- [x] **GREEN:** 영어 `Unfollow`, `You’ll no longer receive updates from %1$s.\nDo you want to unfollow?`, `Cancel`, `Unfollow`를 추가했다.
|
||||||
|
- [x] **GREEN:** 일본어 `フォロー解除`, `%1$sからの最新情報を受け取れなくなります。\nフォローを解除しますか?`, `キャンセル`, `解除する`를 추가했다.
|
||||||
|
- [x] **GREEN 확인:** focused test를 다시 실행해 resource 계약 성공을 확인했다.
|
||||||
|
- [x] **REFACTOR:** Activity에는 resource 참조만 남겼다.
|
||||||
|
|
||||||
|
## Phase 3. 팔로잉 리스트 변경
|
||||||
|
|
||||||
|
**Phase 결과:** 팔로잉 리스트의 팔로잉 중 버튼 UI와 팔로우 해제 route가 요구사항과 일치한다.
|
||||||
|
|
||||||
|
**선행조건:** Phase 1, Phase 2 완료.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P3-T1`, `P3-T2` 완료와 focused test 성공 기록 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 3.1 팔로잉 중 버튼 UI 변경
|
||||||
|
|
||||||
|
**Goal 실행 `P3-T1`:** 팔로잉 리스트 팔로잉 중 버튼을 요청 아이콘과 원형 `gray_800` 배경으로 표시한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T1`, `P2-T1` 완료.
|
||||||
|
- **완료 증거:** `P1-T1` focused test 성공.
|
||||||
|
- **범위 밖:** 팔로우 시작 버튼, 프로필 이미지, nickname layout 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/following/FollowingCreatorAdapter.kt`
|
||||||
|
- Modify: `app/src/main/res/layout/item_follower_list.xml`
|
||||||
|
- Create: `app/src/main/res/drawable/bg_following_circle_gray_800.xml`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorAdapterSourceTest.kt`
|
||||||
|
|
||||||
|
- [x] **RED:** `P1-T1` test 실패 상태를 확인했다.
|
||||||
|
- [x] **RED 확인:** 실패 원인이 팔로잉 중 리소스 불일치임을 기록했다.
|
||||||
|
- [x] **GREEN:** `item.isFollow` branch에서 `binding.ivFollow.setImageResource(R.drawable.ic_new_following_white)`를 사용하도록 최소 변경했다.
|
||||||
|
- [x] **GREEN:** `ivFollow`에 `gray_800`과 `100dp` radius를 사용하는 `bg_following_circle_gray_800`을 적용하고, 팔로잉 중에만 `8dp` padding을 적용했다.
|
||||||
|
- [x] **GREEN 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorAdapterSourceTest"` 성공을 확인했다.
|
||||||
|
- [x] **REFACTOR:** 알림 on/off 분기의 팔로잉 중 이미지 중복을 제거했다.
|
||||||
|
|
||||||
|
#### Task 3.2 팔로잉 리스트 Bottom Sheet route 제거와 확인 Dialog 적용
|
||||||
|
|
||||||
|
**Goal 실행 `P3-T2`:** 팔로잉 리스트 팔로잉 중 클릭이 `V2ModalDialog` 확인 후에만 기존 unfollow를 실행한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T2`, `P2-T1` 완료.
|
||||||
|
- **완료 증거:** `P1-T2` focused test 성공.
|
||||||
|
- **범위 밖:** 팔로우 시작 branch와 알림 설정 Bottom Sheet 자체 수정.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/following/FollowingCreatorActivity.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorActivitySourceTest.kt`
|
||||||
|
|
||||||
|
- [x] **RED:** `P1-T2` test 실패 상태를 확인했다.
|
||||||
|
- [x] **RED 확인:** 실패 원인이 `CreatorFollowNotifyFragment` route와 Dialog 미사용임을 기록했다.
|
||||||
|
- [x] **GREEN:** `onClickUnfollow` callback으로 클릭 대상 creator name을 전달하는 최소 경로를 정했다.
|
||||||
|
- [x] **GREEN:** 팔로잉 리스트 Activity의 팔로잉 중 branch에서 `CreatorFollowNotifyFragment` 생성과 `show()` 호출을 제거했다.
|
||||||
|
- [x] **GREEN:** 팔로잉 중 branch에서 `V2ModalDialog`를 생성해 `show(screenWidth)`로 표시하고 confirm callback에서만 기존 unfollow를 호출했다.
|
||||||
|
- [x] **GREEN 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorActivitySourceTest"` 성공을 확인했다.
|
||||||
|
- [x] **REFACTOR:** 사용하지 않게 된 import와 이번 변경이 만든 중복만 제거했다.
|
||||||
|
|
||||||
|
## Phase 4. 크리에이터 채널 변경
|
||||||
|
|
||||||
|
**Phase 결과:** 크리에이터 채널 팔로잉 중 캡슐이 확인 Dialog 없이 바로 팔로우 해제하지 않는다.
|
||||||
|
|
||||||
|
**선행조건:** Phase 1, Phase 2 완료.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P4-T1` 완료와 focused test 성공 기록 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 4.1 크리에이터 채널 팔로우 해제 확인 Dialog 적용
|
||||||
|
|
||||||
|
**Goal 실행 `P4-T1`:** `CreatorChannelActivity.onFollowCapsuleClicked()` 팔로잉 중 branch에서 Dialog 확인 후 기존 delegate를 호출한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T3`, `P2-T1` 완료.
|
||||||
|
- **완료 증거:** `CreatorChannelActivitySourceTest` 성공.
|
||||||
|
- **범위 밖:** 팔로우 시작 branch, 알림 bell 토글, owner 상태 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
|
||||||
|
- [x] **RED:** `P1-T3` test 실패 상태를 확인했다.
|
||||||
|
- [x] **RED 확인:** 실패 원인이 즉시 `homeActionDelegate?.follow(false, false)` 호출임을 기록했다.
|
||||||
|
- [x] **GREEN:** `!header.isFollow` branch는 기존 즉시 follow 동작으로 유지했다.
|
||||||
|
- [x] **GREEN:** `header.isFollow` branch에서 `V2ModalDialog`를 생성하고 `desc = getString(R.string.creator_unfollow_confirm_desc, header.nickname)`를 전달했다.
|
||||||
|
- [x] **GREEN:** confirm callback 안으로 기존 `homeActionDelegate?.follow(follow = false, notify = false)`를 이동했다.
|
||||||
|
- [x] **GREEN 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 성공을 확인했다.
|
||||||
|
- [x] **REFACTOR:** 기존 `V2ModalDialog` 사용 패턴과 같은 방식으로만 추가했다.
|
||||||
|
|
||||||
|
## Phase 5. 검증
|
||||||
|
|
||||||
|
**Phase 결과:** 요청 범위의 local/source test와 관련 회귀가 통과한다.
|
||||||
|
|
||||||
|
**선행조건:** Phase 1~4 완료.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P5-GATE` 완료와 Verification Log 기록.
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Phase 5 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P5-GATE`:** 팔로잉 리스트와 크리에이터 채널 팔로우 해제 확인 흐름을 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** 모든 구현 Task 완료.
|
||||||
|
- **완료 증거:** 아래 명령과 Expected 통과, Progress와 Verification Log 기록.
|
||||||
|
- **범위 밖:** 실패와 무관한 기능 수정, test 삭제·완화.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorAdapterSourceTest"
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorActivitySourceTest"
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialogTest"
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** 모든 명령이 exit code 0으로 종료되고, 팔로잉 리스트 Bottom Sheet route 제거, 양 화면 `V2ModalDialog` 확인 후 unfollow, 다국어 resource 계약이 검증된다.
|
||||||
|
|
||||||
|
## 실행 순서와 의존성
|
||||||
|
|
||||||
|
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|
||||||
|
|---:|---|---|---|---|
|
||||||
|
| 1 | `P1-T1` | PRD 확정 | 예, `P1-T2`, `P1-T3`와 병행 가능 | test 대상 파일 재확인 |
|
||||||
|
| 2 | `P1-T2` | PRD 확정 | 예, `P1-T1`, `P1-T3`와 병행 가능 | following activity route 재확인 |
|
||||||
|
| 3 | `P1-T3` | PRD 확정 | 예, `P1-T1`, `P1-T2`와 병행 가능 | creator channel source test 재확인 |
|
||||||
|
| 4 | `P2-T1` | Phase 1 RED 확인 | 아니요 | resource name 충돌 확인 |
|
||||||
|
| 5 | `P3-T1` | `P1-T1`, `P2-T1` | 예, `P4-T1`과 병행 가능 | adapter/layout 책임 재확인 |
|
||||||
|
| 6 | `P3-T2` | `P1-T2`, `P2-T1` | 아니요 | creator name 전달 경로 재확인 |
|
||||||
|
| 7 | `P4-T1` | `P1-T3`, `P2-T1` | 예, `P3-T1`과 병행 가능 | currentHeader nickname 사용 가능성 재확인 |
|
||||||
|
| 8 | `P5-GATE` | Phase 3, Phase 4 완료 | 아니요 | 실패 소유 Task로 회귀 수정 Task 추가 |
|
||||||
|
|
||||||
|
```text
|
||||||
|
P1-T1 ┐
|
||||||
|
P1-T2 ├→ P2-T1 → P3-T1 → P3-T2 ┐
|
||||||
|
P1-T3 ┘ └→ P4-T1 ───────┴→ P5-GATE
|
||||||
|
```
|
||||||
|
|
||||||
|
## 변경 금지 항목
|
||||||
|
|
||||||
|
- 코드 구현 승인 없이 production/test 파일을 수정하지 않는다.
|
||||||
|
- `CreatorFollowNotifyFragment`를 삭제하거나 다른 화면의 사용처를 변경하지 않는다.
|
||||||
|
- API, DTO, Repository, ViewModel 계약을 근거 없이 변경하지 않는다.
|
||||||
|
- 신규 dependency 또는 단일 사용 abstraction을 추가하지 않는다.
|
||||||
|
- test를 삭제·skip·완화하지 않는다.
|
||||||
|
- 기존 완료 기록과 Decision Log를 삭제하거나 덮어쓰지 않는다.
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
기존 기록을 삭제하거나 덮어쓰지 않고 실제 실행 결과를 차수별로 누적한다.
|
||||||
|
|
||||||
|
### 문서 작성 1차 실행 — 2026-08-18
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: PRD와 구현 계획 문서를 작성했다.
|
||||||
|
- 왜: 사용자 요청이 코드 구현 없이 문서 생성까지만 요구했기 때문이다.
|
||||||
|
- 어떻게:
|
||||||
|
- `docs/sample/sample-prd.md` 확인 — PRD 구조 기준 확인.
|
||||||
|
- `docs/sample/sample-plan-task.md` 확인 — 계획/TASK 구조 기준 확인.
|
||||||
|
- `codegraph`로 `FollowingCreatorActivity.kt`, `FollowingCreatorAdapter.kt`, `CreatorChannelActivity.kt`, `V2ModalDialog.kt` 관련 흐름 확인.
|
||||||
|
- 남은 항목: 코드 구현은 사용자 승인 전까지 대기.
|
||||||
|
- 다음 행동: 사용자가 구현을 요청하면 `P1-T1`부터 진행한다.
|
||||||
|
|
||||||
|
## Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `2026-08-18` | `DEC-001` | 확정 | 한국어 본문은 `팔로우를 해제할까요?\n%1$s의 소식을 받을 수 없어요.`를 사용한다. | 사용자 A안 선택 | `P2-T1` |
|
||||||
|
| `2026-08-18` | `DEC-002` | 확정 | 시각 보조 브라우저는 사용하지 않는다. | 사용자 응답 | 문서 작성 방식 |
|
||||||
|
| `2026-08-18` | `DEC-003` | 확정 | 현재 요청에서는 코드 구현을 하지 않는다. | 사용자 명시 요청 | 전체 계획 |
|
||||||
|
|
||||||
|
## 발견된 문제
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 없음 | Low | 해결 | 문서 작성 중 구현 차단 이슈 없음 | 없음 | 없음 |
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
|
||||||
|
- 2026-08-18: 문서 생성 작업만 수행했다. 사용자 요청에 따라 production/test 코드 구현과 Gradle test 실행은 수행하지 않았다.
|
||||||
|
- 2026-08-18: 사용자 승인 후 문서대로 구현했다. `visual-qa`, 기기·에뮬레이터 UI 조작, 스크린샷 검증은 사용자 요청에 따라 수행하지 않았고, 사용자가 직접 확인할 수 있도록 남겼다.
|
||||||
|
- 2026-08-18: RED 확인 - `FollowingCreatorAdapterSourceTest`는 기존 팔로잉 중 리소스 불일치로 실패했다. `FollowingCreatorActivitySourceTest`는 `CreatorFollowNotifyFragment` route와 locale resource 누락으로 실패했다. `CreatorChannelActivitySourceTest.follow notify source는 Phase 11 직접 팔로우 알림 액션을 연결한다`는 즉시 unfollow 구현 때문에 실패했다.
|
||||||
|
- 2026-08-18: GREEN 확인 - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorAdapterSourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorActivitySourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialogTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest"` 모두 성공했다.
|
||||||
|
- 2026-08-18: 회귀 검증 - `./gradlew :app:testDebugUnitTest`, `./gradlew :app:ktlintCheck`, `./gradlew :app:assembleDebug`, `git diff --check` 모두 성공했다. 병렬 Gradle 실행 중 한 차례 test result XML 쓰기 충돌이 있었고, 같은 명령을 순차 재실행해 성공했다.
|
||||||
|
- 2026-08-18: 리뷰 게이트 후속 - 영어 문구가 PRD의 `You’ll`과 정확히 일치하는지 검증하는 test를 추가했다. RED에서 ASCII apostrophe 문구 불일치로 실패를 확인한 뒤 resource를 PRD와 맞췄고, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorActivitySourceTest"`, `./gradlew :app:ktlintCheck`, `git diff --check`, `./gradlew :app:testDebugUnitTest`, `./gradlew :app:assembleDebug` 성공을 확인했다.
|
||||||
|
- 2026-08-18: 사용자 수동 테스트 완료 - 사용자가 팔로잉 리스트와 크리에이터 채널의 실제 UI 흐름 수동 테스트를 완료했다고 보고했다. PRD 요구사항 추적표의 수동 검증 상태와 Phase 5 남은 조건을 완료로 갱신했다.
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# 팔로잉 해제 확인과 버튼 UI PRD
|
||||||
|
|
||||||
|
## 문서 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 문서 상태 | 구현 기준 확정 |
|
||||||
|
| 작성일 | `2026-08-18` |
|
||||||
|
| 최종 수정일 | `2026-08-18` |
|
||||||
|
| 대상 제품 | 팔로잉 리스트, 크리에이터 채널 팔로우/팔로잉 액션 |
|
||||||
|
| 작성자·결정권자 | 사용자 요청 기준 |
|
||||||
|
| 관련 API Contract | 변경 없음 |
|
||||||
|
| 관련 구현 계획 | `docs/20260818_팔로잉_해제_확인과_버튼_UI/plan-task.md` |
|
||||||
|
| 관련 review | 없음 |
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
팔로잉 리스트와 크리에이터 채널에서 팔로잉 해제 동작을 더 명확하고 안전하게 만든다. 팔로잉 리스트의 팔로잉 중 버튼은 크리에이터 채널의 팔로잉 중 버튼과 같은 원형 스타일을 사용하고, 팔로잉 해제 요청은 두 화면 모두 `V2ModalDialog` 확인 후에만 진행한다.
|
||||||
|
|
||||||
|
## 2. Problem Statement
|
||||||
|
|
||||||
|
현재 사용자는 다음 문제를 겪는다.
|
||||||
|
|
||||||
|
- 팔로잉 리스트의 팔로잉 중 버튼 UI가 크리에이터 채널의 팔로잉 중 상태와 다르다.
|
||||||
|
- 팔로잉 리스트에서 팔로잉 중 버튼을 누르면 `CreatorFollowNotifyFragment` Bottom Sheet 액션 메뉴가 표시된다.
|
||||||
|
- 크리에이터 채널에서는 팔로잉 중 버튼을 누르면 확인 없이 바로 팔로우 해제가 진행된다.
|
||||||
|
|
||||||
|
문제를 해결했다는 판단은 팔로잉 리스트와 크리에이터 채널 모두에서 팔로잉 해제 전 `V2ModalDialog`가 표시되고, 확인 버튼을 누른 경우에만 기존 팔로우 해제 API 흐름이 실행되는 것으로 한다.
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
|
||||||
|
### 3.1 제품 목표
|
||||||
|
|
||||||
|
- 팔로잉 리스트의 팔로잉 중 버튼을 `ic_new_following_white`, `gray_800`, 크리에이터 채널과 동일한 원형 스타일로 표시한다.
|
||||||
|
- 팔로잉 리스트의 팔로잉 중 버튼 터치 시 Bottom Sheet 액션 메뉴를 사용하지 않는다.
|
||||||
|
- 팔로잉 리스트와 크리에이터 채널 모두 팔로우 해제 전 한 번 더 확인한다.
|
||||||
|
- 취소 시 팔로우 상태와 알림 상태를 변경하지 않는다.
|
||||||
|
- 확인 시 기존 팔로우 해제 프로세스를 한 번만 실행한다.
|
||||||
|
|
||||||
|
### 3.2 UX 목표
|
||||||
|
|
||||||
|
- destructive action인 팔로우 해제의 영향을 Dialog 문구로 명확히 알린다.
|
||||||
|
- 한국어, 일본어, 영어에서 동일한 의미와 `%1$s` 크리에이터 이름 placeholder 계약을 유지한다.
|
||||||
|
- 팔로우 시작, 알림 토글, 기존 성공/실패 Toast 처리는 이번 변경으로 바꾸지 않는다.
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
|
||||||
|
- 코드 구현은 이 문서 작성 작업의 범위가 아니다.
|
||||||
|
- `CreatorFollowNotifyFragment` 자체를 수정하거나 삭제하지 않는다.
|
||||||
|
- 팔로우/언팔로우 API, Repository, ViewModel 계약을 재설계하지 않는다.
|
||||||
|
- 신규 Dialog 컴포넌트, 신규 dependency, 공통 coordinator abstraction을 추가하지 않는다.
|
||||||
|
- 요청 범위 밖의 레거시 리팩터링을 하지 않는다.
|
||||||
|
- 사용자가 명시적으로 요청하지 않았으므로 `app/src/androidTest`, 기기·에뮬레이터 UI 조작, 스크린샷·시각 QA는 범위에서 제외한다.
|
||||||
|
|
||||||
|
## 5. Target Users and Permissions
|
||||||
|
|
||||||
|
| 사용자 | 목표 | 주요 작업 | 사용 환경 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 로그인 사용자 | 팔로잉 중인 크리에이터를 안전하게 해제 | 팔로잉 리스트 또는 크리에이터 채널에서 팔로우 해제 | Android 앱 |
|
||||||
|
|
||||||
|
권한과 인증 정책은 기존 팔로우/언팔로우 흐름을 그대로 따른다.
|
||||||
|
|
||||||
|
## 6. 핵심 사용자 흐름
|
||||||
|
|
||||||
|
1. 사용자가 팔로잉 리스트에서 팔로잉 중인 크리에이터의 액션 버튼을 누른다.
|
||||||
|
2. 앱은 `CreatorFollowNotifyFragment` Bottom Sheet를 표시하지 않고 `V2ModalDialog`를 표시한다.
|
||||||
|
3. 사용자가 `취소`를 누르면 Dialog만 닫히고 팔로우 해제 요청은 보내지 않는다.
|
||||||
|
4. 사용자가 `해제하기`를 누르면 기존 `viewModel.follow(creatorId, follow = false, notify = false)` 흐름으로 팔로우 해제를 진행한다.
|
||||||
|
5. 사용자가 크리에이터 채널에서 팔로잉 중 원형 캡슐을 누른다.
|
||||||
|
6. 앱은 `V2ModalDialog`를 표시한다.
|
||||||
|
7. 사용자가 확인하면 기존 `homeActionDelegate.follow(follow = false, notify = false)` 흐름으로 팔로우 해제를 진행한다.
|
||||||
|
|
||||||
|
## 7. 기능 요구사항
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `UNFOLLOW-001` | 확정 | 팔로잉 리스트의 팔로잉 중 버튼은 `ic_new_following_white`를 사용한다. | 팔로잉 중 branch가 기존 `btn_following_big`, `btn_following_no_alarm_big` 대신 요청 아이콘을 사용한다. | `P1-T1`, `P3-T1` |
|
||||||
|
| `UNFOLLOW-002` | 확정 | 팔로잉 리스트의 팔로잉 중 버튼은 `gray_800` 배경과 크리에이터 채널의 팔로잉 중과 동일한 원형 스타일을 사용한다. | `gray_800` 기반 원형 배경이 적용되고 unfollowed 상태는 기존 팔로우 버튼 정책을 유지한다. | `P1-T1`, `P3-T1` |
|
||||||
|
| `UNFOLLOW-003` | 확정 | 팔로잉 리스트에서 팔로잉 중 버튼 터치 시 Bottom Sheet 액션 메뉴를 표시하지 않는다. | `CreatorFollowNotifyFragment`를 통한 팔로잉 중 액션 메뉴 route가 제거된다. | `P1-T2`, `P3-T2` |
|
||||||
|
| `UNFOLLOW-004` | 확정 | 팔로잉 리스트에서 팔로우 해제 전 `V2ModalDialog`로 확인한다. | 취소는 side effect가 없고, 확인만 기존 unfollow 호출을 실행한다. | `P1-T2`, `P3-T2` |
|
||||||
|
| `UNFOLLOW-005` | 확정 | 크리에이터 채널에서 팔로우 해제 전 `V2ModalDialog`로 확인한다. | 팔로잉 중 캡슐 터치만 Dialog를 표시하고, 팔로우 시작 branch는 기존대로 즉시 실행한다. | `P1-T3`, `P4-T1` |
|
||||||
|
| `UNFOLLOW-006` | 확정 | Dialog 문구는 한국어, 일본어, 영어 string resource로 제공한다. | 모든 locale에서 title, description, cancel, confirm resource가 있고 description은 `%1$s`로 크리에이터 이름을 받는다. | `P2-T1` |
|
||||||
|
|
||||||
|
## 8. UI/UX Expectations
|
||||||
|
|
||||||
|
### 8.1 팔로잉 리스트 버튼
|
||||||
|
|
||||||
|
- 팔로잉 중: `ic_new_following_white`, `@color/gray_800`, 크리에이터 채널 팔로잉 중과 동일한 원형 스타일.
|
||||||
|
- 팔로우 전: 기존 팔로우 버튼 표시와 팔로우 시작 동작 유지.
|
||||||
|
- 알림 on/off에 따른 팔로잉 중 버튼 이미지 분기는 제거하거나 동일한 팔로잉 중 UI로 통합한다.
|
||||||
|
|
||||||
|
### 8.2 팔로우 해제 확인 Dialog
|
||||||
|
|
||||||
|
`V2ModalDialog`를 사용한다.
|
||||||
|
|
||||||
|
| Locale | Title | Description | Cancel | Confirm |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Korean | `팔로우 해제` | `팔로우를 해제할까요?\n%1$s의 소식을 받을 수 없어요.` | `취소` | `해제하기` |
|
||||||
|
| Japanese | `フォロー解除` | `%1$sからの最新情報を受け取れなくなります。\nフォローを解除しますか?` | `キャンセル` | `解除する` |
|
||||||
|
| English | `Unfollow` | `You’ll no longer receive updates from %1$s.\nDo you want to unfollow?` | `Cancel` | `Unfollow` |
|
||||||
|
|
||||||
|
## 9. API 계약
|
||||||
|
|
||||||
|
API 계약 변경은 없다. 기존 팔로우/언팔로우 endpoint와 payload를 그대로 사용한다.
|
||||||
|
|
||||||
|
## 10. 보안과 데이터 취급
|
||||||
|
|
||||||
|
- 인증 토큰, API URL, 내부 오류 정보는 Dialog, Toast, 로그에 노출하지 않는다.
|
||||||
|
- Dialog description에는 화면에 이미 표시되는 크리에이터 이름만 삽입한다.
|
||||||
|
|
||||||
|
## 11. 성능과 품질 요구사항
|
||||||
|
|
||||||
|
- 신규 dependency를 추가하지 않는다.
|
||||||
|
- Dialog 확인 버튼 중복 터치는 기존 `V2ModalDialog`의 중복 클릭 방지 동작을 재사용한다.
|
||||||
|
- 테스트는 `app/src/test`의 local/source test로 제한한다.
|
||||||
|
- UI 변경 자동 테스트는 리소스 선택, route, callback 흐름만 검증하고 layout 크기·constraint·pixel은 직접 검증하지 않는다.
|
||||||
|
|
||||||
|
## 12. 성공 기준
|
||||||
|
|
||||||
|
### 12.1 기능 수용 기준
|
||||||
|
|
||||||
|
- [x] 팔로잉 리스트 팔로잉 중 버튼이 요청 아이콘, 색상, 원형 스타일을 사용한다. (`UNFOLLOW-001`, `UNFOLLOW-002`)
|
||||||
|
- [x] 팔로잉 리스트 팔로잉 중 버튼이 `CreatorFollowNotifyFragment`를 표시하지 않는다. (`UNFOLLOW-003`)
|
||||||
|
- [x] 팔로잉 리스트 팔로우 해제는 `V2ModalDialog` 확인 후에만 실행된다. (`UNFOLLOW-004`)
|
||||||
|
- [x] 크리에이터 채널 팔로우 해제는 `V2ModalDialog` 확인 후에만 실행된다. (`UNFOLLOW-005`)
|
||||||
|
- [x] 한국어, 일본어, 영어 문구가 확정 문구와 일치한다. (`UNFOLLOW-006`)
|
||||||
|
|
||||||
|
### 12.2 추적성 완료 기준
|
||||||
|
|
||||||
|
- [x] 모든 확정 요구사항이 `plan-task.md`의 Task와 연결된다.
|
||||||
|
- [x] 구현 전 계획 문서가 먼저 준비된다.
|
||||||
|
- [x] 코드 구현은 별도 사용자 승인 후 `plan-task.md`에 따라 진행한다.
|
||||||
|
|
||||||
|
## 13. Open Questions
|
||||||
|
|
||||||
|
| ID | 상태 | 결정 필요 사항 | 현재 권고 | 결정 주체 | 영향 Goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 없음 | 확정 | 없음 | 없음 | 사용자 | 없음 |
|
||||||
|
|
||||||
|
## 14. 요구사항 추적표
|
||||||
|
|
||||||
|
| 요구사항 범위 | API Contract | 계획 Phase | Goal | 자동 검증 | 수동 검증 |
|
||||||
|
|---|---|---:|---|---|---|
|
||||||
|
| `UNFOLLOW-001~002` | 변경 없음 | 1, 3 | `P1-T1`, `P3-T1` | `FollowingCreatorAdapterSourceTest` 또는 동등 source/local test | 사용자 수동 테스트 완료 |
|
||||||
|
| `UNFOLLOW-003~004` | 변경 없음 | 1, 3 | `P1-T2`, `P3-T2` | `FollowingCreatorActivitySourceTest` 또는 동등 source/local test | 사용자 수동 테스트 완료 |
|
||||||
|
| `UNFOLLOW-005` | 변경 없음 | 1, 4 | `P1-T3`, `P4-T1` | `CreatorChannelActivitySourceTest` | 사용자 수동 테스트 완료 |
|
||||||
|
| `UNFOLLOW-006` | 변경 없음 | 2 | `P2-T1` | string resource source/local test 또는 관련 source test | 사용자 수동 테스트 완료 |
|
||||||
|
|
||||||
|
## 15. Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `2026-08-18` | `DEC-001` | 확정 | 한국어 Dialog description은 `팔로우를 해제할까요?\n%1$s의 소식을 받을 수 없어요.`로 사용한다. | 사용자 인터뷰에서 A안 확정 | `UNFOLLOW-006`, `P2-T1` |
|
||||||
|
| `2026-08-18` | `DEC-002` | 확정 | 시각 보조 브라우저는 사용하지 않고 텍스트 기반 문서만 작성한다. | 사용자 응답 | 문서 작성 방식 |
|
||||||
|
| `2026-08-18` | `DEC-003` | 확정 | 이번 작업은 PRD와 구현 계획 문서 생성만 수행하고 코드 구현은 하지 않는다. | 사용자 명시 요청 | 전체 범위 |
|
||||||
|
| `2026-08-18` | `DEC-004` | 확정 | 구현 후 수동 테스트 완료 상태로 처리한다. | 사용자 완료 보고 | `UNFOLLOW-001~006`, `P5-GATE` |
|
||||||
|
|
||||||
|
## 16. 변경 관리
|
||||||
|
|
||||||
|
요구사항 변경 시 다음을 확인한다.
|
||||||
|
|
||||||
|
- [x] Decision Log에 변경 이유와 날짜를 기록했다.
|
||||||
|
- [x] 관련 요구사항 상태·본문·수용 기준을 갱신했다.
|
||||||
|
- [x] `plan-task.md`의 범위·Files·체크박스·완료 증거를 코드 변경 전에 갱신했다.
|
||||||
|
- [x] 기존 Progress·검증 기록을 삭제하거나 덮어쓰지 않았다.
|
||||||