refactor(content): 상세 진입을 공통 액션으로 통합한다
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -25,8 +25,6 @@ import com.google.gson.Gson
|
|||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import kr.co.vividnext.sodalive.R
|
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.audio_content.upload.AudioContentUploadActivity
|
||||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
|
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.report.UserReportDialog
|
||||||
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
|
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
|
||||||
import kr.co.vividnext.sodalive.v2.access.ensureV2Access
|
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.components.modal.V2ModalDialog
|
||||||
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment
|
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 isCreatorChannelOwner(): Boolean = currentHeader?.isOwner == true
|
||||||
|
|
||||||
override fun onCreatorChannelAudioContentClicked(audioContentId: Long) {
|
override fun onCreatorChannelAudioContentClicked(
|
||||||
startAudioContentDetail(audioContentId)
|
audioContentId: Long,
|
||||||
|
requiresAdultContentAccess: Boolean
|
||||||
|
) {
|
||||||
|
openAudioContentDetail(audioContentId, requiresAdultContentAccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatorChannelAudioContentChanged() {
|
override fun onCreatorChannelAudioContentChanged() {
|
||||||
@@ -572,12 +575,11 @@ class CreatorChannelActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatorChannelSeriesClicked(seriesId: Long) {
|
override fun onCreatorChannelSeriesClicked(
|
||||||
startActivity(
|
seriesId: Long,
|
||||||
Intent(this, SeriesDetailActivity::class.java).apply {
|
requiresAdultContentAccess: Boolean
|
||||||
putExtra(Constants.EXTRA_SERIES_ID, seriesId)
|
) {
|
||||||
}
|
openSeriesDetail(seriesId, requiresAdultContentAccess)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatorChannelSeriesContentChanged() {
|
override fun onCreatorChannelSeriesContentChanged() {
|
||||||
@@ -964,8 +966,11 @@ class CreatorChannelActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatorChannelLiveReplayClicked(audioContentId: Long) {
|
override fun onCreatorChannelLiveReplayClicked(
|
||||||
startAudioContentDetail(audioContentId)
|
audioContentId: Long,
|
||||||
|
requiresAdultContentAccess: Boolean
|
||||||
|
) {
|
||||||
|
openAudioContentDetail(audioContentId, requiresAdultContentAccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findLiveFragment(): CreatorChannelLiveFragment? {
|
private fun findLiveFragment(): CreatorChannelLiveFragment? {
|
||||||
@@ -1086,11 +1091,7 @@ class CreatorChannelActivity :
|
|||||||
private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse) {
|
private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse) {
|
||||||
when (schedule.type) {
|
when (schedule.type) {
|
||||||
CreatorActivityType.Audio,
|
CreatorActivityType.Audio,
|
||||||
CreatorActivityType.LiveReplay -> startActivity(
|
CreatorActivityType.LiveReplay -> openAudioContentDetail(schedule.targetId)
|
||||||
Intent(this, AudioContentDetailActivity::class.java).apply {
|
|
||||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, schedule.targetId)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
CreatorActivityType.Live -> liveCoordinator.showLiveRoomDetail(schedule.targetId)
|
CreatorActivityType.Live -> liveCoordinator.showLiveRoomDetail(schedule.targetId)
|
||||||
|
|
||||||
@@ -1099,22 +1100,34 @@ class CreatorChannelActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onAudioContentClicked(audioContent: CreatorChannelAudioContentResponse) {
|
private fun onAudioContentClicked(audioContent: CreatorChannelAudioContentResponse) {
|
||||||
startAudioContentDetail(audioContent.audioContentId)
|
openAudioContentDetail(audioContent.audioContentId, audioContent.isAdult)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startAudioContentDetail(audioContentId: Long) {
|
private fun openAudioContentDetail(
|
||||||
startActivity(
|
audioContentId: Long,
|
||||||
Intent(this, AudioContentDetailActivity::class.java).apply {
|
requiresAdultContentAccess: Boolean = false
|
||||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
|
) {
|
||||||
}
|
handleContentAction(
|
||||||
|
ContentActionCommand.AudioDetail(
|
||||||
|
audioContentId = audioContentId,
|
||||||
|
requiresAdultContentAccess = requiresAdultContentAccess
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSeriesClicked(series: CreatorChannelSeriesResponse) {
|
private fun onSeriesClicked(series: CreatorChannelSeriesResponse) {
|
||||||
startActivity(
|
openSeriesDetail(series.seriesId)
|
||||||
Intent(this, SeriesDetailActivity::class.java).apply {
|
}
|
||||||
putExtra(Constants.EXTRA_SERIES_ID, series.seriesId)
|
|
||||||
}
|
private fun openSeriesDetail(
|
||||||
|
seriesId: Long,
|
||||||
|
requiresAdultContentAccess: Boolean = false
|
||||||
|
) {
|
||||||
|
handleContentAction(
|
||||||
|
ContentActionCommand.SeriesDetail(
|
||||||
|
seriesId = seriesId,
|
||||||
|
requiresAdultContentAccess = requiresAdultContentAccess
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class CreatorChannelAudioFragment : BaseFragment<FragmentCreatorChannelAudioBind
|
|||||||
|
|
||||||
private val viewModel: CreatorChannelAudioViewModel by viewModel()
|
private val viewModel: CreatorChannelAudioViewModel by viewModel()
|
||||||
private val audioContentAdapter = CreatorChannelAudioContentAdapter { item ->
|
private val audioContentAdapter = CreatorChannelAudioContentAdapter { item ->
|
||||||
host.onCreatorChannelAudioContentClicked(item.audioContentId)
|
host.onCreatorChannelAudioContentClicked(item.audioContentId, item.showAdultBadge)
|
||||||
}
|
}
|
||||||
private var sortPopup: CreatorChannelSortPopup? = null
|
private var sortPopup: CreatorChannelSortPopup? = null
|
||||||
private var currentContentState: CreatorChannelAudioUiState.Content? = null
|
private var currentContentState: CreatorChannelAudioUiState.Content? = null
|
||||||
@@ -238,7 +238,10 @@ class CreatorChannelAudioFragment : BaseFragment<FragmentCreatorChannelAudioBind
|
|||||||
|
|
||||||
interface Host {
|
interface Host {
|
||||||
fun isCreatorChannelOwner(): Boolean
|
fun isCreatorChannelOwner(): Boolean
|
||||||
fun onCreatorChannelAudioContentClicked(audioContentId: Long)
|
fun onCreatorChannelAudioContentClicked(
|
||||||
|
audioContentId: Long,
|
||||||
|
requiresAdultContentAccess: Boolean
|
||||||
|
)
|
||||||
fun onCreatorChannelAudioContentChanged()
|
fun onCreatorChannelAudioContentChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
|
|||||||
|
|
||||||
private val viewModel: CreatorChannelLiveViewModel by viewModel()
|
private val viewModel: CreatorChannelLiveViewModel by viewModel()
|
||||||
private val replayAdapter = CreatorChannelAudioContentAdapter { item ->
|
private val replayAdapter = CreatorChannelAudioContentAdapter { item ->
|
||||||
host.onCreatorChannelLiveReplayClicked(item.audioContentId)
|
host.onCreatorChannelLiveReplayClicked(item.audioContentId, item.showAdultBadge)
|
||||||
}
|
}
|
||||||
private var lastContentLayoutKey: CreatorChannelLiveContentLayoutKey? = null
|
private var lastContentLayoutKey: CreatorChannelLiveContentLayoutKey? = null
|
||||||
private var sortPopup: CreatorChannelSortPopup? = null
|
private var sortPopup: CreatorChannelSortPopup? = null
|
||||||
@@ -189,7 +189,10 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
|
|||||||
|
|
||||||
interface Host {
|
interface Host {
|
||||||
fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)
|
fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)
|
||||||
fun onCreatorChannelLiveReplayClicked(audioContentId: Long)
|
fun onCreatorChannelLiveReplayClicked(
|
||||||
|
audioContentId: Long,
|
||||||
|
requiresAdultContentAccess: Boolean
|
||||||
|
)
|
||||||
fun onCreatorChannelLiveContentChanged()
|
fun onCreatorChannelLiveContentChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ class CreatorChannelSeriesFragment : BaseFragment<FragmentCreatorChannelSeriesBi
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
private val viewModel: CreatorChannelSeriesViewModel by viewModel()
|
private val viewModel: CreatorChannelSeriesViewModel by viewModel()
|
||||||
private val seriesAdapter = CreatorChannelSeriesAdapter { seriesId ->
|
private val seriesAdapter = CreatorChannelSeriesAdapter { item ->
|
||||||
host.onCreatorChannelSeriesClicked(seriesId)
|
host.onCreatorChannelSeriesClicked(item.seriesId, item.showAdultBadge)
|
||||||
}
|
}
|
||||||
private var sortPopup: CreatorChannelSortPopup? = null
|
private var sortPopup: CreatorChannelSortPopup? = null
|
||||||
private var currentContentState: CreatorChannelSeriesUiState.Content? = null
|
private var currentContentState: CreatorChannelSeriesUiState.Content? = null
|
||||||
@@ -154,7 +154,10 @@ class CreatorChannelSeriesFragment : BaseFragment<FragmentCreatorChannelSeriesBi
|
|||||||
|
|
||||||
interface Host {
|
interface Host {
|
||||||
fun isCreatorChannelOwner(): Boolean
|
fun isCreatorChannelOwner(): Boolean
|
||||||
fun onCreatorChannelSeriesClicked(seriesId: Long)
|
fun onCreatorChannelSeriesClicked(
|
||||||
|
seriesId: Long,
|
||||||
|
requiresAdultContentAccess: Boolean
|
||||||
|
)
|
||||||
fun onCreatorChannelSeriesContentChanged()
|
fun onCreatorChannelSeriesContentChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
import kr.co.vividnext.sodalive.v2.creator.channel.series.model.CreatorChannelSeriesSubtitleUiModel
|
||||||
|
|
||||||
class CreatorChannelSeriesAdapter(
|
class CreatorChannelSeriesAdapter(
|
||||||
private val onSeriesClicked: (Long) -> Unit = {}
|
private val onSeriesClicked: (CreatorChannelSeriesItemUiModel) -> Unit = {}
|
||||||
) : RecyclerView.Adapter<CreatorChannelSeriesAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<CreatorChannelSeriesAdapter.ViewHolder>() {
|
||||||
|
|
||||||
private var items: List<CreatorChannelSeriesItemUiModel> = emptyList()
|
private var items: List<CreatorChannelSeriesItemUiModel> = emptyList()
|
||||||
@@ -40,7 +40,7 @@ class CreatorChannelSeriesAdapter(
|
|||||||
|
|
||||||
class ViewHolder(
|
class ViewHolder(
|
||||||
private val binding: ItemCreatorChannelSeriesBinding,
|
private val binding: ItemCreatorChannelSeriesBinding,
|
||||||
private val onSeriesClicked: (Long) -> Unit
|
private val onSeriesClicked: (CreatorChannelSeriesItemUiModel) -> Unit
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -65,7 +65,7 @@ class CreatorChannelSeriesAdapter(
|
|||||||
tvCreatorChannelSeriesTitle.text = item.title
|
tvCreatorChannelSeriesTitle.text = item.title
|
||||||
tvCreatorChannelSeriesSubtitle.text = formatSubtitle(item.subtitle)
|
tvCreatorChannelSeriesSubtitle.text = formatSubtitle(item.subtitle)
|
||||||
bindProgress(item)
|
bindProgress(item)
|
||||||
root.setOnClickListener { onSeriesClicked(item.seriesId) }
|
root.setOnClickListener { onSeriesClicked(item) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatSubtitle(subtitle: CreatorChannelSeriesSubtitleUiModel): String {
|
private fun formatSubtitle(subtitle: CreatorChannelSeriesSubtitleUiModel): String {
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity
|
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.base.BaseFragment
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.common.ToastMessage
|
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
|
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.main.MainV2Activity
|
||||||
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
|
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
|
||||||
import kr.co.vividnext.sodalive.v2.access.ensureV2Access
|
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.AudioRankingType
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType
|
import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.AudioRankingsUiState
|
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.ContentAudioCardSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentAudioCardUiModel
|
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.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.ContentBannerUiModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioSection
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioUiModel
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioUiModel
|
||||||
@@ -616,8 +616,13 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
|
|
||||||
private fun onBannerClick(item: ContentBannerUiModel) {
|
private fun onBannerClick(item: ContentBannerUiModel) {
|
||||||
val route = item.toContentBannerRoute() ?: return
|
val route = item.toContentBannerRoute() ?: return
|
||||||
ensureV2Access(AccessRequirement.Login) {
|
when (route) {
|
||||||
startActivity(route.toContentBannerIntent(requireContext()))
|
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,
|
audioContentId: Long,
|
||||||
requiresAdultContentAccess: Boolean = false
|
requiresAdultContentAccess: Boolean = false
|
||||||
) {
|
) {
|
||||||
if (audioContentId <= 0L) return
|
handleContentAction(
|
||||||
ensureV2Access(AccessRequirement.forAdultContent(requiresAdultContentAccess)) {
|
ContentActionCommand.AudioDetail(
|
||||||
startActivity(
|
audioContentId = audioContentId,
|
||||||
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
|
requiresAdultContentAccess = requiresAdultContentAccess
|
||||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openRankingAudioContentDetail(item: ContentRankingItem) {
|
private fun openRankingAudioContentDetail(item: ContentRankingItem) {
|
||||||
val audioContentId = item.contentId.toLongOrNull()?.takeIf { it > 0L } ?: return
|
val audioContentId = item.contentId.toLongOrNull() ?: return
|
||||||
openAudioContentDetail(audioContentId)
|
openAudioContentDetail(audioContentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openSeriesDetail(item: ContentOriginalSeriesUiModel) {
|
private fun openSeriesDetail(item: ContentOriginalSeriesUiModel) {
|
||||||
val seriesId = item.seriesId.takeIf { it > 0L } ?: return
|
openSeriesDetail(item.seriesId)
|
||||||
openSeriesDetail(seriesId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openSeriesDetail(
|
private fun openSeriesDetail(
|
||||||
seriesId: Long,
|
seriesId: Long,
|
||||||
requiresAdultContentAccess: Boolean = false
|
requiresAdultContentAccess: Boolean = false
|
||||||
) {
|
) {
|
||||||
if (seriesId <= 0L) return
|
handleContentAction(
|
||||||
ensureV2Access(AccessRequirement.forAdultContent(requiresAdultContentAccess)) {
|
ContentActionCommand.SeriesDetail(
|
||||||
startActivity(
|
seriesId = seriesId,
|
||||||
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
|
requiresAdultContentAccess = requiresAdultContentAccess
|
||||||
putExtra(Constants.EXTRA_SERIES_ID, seriesId)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findAllTabAudioAdultAccess(audioContentId: Long): Boolean {
|
private fun findAllTabAudioAdultAccess(audioContentId: Long): Boolean {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import kr.co.vividnext.sodalive.BuildConfig
|
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.common.Constants
|
||||||
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
|
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
|
||||||
import kr.co.vividnext.sodalive.settings.event.EventItem
|
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) {
|
return when (this) {
|
||||||
is ContentBannerRoute.Event -> Intent(context, EventDetailActivity::class.java).apply {
|
is ContentBannerRoute.Event -> Intent(context, EventDetailActivity::class.java).apply {
|
||||||
putExtra(Constants.EXTRA_EVENT, eventItem)
|
putExtra(Constants.EXTRA_EVENT, eventItem)
|
||||||
@@ -71,9 +70,7 @@ fun ContentBannerRoute.toContentBannerIntent(context: Context): Intent {
|
|||||||
|
|
||||||
is ContentBannerRoute.Creator -> CreatorChannelActivity.newIntent(context, creatorId)
|
is ContentBannerRoute.Creator -> CreatorChannelActivity.newIntent(context, creatorId)
|
||||||
|
|
||||||
is ContentBannerRoute.Series -> Intent(context, SeriesDetailActivity::class.java).apply {
|
is ContentBannerRoute.Series -> null
|
||||||
putExtra(Constants.EXTRA_SERIES_ID, seriesId)
|
|
||||||
}
|
|
||||||
|
|
||||||
is ContentBannerRoute.Link -> Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
is ContentBannerRoute.Link -> Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import androidx.core.view.doOnLayout
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kr.co.vividnext.sodalive.R
|
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.base.BaseActivity
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityContentOverviewBinding
|
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.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.ContentOverviewUiState
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.overview.model.toTitleResId
|
import kr.co.vividnext.sodalive.v2.main.content.overview.model.toTitleResId
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.overview.ui.ContentOverviewAdapter
|
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)) }
|
?: toastMessage.resId?.let { resId -> showToast(getString(resId)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openAudioContentDetail(contentId: Long) {
|
private fun openAudioContentDetail(item: ContentOverviewUiModel) {
|
||||||
if (contentId <= 0) return
|
handleContentAction(
|
||||||
|
ContentActionCommand.AudioDetail(
|
||||||
startActivity(
|
audioContentId = item.contentId,
|
||||||
Intent(this, AudioContentDetailActivity::class.java).apply {
|
requiresAdultContentAccess = item.showAdultBadge
|
||||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
|
)
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import kr.co.vividnext.sodalive.extensions.loadUrl
|
|||||||
import kr.co.vividnext.sodalive.v2.main.content.overview.model.ContentOverviewUiModel
|
import kr.co.vividnext.sodalive.v2.main.content.overview.model.ContentOverviewUiModel
|
||||||
|
|
||||||
class ContentOverviewAdapter(
|
class ContentOverviewAdapter(
|
||||||
private val onContentClick: (Long) -> Unit = {}
|
private val onContentClick: (ContentOverviewUiModel) -> Unit = {}
|
||||||
) : RecyclerView.Adapter<ContentOverviewAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<ContentOverviewAdapter.ViewHolder>() {
|
||||||
|
|
||||||
private var items: List<ContentOverviewUiModel> = emptyList()
|
private var items: List<ContentOverviewUiModel> = emptyList()
|
||||||
@@ -40,7 +40,7 @@ class ContentOverviewAdapter(
|
|||||||
|
|
||||||
class ViewHolder(
|
class ViewHolder(
|
||||||
private val binding: ItemContentAudioCardBinding,
|
private val binding: ItemContentAudioCardBinding,
|
||||||
private val onContentClick: (Long) -> Unit
|
private val onContentClick: (ContentOverviewUiModel) -> Unit
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(item: ContentOverviewUiModel, gridItemWidthPx: Int) = with(binding.audioContentCard.root) {
|
fun bind(item: ContentOverviewUiModel, gridItemWidthPx: Int) = with(binding.audioContentCard.root) {
|
||||||
setGridItemWidthPx(gridItemWidthPx)
|
setGridItemWidthPx(gridItemWidthPx)
|
||||||
@@ -49,8 +49,7 @@ class ContentOverviewAdapter(
|
|||||||
setAdultVisible(item.showAdultBadge)
|
setAdultVisible(item.showAdultBadge)
|
||||||
thumbnailView().loadUrl(item.coverImage)
|
thumbnailView().loadUrl(item.coverImage)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
if (item.contentId <= 0) return@setOnClickListener
|
onContentClick(item)
|
||||||
onContentClick(item.contentId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.main.chat.model.ChatRoomType
|
||||||
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
|
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
|
||||||
import kr.co.vividnext.sodalive.v2.access.ensureV2Access
|
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.HomeFollowingChatSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingCreatorSection
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingCreatorSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingLiveSection
|
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.HomeRecommendationAiCharacterSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationAiCharacterUiModel
|
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.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.HomeRecommendationBannerUiModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCheerCreatorSection
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCheerCreatorSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationGenreCreatorSection
|
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.HomeRecommendationPopularCommunityPostUiModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentDebutCreatorSection
|
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.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.HomeRecommendationRecentlyActiveCreatorUiModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationUiState
|
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.toHomeRecommendationBannerIntent
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerRoute
|
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.toHomeRecommendationAiCharacterIntent
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationAiCharacterRoute
|
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.toHomeRecommendationRecentlyActiveCreatorRoute
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter
|
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeBannerBinder
|
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeBannerBinder
|
||||||
@@ -581,15 +584,25 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
|
|
||||||
private fun onBannerClick(item: HomeRecommendationBannerUiModel) {
|
private fun onBannerClick(item: HomeRecommendationBannerUiModel) {
|
||||||
val route = item.toHomeRecommendationBannerRoute() ?: return
|
val route = item.toHomeRecommendationBannerRoute() ?: return
|
||||||
ensureV2Access(AccessRequirement.Login) {
|
when (route) {
|
||||||
startActivity(route.toHomeRecommendationBannerIntent(requireContext()))
|
is HomeRecommendationBannerRoute.Series -> handleContentAction(
|
||||||
|
ContentActionCommand.SeriesDetail(seriesId = route.seriesId)
|
||||||
|
)
|
||||||
|
else -> ensureV2Access(AccessRequirement.Login) {
|
||||||
|
route.toHomeRecommendationBannerIntent(requireContext())?.let(::startActivity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRecentActivityClick(item: HomeRecommendationRecentlyActiveCreatorUiModel) {
|
private fun onRecentActivityClick(item: HomeRecommendationRecentlyActiveCreatorUiModel) {
|
||||||
val route = item.toHomeRecommendationRecentlyActiveCreatorRoute() ?: return
|
val route = item.toHomeRecommendationRecentlyActiveCreatorRoute() ?: return
|
||||||
ensureV2Access(AccessRequirement.Login) {
|
when (route) {
|
||||||
startActivity(route.toHomeRecommendationRecentlyActiveCreatorIntent(requireContext()))
|
is HomeRecommendationRecentlyActiveCreatorRoute.AudioContent -> handleContentAction(
|
||||||
|
ContentActionCommand.AudioDetail(audioContentId = route.contentId)
|
||||||
|
)
|
||||||
|
is HomeRecommendationRecentlyActiveCreatorRoute.Community -> ensureV2Access(AccessRequirement.Login) {
|
||||||
|
startActivity(CreatorChannelCommunityDetailActivity.newIntent(requireContext(), route.postId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,11 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import kr.co.vividnext.sodalive.BuildConfig
|
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.Constants
|
||||||
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
|
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
|
||||||
import kr.co.vividnext.sodalive.settings.event.EventItem
|
import kr.co.vividnext.sodalive.settings.event.EventItem
|
||||||
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
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.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.characterchatthumbnail.CharacterChatThumbnailItem
|
||||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem
|
import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem
|
||||||
import java.util.Locale
|
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) {
|
return when (this) {
|
||||||
is HomeRecommendationBannerRoute.Event -> {
|
is HomeRecommendationBannerRoute.Event -> {
|
||||||
Intent(context, EventDetailActivity::class.java).apply {
|
Intent(context, EventDetailActivity::class.java).apply {
|
||||||
@@ -112,11 +109,7 @@ fun HomeRecommendationBannerRoute.toHomeRecommendationBannerIntent(context: Cont
|
|||||||
CreatorChannelActivity.newIntent(context, creatorId)
|
CreatorChannelActivity.newIntent(context, creatorId)
|
||||||
}
|
}
|
||||||
|
|
||||||
is HomeRecommendationBannerRoute.Series -> {
|
is HomeRecommendationBannerRoute.Series -> null
|
||||||
Intent(context, SeriesDetailActivity::class.java).apply {
|
|
||||||
putExtra(Constants.EXTRA_SERIES_ID, seriesId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is HomeRecommendationBannerRoute.Link -> Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
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(
|
data class HomeRecommendationCreatorUiModel(
|
||||||
val creatorId: Long,
|
val creatorId: Long,
|
||||||
val nickname: String,
|
val nickname: String,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -601,8 +601,11 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(source.contains("position == CreatorChannelTab.Series.ordinal"))
|
assertTrue(source.contains("position == CreatorChannelTab.Series.ordinal"))
|
||||||
assertTrue(source.contains("findSeriesFragment()?.onCreatorChannelSeriesViewportHeightChanged(minHeight)"))
|
assertTrue(source.contains("findSeriesFragment()?.onCreatorChannelSeriesViewportHeightChanged(minHeight)"))
|
||||||
assertTrue(source.contains("override fun onCreatorChannelSeriesContentChanged()"))
|
assertTrue(source.contains("override fun onCreatorChannelSeriesContentChanged()"))
|
||||||
assertTrue(source.contains("override fun onCreatorChannelSeriesClicked(seriesId: Long)"))
|
assertTrue(source.contains("override fun onCreatorChannelSeriesClicked("))
|
||||||
assertTrue(source.contains("putExtra(Constants.EXTRA_SERIES_ID, seriesId)"))
|
assertTrue(source.contains("seriesId: Long"))
|
||||||
|
assertTrue(source.contains("requiresAdultContentAccess: Boolean"))
|
||||||
|
assertTrue(source.contains("ContentActionCommand.SeriesDetail("))
|
||||||
|
assertTrue(source.contains("requiresAdultContentAccess = requiresAdultContentAccess"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -1582,8 +1585,9 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(source.contains("private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse)"))
|
assertTrue(source.contains("private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse)"))
|
||||||
assertTrue(source.contains("CreatorActivityType.Audio"))
|
assertTrue(source.contains("CreatorActivityType.Audio"))
|
||||||
assertTrue(source.contains("CreatorActivityType.LiveReplay"))
|
assertTrue(source.contains("CreatorActivityType.LiveReplay"))
|
||||||
assertTrue(source.contains("AudioContentDetailActivity::class.java"))
|
assertTrue(source.contains("openAudioContentDetail(schedule.targetId)"))
|
||||||
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, schedule.targetId)"))
|
assertTrue(source.contains("ContentActionCommand.AudioDetail("))
|
||||||
|
assertFalse(source.contains("AudioContentDetailActivity"))
|
||||||
assertTrue(source.contains("CreatorActivityType.Live"))
|
assertTrue(source.contains("CreatorActivityType.Live"))
|
||||||
assertTrue(source.contains("CreatorActivityType.Live -> liveCoordinator.showLiveRoomDetail(schedule.targetId)"))
|
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("private fun onAudioContentClicked(audioContent: CreatorChannelAudioContentResponse)"))
|
||||||
assertTrue(fragment.contains("host.onCreatorChannelAudioContentClicked(audioContent)"))
|
assertTrue(fragment.contains("host.onCreatorChannelAudioContentClicked(audioContent)"))
|
||||||
assertTrue(source.contains("private fun onAudioContentClicked(audioContent: CreatorChannelAudioContentResponse)"))
|
assertTrue(source.contains("private fun onAudioContentClicked(audioContent: CreatorChannelAudioContentResponse)"))
|
||||||
assertTrue(source.contains("AudioContentDetailActivity::class.java"))
|
assertTrue(
|
||||||
assertTrue(source.contains("startAudioContentDetail(audioContent.audioContentId)"))
|
source.contains("openAudioContentDetail(audioContent.audioContentId, audioContent.isAdult)")
|
||||||
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)"))
|
)
|
||||||
|
assertTrue(source.contains("ContentActionCommand.AudioDetail("))
|
||||||
|
assertTrue(source.contains("requiresAdultContentAccess = requiresAdultContentAccess"))
|
||||||
|
assertFalse(source.contains("AudioContentDetailActivity"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -1816,10 +1823,10 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(fragment.contains("onSeriesClick = ::onSeriesClicked"))
|
assertTrue(fragment.contains("onSeriesClick = ::onSeriesClicked"))
|
||||||
assertTrue(fragment.contains("private fun onSeriesClicked(series: CreatorChannelSeriesResponse)"))
|
assertTrue(fragment.contains("private fun onSeriesClicked(series: CreatorChannelSeriesResponse)"))
|
||||||
assertTrue(fragment.contains("host.onCreatorChannelSeriesClicked(series)"))
|
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("private fun onSeriesClicked(series: CreatorChannelSeriesResponse)"))
|
||||||
assertTrue(source.contains("SeriesDetailActivity::class.java"))
|
assertTrue(source.contains("openSeriesDetail(series.seriesId)"))
|
||||||
assertTrue(source.contains("putExtra(Constants.EXTRA_SERIES_ID, series.seriesId)"))
|
assertTrue(source.contains("ContentActionCommand.SeriesDetail("))
|
||||||
|
assertFalse(source.contains("SeriesDetailActivity"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -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_subtitle_content_count"))
|
||||||
assertTrue(adapter.contains("R.string.creator_channel_series_status_proceeding"))
|
assertTrue(adapter.contains("R.string.creator_channel_series_status_proceeding"))
|
||||||
assertTrue(adapter.contains("R.string.creator_channel_series_status_completed"))
|
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"))
|
assertTrue(!adapter.contains("tvCreatorChannelSeriesSubtitle.text = item.subtitle"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,27 +8,39 @@ import java.io.File
|
|||||||
class ContentMainFragmentLoginGuardSourceTest {
|
class ContentMainFragmentLoginGuardSourceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ContentMainFragment 실제 이동은 MainV2 로그인 가드를 통과한다`() {
|
fun `ContentMainFragment 상세 이동은 공통 Content Action Access를 통과한다`() {
|
||||||
val source = projectFile(
|
val fragmentSource = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||||
).readText()
|
).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"))
|
assertTrue(fragmentSource.contains("import kr.co.vividnext.sodalive.v2.content.action.handleContentAction"))
|
||||||
assertGuardedStartActivity(source, "private fun onBannerClick(item: ContentBannerUiModel)")
|
assertTrue(fragmentSource.contains("ContentActionCommand.AudioDetail("))
|
||||||
assertGuardedStartActivity(source, "audioContentId: Long,\n requiresAdultContentAccess: Boolean = false")
|
assertTrue(fragmentSource.contains("ContentActionCommand.SeriesDetail("))
|
||||||
assertGuardedStartActivity(source, "seriesId: Long,\n requiresAdultContentAccess: Boolean = false")
|
assertTrue(handlerSource.contains("action.execute(command, ensureAccess)"))
|
||||||
assertFalse(source.contains("kr.co.vividnext.sodalive.main.MainActivity"))
|
assertTrue(
|
||||||
|
handlerSource.indexOf("action.execute(command, ensureAccess)") <
|
||||||
|
handlerSource.indexOf("context.startActivity(")
|
||||||
|
)
|
||||||
|
assertFalse(fragmentSource.contains("kr.co.vividnext.sodalive.main.MainActivity"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ContentMainFragment invalid id return은 로그인 가드보다 먼저 유지된다`() {
|
fun `Content Action invalid ID 결과는 Access 확인보다 먼저 유지된다`() {
|
||||||
val source = projectFile(
|
val fragmentSource = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||||
).readText()
|
).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")
|
assertTrue(fragmentSource.contains("val route = item.toContentBannerRoute() ?: return"))
|
||||||
assertBeforeGuard(source, "if (audioContentId <= 0L) return")
|
assertTrue(
|
||||||
assertBeforeGuard(source, "if (seriesId <= 0L) return")
|
actionSource.indexOf("if (!isValidId) return ContentActionResult.Ignored") <
|
||||||
|
actionSource.indexOf("if (!ensureAccess(requirement))")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -40,34 +52,15 @@ class ContentMainFragmentLoginGuardSourceTest {
|
|||||||
assertTrue(source.contains("openAudioContentDetail(item.audioContentId, item.showAdultBadge)"))
|
assertTrue(source.contains("openAudioContentDetail(item.audioContentId, item.showAdultBadge)"))
|
||||||
assertTrue(source.contains("openAudioContentDetail(audioContentId, findAllTabAudioAdultAccess(audioContentId))"))
|
assertTrue(source.contains("openAudioContentDetail(audioContentId, findAllTabAudioAdultAccess(audioContentId))"))
|
||||||
assertTrue(source.contains("openSeriesDetail(seriesId, findAllTabSeriesAdultAccess(seriesId))"))
|
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("ContentCommentedAudioUiModel.showAdultBadge"))
|
||||||
assertFalse(source.contains("ContentOriginalSeriesUiModel.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 {
|
private fun projectFile(relativePath: String): File {
|
||||||
val candidates = listOf(File(relativePath), File("../$relativePath"))
|
val candidates = listOf(File(relativePath), File("../$relativePath"))
|
||||||
return candidates.firstOrNull { it.exists() }
|
return candidates.firstOrNull { it.exists() }
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.v2.main.content
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import kr.co.vividnext.sodalive.BuildConfig
|
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.common.Constants
|
||||||
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
|
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
|
||||||
import kr.co.vividnext.sodalive.settings.event.EventItem
|
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 kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerRoute
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@@ -140,7 +140,7 @@ class ContentMainFragmentSourceTest {
|
|||||||
assertTrue(source.contains("loadMore()"))
|
assertTrue(source.contains("loadMore()"))
|
||||||
assertTrue(source.contains("consumePaginationErrorMessage()"))
|
assertTrue(source.contains("consumePaginationErrorMessage()"))
|
||||||
assertTrue(source.contains("openAudioContentDetail(audioContentId)"))
|
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("override fun onDestroyView()"))
|
||||||
assertTrue(source.contains("sortPopup?.dismiss()"))
|
assertTrue(source.contains("sortPopup?.dismiss()"))
|
||||||
assertTrue(source.contains("sortPopup = null"))
|
assertTrue(source.contains("sortPopup = null"))
|
||||||
@@ -240,27 +240,28 @@ class ContentMainFragmentSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `content 추천 source는 오디오와 시리즈 routing extra를 사용한다`() {
|
fun `content 추천 source는 오디오와 시리즈 상세를 Content Action에 위임한다`() {
|
||||||
val source = projectFile(
|
val source = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||||
).readText()
|
).readText()
|
||||||
|
|
||||||
assertTrue(source.contains("AudioContentDetailActivity::class.java"))
|
assertTrue(source.contains("handleContentAction("))
|
||||||
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)"))
|
assertTrue(source.contains("ContentActionCommand.AudioDetail("))
|
||||||
assertTrue(source.contains("SeriesDetailActivity::class.java"))
|
assertTrue(source.contains("ContentActionCommand.SeriesDetail("))
|
||||||
assertTrue(source.contains("putExtra(Constants.EXTRA_SERIES_ID, seriesId)"))
|
assertFalse(source.contains("AudioContentDetailActivity"))
|
||||||
|
assertFalse(source.contains("SeriesDetailActivity"))
|
||||||
assertTrue(source.contains("toContentBannerRoute()"))
|
assertTrue(source.contains("toContentBannerRoute()"))
|
||||||
assertTrue(source.contains("toContentBannerIntent(requireContext())"))
|
assertTrue(source.contains("toContentBannerIntent(requireContext())"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `content 랭킹 routing은 유효하지 않은 contentId를 무시한다`() {
|
fun `content 랭킹 routing은 숫자가 아닌 contentId를 무시한다`() {
|
||||||
val source = projectFile(
|
val source = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||||
).readText()
|
).readText()
|
||||||
|
|
||||||
assertTrue(source.contains("private fun openRankingAudioContentDetail(item: ContentRankingItem)"))
|
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)"))
|
assertTrue(source.contains("openAudioContentDetail(audioContentId)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,21 +466,24 @@ class ContentMainFragmentSourceTest {
|
|||||||
fun `content banner route creates expected intents`() {
|
fun `content banner route creates expected intents`() {
|
||||||
val context = RuntimeEnvironment.getApplication() as Context
|
val context = RuntimeEnvironment.getApplication() as Context
|
||||||
val eventItem = EventItem(id = 1L, thumbnailImageUrl = "https://example.com/event.png")
|
val eventItem = EventItem(id = 1L, thumbnailImageUrl = "https://example.com/event.png")
|
||||||
val eventIntent = ContentBannerRoute.Event(eventItem).toContentBannerIntent(context)
|
val eventIntent = requireNotNull(ContentBannerRoute.Event(eventItem).toContentBannerIntent(context))
|
||||||
val creatorIntent = ContentBannerRoute.Creator(2L).toContentBannerIntent(context)
|
val creatorIntent = requireNotNull(ContentBannerRoute.Creator(2L).toContentBannerIntent(context))
|
||||||
val seriesIntent = ContentBannerRoute.Series(3L).toContentBannerIntent(context)
|
val seriesIntent = ContentBannerRoute.Series(3L).toContentBannerIntent(context)
|
||||||
val webIntent = ContentBannerRoute.Link("https://example.com", isWebUrl = true).toContentBannerIntent(context)
|
val webIntent = requireNotNull(
|
||||||
val deepLinkIntent = ContentBannerRoute.Link(
|
ContentBannerRoute.Link("https://example.com", isWebUrl = true).toContentBannerIntent(context)
|
||||||
url = "${BuildConfig.APPSCHEME}://series/3",
|
)
|
||||||
isWebUrl = false
|
val deepLinkIntent = requireNotNull(
|
||||||
).toContentBannerIntent(context)
|
ContentBannerRoute.Link(
|
||||||
|
url = "${BuildConfig.APPSCHEME}://series/3",
|
||||||
|
isWebUrl = false
|
||||||
|
).toContentBannerIntent(context)
|
||||||
|
)
|
||||||
|
|
||||||
assertEquals(EventDetailActivity::class.java.name, eventIntent.component?.className)
|
assertEquals(EventDetailActivity::class.java.name, eventIntent.component?.className)
|
||||||
assertEquals(eventItem, eventIntent.getParcelableExtra(Constants.EXTRA_EVENT))
|
assertEquals(eventItem, eventIntent.getParcelableExtra(Constants.EXTRA_EVENT))
|
||||||
assertEquals(CreatorChannelActivity::class.java.name, creatorIntent.component?.className)
|
assertEquals(CreatorChannelActivity::class.java.name, creatorIntent.component?.className)
|
||||||
assertEquals(2L, creatorIntent.getLongExtra(CreatorChannelActivity.EXTRA_CREATOR_ID, 0L))
|
assertEquals(2L, creatorIntent.getLongExtra(CreatorChannelActivity.EXTRA_CREATOR_ID, 0L))
|
||||||
assertEquals(SeriesDetailActivity::class.java.name, seriesIntent.component?.className)
|
assertNull(seriesIntent)
|
||||||
assertEquals(3L, seriesIntent.getLongExtra(Constants.EXTRA_SERIES_ID, 0L))
|
|
||||||
assertEquals(android.content.Intent.ACTION_VIEW, webIntent.action)
|
assertEquals(android.content.Intent.ACTION_VIEW, webIntent.action)
|
||||||
assertEquals("https://example.com", webIntent.data.toString())
|
assertEquals("https://example.com", webIntent.data.toString())
|
||||||
assertEquals(android.content.Intent.ACTION_VIEW, deepLinkIntent.action)
|
assertEquals(android.content.Intent.ACTION_VIEW, deepLinkIntent.action)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.main.content.overview
|
package kr.co.vividnext.sodalive.v2.main.content.overview
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@@ -63,24 +64,26 @@ class ContentOverviewActivitySourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ContentOverviewActivity opens audio detail with content id`() {
|
fun `ContentOverviewActivity는 ID와 성인 여부를 Content Action에 전달한다`() {
|
||||||
val source = projectFile(
|
val source = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivity.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivity.kt"
|
||||||
).readText()
|
).readText()
|
||||||
|
|
||||||
assertTrue(source.contains("AudioContentDetailActivity::class.java"))
|
assertTrue(source.contains("handleContentAction("))
|
||||||
assertTrue(source.contains("if (contentId <= 0) return"))
|
assertTrue(source.contains("ContentActionCommand.AudioDetail("))
|
||||||
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)"))
|
assertTrue(source.contains("audioContentId = item.contentId"))
|
||||||
|
assertTrue(source.contains("requiresAdultContentAccess = item.showAdultBadge"))
|
||||||
|
assertFalse(source.contains("AudioContentDetailActivity"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ContentOverviewAdapter ignores invalid content id clicks`() {
|
fun `ContentOverviewAdapter는 Action 검증을 위해 전체 항목을 전달한다`() {
|
||||||
val source = projectFile(
|
val source = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ui/ContentOverviewAdapter.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ui/ContentOverviewAdapter.kt"
|
||||||
).readText()
|
).readText()
|
||||||
|
|
||||||
assertTrue(source.contains("if (item.contentId <= 0) return@setOnClickListener"))
|
assertTrue(source.contains("onContentClick(item)"))
|
||||||
assertTrue(source.contains("onContentClick(item.contentId)"))
|
assertFalse(source.contains("if (item.contentId <= 0)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -23,14 +23,11 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.BuildConfig
|
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.Constants
|
||||||
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
|
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
|
||||||
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
|
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
|
||||||
import kr.co.vividnext.sodalive.settings.event.EventItem
|
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.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.HomeActiveCreatorItem
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.data.HomeAiCharacterItem
|
import kr.co.vividnext.sodalive.v2.main.home.data.HomeAiCharacterItem
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.data.HomeBannerItem
|
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.toHomeRecommendationBannerRoute
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationAiCharacterIntent
|
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.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.toHomeRecommendationRecentlyActiveCreatorRoute
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomePopularCommunityPosts
|
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomePopularCommunityPosts
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups
|
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.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Assert.assertSame
|
import org.junit.Assert.assertSame
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -872,24 +869,31 @@ class HomeMainFragmentLayoutTest {
|
|||||||
fun `home banner route creates activity and link intents with expected extras`() {
|
fun `home banner route creates activity and link intents with expected extras`() {
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
val eventItem = EventItem(id = 1L, thumbnailImageUrl = "https://example.com/event.png")
|
val eventItem = EventItem(id = 1L, thumbnailImageUrl = "https://example.com/event.png")
|
||||||
val eventIntent = HomeRecommendationBannerRoute.Event(eventItem).toHomeRecommendationBannerIntent(context)
|
val eventIntent = requireNotNull(
|
||||||
val creatorIntent = HomeRecommendationBannerRoute.Creator(2L).toHomeRecommendationBannerIntent(context)
|
HomeRecommendationBannerRoute.Event(eventItem).toHomeRecommendationBannerIntent(context)
|
||||||
|
)
|
||||||
|
val creatorIntent = requireNotNull(
|
||||||
|
HomeRecommendationBannerRoute.Creator(2L).toHomeRecommendationBannerIntent(context)
|
||||||
|
)
|
||||||
val seriesIntent = HomeRecommendationBannerRoute.Series(3L).toHomeRecommendationBannerIntent(context)
|
val seriesIntent = HomeRecommendationBannerRoute.Series(3L).toHomeRecommendationBannerIntent(context)
|
||||||
val webIntent = HomeRecommendationBannerRoute.Link(
|
val webIntent = requireNotNull(
|
||||||
url = "https://example.com",
|
HomeRecommendationBannerRoute.Link(
|
||||||
isWebUrl = true
|
url = "https://example.com",
|
||||||
).toHomeRecommendationBannerIntent(context)
|
isWebUrl = true
|
||||||
val deepLinkIntent = HomeRecommendationBannerRoute.Link(
|
).toHomeRecommendationBannerIntent(context)
|
||||||
url = "${BuildConfig.APPSCHEME}://series/3",
|
)
|
||||||
isWebUrl = false
|
val deepLinkIntent = requireNotNull(
|
||||||
).toHomeRecommendationBannerIntent(context)
|
HomeRecommendationBannerRoute.Link(
|
||||||
|
url = "${BuildConfig.APPSCHEME}://series/3",
|
||||||
|
isWebUrl = false
|
||||||
|
).toHomeRecommendationBannerIntent(context)
|
||||||
|
)
|
||||||
|
|
||||||
assertEquals(EventDetailActivity::class.java.name, eventIntent.component?.className)
|
assertEquals(EventDetailActivity::class.java.name, eventIntent.component?.className)
|
||||||
assertEquals(eventItem, eventIntent.getParcelableExtra(Constants.EXTRA_EVENT))
|
assertEquals(eventItem, eventIntent.getParcelableExtra(Constants.EXTRA_EVENT))
|
||||||
assertEquals(CreatorChannelActivity::class.java.name, creatorIntent.component?.className)
|
assertEquals(CreatorChannelActivity::class.java.name, creatorIntent.component?.className)
|
||||||
assertEquals(2L, creatorIntent.getLongExtra(CreatorChannelActivity.EXTRA_CREATOR_ID, 0L))
|
assertEquals(2L, creatorIntent.getLongExtra(CreatorChannelActivity.EXTRA_CREATOR_ID, 0L))
|
||||||
assertEquals(SeriesDetailActivity::class.java.name, seriesIntent.component?.className)
|
assertNull(seriesIntent)
|
||||||
assertEquals(3L, seriesIntent.getLongExtra(Constants.EXTRA_SERIES_ID, 0L))
|
|
||||||
assertEquals(android.content.Intent.ACTION_VIEW, webIntent.action)
|
assertEquals(android.content.Intent.ACTION_VIEW, webIntent.action)
|
||||||
assertEquals("https://example.com", webIntent.data.toString())
|
assertEquals("https://example.com", webIntent.data.toString())
|
||||||
assertEquals(android.content.Intent.ACTION_VIEW, deepLinkIntent.action)
|
assertEquals(android.content.Intent.ACTION_VIEW, deepLinkIntent.action)
|
||||||
@@ -947,17 +951,19 @@ class HomeMainFragmentLayoutTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `home recent activity route creates expected intents`() {
|
fun `home 오디오와 시리즈 route는 Content Action을 사용한다`() {
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
val fragmentSource = projectFile(
|
||||||
val audioIntent = HomeRecommendationRecentlyActiveCreatorRoute.AudioContent(11L)
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
|
||||||
.toHomeRecommendationRecentlyActiveCreatorIntent(context)
|
).readText()
|
||||||
val communityIntent = HomeRecommendationRecentlyActiveCreatorRoute.Community(13L)
|
val modelSource = projectFile(
|
||||||
.toHomeRecommendationRecentlyActiveCreatorIntent(context)
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiModels.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
assertEquals(AudioContentDetailActivity::class.java.name, audioIntent.component?.className)
|
assertTrue(fragmentSource.contains("ContentActionCommand.AudioDetail("))
|
||||||
assertEquals(11L, audioIntent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0L))
|
assertTrue(fragmentSource.contains("ContentActionCommand.SeriesDetail("))
|
||||||
assertEquals(CreatorChannelCommunityDetailActivity::class.java.name, communityIntent.component?.className)
|
assertTrue(fragmentSource.contains("handleContentAction("))
|
||||||
assertEquals(13L, communityIntent.getLongExtra("extra_post_id", 0L))
|
assertFalse(modelSource.contains("AudioContentDetailActivity"))
|
||||||
|
assertFalse(modelSource.contains("SeriesDetailActivity"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -343,7 +343,7 @@
|
|||||||
- 2026-07-14: Task 1.11에서는 production 코드를 추가 변경하지 않고 기존 source test 클래스 두 곳과 계획 문서만 보강했다.
|
- 2026-07-14: Task 1.11에서는 production 코드를 추가 변경하지 않고 기존 source test 클래스 두 곳과 계획 문서만 보강했다.
|
||||||
|
|
||||||
### Phase 2: Content 공통 Action
|
### Phase 2: Content 공통 Action
|
||||||
- [ ] **Task 2.1: 오디오/시리즈 진입 호출부와 계약 확정**
|
- [x] **Task 2.1: 오디오/시리즈 진입 호출부와 계약 확정**
|
||||||
- 확인 예정 파일:
|
- 확인 예정 파일:
|
||||||
- `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`
|
- `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/ContentMainFragment.kt`
|
||||||
@@ -353,23 +353,56 @@
|
|||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 화면 전용 UI model을 받지 않는 최소 command/result 계약이 정해진다.
|
- 화면 전용 UI model을 받지 않는 최소 command/result 계약이 정해진다.
|
||||||
- Activity result 또는 호출 화면 후처리가 있는 경로는 Content result adapter와 단일 handler 필요 여부가 결정된다.
|
- Activity result 또는 호출 화면 후처리가 있는 경로는 Content result adapter와 단일 handler 필요 여부가 결정된다.
|
||||||
|
- 확인 결과:
|
||||||
|
- 공개 입력은 `audioContentId` 또는 `seriesId`와 호출부가 이미 알고 있는 성인 콘텐츠 여부만 받는 `AudioDetail`/`SeriesDetail` command로 제한한다. Home/Content/Creator 전용 UI model과 Response는 Action에 전달하지 않는다.
|
||||||
|
- Action 결과는 유효하지 않은 ID의 `Ignored`, Access가 거부된 `Blocked`, 허용된 오디오/시리즈 navigation 결과로 구분한다. `ContentActionHandler`는 허용 결과만 기존 `Constants.EXTRA_AUDIO_CONTENT_ID` 또는 `Constants.EXTRA_SERIES_ID`로 변환한다.
|
||||||
|
- Home banner의 series와 최근 활동의 audio는 성인 여부를 제공하지 않으므로 기존처럼 `AccessRequirement.Login`을 사용한다. Home의 event/creator/link와 community route는 Content Action 범위에 포함하지 않는다.
|
||||||
|
- Content 추천 카드와 전체 탭은 `showAdultBadge`를 알고 있는 경우 이를 전달한다. 댓글/랭킹/원작 series/banner처럼 성인 여부가 없는 경로는 로그인만 확인한다.
|
||||||
|
- Content Overview audio, Creator Channel Home audio, Audio tab, Live replay, Series tab은 성인 여부를 보유하므로 command에 전달한다. Creator Channel schedule audio/live replay와 Home series는 성인 여부가 없어 로그인만 확인한다.
|
||||||
|
- 상세 진입은 모두 일반 `startActivity`이며 `ActivityResultLauncher`, route source extra, 호출 화면 후처리가 없다. 따라서 별도 Content result adapter나 feature composition handler를 추가하지 않는다.
|
||||||
|
- `MainV2Activity`의 audio notification/deeplink/mini player와 series deeplink는 Phase 1에서 route 소비 순서와 boolean 반환 계약을 별도로 고정한 시스템 route이므로, PRD에 합의된 Home/Content/Content Overview/Creator Channel 전환 범위에는 포함하지 않는다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 2026-07-14: `rg`와 대상 source 확인으로 V2 상세 Activity 직접 참조, ID/extra, 성인 여부 보유 여부, Activity result 사용 여부를 대조해 위 command/result 계약과 전환 범위를 확정했다.
|
||||||
|
- 2026-07-14: 격리 worktree에서 변경 전 `./gradlew :app:testDebugUnitTest`를 실행해 전체 debug 단위 테스트가 `BUILD SUCCESSFUL`로 통과하는 기준선을 확인했다. 기존 dependency/manifest/Kotlin deprecation 경고는 있었지만 실패는 없었다.
|
||||||
|
|
||||||
- [ ] **Task 2.2: Content Action 순수 계약 테스트 및 구현**
|
- [x] **Task 2.2: Content Action 순수 계약 테스트 및 구현**
|
||||||
- 생성 예정 파일:
|
- 생성 예정 파일:
|
||||||
- `app/src/main/java/kr/co/vividnext/sodalive/v2/content/action/ContentAction.kt`
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/content/action/ContentAction.kt`
|
||||||
- `app/src/main/java/kr/co/vividnext/sodalive/v2/content/action/ContentActionCommand.kt`
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/content/action/ContentActionCommand.kt`
|
||||||
- `app/src/test/java/kr/co/vividnext/sodalive/v2/content/action/ContentActionTest.kt`
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/content/action/ContentActionTest.kt`
|
||||||
- 검증 대상: invalid ID, audio/series route, 성인 Access 요구사항 전달, 허용/차단 결과.
|
- 검증 대상: invalid ID, audio/series route, 성인 Access 요구사항 전달, 허용/차단 결과.
|
||||||
- 검증 기준: feature UI model과 DTO 의존 없이 입력·출력이 테스트된다.
|
- 검증 기준: feature UI model과 DTO 의존 없이 입력·출력이 테스트된다.
|
||||||
|
- RED 검증 기록:
|
||||||
|
- 2026-07-14: `ContentActionTest`를 먼저 추가하고 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.content.action.ContentActionTest"`를 실행해 `ContentAction`, `ContentActionCommand`, `ContentActionResult`, `ContentActionHandler` 미정의 컴파일 오류와 `BUILD FAILED`를 확인했다.
|
||||||
|
- GREEN 검증 기록:
|
||||||
|
- 2026-07-14: `ContentActionCommand`, `ContentAction`, `ContentActionResult`, `ContentActionHandler`를 최소 구현하고 같은 단일 테스트 명령을 재실행해 7개 테스트와 빌드가 통과했다. Action은 ID/성인 여부와 Access 함수만 입력받고 feature UI model, DTO, Android UI에 의존하지 않는다.
|
||||||
|
|
||||||
- [ ] **Task 2.3: Content navigation adapter 구현 및 전체 호출부 전환**
|
- [x] **Task 2.3: Content navigation adapter 구현 및 전체 호출부 전환**
|
||||||
- 생성/수정 예정 파일:
|
- 생성/수정 예정 파일:
|
||||||
- `app/src/main/java/kr/co/vividnext/sodalive/v2/content/action/ContentActionHandler.kt`
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/content/action/ContentActionHandler.kt`
|
||||||
- Task 2.1에서 확정한 Home/Content/Overview/Creator Channel 호출 파일
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`
|
||||||
|
- `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/ContentMainFragment.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRecommendationsUiModels.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivity.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ui/ContentOverviewAdapter.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragment.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesFragment.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/ui/CreatorChannelSeriesAdapter.kt`
|
||||||
|
- 위 호출부 계약을 검증하는 기존 source/layout 테스트
|
||||||
- 작업: 기존 extra와 Access UX를 보존하면서 오디오/시리즈 상세 진입을 한 곳으로 통합한다.
|
- 작업: 기존 extra와 Access UX를 보존하면서 오디오/시리즈 상세 진입을 한 곳으로 통합한다.
|
||||||
- 검증 기준: 대상 UI에서 상세 Activity Intent 직접 생성이 제거되고 Content Action을 사용한다.
|
- 검증 기준: 대상 UI에서 상세 Activity Intent 직접 생성이 제거되고 Content Action을 사용한다.
|
||||||
|
- RED 검증 기록:
|
||||||
|
- 2026-07-14: 기존 source/layout 테스트를 새 Content Action 결선 계약으로 먼저 변경하고 관련 6개 테스트 클래스 207건을 `--rerun-tasks`로 실행했다. 대상 화면의 직접 상세 Intent, series banner Intent, ID-only callback이 남아 있어 예상한 13건이 실패하고 `BUILD FAILED`가 발생함을 확인했다.
|
||||||
|
- GREEN 검증 기록:
|
||||||
|
- 2026-07-14: Home, Content, Content Overview, Creator Channel 호출부를 `handleContentAction`으로 전환하고 대상 feature/model source의 `AudioContentDetailActivity`/`SeriesDetailActivity` 직접 참조를 제거했다. 상세 Activity와 기존 ID extra 생성은 `ContentActionHandler`만 소유한다.
|
||||||
|
- 2026-07-14: Content Overview와 Creator Channel Audio/Live replay/Series tab callback이 기존 UI model의 `showAdultBadge`를 command까지 전달하도록 연결했다. 성인 정보를 제공하지 않는 Home, schedule, Creator Home series 경로는 기존 Login 요구사항을 유지했다.
|
||||||
|
- 2026-07-14: 같은 6개 테스트 클래스 207건을 `--rerun-tasks`로 재실행해 실패, 오류, 스킵 없이 `BUILD SUCCESSFUL`로 통과했다.
|
||||||
|
- 2026-07-14: 결선 GREEN 중 발견한 기존 전체 탭 source assertion은 다른 helper의 `openSeriesDetail(seriesId)` 문자열에 우연히 의존하고 있었다. 실제 계약인 `openSeriesDetail(seriesId, findAllTabSeriesAdultAccess(seriesId))`를 검증하도록 강화한 뒤 단일 테스트와 207건 결선을 다시 통과시켰다.
|
||||||
|
|
||||||
- [ ] **Task 2.4: Content Phase 회귀 검증**
|
- [x] **Task 2.4: Content Phase 회귀 검증**
|
||||||
- 실행 명령:
|
- 실행 명령:
|
||||||
```bash
|
```bash
|
||||||
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.content.action.*"
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.content.action.*"
|
||||||
@@ -378,8 +411,16 @@
|
|||||||
./gradlew :app:compileDebugKotlin
|
./gradlew :app:compileDebugKotlin
|
||||||
./gradlew :app:ktlintCheck
|
./gradlew :app:ktlintCheck
|
||||||
git diff --check
|
git diff --check
|
||||||
```
|
```
|
||||||
- 수동 확인: Home, Content, Content Overview, Creator Channel에서 audio/series 상세 진입과 성인 차단 UX가 기존과 동일하다.
|
- 수동 확인: Home, Content, Content Overview, Creator Channel에서 audio/series 상세 진입과 성인 차단 UX가 기존과 동일하다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 2026-07-14: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.content.action.*"`, `--tests "kr.co.vividnext.sodalive.v2.main.content.*"`, `--tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, 추가 Home 회귀 `--tests "kr.co.vividnext.sodalive.v2.main.home.*"`를 각각 실행해 모두 `BUILD SUCCESSFUL`을 확인했다. Content 묶음은 99개 테스트가 통과했다.
|
||||||
|
- 2026-07-14: Phase 1에서 작성된 `ContentMainFragmentLoginGuardSourceTest`가 이전 화면 내부 `ensureV2Access` 구현을 고정해 Content 묶음 3건이 실패했다. 실제 Access 계약은 `ContentMainFragment -> ContentAction -> ContentActionHandler` 순으로 이동했으므로 invalid ID, 성인 요구사항, Access-before-navigation을 새 소유자에서 검증하도록 테스트를 갱신한 뒤 단일 클래스와 Content 99건을 다시 통과시켰다.
|
||||||
|
- 2026-07-14: `./gradlew :app:testDebugUnitTest`를 실행해 전체 debug 단위 테스트가 44초에 `BUILD SUCCESSFUL`로 통과했고, `./gradlew :app:compileDebugKotlin`도 `BUILD SUCCESSFUL`로 통과했다.
|
||||||
|
- 2026-07-14: `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`로 통과했고 test source 리포트는 0건이다. `./gradlew :app:ktlintCheck`는 이번 변경과 무관한 기존 main source 기준선 위반 550줄로 실패했으며, 리포트에서 Phase 2 변경 Kotlin 파일의 위반은 0건임을 확인했다. 레거시 파일은 수정하지 않았다.
|
||||||
|
- 2026-07-14: 대상 Home, Content, Content Overview, Creator Channel 운영 source에서 `AudioContentDetailActivity`, `SeriesDetailActivity` 및 상세 ID extra를 검색한 결과 잔존이 없고, `ContentActionHandler`만 기존 Activity/extra 생성을 소유함을 확인했다. Phase 1에서 계약을 고정한 `MainV2Activity` 시스템 route는 범위대로 유지했다.
|
||||||
|
- 2026-07-14: `./gradlew tasks --all`은 `BUILD SUCCESSFUL`로 통과했다. `adb devices`는 연결된 기기가 없음을 반환해 실기기 수동 탭 검증은 수행하지 못했고, 대신 Robolectric handler 테스트로 허용/차단과 기존 extra를 검증했다.
|
||||||
|
- 2026-07-14: `git diff --check HEAD`는 whitespace 오류 없이 통과했다.
|
||||||
|
|
||||||
### Phase 3: Live 공통 Action
|
### Phase 3: Live 공통 Action
|
||||||
- [ ] **Task 3.1: 기존 Live 흐름 차이와 레거시 의존 목록 확정**
|
- [ ] **Task 3.1: 기존 Live 흐름 차이와 레거시 의존 목록 확정**
|
||||||
@@ -569,3 +610,4 @@
|
|||||||
- 2026-07-14: Community 업로드 후 Home/Community 탭 갱신 설계를 구체화했다. 초기 구현은 전역 observer 대신 `CommunityChange` 결과와 Creator Channel의 단일 `handleCommunityChange` composition handler를 사용하며, 레거시 `ActivityResult`는 `v2` adapter에서 변경 결과로 변환한다.
|
- 2026-07-14: Community 업로드 후 Home/Community 탭 갱신 설계를 구체화했다. 초기 구현은 전역 observer 대신 `CommunityChange` 결과와 Creator Channel의 단일 `handleCommunityChange` composition handler를 사용하며, 레거시 `ActivityResult`는 `v2` adapter에서 변경 결과로 변환한다.
|
||||||
- 2026-07-14: 사용자 확인에 따라 Activity result가 필요한 모든 도메인 Action에 공통 결과 전달 원칙을 확장했다. 기본 방식은 `ActivityResult -> 명시적 도메인 결과 -> feature 단일 handler`이며, Action/Repository 직접 결과도 동일 handler로 합류한다. `SharedFlow`/observer는 Activity 밖의 독립 소비자와 lifecycle/replay 요구가 확인될 때만 도입한다.
|
- 2026-07-14: 사용자 확인에 따라 Activity result가 필요한 모든 도메인 Action에 공통 결과 전달 원칙을 확장했다. 기본 방식은 `ActivityResult -> 명시적 도메인 결과 -> feature 단일 handler`이며, Action/Repository 직접 결과도 동일 handler로 합류한다. `SharedFlow`/observer는 Activity 밖의 독립 소비자와 lifecycle/replay 요구가 확인될 때만 도입한다.
|
||||||
- 2026-07-14: Phase 0/1 리뷰 답변을 반영해 Legacy MyPage 호출부와 로그인 token 정책 확인, FanTalk source assertion 수동 삭제 사유를 기록했다. 문서 변경 후 `./gradlew tasks --all`은 `BUILD SUCCESSFUL`, `git diff --check HEAD`는 무출력으로 통과했다.
|
- 2026-07-14: Phase 0/1 리뷰 답변을 반영해 Legacy MyPage 호출부와 로그인 token 정책 확인, FanTalk source assertion 수동 삭제 사유를 기록했다. 문서 변경 후 `./gradlew tasks --all`은 `BUILD SUCCESSFUL`, `git diff --check HEAD`는 무출력으로 통과했다.
|
||||||
|
- 2026-07-14: Phase 2 Content Action 전환 후 전체 `:app:testDebugUnitTest`, `:app:compileDebugKotlin`, `tasks --all`, `git diff --check HEAD`가 통과했다. 전체 ktlint는 기존 main source 550줄 기준선 위반으로 실패했지만 Phase 2 변경 Kotlin 파일과 test source의 신규 위반은 0건이다. 연결된 Android 기기가 없어 실기기 수동 탭 검증은 후속 확인 항목으로 남겼다.
|
||||||
|
|||||||
@@ -85,6 +85,11 @@ Access Phase 테스트 예시:
|
|||||||
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.*"
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.*"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Content Action Phase 테스트 예시:
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.content.action.ContentActionTest"
|
||||||
|
```
|
||||||
|
|
||||||
참고:
|
참고:
|
||||||
- 신규 Kotlin 테스트 메서드는 backtick 함수명을 사용하고, 테스트명은 한글 문장으로 작성한다.
|
- 신규 Kotlin 테스트 메서드는 backtick 함수명을 사용하고, 테스트명은 한글 문장으로 작성한다.
|
||||||
- 기존 영어 테스트명을 수정하지 않는 최소 변경 상황을 제외하고, 새 테스트명에 영어 문장을 사용하지 않는다.
|
- 기존 영어 테스트명을 수정하지 않는 최소 변경 상황을 제외하고, 새 테스트명에 영어 문장을 사용하지 않는다.
|
||||||
|
|||||||
Reference in New Issue
Block a user