refactor(content): 상세 진입을 공통 액션으로 통합한다

This commit is contained in:
2026-07-14 22:11:10 +09:00
parent 1c5fbb38f1
commit fe79431744
23 changed files with 578 additions and 208 deletions

View File

@@ -0,0 +1,46 @@
package kr.co.vividnext.sodalive.v2.content.action
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
class ContentAction {
fun execute(
command: ContentActionCommand,
ensureAccess: (AccessRequirement) -> Boolean
): ContentActionResult {
val isValidId = when (command) {
is ContentActionCommand.AudioDetail -> command.audioContentId > 0L
is ContentActionCommand.SeriesDetail -> command.seriesId > 0L
}
if (!isValidId) return ContentActionResult.Ignored
val requiresAdultContentAccess = when (command) {
is ContentActionCommand.AudioDetail -> command.requiresAdultContentAccess
is ContentActionCommand.SeriesDetail -> command.requiresAdultContentAccess
}
val requirement = AccessRequirement.forAdultContent(requiresAdultContentAccess)
if (!ensureAccess(requirement)) return ContentActionResult.Blocked(requirement)
return when (command) {
is ContentActionCommand.AudioDetail ->
ContentActionResult.NavigateToAudioDetail(command.audioContentId)
is ContentActionCommand.SeriesDetail ->
ContentActionResult.NavigateToSeriesDetail(command.seriesId)
}
}
}
sealed interface ContentActionResult {
data object Ignored : ContentActionResult
data class Blocked(
val requirement: AccessRequirement
) : ContentActionResult
data class NavigateToAudioDetail(
val audioContentId: Long
) : ContentActionResult
data class NavigateToSeriesDetail(
val seriesId: Long
) : ContentActionResult
}

View File

@@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.v2.content.action
sealed interface ContentActionCommand {
data class AudioDetail(
val audioContentId: Long,
val requiresAdultContentAccess: Boolean = false
) : ContentActionCommand
data class SeriesDetail(
val seriesId: Long,
val requiresAdultContentAccess: Boolean = false
) : ContentActionCommand
}

View File

@@ -0,0 +1,46 @@
package kr.co.vividnext.sodalive.v2.content.action
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.fragment.app.Fragment
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
import kr.co.vividnext.sodalive.v2.access.ensureV2Access
internal class ContentActionHandler(
private val action: ContentAction = ContentAction()
) {
fun handle(
context: Context,
command: ContentActionCommand,
ensureAccess: (AccessRequirement) -> Boolean
): ContentActionResult {
val result = action.execute(command, ensureAccess)
when (result) {
is ContentActionResult.NavigateToAudioDetail -> context.startActivity(
Intent(context, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, result.audioContentId)
}
)
is ContentActionResult.NavigateToSeriesDetail -> context.startActivity(
Intent(context, SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, result.seriesId)
}
)
is ContentActionResult.Blocked,
ContentActionResult.Ignored -> Unit
}
return result
}
}
fun Activity.handleContentAction(command: ContentActionCommand): ContentActionResult {
return ContentActionHandler().handle(this, command, ::ensureV2Access)
}
fun Fragment.handleContentAction(command: ContentActionCommand): ContentActionResult {
return ContentActionHandler().handle(requireContext(), command, ::ensureV2Access)
}

View File

@@ -25,8 +25,6 @@ import com.google.gson.Gson
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
@@ -49,6 +47,8 @@ import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog
import kr.co.vividnext.sodalive.report.UserReportDialog
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
import kr.co.vividnext.sodalive.v2.access.ensureV2Access
import kr.co.vividnext.sodalive.v2.content.action.ContentActionCommand
import kr.co.vividnext.sodalive.v2.content.action.handleContentAction
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment
@@ -561,8 +561,11 @@ class CreatorChannelActivity :
override fun isCreatorChannelOwner(): Boolean = currentHeader?.isOwner == true
override fun onCreatorChannelAudioContentClicked(audioContentId: Long) {
startAudioContentDetail(audioContentId)
override fun onCreatorChannelAudioContentClicked(
audioContentId: Long,
requiresAdultContentAccess: Boolean
) {
openAudioContentDetail(audioContentId, requiresAdultContentAccess)
}
override fun onCreatorChannelAudioContentChanged() {
@@ -572,12 +575,11 @@ class CreatorChannelActivity :
}
}
override fun onCreatorChannelSeriesClicked(seriesId: Long) {
startActivity(
Intent(this, SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, seriesId)
}
)
override fun onCreatorChannelSeriesClicked(
seriesId: Long,
requiresAdultContentAccess: Boolean
) {
openSeriesDetail(seriesId, requiresAdultContentAccess)
}
override fun onCreatorChannelSeriesContentChanged() {
@@ -964,8 +966,11 @@ class CreatorChannelActivity :
}
}
override fun onCreatorChannelLiveReplayClicked(audioContentId: Long) {
startAudioContentDetail(audioContentId)
override fun onCreatorChannelLiveReplayClicked(
audioContentId: Long,
requiresAdultContentAccess: Boolean
) {
openAudioContentDetail(audioContentId, requiresAdultContentAccess)
}
private fun findLiveFragment(): CreatorChannelLiveFragment? {
@@ -1086,11 +1091,7 @@ class CreatorChannelActivity :
private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse) {
when (schedule.type) {
CreatorActivityType.Audio,
CreatorActivityType.LiveReplay -> startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, schedule.targetId)
}
)
CreatorActivityType.LiveReplay -> openAudioContentDetail(schedule.targetId)
CreatorActivityType.Live -> liveCoordinator.showLiveRoomDetail(schedule.targetId)
@@ -1099,22 +1100,34 @@ class CreatorChannelActivity :
}
private fun onAudioContentClicked(audioContent: CreatorChannelAudioContentResponse) {
startAudioContentDetail(audioContent.audioContentId)
openAudioContentDetail(audioContent.audioContentId, audioContent.isAdult)
}
private fun startAudioContentDetail(audioContentId: Long) {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
}
private fun openAudioContentDetail(
audioContentId: Long,
requiresAdultContentAccess: Boolean = false
) {
handleContentAction(
ContentActionCommand.AudioDetail(
audioContentId = audioContentId,
requiresAdultContentAccess = requiresAdultContentAccess
)
)
}
private fun onSeriesClicked(series: CreatorChannelSeriesResponse) {
startActivity(
Intent(this, SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, series.seriesId)
}
openSeriesDetail(series.seriesId)
}
private fun openSeriesDetail(
seriesId: Long,
requiresAdultContentAccess: Boolean = false
) {
handleContentAction(
ContentActionCommand.SeriesDetail(
seriesId = seriesId,
requiresAdultContentAccess = requiresAdultContentAccess
)
)
}

View File

@@ -27,7 +27,7 @@ class CreatorChannelAudioFragment : BaseFragment<FragmentCreatorChannelAudioBind
private val viewModel: CreatorChannelAudioViewModel by viewModel()
private val audioContentAdapter = CreatorChannelAudioContentAdapter { item ->
host.onCreatorChannelAudioContentClicked(item.audioContentId)
host.onCreatorChannelAudioContentClicked(item.audioContentId, item.showAdultBadge)
}
private var sortPopup: CreatorChannelSortPopup? = null
private var currentContentState: CreatorChannelAudioUiState.Content? = null
@@ -238,7 +238,10 @@ class CreatorChannelAudioFragment : BaseFragment<FragmentCreatorChannelAudioBind
interface Host {
fun isCreatorChannelOwner(): Boolean
fun onCreatorChannelAudioContentClicked(audioContentId: Long)
fun onCreatorChannelAudioContentClicked(
audioContentId: Long,
requiresAdultContentAccess: Boolean
)
fun onCreatorChannelAudioContentChanged()
}

View File

@@ -25,7 +25,7 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
private val viewModel: CreatorChannelLiveViewModel by viewModel()
private val replayAdapter = CreatorChannelAudioContentAdapter { item ->
host.onCreatorChannelLiveReplayClicked(item.audioContentId)
host.onCreatorChannelLiveReplayClicked(item.audioContentId, item.showAdultBadge)
}
private var lastContentLayoutKey: CreatorChannelLiveContentLayoutKey? = null
private var sortPopup: CreatorChannelSortPopup? = null
@@ -189,7 +189,10 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
interface Host {
fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)
fun onCreatorChannelLiveReplayClicked(audioContentId: Long)
fun onCreatorChannelLiveReplayClicked(
audioContentId: Long,
requiresAdultContentAccess: Boolean
)
fun onCreatorChannelLiveContentChanged()
}

View File

@@ -19,8 +19,8 @@ class CreatorChannelSeriesFragment : BaseFragment<FragmentCreatorChannelSeriesBi
) {
private val viewModel: CreatorChannelSeriesViewModel by viewModel()
private val seriesAdapter = CreatorChannelSeriesAdapter { seriesId ->
host.onCreatorChannelSeriesClicked(seriesId)
private val seriesAdapter = CreatorChannelSeriesAdapter { item ->
host.onCreatorChannelSeriesClicked(item.seriesId, item.showAdultBadge)
}
private var sortPopup: CreatorChannelSortPopup? = null
private var currentContentState: CreatorChannelSeriesUiState.Content? = null
@@ -154,7 +154,10 @@ class CreatorChannelSeriesFragment : BaseFragment<FragmentCreatorChannelSeriesBi
interface Host {
fun isCreatorChannelOwner(): Boolean
fun onCreatorChannelSeriesClicked(seriesId: Long)
fun onCreatorChannelSeriesClicked(
seriesId: Long,
requiresAdultContentAccess: Boolean
)
fun onCreatorChannelSeriesContentChanged()
}

View File

@@ -15,7 +15,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.series.model.CreatorChannelSe
import kr.co.vividnext.sodalive.v2.creator.channel.series.model.CreatorChannelSeriesSubtitleUiModel
class CreatorChannelSeriesAdapter(
private val onSeriesClicked: (Long) -> Unit = {}
private val onSeriesClicked: (CreatorChannelSeriesItemUiModel) -> Unit = {}
) : RecyclerView.Adapter<CreatorChannelSeriesAdapter.ViewHolder>() {
private var items: List<CreatorChannelSeriesItemUiModel> = emptyList()
@@ -40,7 +40,7 @@ class CreatorChannelSeriesAdapter(
class ViewHolder(
private val binding: ItemCreatorChannelSeriesBinding,
private val onSeriesClicked: (Long) -> Unit
private val onSeriesClicked: (CreatorChannelSeriesItemUiModel) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
init {
@@ -65,7 +65,7 @@ class CreatorChannelSeriesAdapter(
tvCreatorChannelSeriesTitle.text = item.title
tvCreatorChannelSeriesSubtitle.text = formatSubtitle(item.subtitle)
bindProgress(item)
root.setOnClickListener { onSeriesClicked(item.seriesId) }
root.setOnClickListener { onSeriesClicked(item) }
}
private fun formatSubtitle(subtitle: CreatorChannelSeriesSubtitleUiModel): String {

View File

@@ -14,10 +14,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
@@ -31,6 +28,8 @@ import kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelSortPopup
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
import kr.co.vividnext.sodalive.v2.access.ensureV2Access
import kr.co.vividnext.sodalive.v2.content.action.ContentActionCommand
import kr.co.vividnext.sodalive.v2.content.action.handleContentAction
import kr.co.vividnext.sodalive.v2.main.content.data.AudioRankingType
import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType
import kr.co.vividnext.sodalive.v2.main.content.model.AudioRankingsUiState
@@ -38,6 +37,7 @@ import kr.co.vividnext.sodalive.v2.main.content.model.AudioRecommendationsUiStat
import kr.co.vividnext.sodalive.v2.main.content.model.ContentAudioCardSection
import kr.co.vividnext.sodalive.v2.main.content.model.ContentAudioCardUiModel
import kr.co.vividnext.sodalive.v2.main.content.model.ContentBannerSection
import kr.co.vividnext.sodalive.v2.main.content.model.ContentBannerRoute
import kr.co.vividnext.sodalive.v2.main.content.model.ContentBannerUiModel
import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioSection
import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioUiModel
@@ -616,8 +616,13 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
private fun onBannerClick(item: ContentBannerUiModel) {
val route = item.toContentBannerRoute() ?: return
ensureV2Access(AccessRequirement.Login) {
startActivity(route.toContentBannerIntent(requireContext()))
when (route) {
is ContentBannerRoute.Series -> handleContentAction(
ContentActionCommand.SeriesDetail(seriesId = route.seriesId)
)
else -> ensureV2Access(AccessRequirement.Login) {
route.toContentBannerIntent(requireContext())?.let(::startActivity)
}
}
}
@@ -645,38 +650,33 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
audioContentId: Long,
requiresAdultContentAccess: Boolean = false
) {
if (audioContentId <= 0L) return
ensureV2Access(AccessRequirement.forAdultContent(requiresAdultContentAccess)) {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
}
handleContentAction(
ContentActionCommand.AudioDetail(
audioContentId = audioContentId,
requiresAdultContentAccess = requiresAdultContentAccess
)
}
)
}
private fun openRankingAudioContentDetail(item: ContentRankingItem) {
val audioContentId = item.contentId.toLongOrNull()?.takeIf { it > 0L } ?: return
val audioContentId = item.contentId.toLongOrNull() ?: return
openAudioContentDetail(audioContentId)
}
private fun openSeriesDetail(item: ContentOriginalSeriesUiModel) {
val seriesId = item.seriesId.takeIf { it > 0L } ?: return
openSeriesDetail(seriesId)
openSeriesDetail(item.seriesId)
}
private fun openSeriesDetail(
seriesId: Long,
requiresAdultContentAccess: Boolean = false
) {
if (seriesId <= 0L) return
ensureV2Access(AccessRequirement.forAdultContent(requiresAdultContentAccess)) {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, seriesId)
}
handleContentAction(
ContentActionCommand.SeriesDetail(
seriesId = seriesId,
requiresAdultContentAccess = requiresAdultContentAccess
)
}
)
}
private fun findAllTabAudioAdultAccess(audioContentId: Long): Boolean {

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.event.EventItem
@@ -63,7 +62,7 @@ fun ContentBannerUiModel.toContentBannerRoute(): ContentBannerRoute? {
)
}
fun ContentBannerRoute.toContentBannerIntent(context: Context): Intent {
fun ContentBannerRoute.toContentBannerIntent(context: Context): Intent? {
return when (this) {
is ContentBannerRoute.Event -> Intent(context, EventDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_EVENT, eventItem)
@@ -71,9 +70,7 @@ fun ContentBannerRoute.toContentBannerIntent(context: Context): Intent {
is ContentBannerRoute.Creator -> CreatorChannelActivity.newIntent(context, creatorId)
is ContentBannerRoute.Series -> Intent(context, SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, seriesId)
}
is ContentBannerRoute.Series -> null
is ContentBannerRoute.Link -> Intent(Intent.ACTION_VIEW, Uri.parse(url))
}

View File

@@ -8,11 +8,12 @@ import androidx.core.view.doOnLayout
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.databinding.ActivityContentOverviewBinding
import kr.co.vividnext.sodalive.v2.content.action.ContentActionCommand
import kr.co.vividnext.sodalive.v2.content.action.handleContentAction
import kr.co.vividnext.sodalive.v2.main.content.overview.data.ContentOverviewType
import kr.co.vividnext.sodalive.v2.main.content.overview.model.ContentOverviewUiModel
import kr.co.vividnext.sodalive.v2.main.content.overview.model.ContentOverviewUiState
import kr.co.vividnext.sodalive.v2.main.content.overview.model.toTitleResId
import kr.co.vividnext.sodalive.v2.main.content.overview.ui.ContentOverviewAdapter
@@ -125,13 +126,12 @@ class ContentOverviewActivity : BaseActivity<ActivityContentOverviewBinding>(
?: toastMessage.resId?.let { resId -> showToast(getString(resId)) }
}
private fun openAudioContentDetail(contentId: Long) {
if (contentId <= 0) return
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
private fun openAudioContentDetail(item: ContentOverviewUiModel) {
handleContentAction(
ContentActionCommand.AudioDetail(
audioContentId = item.contentId,
requiresAdultContentAccess = item.showAdultBadge
)
)
}

View File

@@ -8,7 +8,7 @@ import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.v2.main.content.overview.model.ContentOverviewUiModel
class ContentOverviewAdapter(
private val onContentClick: (Long) -> Unit = {}
private val onContentClick: (ContentOverviewUiModel) -> Unit = {}
) : RecyclerView.Adapter<ContentOverviewAdapter.ViewHolder>() {
private var items: List<ContentOverviewUiModel> = emptyList()
@@ -40,7 +40,7 @@ class ContentOverviewAdapter(
class ViewHolder(
private val binding: ItemContentAudioCardBinding,
private val onContentClick: (Long) -> Unit
private val onContentClick: (ContentOverviewUiModel) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: ContentOverviewUiModel, gridItemWidthPx: Int) = with(binding.audioContentCard.root) {
setGridItemWidthPx(gridItemWidthPx)
@@ -49,8 +49,7 @@ class ContentOverviewAdapter(
setAdultVisible(item.showAdultBadge)
thumbnailView().loadUrl(item.coverImage)
setOnClickListener {
if (item.contentId <= 0) return@setOnClickListener
onContentClick(item.contentId)
onContentClick(item)
}
}
}

View File

@@ -26,6 +26,8 @@ import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiItem
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomType
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
import kr.co.vividnext.sodalive.v2.access.ensureV2Access
import kr.co.vividnext.sodalive.v2.content.action.ContentActionCommand
import kr.co.vividnext.sodalive.v2.content.action.handleContentAction
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingChatSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingCreatorSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingLiveSection
@@ -39,6 +41,7 @@ import kr.co.vividnext.sodalive.v2.main.home.model.HomeCreatorRankingUiState
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationAiCharacterSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationAiCharacterUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerRoute
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCheerCreatorSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationGenreCreatorSection
@@ -48,13 +51,13 @@ import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularComm
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentDebutCreatorSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyActiveCreatorSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyActiveCreatorRoute
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyActiveCreatorUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationUiState
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerIntent
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerRoute
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationAiCharacterIntent
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationAiCharacterRoute
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationRecentlyActiveCreatorIntent
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationRecentlyActiveCreatorRoute
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeBannerBinder
@@ -581,15 +584,25 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
private fun onBannerClick(item: HomeRecommendationBannerUiModel) {
val route = item.toHomeRecommendationBannerRoute() ?: return
ensureV2Access(AccessRequirement.Login) {
startActivity(route.toHomeRecommendationBannerIntent(requireContext()))
when (route) {
is HomeRecommendationBannerRoute.Series -> handleContentAction(
ContentActionCommand.SeriesDetail(seriesId = route.seriesId)
)
else -> ensureV2Access(AccessRequirement.Login) {
route.toHomeRecommendationBannerIntent(requireContext())?.let(::startActivity)
}
}
}
private fun onRecentActivityClick(item: HomeRecommendationRecentlyActiveCreatorUiModel) {
val route = item.toHomeRecommendationRecentlyActiveCreatorRoute() ?: return
ensureV2Access(AccessRequirement.Login) {
startActivity(route.toHomeRecommendationRecentlyActiveCreatorIntent(requireContext()))
when (route) {
is HomeRecommendationRecentlyActiveCreatorRoute.AudioContent -> handleContentAction(
ContentActionCommand.AudioDetail(audioContentId = route.contentId)
)
is HomeRecommendationRecentlyActiveCreatorRoute.Community -> ensureV2Access(AccessRequirement.Login) {
startActivity(CreatorChannelCommunityDetailActivity.newIntent(requireContext(), route.postId))
}
}
}

View File

@@ -4,14 +4,11 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.event.EventItem
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityDetailActivity
import kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.CharacterChatThumbnailItem
import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem
import java.util.Locale
@@ -100,7 +97,7 @@ fun HomeRecommendationBannerUiModel.toHomeRecommendationBannerRoute(): HomeRecom
)
}
fun HomeRecommendationBannerRoute.toHomeRecommendationBannerIntent(context: Context): Intent {
fun HomeRecommendationBannerRoute.toHomeRecommendationBannerIntent(context: Context): Intent? {
return when (this) {
is HomeRecommendationBannerRoute.Event -> {
Intent(context, EventDetailActivity::class.java).apply {
@@ -112,11 +109,7 @@ fun HomeRecommendationBannerRoute.toHomeRecommendationBannerIntent(context: Cont
CreatorChannelActivity.newIntent(context, creatorId)
}
is HomeRecommendationBannerRoute.Series -> {
Intent(context, SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, seriesId)
}
}
is HomeRecommendationBannerRoute.Series -> null
is HomeRecommendationBannerRoute.Link -> Intent(Intent.ACTION_VIEW, Uri.parse(url))
}
@@ -151,20 +144,6 @@ fun HomeRecommendationRecentlyActiveCreatorUiModel.toHomeRecommendationRecentlyA
}
}
fun HomeRecommendationRecentlyActiveCreatorRoute.toHomeRecommendationRecentlyActiveCreatorIntent(context: Context):
Intent {
return when (this) {
is HomeRecommendationRecentlyActiveCreatorRoute.AudioContent -> {
Intent(context, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
}
is HomeRecommendationRecentlyActiveCreatorRoute.Community ->
CreatorChannelCommunityDetailActivity.newIntent(context, postId)
}
}
data class HomeRecommendationCreatorUiModel(
val creatorId: Long,
val nickname: String,

View File

@@ -0,0 +1,193 @@
package kr.co.vividnext.sodalive.v2.content.action
import android.app.Activity
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import java.io.File
@RunWith(RobolectricTestRunner::class)
@Config(application = android.app.Application::class)
class ContentActionTest {
private val action = ContentAction()
@Test
fun `유효하지 않은 ID는 Access를 확인하지 않고 무시한다`() {
var accessCheckCount = 0
val commands = listOf(
ContentActionCommand.AudioDetail(audioContentId = 0L),
ContentActionCommand.SeriesDetail(seriesId = -1L)
)
val results = commands.map { command ->
action.execute(command) {
accessCheckCount += 1
true
}
}
assertEquals(listOf(ContentActionResult.Ignored, ContentActionResult.Ignored), results)
assertEquals(0, accessCheckCount)
}
@Test
fun `오디오 상세는 로그인 Access가 허용되면 navigation 결과를 반환한다`() {
var capturedRequirement: AccessRequirement? = null
val result = action.execute(ContentActionCommand.AudioDetail(audioContentId = 11L)) { requirement ->
capturedRequirement = requirement
true
}
assertEquals(AccessRequirement.Login, capturedRequirement)
assertEquals(ContentActionResult.NavigateToAudioDetail(audioContentId = 11L), result)
}
@Test
fun `성인 시리즈 상세는 성인 Access가 허용되면 navigation 결과를 반환한다`() {
var capturedRequirement: AccessRequirement? = null
val result = action.execute(
ContentActionCommand.SeriesDetail(
seriesId = 22L,
requiresAdultContentAccess = true
)
) { requirement ->
capturedRequirement = requirement
true
}
assertEquals(AccessRequirement.AdultContent, capturedRequirement)
assertEquals(ContentActionResult.NavigateToSeriesDetail(seriesId = 22L), result)
}
@Test
fun `Access가 거부되면 차단 결과를 반환한다`() {
val result = action.execute(
ContentActionCommand.AudioDetail(
audioContentId = 33L,
requiresAdultContentAccess = true
)
) { false }
assertEquals(ContentActionResult.Blocked(AccessRequirement.AdultContent), result)
}
@Test
fun `Handler는 허용된 오디오 상세에 기존 ID extra를 전달한다`() {
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
val result = ContentActionHandler().handle(
context = activity,
command = ContentActionCommand.AudioDetail(audioContentId = 44L),
ensureAccess = { true }
)
val intent = shadowOf(activity).nextStartedActivity
assertEquals(ContentActionResult.NavigateToAudioDetail(audioContentId = 44L), result)
assertEquals(AudioContentDetailActivity::class.java.name, intent.component?.className)
assertEquals(44L, intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0L))
}
@Test
fun `Handler는 허용된 시리즈 상세에 기존 ID extra를 전달한다`() {
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
val result = ContentActionHandler().handle(
context = activity,
command = ContentActionCommand.SeriesDetail(seriesId = 55L),
ensureAccess = { true }
)
val intent = shadowOf(activity).nextStartedActivity
assertEquals(ContentActionResult.NavigateToSeriesDetail(seriesId = 55L), result)
assertEquals(SeriesDetailActivity::class.java.name, intent.component?.className)
assertEquals(55L, intent.getLongExtra(Constants.EXTRA_SERIES_ID, 0L))
}
@Test
fun `Handler는 차단 결과에서 Activity를 시작하지 않는다`() {
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
val result = ContentActionHandler().handle(
context = activity,
command = ContentActionCommand.AudioDetail(audioContentId = 66L),
ensureAccess = { false }
)
assertEquals(ContentActionResult.Blocked(AccessRequirement.Login), result)
assertNull(shadowOf(activity).nextStartedActivity)
}
@Test
fun `대상 화면은 상세 Activity 대신 Content Action을 사용한다`() {
val actionSources = listOf(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt",
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt",
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivity.kt",
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).map { path -> projectFile(path).readText() }
val navigationOwnerSources = actionSources + listOf(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiModels.kt",
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRecommendationsUiModels.kt"
).map { path -> projectFile(path).readText() }
actionSources.forEach { source ->
assertTrue(source.contains("handleContentAction("))
}
navigationOwnerSources.forEach { source ->
assertFalse(source.contains("AudioContentDetailActivity"))
assertFalse(source.contains("SeriesDetailActivity"))
}
}
@Test
fun `Creator tab은 알고 있는 성인 여부를 Content Action까지 전달한다`() {
val audioFragmentSource = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragment.kt"
).readText()
val liveFragmentSource = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt"
).readText()
val seriesFragmentSource = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesFragment.kt"
).readText()
assertTrue(
audioFragmentSource.contains(
"host.onCreatorChannelAudioContentClicked(item.audioContentId, item.showAdultBadge)"
)
)
assertTrue(
liveFragmentSource.contains(
"host.onCreatorChannelLiveReplayClicked(item.audioContentId, item.showAdultBadge)"
)
)
assertTrue(
seriesFragmentSource.contains(
"host.onCreatorChannelSeriesClicked(item.seriesId, item.showAdultBadge)"
)
)
}
private fun projectFile(relativePath: String): File {
val userDir = requireNotNull(System.getProperty("user.dir"))
val fromRoot = File(userDir, relativePath)
if (fromRoot.exists()) return fromRoot
val projectRoot = requireNotNull(File(userDir).parentFile)
return projectRoot.resolve(relativePath)
}
}

View File

@@ -601,8 +601,11 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("position == CreatorChannelTab.Series.ordinal"))
assertTrue(source.contains("findSeriesFragment()?.onCreatorChannelSeriesViewportHeightChanged(minHeight)"))
assertTrue(source.contains("override fun onCreatorChannelSeriesContentChanged()"))
assertTrue(source.contains("override fun onCreatorChannelSeriesClicked(seriesId: Long)"))
assertTrue(source.contains("putExtra(Constants.EXTRA_SERIES_ID, seriesId)"))
assertTrue(source.contains("override fun onCreatorChannelSeriesClicked("))
assertTrue(source.contains("seriesId: Long"))
assertTrue(source.contains("requiresAdultContentAccess: Boolean"))
assertTrue(source.contains("ContentActionCommand.SeriesDetail("))
assertTrue(source.contains("requiresAdultContentAccess = requiresAdultContentAccess"))
}
@Test
@@ -1582,8 +1585,9 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse)"))
assertTrue(source.contains("CreatorActivityType.Audio"))
assertTrue(source.contains("CreatorActivityType.LiveReplay"))
assertTrue(source.contains("AudioContentDetailActivity::class.java"))
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, schedule.targetId)"))
assertTrue(source.contains("openAudioContentDetail(schedule.targetId)"))
assertTrue(source.contains("ContentActionCommand.AudioDetail("))
assertFalse(source.contains("AudioContentDetailActivity"))
assertTrue(source.contains("CreatorActivityType.Live"))
assertTrue(source.contains("CreatorActivityType.Live -> liveCoordinator.showLiveRoomDetail(schedule.targetId)"))
}
@@ -1679,9 +1683,12 @@ class CreatorChannelActivitySourceTest {
assertTrue(fragment.contains("private fun onAudioContentClicked(audioContent: CreatorChannelAudioContentResponse)"))
assertTrue(fragment.contains("host.onCreatorChannelAudioContentClicked(audioContent)"))
assertTrue(source.contains("private fun onAudioContentClicked(audioContent: CreatorChannelAudioContentResponse)"))
assertTrue(source.contains("AudioContentDetailActivity::class.java"))
assertTrue(source.contains("startAudioContentDetail(audioContent.audioContentId)"))
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)"))
assertTrue(
source.contains("openAudioContentDetail(audioContent.audioContentId, audioContent.isAdult)")
)
assertTrue(source.contains("ContentActionCommand.AudioDetail("))
assertTrue(source.contains("requiresAdultContentAccess = requiresAdultContentAccess"))
assertFalse(source.contains("AudioContentDetailActivity"))
}
@Test
@@ -1816,10 +1823,10 @@ class CreatorChannelActivitySourceTest {
assertTrue(fragment.contains("onSeriesClick = ::onSeriesClicked"))
assertTrue(fragment.contains("private fun onSeriesClicked(series: CreatorChannelSeriesResponse)"))
assertTrue(fragment.contains("host.onCreatorChannelSeriesClicked(series)"))
assertTrue(source.contains("import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity"))
assertTrue(source.contains("private fun onSeriesClicked(series: CreatorChannelSeriesResponse)"))
assertTrue(source.contains("SeriesDetailActivity::class.java"))
assertTrue(source.contains("putExtra(Constants.EXTRA_SERIES_ID, series.seriesId)"))
assertTrue(source.contains("openSeriesDetail(series.seriesId)"))
assertTrue(source.contains("ContentActionCommand.SeriesDetail("))
assertFalse(source.contains("SeriesDetailActivity"))
}
@Test

View File

@@ -117,6 +117,8 @@ class CreatorChannelSeriesFragmentLayoutTest {
assertTrue(adapter.contains("R.string.creator_channel_series_subtitle_content_count"))
assertTrue(adapter.contains("R.string.creator_channel_series_status_proceeding"))
assertTrue(adapter.contains("R.string.creator_channel_series_status_completed"))
assertTrue(fragment.contains("host.onCreatorChannelSeriesClicked(item.seriesId, item.showAdultBadge)"))
assertTrue(adapter.contains("onSeriesClicked(item)"))
assertTrue(!adapter.contains("tvCreatorChannelSeriesSubtitle.text = item.subtitle"))
}

View File

@@ -8,27 +8,39 @@ import java.io.File
class ContentMainFragmentLoginGuardSourceTest {
@Test
fun `ContentMainFragment 실제 이동은 MainV2 로그인 가드를 통과한다`() {
val source = projectFile(
fun `ContentMainFragment 상세 이동은 공통 Content Action Access를 통과한다`() {
val fragmentSource = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
).readText()
val handlerSource = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/content/action/ContentActionHandler.kt"
).readText()
assertTrue(source.contains("import kr.co.vividnext.sodalive.v2.access.ensureV2Access"))
assertGuardedStartActivity(source, "private fun onBannerClick(item: ContentBannerUiModel)")
assertGuardedStartActivity(source, "audioContentId: Long,\n requiresAdultContentAccess: Boolean = false")
assertGuardedStartActivity(source, "seriesId: Long,\n requiresAdultContentAccess: Boolean = false")
assertFalse(source.contains("kr.co.vividnext.sodalive.main.MainActivity"))
assertTrue(fragmentSource.contains("import kr.co.vividnext.sodalive.v2.content.action.handleContentAction"))
assertTrue(fragmentSource.contains("ContentActionCommand.AudioDetail("))
assertTrue(fragmentSource.contains("ContentActionCommand.SeriesDetail("))
assertTrue(handlerSource.contains("action.execute(command, ensureAccess)"))
assertTrue(
handlerSource.indexOf("action.execute(command, ensureAccess)") <
handlerSource.indexOf("context.startActivity(")
)
assertFalse(fragmentSource.contains("kr.co.vividnext.sodalive.main.MainActivity"))
}
@Test
fun `ContentMainFragment invalid id return은 로그인 가드보다 먼저 유지된다`() {
val source = projectFile(
fun `Content Action invalid ID 결과는 Access 확인보다 먼저 유지된다`() {
val fragmentSource = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
).readText()
val actionSource = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/content/action/ContentAction.kt"
).readText()
assertBeforeGuard(source, "val route = item.toContentBannerRoute() ?: return")
assertBeforeGuard(source, "if (audioContentId <= 0L) return")
assertBeforeGuard(source, "if (seriesId <= 0L) return")
assertTrue(fragmentSource.contains("val route = item.toContentBannerRoute() ?: return"))
assertTrue(
actionSource.indexOf("if (!isValidId) return ContentActionResult.Ignored") <
actionSource.indexOf("if (!ensureAccess(requirement))")
)
}
@Test
@@ -40,34 +52,15 @@ class ContentMainFragmentLoginGuardSourceTest {
assertTrue(source.contains("openAudioContentDetail(item.audioContentId, item.showAdultBadge)"))
assertTrue(source.contains("openAudioContentDetail(audioContentId, findAllTabAudioAdultAccess(audioContentId))"))
assertTrue(source.contains("openSeriesDetail(seriesId, findAllTabSeriesAdultAccess(seriesId))"))
assertTrue(source.contains("AccessRequirement.forAdultContent(requiresAdultContentAccess)"))
val actionSource = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/content/action/ContentAction.kt"
).readText()
assertTrue(actionSource.contains("AccessRequirement.forAdultContent(requiresAdultContentAccess)"))
assertFalse(source.contains("ContentCommentedAudioUiModel.showAdultBadge"))
assertFalse(source.contains("ContentOriginalSeriesUiModel.showAdultBadge"))
}
private fun assertGuardedStartActivity(source: String, functionSignature: String) {
val functionSource = source.substringFrom(functionSignature)
assertTrue(
"$functionSignature must call ensureV2Access before startActivity.",
functionSource.indexOf("ensureV2Access") in 0 until functionSource.indexOf("startActivity")
)
}
private fun assertBeforeGuard(source: String, expectedReturn: String) {
val returnIndex = source.indexOf(expectedReturn)
val guardIndex = source.indexOf("ensureV2Access", returnIndex)
assertTrue("Missing source: $expectedReturn", returnIndex >= 0)
assertTrue("$expectedReturn must stay before guard.", guardIndex > returnIndex)
}
private fun String.substringFrom(marker: String): String {
val startIndex = indexOf(marker)
assertTrue("Missing function: $marker", startIndex >= 0)
val nextFunctionIndex = indexOf("\n private fun ", startIndex + marker.length).takeIf { it >= 0 } ?: length
return substring(startIndex, nextFunctionIndex)
}
private fun projectFile(relativePath: String): File {
val candidates = listOf(File(relativePath), File("../$relativePath"))
return candidates.firstOrNull { it.exists() }

View File

@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.v2.main.content
import android.app.Application
import android.content.Context
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.event.EventItem
@@ -14,6 +13,7 @@ import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerIntent
import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerRoute
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@@ -140,7 +140,7 @@ class ContentMainFragmentSourceTest {
assertTrue(source.contains("loadMore()"))
assertTrue(source.contains("consumePaginationErrorMessage()"))
assertTrue(source.contains("openAudioContentDetail(audioContentId)"))
assertTrue(source.contains("openSeriesDetail(seriesId)"))
assertTrue(source.contains("openSeriesDetail(seriesId, findAllTabSeriesAdultAccess(seriesId))"))
assertTrue(source.contains("override fun onDestroyView()"))
assertTrue(source.contains("sortPopup?.dismiss()"))
assertTrue(source.contains("sortPopup = null"))
@@ -240,27 +240,28 @@ class ContentMainFragmentSourceTest {
}
@Test
fun `content 추천 source는 오디오와 시리즈 routing extra를 사용한다`() {
fun `content 추천 source는 오디오와 시리즈 상세를 Content Action에 위임한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
).readText()
assertTrue(source.contains("AudioContentDetailActivity::class.java"))
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)"))
assertTrue(source.contains("SeriesDetailActivity::class.java"))
assertTrue(source.contains("putExtra(Constants.EXTRA_SERIES_ID, seriesId)"))
assertTrue(source.contains("handleContentAction("))
assertTrue(source.contains("ContentActionCommand.AudioDetail("))
assertTrue(source.contains("ContentActionCommand.SeriesDetail("))
assertFalse(source.contains("AudioContentDetailActivity"))
assertFalse(source.contains("SeriesDetailActivity"))
assertTrue(source.contains("toContentBannerRoute()"))
assertTrue(source.contains("toContentBannerIntent(requireContext())"))
}
@Test
fun `content 랭킹 routing은 유효하지 않은 contentId를 무시한다`() {
fun `content 랭킹 routing은 숫자가 아닌 contentId를 무시한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
).readText()
assertTrue(source.contains("private fun openRankingAudioContentDetail(item: ContentRankingItem)"))
assertTrue(source.contains("item.contentId.toLongOrNull()?.takeIf { it > 0L } ?: return"))
assertTrue(source.contains("item.contentId.toLongOrNull() ?: return"))
assertTrue(source.contains("openAudioContentDetail(audioContentId)"))
}
@@ -465,21 +466,24 @@ class ContentMainFragmentSourceTest {
fun `content banner route creates expected intents`() {
val context = RuntimeEnvironment.getApplication() as Context
val eventItem = EventItem(id = 1L, thumbnailImageUrl = "https://example.com/event.png")
val eventIntent = ContentBannerRoute.Event(eventItem).toContentBannerIntent(context)
val creatorIntent = ContentBannerRoute.Creator(2L).toContentBannerIntent(context)
val eventIntent = requireNotNull(ContentBannerRoute.Event(eventItem).toContentBannerIntent(context))
val creatorIntent = requireNotNull(ContentBannerRoute.Creator(2L).toContentBannerIntent(context))
val seriesIntent = ContentBannerRoute.Series(3L).toContentBannerIntent(context)
val webIntent = ContentBannerRoute.Link("https://example.com", isWebUrl = true).toContentBannerIntent(context)
val deepLinkIntent = ContentBannerRoute.Link(
url = "${BuildConfig.APPSCHEME}://series/3",
isWebUrl = false
).toContentBannerIntent(context)
val webIntent = requireNotNull(
ContentBannerRoute.Link("https://example.com", isWebUrl = true).toContentBannerIntent(context)
)
val deepLinkIntent = requireNotNull(
ContentBannerRoute.Link(
url = "${BuildConfig.APPSCHEME}://series/3",
isWebUrl = false
).toContentBannerIntent(context)
)
assertEquals(EventDetailActivity::class.java.name, eventIntent.component?.className)
assertEquals(eventItem, eventIntent.getParcelableExtra(Constants.EXTRA_EVENT))
assertEquals(CreatorChannelActivity::class.java.name, creatorIntent.component?.className)
assertEquals(2L, creatorIntent.getLongExtra(CreatorChannelActivity.EXTRA_CREATOR_ID, 0L))
assertEquals(SeriesDetailActivity::class.java.name, seriesIntent.component?.className)
assertEquals(3L, seriesIntent.getLongExtra(Constants.EXTRA_SERIES_ID, 0L))
assertNull(seriesIntent)
assertEquals(android.content.Intent.ACTION_VIEW, webIntent.action)
assertEquals("https://example.com", webIntent.data.toString())
assertEquals(android.content.Intent.ACTION_VIEW, deepLinkIntent.action)

View File

@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.main.content.overview
import android.app.Application
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@@ -63,24 +64,26 @@ class ContentOverviewActivitySourceTest {
}
@Test
fun `ContentOverviewActivity opens audio detail with content id`() {
fun `ContentOverviewActivity는 ID와 성인 여부를 Content Action에 전달한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivity.kt"
).readText()
assertTrue(source.contains("AudioContentDetailActivity::class.java"))
assertTrue(source.contains("if (contentId <= 0) return"))
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)"))
assertTrue(source.contains("handleContentAction("))
assertTrue(source.contains("ContentActionCommand.AudioDetail("))
assertTrue(source.contains("audioContentId = item.contentId"))
assertTrue(source.contains("requiresAdultContentAccess = item.showAdultBadge"))
assertFalse(source.contains("AudioContentDetailActivity"))
}
@Test
fun `ContentOverviewAdapter ignores invalid content id clicks`() {
fun `ContentOverviewAdapter는 Action 검증을 위해 전체 항목을 전달한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ui/ContentOverviewAdapter.kt"
).readText()
assertTrue(source.contains("if (item.contentId <= 0) return@setOnClickListener"))
assertTrue(source.contains("onContentClick(item.contentId)"))
assertTrue(source.contains("onContentClick(item)"))
assertFalse(source.contains("if (item.contentId <= 0)"))
}
@Test

View File

@@ -23,14 +23,11 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.event.EventItem
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityDetailActivity
import kr.co.vividnext.sodalive.v2.main.home.data.HomeActiveCreatorItem
import kr.co.vividnext.sodalive.v2.main.home.data.HomeAiCharacterItem
import kr.co.vividnext.sodalive.v2.main.home.data.HomeBannerItem
@@ -61,7 +58,6 @@ import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerInt
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerRoute
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationAiCharacterIntent
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationAiCharacterRoute
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationRecentlyActiveCreatorIntent
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationRecentlyActiveCreatorRoute
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomePopularCommunityPosts
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups
@@ -84,6 +80,7 @@ import kr.co.vividnext.sodalive.v2.main.home.model.toUiModel
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -872,24 +869,31 @@ class HomeMainFragmentLayoutTest {
fun `home banner route creates activity and link intents with expected extras`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val eventItem = EventItem(id = 1L, thumbnailImageUrl = "https://example.com/event.png")
val eventIntent = HomeRecommendationBannerRoute.Event(eventItem).toHomeRecommendationBannerIntent(context)
val creatorIntent = HomeRecommendationBannerRoute.Creator(2L).toHomeRecommendationBannerIntent(context)
val eventIntent = requireNotNull(
HomeRecommendationBannerRoute.Event(eventItem).toHomeRecommendationBannerIntent(context)
)
val creatorIntent = requireNotNull(
HomeRecommendationBannerRoute.Creator(2L).toHomeRecommendationBannerIntent(context)
)
val seriesIntent = HomeRecommendationBannerRoute.Series(3L).toHomeRecommendationBannerIntent(context)
val webIntent = HomeRecommendationBannerRoute.Link(
url = "https://example.com",
isWebUrl = true
).toHomeRecommendationBannerIntent(context)
val deepLinkIntent = HomeRecommendationBannerRoute.Link(
url = "${BuildConfig.APPSCHEME}://series/3",
isWebUrl = false
).toHomeRecommendationBannerIntent(context)
val webIntent = requireNotNull(
HomeRecommendationBannerRoute.Link(
url = "https://example.com",
isWebUrl = true
).toHomeRecommendationBannerIntent(context)
)
val deepLinkIntent = requireNotNull(
HomeRecommendationBannerRoute.Link(
url = "${BuildConfig.APPSCHEME}://series/3",
isWebUrl = false
).toHomeRecommendationBannerIntent(context)
)
assertEquals(EventDetailActivity::class.java.name, eventIntent.component?.className)
assertEquals(eventItem, eventIntent.getParcelableExtra(Constants.EXTRA_EVENT))
assertEquals(CreatorChannelActivity::class.java.name, creatorIntent.component?.className)
assertEquals(2L, creatorIntent.getLongExtra(CreatorChannelActivity.EXTRA_CREATOR_ID, 0L))
assertEquals(SeriesDetailActivity::class.java.name, seriesIntent.component?.className)
assertEquals(3L, seriesIntent.getLongExtra(Constants.EXTRA_SERIES_ID, 0L))
assertNull(seriesIntent)
assertEquals(android.content.Intent.ACTION_VIEW, webIntent.action)
assertEquals("https://example.com", webIntent.data.toString())
assertEquals(android.content.Intent.ACTION_VIEW, deepLinkIntent.action)
@@ -947,17 +951,19 @@ class HomeMainFragmentLayoutTest {
}
@Test
fun `home recent activity route creates expected intents`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val audioIntent = HomeRecommendationRecentlyActiveCreatorRoute.AudioContent(11L)
.toHomeRecommendationRecentlyActiveCreatorIntent(context)
val communityIntent = HomeRecommendationRecentlyActiveCreatorRoute.Community(13L)
.toHomeRecommendationRecentlyActiveCreatorIntent(context)
fun `home 오디오와 시리즈 route는 Content Action을 사용한다`() {
val fragmentSource = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
).readText()
val modelSource = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiModels.kt"
).readText()
assertEquals(AudioContentDetailActivity::class.java.name, audioIntent.component?.className)
assertEquals(11L, audioIntent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0L))
assertEquals(CreatorChannelCommunityDetailActivity::class.java.name, communityIntent.component?.className)
assertEquals(13L, communityIntent.getLongExtra("extra_post_id", 0L))
assertTrue(fragmentSource.contains("ContentActionCommand.AudioDetail("))
assertTrue(fragmentSource.contains("ContentActionCommand.SeriesDetail("))
assertTrue(fragmentSource.contains("handleContentAction("))
assertFalse(modelSource.contains("AudioContentDetailActivity"))
assertFalse(modelSource.contains("SeriesDetailActivity"))
}
@Test