fix(creator): 채널 라이브 진입을 보강한다

This commit is contained in:
2026-06-17 10:58:49 +09:00
parent 34876cf46f
commit f2f2a3143d
5 changed files with 330 additions and 30 deletions

View File

@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.write package kr.co.vividnext.sodalive.explorer.profile.creator_community.write
import android.Manifest import android.Manifest
import android.app.Activity
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -26,9 +27,11 @@ import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import java.io.File import java.io.File
class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWriteBinding>( class CreatorCommunityWriteActivity :
BaseActivity<ActivityCreatorCommunityWriteBinding>(
ActivityCreatorCommunityWriteBinding::inflate ActivityCreatorCommunityWriteBinding::inflate
), RecordingVoiceFragment.OnAudioRecordedListener { ),
RecordingVoiceFragment.OnAudioRecordedListener {
private val viewModel: CreatorCommunityWriteViewModel by inject() private val viewModel: CreatorCommunityWriteViewModel by inject()
@@ -62,7 +65,8 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
context = this, context = this,
isEnabledFreeStyleCrop = true, isEnabledFreeStyleCrop = true,
config = ImagePickerCropper.Config( config = ImagePickerCropper.Config(
aspectX = 1f, aspectY = 1f, aspectX = 1f,
aspectY = 1f,
compressFormat = Bitmap.CompressFormat.JPEG, compressFormat = Bitmap.CompressFormat.JPEG,
compressQuality = 90 compressQuality = 90
), ),
@@ -112,7 +116,10 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
binding.llPriceFree.setOnClickListener { viewModel.setPriceFree(true) } binding.llPriceFree.setOnClickListener { viewModel.setPriceFree(true) }
binding.tvCancel.setOnClickListener { finish() } binding.tvCancel.setOnClickListener { finish() }
binding.tvUpload.setOnClickListener { binding.tvUpload.setOnClickListener {
viewModel.createCommunityPost { finish() } viewModel.createCommunityPost {
setResult(Activity.RESULT_OK)
finish()
}
} }
} }

View File

@@ -6,11 +6,13 @@ import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.view.Gravity
import android.view.animation.Interpolator import android.view.animation.Interpolator
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.MeasureSpec import android.view.View.MeasureSpec
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@@ -22,6 +24,7 @@ import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.google.gson.Gson
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.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
@@ -29,18 +32,27 @@ import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
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.base.SodaDialog
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelBinding import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog
import kr.co.vividnext.sodalive.mypage.MyPageViewModel
import kr.co.vividnext.sodalive.mypage.auth.Auth
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
import kr.co.vividnext.sodalive.report.ProfileReportDialog import kr.co.vividnext.sodalive.report.ProfileReportDialog
import kr.co.vividnext.sodalive.report.UserReportDialog import kr.co.vividnext.sodalive.report.UserReportDialog
import kr.co.vividnext.sodalive.settings.ContentSettingsActivity
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel
@@ -49,11 +61,16 @@ import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTitleBarState import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTitleBarState
import kr.co.vividnext.sodalive.v2.main.MainV2Activity import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivity import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivity
import kr.co.vividnext.sodalive.splash.SplashActivity
import kr.co.vividnext.sodalive.user.login.LoginActivity
import org.koin.android.ext.android.inject
class CreatorChannelActivity : class CreatorChannelActivity :
BaseActivity<ActivityCreatorChannelBinding>(ActivityCreatorChannelBinding::inflate), BaseActivity<ActivityCreatorChannelBinding>(ActivityCreatorChannelBinding::inflate),
CreatorChannelHomeFragment.Host { CreatorChannelHomeFragment.Host {
private val liveViewModel: LiveViewModel by inject()
private val myPageViewModel: MyPageViewModel by inject()
private var creatorId: Long = 0L private var creatorId: Long = 0L
private var currentHeader: CreatorChannelHeaderUiModel? = null private var currentHeader: CreatorChannelHeaderUiModel? = null
private var homeActionDelegate: CreatorChannelHomeFragment.HomeActionDelegate? = null private var homeActionDelegate: CreatorChannelHomeFragment.HomeActionDelegate? = null
@@ -63,7 +80,39 @@ class CreatorChannelActivity :
private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
private var isOwnerFabExpanded: Boolean = false private var isOwnerFabExpanded: Boolean = false
private var isOwnerFabAnimating: Boolean = false private var isOwnerFabAnimating: Boolean = false
private lateinit var loadingDialog: LoadingDialog
private val baseTitleBarHeight: Int by lazy { 60.dpToPx().toInt() } private val baseTitleBarHeight: Int by lazy { 60.dpToPx().toInt() }
private val liveCoordinator: CreatorChannelLiveCoordinator by lazy {
CreatorChannelLiveCoordinator(
activity = this,
layoutInflater = layoutInflater,
fragmentManager = supportFragmentManager,
liveViewModel = liveViewModel,
screenWidthProvider = { screenWidth },
refreshHome = { homeActionDelegate?.refreshHome() }
)
}
private val communityWriteLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
homeActionDelegate?.refreshHome()
}
}
private val liveRoomCreateLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
homeActionDelegate?.refreshHome()
val roomId = result.data?.getLongExtra(Constants.EXTRA_ROOM_ID, 0L)
val channelName = result.data?.getStringExtra(Constants.EXTRA_ROOM_CHANNEL_NAME)
if (channelName != null) {
roomId?.takeIf { it > 0L }?.let(liveCoordinator::enterLiveRoom)
} else {
showToast(getString(R.string.creator_channel_live_created_message))
}
}
}
override val shouldApplySystemBarTopInset: Boolean = false override val shouldApplySystemBarTopInset: Boolean = false
@@ -80,6 +129,21 @@ class CreatorChannelActivity :
setupOwnerFabInsets() setupOwnerFabInsets()
setupScrollListener() setupScrollListener()
setupClickListeners() setupClickListeners()
setupLiveEntryObservers()
}
private fun setupLiveEntryObservers() {
loadingDialog = LoadingDialog(this, layoutInflater)
liveViewModel.toastLiveData.observe(this) {
it?.let(::showToast)
}
liveViewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, getString(R.string.screen_live_loading))
} else {
loadingDialog.dismiss()
}
}
} }
private fun setupClickListeners() { private fun setupClickListeners() {
@@ -423,7 +487,7 @@ class CreatorChannelActivity :
private fun onOwnerFabCommunityClicked() { private fun onOwnerFabCommunityClicked() {
collapseOwnerFab(animate = false) collapseOwnerFab(animate = false)
startActivity(Intent(this, CreatorCommunityWriteActivity::class.java)) communityWriteLauncher.launch(Intent(this, CreatorCommunityWriteActivity::class.java))
} }
private fun onOwnerFabAudioClicked() { private fun onOwnerFabAudioClicked() {
@@ -433,7 +497,7 @@ class CreatorChannelActivity :
private fun onOwnerFabLiveClicked() { private fun onOwnerFabLiveClicked() {
collapseOwnerFab(animate = false) collapseOwnerFab(animate = false)
startActivity(Intent(this, LiveRoomCreateActivity::class.java)) liveRoomCreateLauncher.launch(Intent(this, LiveRoomCreateActivity::class.java))
} }
override fun onCreatorChannelDonationClicked() { override fun onCreatorChannelDonationClicked() {
@@ -453,6 +517,77 @@ class CreatorChannelActivity :
dialog.show(screenWidth - 26.7f.dpToPx().toInt()) dialog.show(screenWidth - 26.7f.dpToPx().toInt())
} }
override fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse) {
ensureLoginAndAdultAuth(isAdult = live.isAdult) {
liveCoordinator.enterLiveRoom(live.liveId)
}
}
private fun ensureLoginAndAdultAuth(isAdult: Boolean, onAuthed: () -> Unit) {
if (SharedPreferenceManager.token.isBlank()) {
showLoginActivity()
return
}
if (isAdult) {
val isKoreanCountry = SharedPreferenceManager.countryCode.ifBlank { "KR" } == "KR"
if (isKoreanCountry && !SharedPreferenceManager.isAuth) {
SodaDialog(
activity = this,
layoutInflater = layoutInflater,
title = getString(R.string.auth_title),
desc = getString(R.string.auth_desc_live),
confirmButtonTitle = getString(R.string.auth_go),
confirmButtonClick = { startAuthFlow() },
cancelButtonTitle = getString(R.string.cancel),
cancelButtonClick = {},
descGravity = Gravity.CENTER
).show(screenWidth)
return
}
if (!SharedPreferenceManager.isAdultContentVisible) {
startActivity(
Intent(this, ContentSettingsActivity::class.java).apply {
putExtra(Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE, true)
}
)
return
}
}
onAuthed()
}
private fun showLoginActivity() {
if (SharedPreferenceManager.token.isBlank()) {
startActivity(
Intent(this, LoginActivity::class.java).apply {
putExtra(Constants.EXTRA_DATA, intent.extras)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
)
}
}
private fun startAuthFlow() {
Auth.auth(this, this) { json ->
val bootpayResponse = Gson().fromJson(json, BootpayResponse::class.java)
val request = AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId)
runOnUiThread {
myPageViewModel.authVerify(request) {
startActivity(
Intent(this, SplashActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
}
)
finish()
}
}
}
}
private fun updateViewPagerHeight() { private fun updateViewPagerHeight() {
binding.viewPager.post { binding.viewPager.post {
val recyclerView = binding.viewPager.getChildAt(0) as? RecyclerView ?: return@post val recyclerView = binding.viewPager.getChildAt(0) as? RecyclerView ?: return@post
@@ -478,7 +613,7 @@ class CreatorChannelActivity :
} }
) )
CreatorActivityType.Live -> showLiveRoomDetail(schedule.targetId) CreatorActivityType.Live -> liveCoordinator.showLiveRoomDetail(schedule.targetId)
CreatorActivityType.Community -> Unit CreatorActivityType.Community -> Unit
} }
@@ -500,20 +635,6 @@ class CreatorChannelActivity :
) )
} }
private fun showLiveRoomDetail(roomId: Long) {
val detailFragment = LiveRoomDetailFragment(
roomId,
onClickParticipant = {},
onClickReservation = {},
onClickModify = {},
onClickStart = {},
onClickCancel = {}
)
if (detailFragment.isAdded) return
detailFragment.show(supportFragmentManager, detailFragment.tag)
}
override fun onDestroy() { override fun onDestroy() {
tabLayoutMediator?.detach() tabLayoutMediator?.detach()
pageChangeCallback?.let { callback -> pageChangeCallback?.let { callback ->

View File

@@ -7,6 +7,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.databinding.FragmentCreatorChannelHomeBinding import kr.co.vividnext.sodalive.databinding.FragmentCreatorChannelHomeBinding
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel
@@ -20,6 +21,7 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
private val viewModel: CreatorChannelHomeViewModel by viewModel() private val viewModel: CreatorChannelHomeViewModel by viewModel()
private val sectionAdapter = CreatorChannelHomeSectionAdapter( private val sectionAdapter = CreatorChannelHomeSectionAdapter(
onLiveClick = ::onCurrentLiveClicked,
onScheduleClick = ::onScheduleClicked, onScheduleClick = ::onScheduleClicked,
onAudioContentClick = ::onAudioContentClicked, onAudioContentClick = ::onAudioContentClicked,
onSeriesClick = ::onSeriesClicked, onSeriesClick = ::onSeriesClicked,
@@ -59,6 +61,12 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
override fun postChannelDonation(can: Int, isSecret: Boolean, message: String) { override fun postChannelDonation(can: Int, isSecret: Boolean, message: String) {
viewModel.postChannelDonation(can = can, isSecret = isSecret, message = message) viewModel.postChannelDonation(can = can, isSecret = isSecret, message = message)
} }
override fun refreshHome() {
if (creatorId > 0L) {
viewModel.loadHome(creatorId)
}
}
} }
) )
if (creatorId > 0L) { if (creatorId > 0L) {
@@ -115,6 +123,10 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
host.onCreatorChannelDonationClicked() host.onCreatorChannelDonationClicked()
} }
private fun onCurrentLiveClicked(live: CreatorChannelLiveResponse) {
host.onCreatorChannelCurrentLiveClicked(live)
}
interface Host { interface Host {
fun onCreatorChannelHeaderChanged(header: CreatorChannelHeaderUiModel) fun onCreatorChannelHeaderChanged(header: CreatorChannelHeaderUiModel)
fun onCreatorChannelFollowProgressChanged(inProgress: Boolean) fun onCreatorChannelFollowProgressChanged(inProgress: Boolean)
@@ -125,6 +137,7 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
fun onCreatorChannelHomeActionDelegateReady(delegate: HomeActionDelegate?) fun onCreatorChannelHomeActionDelegateReady(delegate: HomeActionDelegate?)
fun onCreatorChannelHomeContentChanged() fun onCreatorChannelHomeContentChanged()
fun onCreatorChannelDonationClicked() fun onCreatorChannelDonationClicked()
fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)
} }
interface HomeActionDelegate { interface HomeActionDelegate {
@@ -134,6 +147,7 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
fun reportUser(reason: String) fun reportUser(reason: String)
fun reportProfile() fun reportProfile()
fun postChannelDonation(can: Int, isSecret: Boolean, message: String) fun postChannelDonation(can: Int, isSecret: Boolean, message: String)
fun refreshHome()
} }
companion object { companion object {

View File

@@ -23,6 +23,7 @@ import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelCommunityPostResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelCommunityPostResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeSection import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeSection
@@ -35,6 +36,7 @@ import java.util.TimeZone
import kotlin.math.roundToInt import kotlin.math.roundToInt
class CreatorChannelHomeSectionAdapter( class CreatorChannelHomeSectionAdapter(
private val onLiveClick: (CreatorChannelLiveResponse) -> Unit = {},
private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit = {}, private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit = {},
private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit = {}, private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit = {},
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit = {}, private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit = {},
@@ -52,7 +54,7 @@ class CreatorChannelHomeSectionAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return SectionViewHolder(view, onScheduleClick, onAudioContentClick, onSeriesClick, onDonationClick) return SectionViewHolder(view, onLiveClick, onScheduleClick, onAudioContentClick, onSeriesClick, onDonationClick)
} }
override fun onBindViewHolder(holder: SectionViewHolder, position: Int) { override fun onBindViewHolder(holder: SectionViewHolder, position: Int) {
@@ -63,6 +65,7 @@ class CreatorChannelHomeSectionAdapter(
class SectionViewHolder( class SectionViewHolder(
view: View, view: View,
private val onLiveClick: (CreatorChannelLiveResponse) -> Unit,
private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit, private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit,
private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit, private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit,
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit, private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit,
@@ -74,6 +77,7 @@ class CreatorChannelHomeSectionAdapter(
private val currentLivePrice: TextView? = view.findViewById(R.id.tv_current_live_price) private val currentLivePrice: TextView? = view.findViewById(R.id.tv_current_live_price)
private val currentLiveAdult: TextView? = view.findViewById(R.id.tv_current_live_adult) private val currentLiveAdult: TextView? = view.findViewById(R.id.tv_current_live_adult)
private val currentLivePriceLayout: View? = view.findViewById(R.id.layout_current_live_price) private val currentLivePriceLayout: View? = view.findViewById(R.id.layout_current_live_price)
private val currentLiveCard: View? = view.findViewById(R.id.layout_current_live_card)
private val latestAudioThumbnail: ImageView? = view.findViewById(R.id.iv_latest_audio_thumbnail) private val latestAudioThumbnail: ImageView? = view.findViewById(R.id.iv_latest_audio_thumbnail)
private val latestAudioPointTag: ImageView? = view.findViewById(R.id.iv_latest_audio_point_tag) private val latestAudioPointTag: ImageView? = view.findViewById(R.id.iv_latest_audio_point_tag)
private val latestAudioTitle: TextView? = view.findViewById(R.id.tv_latest_audio_title) private val latestAudioTitle: TextView? = view.findViewById(R.id.tv_latest_audio_title)
@@ -142,10 +146,11 @@ class CreatorChannelHomeSectionAdapter(
private fun bindCurrentLive(item: CreatorChannelHomeSection.CurrentLive) { private fun bindCurrentLive(item: CreatorChannelHomeSection.CurrentLive) {
currentLiveTitle?.text = item.live.title currentLiveTitle?.text = item.live.title
currentLiveStartTime?.text = item.live.beginDateTimeUtc currentLiveStartTime?.text = formatCreatorChannelLiveDateTime(item.live.beginDateTimeUtc)
currentLivePrice?.text = item.live.price.toString() currentLivePrice?.text = item.live.price.toString()
currentLivePriceLayout?.isVisible = item.live.price > 0 currentLivePriceLayout?.isVisible = item.live.price > 0
currentLiveAdult?.isVisible = item.live.isAdult currentLiveAdult?.isVisible = item.live.isAdult
currentLiveCard?.setOnClickListener { onLiveClick(item.live) }
} }
private fun bindLatestAudioContent(item: CreatorChannelHomeSection.LatestAudioContent) { private fun bindLatestAudioContent(item: CreatorChannelHomeSection.LatestAudioContent) {
@@ -614,6 +619,12 @@ internal fun formatCreatorChannelScheduleTime(
locale: Locale = Locale.getDefault() locale: Locale = Locale.getDefault()
): String = formatCreatorChannelScheduleUtc(scheduledAtUtc, "a hh:mm", timeZone, locale) ): String = formatCreatorChannelScheduleUtc(scheduledAtUtc, "a hh:mm", timeZone, locale)
internal fun formatCreatorChannelLiveDateTime(
beginDateTimeUtc: String,
timeZone: TimeZone = TimeZone.getDefault(),
locale: Locale = Locale.getDefault()
): String = formatCreatorChannelUtcOrNull(beginDateTimeUtc, "yyyy.MM.dd HH:mm:ss", timeZone, locale).orEmpty()
internal fun formatCreatorChannelDebutActivityValue( internal fun formatCreatorChannelDebutActivityValue(
debutDateUtc: String?, debutDateUtc: String?,
dDay: String, dDay: String,

View File

@@ -11,6 +11,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelSer
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelSeriesCardWidthDp import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelSeriesCardWidthDp
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelScheduleTimelineLineCount import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelScheduleTimelineLineCount
import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelDebutActivityValue import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelDebutActivityValue
import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelLiveDateTime
import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDate import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDate
import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDayOfWeek import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDayOfWeek
import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleTime import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleTime
@@ -904,6 +905,132 @@ class CreatorChannelActivitySourceTest {
assertEquals("오전 12:00", formatCreatorChannelScheduleTime("2026-06-29T15:00:00Z", timeZone, locale)) assertEquals("오전 12:00", formatCreatorChannelScheduleTime("2026-06-29T15:00:00Z", timeZone, locale))
} }
@Test
fun `현재 진행 중인 라이브 시간은 디바이스 timezone 기준 초 단위 날짜 시간으로 표시한다`() {
val timeZone = TimeZone.getTimeZone("Asia/Seoul")
val locale = Locale.KOREA
assertEquals("2026.06.30 00:00:01", formatCreatorChannelLiveDateTime("2026-06-29T15:00:01Z", timeZone, locale))
}
@Test
fun `owner FAB 라이브 생성 결과는 기존 enterLiveRoom 플로우로 입장한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val coordinator = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveCoordinator.kt"
).readText()
assertTrue(source.contains("ActivityResultContracts.StartActivityForResult()"))
assertTrue(source.contains("private val liveViewModel: LiveViewModel by inject()"))
assertTrue(source.contains("private val liveCoordinator: CreatorChannelLiveCoordinator by lazy"))
assertTrue(source.contains("private val liveRoomCreateLauncher"))
assertTrue(source.contains("result.resultCode == RESULT_OK"))
assertTrue(source.contains("result.data?.getLongExtra(Constants.EXTRA_ROOM_ID, 0L)"))
assertTrue(source.contains("result.data?.getStringExtra(Constants.EXTRA_ROOM_CHANNEL_NAME)"))
assertTrue(source.contains("if (channelName != null)"))
assertTrue(source.contains("?.takeIf { it > 0L }"))
assertTrue(source.contains("?.let(liveCoordinator::enterLiveRoom)"))
assertTrue(source.contains("showToast(getString(R.string.creator_channel_live_created_message))"))
assertTrue(source.contains("homeActionDelegate?.refreshHome()"))
assertTrue(coordinator.contains("fun enterLiveRoom(roomId: Long)"))
assertTrue(coordinator.contains("liveViewModel.getRoomDetail(roomId)"))
assertTrue(coordinator.contains("it.manager.id == SharedPreferenceManager.userId"))
assertTrue(coordinator.contains("liveViewModel.enterRoom(roomId, onEnterRoomSuccess)"))
assertTrue(coordinator.contains("LiveRoomPasswordDialog("))
assertTrue(coordinator.contains("LivePaymentDialog("))
assertTrue(coordinator.contains("putExtra(Constants.EXTRA_ROOM_ID, roomId)"))
assertTrue(source.contains("liveRoomCreateLauncher.launch(Intent(this, LiveRoomCreateActivity::class.java))"))
assertFalse(source.contains("startActivity(Intent(this, LiveRoomCreateActivity::class.java))"))
}
@Test
fun `크리에이터 채널 라이브 로직은 coordinator로 분리한다`() {
val activity = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val coordinator = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveCoordinator.kt"
).readText()
assertTrue(coordinator.contains("class CreatorChannelLiveCoordinator("))
assertTrue(coordinator.contains("private val liveViewModel: LiveViewModel"))
assertTrue(coordinator.contains("private val refreshHome: () -> Unit"))
assertTrue(activity.contains("liveCoordinator.enterLiveRoom(live.liveId)"))
assertTrue(activity.contains("CreatorActivityType.Live -> liveCoordinator.showLiveRoomDetail(schedule.targetId)"))
assertFalse(activity.contains("private fun enterLiveRoom(roomId: Long)"))
assertFalse(activity.contains("private fun showPaidLiveEntryDialog("))
assertFalse(activity.contains("private fun openLiveRoom(roomId: Long)"))
}
@Test
fun `현재 라이브 입장 API 진행과 오류는 LoadingDialog와 toast로 표시한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
assertTrue(source.contains("private lateinit var loadingDialog: LoadingDialog"))
assertTrue(source.contains("loadingDialog = LoadingDialog(this, layoutInflater)"))
assertTrue(source.contains("liveViewModel.toastLiveData.observe(this)"))
assertTrue(source.contains("it?.let(::showToast)"))
assertTrue(source.contains("liveViewModel.isLoading.observe(this)"))
assertTrue(source.contains("loadingDialog.show(screenWidth, getString(R.string.screen_live_loading))"))
assertTrue(source.contains("loadingDialog.dismiss()"))
assertTrue(source.contains("setupLiveEntryObservers()"))
}
@Test
fun `현재 라이브 카드는 동일한 enterLiveRoom 플로우로 입장한다`() {
val activity = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val fragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt"
).readText()
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
).readText()
assertTrue(adapter.contains("private val onLiveClick: (CreatorChannelLiveResponse) -> Unit = {}"))
assertTrue(adapter.contains("private val currentLiveCard: View?"))
assertTrue(adapter.contains("currentLiveCard?.setOnClickListener"))
assertTrue(adapter.contains("onLiveClick(item.live)"))
assertTrue(fragment.contains("onLiveClick = ::onCurrentLiveClicked"))
assertTrue(fragment.contains("private fun onCurrentLiveClicked(live: CreatorChannelLiveResponse)"))
assertTrue(fragment.contains("host.onCreatorChannelCurrentLiveClicked(live)"))
assertTrue(activity.contains("override fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)"))
assertTrue(activity.contains("ensureLoginAndAdultAuth(isAdult = live.isAdult)"))
assertTrue(activity.contains("liveCoordinator.enterLiveRoom(live.liveId)"))
assertTrue(activity.contains("private fun ensureLoginAndAdultAuth(isAdult: Boolean, onAuthed: () -> Unit)"))
assertTrue(activity.contains("SharedPreferenceManager.token.isBlank()"))
assertTrue(activity.contains("SodaDialog("))
assertTrue(activity.contains("R.string.auth_desc_live"))
assertTrue(activity.contains("ContentSettingsActivity::class.java"))
assertTrue(activity.contains("Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE"))
}
@Test
fun `커뮤니티 작성 완료 결과는 홈 Fragment를 새로고침한다`() {
val activity = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val fragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt"
).readText()
val writeActivity = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/write/CreatorCommunityWriteActivity.kt"
).readText()
assertTrue(activity.contains("private val communityWriteLauncher"))
assertTrue(activity.contains("homeActionDelegate?.refreshHome()"))
assertTrue(activity.contains("communityWriteLauncher.launch(Intent(this, CreatorCommunityWriteActivity::class.java))"))
assertFalse(activity.contains("startActivity(Intent(this, CreatorCommunityWriteActivity::class.java))"))
assertTrue(fragment.contains("fun refreshHome()"))
assertTrue(fragment.contains("viewModel.loadHome(creatorId)"))
assertTrue(writeActivity.contains("setResult(Activity.RESULT_OK)"))
}
@Test @Test
fun `일정 클릭은 콘텐츠 상세와 라이브 상세 이동 계약을 연결한다`() { fun `일정 클릭은 콘텐츠 상세와 라이브 상세 이동 계약을 연결한다`() {
val source = projectFile( val source = projectFile(
@@ -922,9 +1049,29 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("AudioContentDetailActivity::class.java")) assertTrue(source.contains("AudioContentDetailActivity::class.java"))
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, schedule.targetId)")) assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, schedule.targetId)"))
assertTrue(source.contains("CreatorActivityType.Live")) assertTrue(source.contains("CreatorActivityType.Live"))
assertTrue(source.contains("CreatorActivityType.Live -> showLiveRoomDetail(schedule.targetId)")) assertTrue(source.contains("CreatorActivityType.Live -> liveCoordinator.showLiveRoomDetail(schedule.targetId)"))
assertTrue(source.contains("LiveRoomDetailFragment(")) }
assertTrue(source.contains("detailFragment.show(supportFragmentManager, detailFragment.tag)"))
@Test
fun `일정 라이브 상세는 예약 수정 시작 취소 액션을 연결한다`() {
val coordinator = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveCoordinator.kt"
).readText()
assertTrue(coordinator.contains("fun showLiveRoomDetail(roomId: Long)"))
assertTrue(coordinator.contains("LiveRoomDetailFragment("))
assertTrue(coordinator.contains("onClickReservation = { reservationRoom(roomId) }"))
assertTrue(coordinator.contains("onClickModify = { roomDetailResponse -> modifyLive(roomDetailResponse) }"))
assertTrue(coordinator.contains("onClickStart = { startLive(roomId) }"))
assertTrue(coordinator.contains("onClickCancel = { cancelLive(roomId) }"))
assertTrue(coordinator.contains("private fun reservationRoom(roomId: Long)"))
assertTrue(coordinator.contains("private fun processLiveReservation(roomId: Long, password: String? = null)"))
assertTrue(coordinator.contains("LiveReservationCompleteActivity::class.java"))
assertTrue(coordinator.contains("LiveRoomEditActivity::class.java"))
assertTrue(coordinator.contains("liveViewModel.startLive(roomId)"))
assertTrue(coordinator.contains("LiveCancelDialog("))
assertTrue(coordinator.contains("liveViewModel.cancelLive(roomId, reason)"))
assertTrue(coordinator.contains("refreshHome()"))
} }
@Test @Test
@@ -1471,9 +1618,9 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("private fun onOwnerFabCommunityClicked()")) assertTrue(source.contains("private fun onOwnerFabCommunityClicked()"))
assertTrue(source.contains("private fun onOwnerFabAudioClicked()")) assertTrue(source.contains("private fun onOwnerFabAudioClicked()"))
assertTrue(source.contains("private fun onOwnerFabLiveClicked()")) assertTrue(source.contains("private fun onOwnerFabLiveClicked()"))
assertTrue(source.contains("startActivity(Intent(this, CreatorCommunityWriteActivity::class.java))")) assertTrue(source.contains("communityWriteLauncher.launch(Intent(this, CreatorCommunityWriteActivity::class.java))"))
assertTrue(source.contains("startActivity(Intent(this, AudioContentUploadActivity::class.java))")) assertTrue(source.contains("startActivity(Intent(this, AudioContentUploadActivity::class.java))"))
assertTrue(source.contains("startActivity(Intent(this, LiveRoomCreateActivity::class.java))")) assertTrue(source.contains("liveRoomCreateLauncher.launch(Intent(this, LiveRoomCreateActivity::class.java))"))
assertTrue(source.contains("collapseOwnerFab(animate = false)")) assertTrue(source.contains("collapseOwnerFab(animate = false)"))
} }