feat(main-v2): 메인 하단 내비게이션을 추가한다
@@ -111,6 +111,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".main.MainActivity" />
|
<activity android:name=".main.MainActivity" />
|
||||||
|
<activity android:name=".v2.main.MainV2Activity" />
|
||||||
<activity android:name=".user.login.LoginActivity" />
|
<activity android:name=".user.login.LoginActivity" />
|
||||||
<activity android:name=".audio_content.all.AudioContentAllActivity" />
|
<activity android:name=".audio_content.all.AudioContentAllActivity" />
|
||||||
<activity android:name=".settings.language.LanguageSettingsActivity" />
|
<activity android:name=".settings.language.LanguageSettingsActivity" />
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import com.bumptech.glide.request.transition.Transition
|
|||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class AudioContentPlayService :
|
class AudioContentPlayService :
|
||||||
@@ -471,7 +471,7 @@ class AudioContentPlayService :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateNotification() {
|
private fun updateNotification() {
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
val intent = Intent(this, MainV2Activity::class.java)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlayli
|
|||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.common.Utils
|
import kr.co.vividnext.sodalive.common.Utils
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@@ -153,7 +153,7 @@ class AudioContentPlayerService : MediaSessionService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initMediaSession() {
|
private fun initMediaSession() {
|
||||||
val contextIntent = Intent(applicationContext, MainActivity::class.java).apply {
|
val contextIntent = Intent(applicationContext, MainV2Activity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
}
|
}
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ import kr.co.vividnext.sodalive.user.UserViewModel
|
|||||||
import kr.co.vividnext.sodalive.user.find_password.FindPasswordViewModel
|
import kr.co.vividnext.sodalive.user.find_password.FindPasswordViewModel
|
||||||
import kr.co.vividnext.sodalive.user.login.LoginViewModel
|
import kr.co.vividnext.sodalive.user.login.LoginViewModel
|
||||||
import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
|
import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2ViewModel
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
@@ -297,6 +298,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
|||||||
viewModel { TermsViewModel(get()) }
|
viewModel { TermsViewModel(get()) }
|
||||||
viewModel { FindPasswordViewModel(get()) }
|
viewModel { FindPasswordViewModel(get()) }
|
||||||
viewModel { MainViewModel(get(), get(), get(), get(), get()) }
|
viewModel { MainViewModel(get(), get(), get(), get(), get()) }
|
||||||
|
viewModel { MainV2ViewModel(get(), get(), get(), get(), get()) }
|
||||||
viewModel { LiveViewModel(get(), get(), get(), get(), get()) }
|
viewModel { LiveViewModel(get(), get(), get(), get(), get()) }
|
||||||
viewModel { MyPageViewModel(get(), get(), get()) }
|
viewModel { MyPageViewModel(get(), get(), get()) }
|
||||||
viewModel { CanStatusViewModel(get()) }
|
viewModel { CanStatusViewModel(get()) }
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import kr.co.vividnext.sodalive.common.Constants
|
|||||||
import kr.co.vividnext.sodalive.databinding.ActivityLiveReservationCompleteBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityLiveReservationCompleteBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.convertDateFormat
|
import kr.co.vividnext.sodalive.extensions.convertDateFormat
|
||||||
import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse
|
import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
|
||||||
import kr.co.vividnext.sodalive.settings.language.LanguageManager
|
import kr.co.vividnext.sodalive.settings.language.LanguageManager
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class LiveReservationCompleteActivity : BaseActivity<ActivityLiveReservationComp
|
|||||||
binding.tvRemainingCan.text = "${response.remainingCan}"
|
binding.tvRemainingCan.text = "${response.remainingCan}"
|
||||||
|
|
||||||
binding.tvGoHome.setOnClickListener {
|
binding.tvGoHome.setOnClickListener {
|
||||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
val intent = Intent(applicationContext, MainV2Activity::class.java)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
|
|||||||
import kr.co.vividnext.sodalive.message.MessageActivity
|
import kr.co.vividnext.sodalive.message.MessageActivity
|
||||||
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentActivity
|
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentActivity
|
||||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class DeepLinkActivity : AppCompatActivity() {
|
class DeepLinkActivity : AppCompatActivity() {
|
||||||
@@ -63,7 +64,7 @@ class DeepLinkActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(applicationContext, MainActivity::class.java).apply {
|
Intent(applicationContext, MainV2Activity::class.java).apply {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
deepLinkExtras?.let { putExtra(Constants.EXTRA_DATA, it) }
|
deepLinkExtras?.let { putExtra(Constants.EXTRA_DATA, it) }
|
||||||
}
|
}
|
||||||
@@ -465,7 +466,7 @@ class DeepLinkActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(applicationContext, MainActivity::class.java).apply {
|
Intent(applicationContext, MainV2Activity::class.java).apply {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
putExtra(Constants.EXTRA_DATA, extras)
|
putExtra(Constants.EXTRA_DATA, extras)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import kr.co.vividnext.sodalive.settings.notice.NoticeActivity
|
|||||||
import kr.co.vividnext.sodalive.settings.notice.NoticeDetailActivity
|
import kr.co.vividnext.sodalive.settings.notice.NoticeDetailActivity
|
||||||
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
||||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@@ -275,19 +276,19 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
|||||||
binding.rlProfileContainer.visibility = View.GONE
|
binding.rlProfileContainer.visibility = View.GONE
|
||||||
binding.llProfileLoginContainer.visibility = View.VISIBLE
|
binding.llProfileLoginContainer.visibility = View.VISIBLE
|
||||||
binding.llProfileLoginContainer.setOnClickListener {
|
binding.llProfileLoginContainer.setOnClickListener {
|
||||||
(requireActivity() as MainActivity).showLoginActivity()
|
showLoginActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvCanAmount.text =
|
binding.tvCanAmount.text =
|
||||||
SodaLiveApplicationHolder.get().getString(R.string.common_zero)
|
SodaLiveApplicationHolder.get().getString(R.string.common_zero)
|
||||||
binding.tvCanAmount.setOnClickListener {
|
binding.tvCanAmount.setOnClickListener {
|
||||||
(requireActivity() as MainActivity).showLoginActivity()
|
showLoginActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvPointAmount.text =
|
binding.tvPointAmount.text =
|
||||||
SodaLiveApplicationHolder.get().getString(R.string.common_zero)
|
SodaLiveApplicationHolder.get().getString(R.string.common_zero)
|
||||||
binding.tvPointAmount.setOnClickListener {
|
binding.tvPointAmount.setOnClickListener {
|
||||||
(requireActivity() as MainActivity).showLoginActivity()
|
showLoginActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvChargeCan.visibility = View.INVISIBLE
|
binding.tvChargeCan.visibility = View.INVISIBLE
|
||||||
@@ -499,4 +500,10 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showLoginActivity() {
|
||||||
|
when (val activity = requireActivity()) {
|
||||||
|
is MainActivity -> activity.showLoginActivity()
|
||||||
|
is MainV2Activity -> activity.showLoginActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
|||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityCanStatusBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityCanStatusBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
|
||||||
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
||||||
import kr.co.vividnext.sodalive.mypage.can.status.charge.CanChargeStatusFragment
|
import kr.co.vividnext.sodalive.mypage.can.status.charge.CanChargeStatusFragment
|
||||||
import kr.co.vividnext.sodalive.mypage.can.status.use.CanUseStatusFragment
|
import kr.co.vividnext.sodalive.mypage.can.status.use.CanUseStatusFragment
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class CanStatusActivity : BaseActivity<ActivityCanStatusBinding>(
|
class CanStatusActivity : BaseActivity<ActivityCanStatusBinding>(
|
||||||
@@ -137,7 +137,7 @@ class CanStatusActivity : BaseActivity<ActivityCanStatusBinding>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onClickBackButton() {
|
private fun onClickBackButton() {
|
||||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
val intent = Intent(applicationContext, MainV2Activity::class.java)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
|||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityPointStatusBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityPointStatusBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
|
||||||
import kr.co.vividnext.sodalive.mypage.point.reward.PointRewardStatusFragment
|
import kr.co.vividnext.sodalive.mypage.point.reward.PointRewardStatusFragment
|
||||||
import kr.co.vividnext.sodalive.mypage.point.use.PointUseStatusFragment
|
import kr.co.vividnext.sodalive.mypage.point.use.PointUseStatusFragment
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class PointStatusActivity : BaseActivity<ActivityPointStatusBinding>(
|
class PointStatusActivity : BaseActivity<ActivityPointStatusBinding>(
|
||||||
@@ -120,7 +120,7 @@ class PointStatusActivity : BaseActivity<ActivityPointStatusBinding>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onClickBackButton() {
|
private fun onClickBackButton() {
|
||||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
val intent = Intent(applicationContext, MainV2Activity::class.java)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
|||||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivitySplashBinding
|
import kr.co.vividnext.sodalive.databinding.ActivitySplashBinding
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
|
|
||||||
@SuppressLint("CustomSplashScreen")
|
@SuppressLint("CustomSplashScreen")
|
||||||
class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding::inflate) {
|
class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding::inflate) {
|
||||||
@@ -174,7 +174,7 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding
|
|||||||
private fun showMainActivity(extras: Bundle?) {
|
private fun showMainActivity(extras: Bundle?) {
|
||||||
handler.postDelayed({
|
handler.postDelayed({
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(applicationContext, MainActivity::class.java).apply {
|
Intent(applicationContext, MainV2Activity::class.java).apply {
|
||||||
putExtra(Constants.EXTRA_DATA, extras)
|
putExtra(Constants.EXTRA_DATA, extras)
|
||||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package kr.co.vividnext.sodalive.user.login
|
package kr.co.vividnext.sodalive.user.login
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
@@ -45,9 +44,9 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
|||||||
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.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityLoginBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityLoginBinding
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
|
||||||
import kr.co.vividnext.sodalive.user.find_password.FindPasswordActivity
|
import kr.co.vividnext.sodalive.user.find_password.FindPasswordActivity
|
||||||
import kr.co.vividnext.sodalive.user.signup.SignUpActivity
|
import kr.co.vividnext.sodalive.user.signup.SignUpActivity
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
@@ -434,7 +433,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::i
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToMain() {
|
private fun navigateToMain() {
|
||||||
val nextIntent = Intent(this@LoginActivity, MainActivity::class.java)
|
val nextIntent = Intent(this@LoginActivity, MainV2Activity::class.java)
|
||||||
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
||||||
?: if (intent.extras != null) {
|
?: if (intent.extras != null) {
|
||||||
intent.extras
|
intent.extras
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
|||||||
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.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivitySignupBinding
|
import kr.co.vividnext.sodalive.databinding.ActivitySignupBinding
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
|
||||||
import kr.co.vividnext.sodalive.settings.terms.TermsActivity
|
import kr.co.vividnext.sodalive.settings.terms.TermsActivity
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
@@ -152,7 +152,7 @@ class SignUpActivity : BaseActivity<ActivitySignupBinding>(ActivitySignupBinding
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToMain() {
|
private fun navigateToMain() {
|
||||||
val nextIntent = Intent(this@SignUpActivity, MainActivity::class.java)
|
val nextIntent = Intent(this@SignUpActivity, MainV2Activity::class.java)
|
||||||
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
||||||
?: if (intent.extras != null) {
|
?: if (intent.extras != null) {
|
||||||
intent.extras
|
intent.extras
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
|
import kr.co.vividnext.sodalive.databinding.FragmentV2MainChatBinding
|
||||||
|
|
||||||
|
class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
|
||||||
|
FragmentV2MainChatBinding::inflate
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
|
import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
|
||||||
|
|
||||||
|
class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||||
|
FragmentV2MainContentBinding::inflate
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
|
import kr.co.vividnext.sodalive.databinding.FragmentV2MainHomeBinding
|
||||||
|
|
||||||
|
class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||||
|
FragmentV2MainHomeBinding::inflate
|
||||||
|
)
|
||||||
@@ -0,0 +1,654 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.MediaMetadata
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.session.MediaController
|
||||||
|
import androidx.media3.session.SessionToken
|
||||||
|
import coil.load
|
||||||
|
import coil.transform.RoundedCornersTransformation
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
|
import com.gun0912.tedpermission.PermissionListener
|
||||||
|
import com.gun0912.tedpermission.normal.TedPermission
|
||||||
|
import com.orhanobut.logger.Logger
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerFragment
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||||
|
import kr.co.vividnext.sodalive.audition.AuditionActivity
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.databinding.ActivityMainV2Binding
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
|
||||||
|
import kr.co.vividnext.sodalive.main.EventPopupDialogFragment
|
||||||
|
import kr.co.vividnext.sodalive.message.MessageActivity
|
||||||
|
import kr.co.vividnext.sodalive.mypage.MyPageFragment
|
||||||
|
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
|
||||||
|
import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsDialog
|
||||||
|
import kr.co.vividnext.sodalive.user.login.LoginActivity
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding::inflate) {
|
||||||
|
|
||||||
|
private val viewModel: MainV2ViewModel by inject()
|
||||||
|
|
||||||
|
private lateinit var notificationSettingsDialog: NotificationSettingsDialog
|
||||||
|
private var mediaController: MediaController? = null
|
||||||
|
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private val showMiniPlayerRunnable = Runnable { initAndVisibleMiniPlayer() }
|
||||||
|
private var playerStateJob: Job? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
overrideRootWindowInsets()
|
||||||
|
|
||||||
|
checkPermissions()
|
||||||
|
trackAppLaunchIfNeeded()
|
||||||
|
pushTokenUpdate()
|
||||||
|
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
updatePidAndGaid()
|
||||||
|
getEventPopup()
|
||||||
|
observePlayerState()
|
||||||
|
handler.postDelayed({ executeDeeplink(intent) }, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun overrideRootWindowInsets() {
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
|
||||||
|
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
|
||||||
|
|
||||||
|
val left = max(systemBars.left, ime.left)
|
||||||
|
val top = systemBars.top
|
||||||
|
val right = max(systemBars.right, ime.right)
|
||||||
|
v.setPadding(left, top, right, 0)
|
||||||
|
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
ViewCompat.requestApplyInsets(binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
|
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
executeDeeplink(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
getMemberInfo()
|
||||||
|
startService(
|
||||||
|
Intent(this, AudioContentPlayService::class.java).apply {
|
||||||
|
action = AudioContentPlayService.MusicAction.INIT.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
deInitMiniPlayer()
|
||||||
|
playerStateJob?.cancel()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupView() {
|
||||||
|
notificationSettingsDialog = NotificationSettingsDialog(
|
||||||
|
this,
|
||||||
|
layoutInflater
|
||||||
|
) { isNotifiedLive, isNotifiedUploadContent, isNotifiedMessage ->
|
||||||
|
viewModel.updateNotificationSettings(
|
||||||
|
isNotifiedLive,
|
||||||
|
isNotifiedUploadContent,
|
||||||
|
isNotifiedMessage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupBottomNavigation()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showLoginActivity() {
|
||||||
|
if (SharedPreferenceManager.token.isBlank()) {
|
||||||
|
val extras = intent.extras
|
||||||
|
startActivity(
|
||||||
|
Intent(applicationContext, LoginActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_DATA, extras)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openChatTab() {
|
||||||
|
viewModel.clickTab(MainV2Tab.CHAT)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupBottomNavigation() {
|
||||||
|
binding.bottomNavigation.setOnItemSelectedListener { item ->
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.menu_main_v2_home -> viewModel.clickTab(MainV2Tab.HOME)
|
||||||
|
R.id.menu_main_v2_content -> viewModel.clickTab(MainV2Tab.CONTENT)
|
||||||
|
R.id.menu_main_v2_chat -> viewModel.clickTab(MainV2Tab.CHAT)
|
||||||
|
R.id.menu_main_v2_my -> viewModel.clickTab(MainV2Tab.MY)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.bottomNavigation.apply {
|
||||||
|
itemIconTintList = null
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.currentTab.observe(this) { tab ->
|
||||||
|
val itemId = when (tab) {
|
||||||
|
MainV2Tab.HOME -> R.id.menu_main_v2_home
|
||||||
|
MainV2Tab.CONTENT -> R.id.menu_main_v2_content
|
||||||
|
MainV2Tab.CHAT -> R.id.menu_main_v2_chat
|
||||||
|
MainV2Tab.MY -> R.id.menu_main_v2_my
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.bottomNavigation.selectedItemId != itemId) {
|
||||||
|
binding.bottomNavigation.selectedItemId = itemId
|
||||||
|
}
|
||||||
|
|
||||||
|
changeFragment(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeFragment(currentTab: MainV2Tab) {
|
||||||
|
val tag = currentTab.toString()
|
||||||
|
val fragmentManager = supportFragmentManager
|
||||||
|
val fragmentTransaction = fragmentManager.beginTransaction()
|
||||||
|
|
||||||
|
fragmentManager.primaryNavigationFragment?.let {
|
||||||
|
fragmentTransaction.hide(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fragment = fragmentManager.findFragmentByTag(tag)
|
||||||
|
if (fragment == null) {
|
||||||
|
fragment = when (currentTab) {
|
||||||
|
MainV2Tab.HOME -> HomeMainFragment()
|
||||||
|
MainV2Tab.CONTENT -> ContentMainFragment()
|
||||||
|
MainV2Tab.CHAT -> ChatMainFragment()
|
||||||
|
MainV2Tab.MY -> MyPageFragment()
|
||||||
|
}
|
||||||
|
fragmentTransaction.add(R.id.fl_container, fragment, tag)
|
||||||
|
} else {
|
||||||
|
fragmentTransaction.show(fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fragmentTransaction.setPrimaryNavigationFragment(fragment)
|
||||||
|
fragmentTransaction.setReorderingAllowed(true)
|
||||||
|
fragmentTransaction.commitNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observePlayerState() {
|
||||||
|
playerStateJob = lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
SharedPreferenceManager.isPlayerServiceRunningFlow.collect { isRunning ->
|
||||||
|
if (isRunning) {
|
||||||
|
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||||
|
handler.postDelayed(showMiniPlayerRunnable, 1500)
|
||||||
|
} else {
|
||||||
|
deInitMiniPlayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initAndVisibleMiniPlayer() {
|
||||||
|
binding.clMiniPlayer.visibility = View.VISIBLE
|
||||||
|
binding.clMiniPlayer.setOnClickListener { showPlayerFragment() }
|
||||||
|
binding.ivPlayerStop.setOnClickListener {
|
||||||
|
startService(
|
||||||
|
Intent(applicationContext, AudioContentPlayerService::class.java).apply {
|
||||||
|
action = "STOP_SERVICE"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
connectPlayerService()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectPlayerService() {
|
||||||
|
if (mediaController != null || mediaControllerFuture != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
|
||||||
|
val sessionToken = SessionToken(applicationContext, componentName)
|
||||||
|
val controllerFuture =
|
||||||
|
MediaController.Builder(applicationContext, sessionToken).buildAsync()
|
||||||
|
mediaControllerFuture = controllerFuture
|
||||||
|
controllerFuture.addListener(
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (mediaController != null) {
|
||||||
|
controllerFuture.get().release()
|
||||||
|
return@addListener
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaController = controllerFuture.get()
|
||||||
|
setupMediaController()
|
||||||
|
updateMediaMetadata(mediaController?.mediaMetadata)
|
||||||
|
|
||||||
|
binding.ivPlayerPlayOrPause.setImageResource(
|
||||||
|
if (mediaController?.isPlaying == true) {
|
||||||
|
R.drawable.ic_player_pause
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_player_play
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.ivPlayerPlayOrPause.setOnClickListener {
|
||||||
|
mediaController?.let {
|
||||||
|
if (it.playWhenReady) {
|
||||||
|
it.pause()
|
||||||
|
} else {
|
||||||
|
it.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
Logger.e(throwable, "Failed to connect player service")
|
||||||
|
} finally {
|
||||||
|
mediaControllerFuture = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ContextCompat.getMainExecutor(applicationContext)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMediaMetadata(metadata: MediaMetadata?) {
|
||||||
|
metadata?.let {
|
||||||
|
binding.tvPlayerTitle.text = it.title
|
||||||
|
binding.tvPlayerNickname.text = it.artist
|
||||||
|
|
||||||
|
binding.ivPlayerCover.load(it.artworkUri) {
|
||||||
|
crossfade(true)
|
||||||
|
placeholder(R.drawable.ic_place_holder)
|
||||||
|
transformations(RoundedCornersTransformation(4f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupMediaController() {
|
||||||
|
if (mediaController == null) {
|
||||||
|
deInitMiniPlayer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaController!!.addListener(object : Player.Listener {
|
||||||
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
|
updateMediaMetadata(mediaItem?.mediaMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||||
|
binding.ivPlayerPlayOrPause.setImageResource(
|
||||||
|
if (playWhenReady) {
|
||||||
|
R.drawable.ic_player_pause
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_player_play
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deInitMiniPlayer() {
|
||||||
|
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||||
|
binding.clMiniPlayer.visibility = View.GONE
|
||||||
|
mediaControllerFuture?.cancel(true)
|
||||||
|
mediaControllerFuture = null
|
||||||
|
mediaController?.release()
|
||||||
|
mediaController = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPlayerFragment() {
|
||||||
|
val playerFragment = AudioContentPlayerFragment(screenWidth, arrayListOf())
|
||||||
|
playerFragment.show(supportFragmentManager, playerFragment.tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkPermissions() {
|
||||||
|
val permissions = mutableListOf(Manifest.permission.RECORD_AUDIO)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
permissions.add(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
TedPermission.create()
|
||||||
|
.setPermissionListener(object : PermissionListener {
|
||||||
|
override fun onPermissionGranted() {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setDeniedMessage(R.string.record_audio_permission_denied_message)
|
||||||
|
.setPermissions(*permissions.toTypedArray())
|
||||||
|
.check()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trackAppLaunchIfNeeded() {
|
||||||
|
handler.postDelayed({
|
||||||
|
val alreadyTrackingAppLaunch = SharedPreferenceManager.alreadyTrackingAppLaunch
|
||||||
|
val pid = SharedPreferenceManager.marketingPid
|
||||||
|
|
||||||
|
if (!alreadyTrackingAppLaunch && pid.isNotBlank()) {
|
||||||
|
SharedPreferenceManager.alreadyTrackingAppLaunch = true
|
||||||
|
viewModel.adTrackingAppLaunch(pid = pid)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pushTokenUpdate() {
|
||||||
|
FirebaseMessaging.getInstance().token.addOnCompleteListener {
|
||||||
|
if (!it.isSuccessful) {
|
||||||
|
Logger.v("Fetching FCM registration token failed", it.exception)
|
||||||
|
return@addOnCompleteListener
|
||||||
|
}
|
||||||
|
|
||||||
|
val pushToken = it.result
|
||||||
|
if (pushToken != null) {
|
||||||
|
SharedPreferenceManager.pushToken = pushToken
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
viewModel.pushTokenUpdate(pushToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePidAndGaid() {
|
||||||
|
handler.postDelayed({
|
||||||
|
viewModel.fetchAndUpdateGaidAndPid(context = applicationContext)
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMemberInfo() {
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
viewModel.getMemberInfo(context = applicationContext) {
|
||||||
|
notificationSettingsDialog.show(screenWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEventPopup() {
|
||||||
|
viewModel.getEventPopup {
|
||||||
|
if (SharedPreferenceManager.notShowingEventPopupId != it.id) {
|
||||||
|
EventPopupDialogFragment(
|
||||||
|
screenWidth = screenWidth,
|
||||||
|
eventItem = it
|
||||||
|
) {
|
||||||
|
startActivity(
|
||||||
|
Intent(applicationContext, EventDetailActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_EVENT, it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.show(supportFragmentManager, EventPopupDialogFragment::class.java.simpleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeDeeplink(intent: Intent) {
|
||||||
|
val bundle = intent.getBundleExtra(Constants.EXTRA_DATA) ?: return
|
||||||
|
val deepLinkUrl = bundle.getString("deep_link")
|
||||||
|
val routeBundle = if (!deepLinkUrl.isNullOrBlank()) {
|
||||||
|
buildBundleFromDeepLinkUrl(deepLinkUrl) ?: bundle
|
||||||
|
} else {
|
||||||
|
bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
if (executeBundleRoute(routeBundle)) {
|
||||||
|
clearDeferredDeepLink()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildBundleFromDeepLinkUrl(deepLinkUrl: String): Bundle? {
|
||||||
|
val data = runCatching { deepLinkUrl.toUri() }.getOrNull() ?: return null
|
||||||
|
val extras = Bundle().apply {
|
||||||
|
putString("deep_link", deepLinkUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putQuery(key: String) {
|
||||||
|
val value = data.getQueryParameter(key)
|
||||||
|
if (!value.isNullOrBlank()) {
|
||||||
|
extras.putString(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
putQuery("channel_id")
|
||||||
|
putQuery("message_id")
|
||||||
|
putQuery("audition_id")
|
||||||
|
putQuery("content_id")
|
||||||
|
putQuery("deep_link_value")
|
||||||
|
putQuery("deep_link_sub5")
|
||||||
|
putQuery(Constants.EXTRA_COMMUNITY_CREATOR_ID)
|
||||||
|
putQuery(Constants.EXTRA_COMMUNITY_POST_ID)
|
||||||
|
|
||||||
|
applyPathDeepLink(data = data) { key, value ->
|
||||||
|
if (!value.isNullOrBlank() && !extras.containsKey(key)) {
|
||||||
|
extras.putString(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extras
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyPathDeepLink(
|
||||||
|
data: android.net.Uri,
|
||||||
|
putIfAbsent: (key: String, value: String?) -> Unit
|
||||||
|
) {
|
||||||
|
val host = data.host?.lowercase(Locale.ROOT).orEmpty()
|
||||||
|
val pathSegments = data.pathSegments.filter { it.isNotBlank() }
|
||||||
|
val pathType: String
|
||||||
|
val pathId: String?
|
||||||
|
|
||||||
|
if (host.isNotBlank() && host != "payverse") {
|
||||||
|
pathType = host
|
||||||
|
pathId = pathSegments.firstOrNull()
|
||||||
|
} else if (pathSegments.isNotEmpty()) {
|
||||||
|
pathType = pathSegments[0].lowercase(Locale.ROOT)
|
||||||
|
pathId = pathSegments.getOrNull(1)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when (pathType) {
|
||||||
|
"content" -> {
|
||||||
|
putIfAbsent("content_id", pathId)
|
||||||
|
putIfAbsent("deep_link_value", "content")
|
||||||
|
putIfAbsent("deep_link_sub5", pathId)
|
||||||
|
}
|
||||||
|
|
||||||
|
"series" -> {
|
||||||
|
putIfAbsent("deep_link_value", "series")
|
||||||
|
putIfAbsent("deep_link_sub5", pathId)
|
||||||
|
}
|
||||||
|
|
||||||
|
"community" -> {
|
||||||
|
putIfAbsent("deep_link_value", "community")
|
||||||
|
putIfAbsent(Constants.EXTRA_COMMUNITY_CREATOR_ID, pathId)
|
||||||
|
putIfAbsent("deep_link_sub5", pathId)
|
||||||
|
}
|
||||||
|
|
||||||
|
"message" -> {
|
||||||
|
putIfAbsent("deep_link_value", "message")
|
||||||
|
putIfAbsent("message_id", pathId)
|
||||||
|
putIfAbsent("deep_link_sub5", pathId)
|
||||||
|
}
|
||||||
|
|
||||||
|
"audition" -> {
|
||||||
|
putIfAbsent("deep_link_value", "audition")
|
||||||
|
putIfAbsent("audition_id", pathId)
|
||||||
|
putIfAbsent("deep_link_sub5", pathId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeBundleRoute(bundle: Bundle): Boolean {
|
||||||
|
val channelId = bundle.getString("channel_id")?.toLongOrNull()
|
||||||
|
?: bundle.getLong(Constants.EXTRA_USER_ID).takeIf { it > 0 }
|
||||||
|
val messageId = bundle.getString("message_id")?.toLongOrNull()
|
||||||
|
?: bundle.getLong(Constants.EXTRA_MESSAGE_ID).takeIf { it > 0 }
|
||||||
|
val contentId = bundle.getString("content_id")?.toLongOrNull()
|
||||||
|
?: bundle.getLong(Constants.EXTRA_AUDIO_CONTENT_ID).takeIf { it > 0 }
|
||||||
|
val auditionId = bundle.getString("audition_id")?.toLongOrNull()
|
||||||
|
?: bundle.getLong(Constants.EXTRA_AUDITION_ID).takeIf { it > 0 }
|
||||||
|
val communityCreatorId = bundle.getString(Constants.EXTRA_COMMUNITY_CREATOR_ID)?.toLongOrNull()
|
||||||
|
?: bundle.getLong(Constants.EXTRA_COMMUNITY_CREATOR_ID).takeIf { it > 0 }
|
||||||
|
val communityPostId = bundle.getString(Constants.EXTRA_COMMUNITY_POST_ID)?.toLongOrNull()
|
||||||
|
?: bundle.getLong(Constants.EXTRA_COMMUNITY_POST_ID).takeIf { it > 0 }
|
||||||
|
|
||||||
|
when {
|
||||||
|
channelId != null && channelId > 0 -> {
|
||||||
|
startActivity(
|
||||||
|
Intent(applicationContext, UserProfileActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_USER_ID, channelId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
contentId != null && contentId > 0 -> {
|
||||||
|
startActivity(
|
||||||
|
Intent(applicationContext, AudioContentDetailActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
messageId != null && messageId > 0 -> {
|
||||||
|
startActivity(Intent(applicationContext, MessageActivity::class.java))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
communityCreatorId != null && communityCreatorId > 0 -> {
|
||||||
|
startActivity(
|
||||||
|
Intent(applicationContext, CreatorCommunityAllActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, communityCreatorId)
|
||||||
|
if (communityPostId != null && communityPostId > 0) {
|
||||||
|
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, communityPostId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
auditionId != null && auditionId > 0 -> {
|
||||||
|
startActivity(Intent(applicationContext, AuditionActivity::class.java))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val deepLinkValue = bundle.getString("deep_link_value")
|
||||||
|
val deepLinkValueId = bundle.getString("deep_link_sub5")?.toLongOrNull()
|
||||||
|
return !deepLinkValue.isNullOrBlank() && routeByDeepLinkValue(deepLinkValue, deepLinkValueId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun routeByDeepLinkValue(deepLinkValue: String, deepLinkValueId: Long?): Boolean {
|
||||||
|
return when (deepLinkValue.lowercase(Locale.ROOT)) {
|
||||||
|
"series" -> {
|
||||||
|
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
startActivity(
|
||||||
|
Intent(applicationContext, SeriesDetailActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_SERIES_ID, deepLinkValueId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
"content" -> {
|
||||||
|
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
startActivity(
|
||||||
|
Intent(applicationContext, AudioContentDetailActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, deepLinkValueId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
"channel" -> {
|
||||||
|
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
startActivity(
|
||||||
|
Intent(applicationContext, UserProfileActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_USER_ID, deepLinkValueId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
"community" -> {
|
||||||
|
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
startActivity(
|
||||||
|
Intent(applicationContext, CreatorCommunityAllActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, deepLinkValueId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
"message" -> {
|
||||||
|
startActivity(Intent(applicationContext, MessageActivity::class.java))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
"audition" -> {
|
||||||
|
startActivity(Intent(applicationContext, AuditionActivity::class.java))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearDeferredDeepLink() {
|
||||||
|
SharedPreferenceManager.marketingUtmSource = ""
|
||||||
|
SharedPreferenceManager.marketingUtmMedium = ""
|
||||||
|
SharedPreferenceManager.marketingUtmCampaign = ""
|
||||||
|
SharedPreferenceManager.marketingLinkValue = ""
|
||||||
|
SharedPreferenceManager.marketingLinkValueId = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isLoggedIn(): Boolean {
|
||||||
|
return SharedPreferenceManager.token.isNotBlank() && SharedPreferenceManager.token.length > 10
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main
|
||||||
|
|
||||||
|
enum class MainV2Tab {
|
||||||
|
HOME,
|
||||||
|
CONTENT,
|
||||||
|
CHAT,
|
||||||
|
MY
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.google.android.gms.ads.identifier.AdvertisingIdClient
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.AddAllPlaybackTrackingRequest
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.PlaybackTrackingData
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.PlaybackTrackingRepository
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.main.MarketingInfoUpdateRequest
|
||||||
|
import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest
|
||||||
|
import kr.co.vividnext.sodalive.settings.ContentType
|
||||||
|
import kr.co.vividnext.sodalive.settings.event.EventItem
|
||||||
|
import kr.co.vividnext.sodalive.settings.event.EventRepository
|
||||||
|
import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest
|
||||||
|
import kr.co.vividnext.sodalive.tracking.AdTrackingRepository
|
||||||
|
import kr.co.vividnext.sodalive.tracking.FirebaseTracking
|
||||||
|
import kr.co.vividnext.sodalive.tracking.NotiflyClient
|
||||||
|
import kr.co.vividnext.sodalive.user.UserRepository
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
class MainV2ViewModel(
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val eventRepository: EventRepository,
|
||||||
|
private val adTrackingRepository: AdTrackingRepository,
|
||||||
|
private val audioContentRepository: AudioContentRepository,
|
||||||
|
private val playbackTrackingRepository: PlaybackTrackingRepository
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val _currentTab = MutableLiveData(MainV2Tab.HOME)
|
||||||
|
val currentTab: LiveData<MainV2Tab>
|
||||||
|
get() = _currentTab
|
||||||
|
|
||||||
|
fun clickTab(tab: MainV2Tab) {
|
||||||
|
if (_currentTab.value != tab) {
|
||||||
|
_currentTab.postValue(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateNotificationSettings(
|
||||||
|
isNotifiedLive: Boolean,
|
||||||
|
isNotifiedUploadContent: Boolean,
|
||||||
|
isNotifiedMessage: Boolean
|
||||||
|
) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
userRepository.updateNotificationSettings(
|
||||||
|
request = UpdateNotificationSettingRequest(
|
||||||
|
isNotifiedLive,
|
||||||
|
isNotifiedUploadContent,
|
||||||
|
isNotifiedMessage
|
||||||
|
),
|
||||||
|
token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({}, {})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pushTokenUpdate(pushToken: String) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
userRepository
|
||||||
|
.updatePushToken(
|
||||||
|
PushTokenUpdateRequest(pushToken = pushToken),
|
||||||
|
"Bearer ${SharedPreferenceManager.token}"
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({}, {})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMemberInfo(context: Context, showNotificationSettingsDialog: () -> Unit) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
userRepository.getMemberInfo(token = "Bearer ${SharedPreferenceManager.token}")
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
if (it.success && it.data != null) {
|
||||||
|
val data = it.data
|
||||||
|
SharedPreferenceManager.can = data.can
|
||||||
|
SharedPreferenceManager.point = data.point
|
||||||
|
SharedPreferenceManager.role = data.role.name
|
||||||
|
SharedPreferenceManager.isAuth = data.isAuth
|
||||||
|
|
||||||
|
val localCountryCode = SharedPreferenceManager.countryCode.ifBlank { "KR" }
|
||||||
|
val resolvedCountryCode = data.countryCode?.ifBlank { "KR" } ?: localCountryCode
|
||||||
|
val resolvedIsAdultContentVisible =
|
||||||
|
data.isAdultContentVisible ?: SharedPreferenceManager.isAdultContentVisible
|
||||||
|
val resolvedContentType =
|
||||||
|
data.contentType
|
||||||
|
?: ContentType.entries.getOrNull(SharedPreferenceManager.contentPreference)
|
||||||
|
?: ContentType.ALL
|
||||||
|
|
||||||
|
SharedPreferenceManager.countryCode = resolvedCountryCode
|
||||||
|
SharedPreferenceManager.isAdultContentVisible = resolvedIsAdultContentVisible
|
||||||
|
SharedPreferenceManager.contentPreference = resolvedContentType.ordinal
|
||||||
|
SharedPreferenceManager.isAuditionNotification =
|
||||||
|
data.auditionNotice ?: false
|
||||||
|
if (
|
||||||
|
data.followingChannelUploadContentNotice == null &&
|
||||||
|
data.followingChannelLiveNotice == null &&
|
||||||
|
data.messageNotice == null
|
||||||
|
) {
|
||||||
|
showNotificationSettingsDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
val dateFormat = SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd, HH:mm:ss",
|
||||||
|
Locale.getDefault()
|
||||||
|
)
|
||||||
|
val lastActiveDate = dateFormat.format(Date())
|
||||||
|
|
||||||
|
val params = mutableMapOf(
|
||||||
|
"nickname" to SharedPreferenceManager.nickname,
|
||||||
|
"last_active_date" to lastActiveDate,
|
||||||
|
"charge_count" to data.chargeCount,
|
||||||
|
"signup_date" to data.signupDate,
|
||||||
|
"is_auth" to data.isAuth,
|
||||||
|
"gender" to data.gender,
|
||||||
|
"can" to data.can
|
||||||
|
)
|
||||||
|
|
||||||
|
NotiflyClient.setUser(
|
||||||
|
context = context,
|
||||||
|
userId = SharedPreferenceManager.userId,
|
||||||
|
params = params
|
||||||
|
)
|
||||||
|
FirebaseTracking.login("email")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAllPlaybackTracking() {
|
||||||
|
val trackingDataList = playbackTrackingRepository.getAllPlaybackTracking()
|
||||||
|
.filter { it.endPosition != null }
|
||||||
|
.filter { it.endPosition!! - it.startPosition >= 4000 }
|
||||||
|
.map {
|
||||||
|
PlaybackTrackingData(it.contentId, it.playDateTime, it.isPreview)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackingDataList.isNotEmpty()) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
audioContentRepository.addAllPlaybackTracking(
|
||||||
|
request = AddAllPlaybackTrackingRequest(trackingDataList = trackingDataList),
|
||||||
|
token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
if (it.success) {
|
||||||
|
playbackTrackingRepository.removeAllPlaybackTracking()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEventPopup(onSuccess: (EventItem) -> Unit) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
eventRepository.getEventPopup(token = "Bearer ${SharedPreferenceManager.token}")
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
if (it.success && it.data != null) {
|
||||||
|
onSuccess(it.data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchAndUpdateGaidAndPid(context: Context) {
|
||||||
|
Executors.newSingleThreadExecutor().execute {
|
||||||
|
try {
|
||||||
|
val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context)
|
||||||
|
val request = MarketingInfoUpdateRequest(
|
||||||
|
adid = adInfo.id.orEmpty(),
|
||||||
|
pid = SharedPreferenceManager.marketingPid
|
||||||
|
)
|
||||||
|
updateMarketingInfo(request)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
updateMarketingInfo(
|
||||||
|
MarketingInfoUpdateRequest(
|
||||||
|
adid = "",
|
||||||
|
pid = SharedPreferenceManager.marketingPid
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun adTrackingAppLaunch(pid: String) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
adTrackingRepository.appLaunch(pid)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({}, {})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMarketingInfo(request: MarketingInfoUpdateRequest) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
userRepository.updateMarketingInfo(
|
||||||
|
request,
|
||||||
|
token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({}, {})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="@color/white" android:state_checked="true" />
|
||||||
|
<item android:color="@color/white" android:state_selected="true" />
|
||||||
|
<item android:color="@color/gray_600" />
|
||||||
|
</selector>
|
||||||
BIN
app/src/main/res/drawable-mdpi/ic_nav_chat.png
Normal file
|
After Width: | Height: | Size: 602 B |
BIN
app/src/main/res/drawable-mdpi/ic_nav_chat_selected.png
Normal file
|
After Width: | Height: | Size: 446 B |
BIN
app/src/main/res/drawable-mdpi/ic_nav_content.png
Normal file
|
After Width: | Height: | Size: 537 B |
BIN
app/src/main/res/drawable-mdpi/ic_nav_content_selected.png
Normal file
|
After Width: | Height: | Size: 378 B |
BIN
app/src/main/res/drawable-mdpi/ic_nav_home.png
Normal file
|
After Width: | Height: | Size: 564 B |
BIN
app/src/main/res/drawable-mdpi/ic_nav_home_selected.png
Normal file
|
After Width: | Height: | Size: 408 B |
BIN
app/src/main/res/drawable-mdpi/ic_nav_my.png
Normal file
|
After Width: | Height: | Size: 723 B |
5
app/src/main/res/drawable/ic_nav_chat_tab.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_nav_chat_selected" android:state_checked="true" />
|
||||||
|
<item android:drawable="@drawable/ic_nav_chat" />
|
||||||
|
</selector>
|
||||||
5
app/src/main/res/drawable/ic_nav_content_tab.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_nav_content_selected" android:state_checked="true" />
|
||||||
|
<item android:drawable="@drawable/ic_nav_content" />
|
||||||
|
</selector>
|
||||||
5
app/src/main/res/drawable/ic_nav_home_tab.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_nav_home_selected" android:state_checked="true" />
|
||||||
|
<item android:drawable="@drawable/ic_nav_home" />
|
||||||
|
</selector>
|
||||||
5
app/src/main/res/drawable/ic_nav_my_tab.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_tabbar_my_selected" android:state_checked="true" />
|
||||||
|
<item android:drawable="@drawable/ic_nav_my" />
|
||||||
|
</selector>
|
||||||
108
app/src/main/res/layout/activity_main_v2.xml
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/fl_container"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/cl_mini_player"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/cl_mini_player"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/color_222222"
|
||||||
|
android:paddingHorizontal="13.3dp"
|
||||||
|
android:paddingVertical="10.7dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_player_cover"
|
||||||
|
android:layout_width="36.7dp"
|
||||||
|
android:layout_height="36.7dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@mipmap/ic_launcher" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_player_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="10.7dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="@color/color_eeeeee"
|
||||||
|
android:textSize="13sp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/iv_player_play_or_pause"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/iv_player_cover"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/iv_player_cover"
|
||||||
|
tools:text="JFLA 커버곡 Avicii for your self" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_player_nickname"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2.3dp"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:textColor="@color/color_d2d2d2"
|
||||||
|
android:textSize="11sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/tv_player_title"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/tv_player_title"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/tv_player_title"
|
||||||
|
tools:ignore="SmallSp"
|
||||||
|
tools:text="JFLA 커버곡 Avicii for your self" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_player_play_or_pause"
|
||||||
|
android:layout_width="25dp"
|
||||||
|
android:layout_height="25dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/iv_player_stop"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/iv_player_stop"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/iv_player_stop"
|
||||||
|
tools:src="@drawable/btn_bar_play" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_player_stop"
|
||||||
|
android:layout_width="25dp"
|
||||||
|
android:layout_height="25dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_noti_stop"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
|
android:id="@+id/bottom_navigation"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/black"
|
||||||
|
app:itemActiveIndicatorStyle="@null"
|
||||||
|
app:itemIconTint="@null"
|
||||||
|
app:itemTextAppearanceActive="@style/Typography.Caption3"
|
||||||
|
app:itemTextAppearanceActiveBoldEnabled="false"
|
||||||
|
app:itemTextAppearanceInactive="@style/Typography.Caption3"
|
||||||
|
app:itemTextColor="@color/color_main_v2_bottom_navigation_label"
|
||||||
|
app:labelVisibilityMode="labeled"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:menu="@menu/menu_main_v2_bottom_navigation" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/black"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
|||||||
5
app/src/main/res/layout/fragment_v2_main_chat.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black" />
|
||||||
5
app/src/main/res/layout/fragment_v2_main_content.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black" />
|
||||||
5
app/src/main/res/layout/fragment_v2_main_home.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black" />
|
||||||
19
app/src/main/res/menu/menu_main_v2_bottom_navigation.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_main_v2_home"
|
||||||
|
android:icon="@drawable/ic_nav_home_tab"
|
||||||
|
android:title="@string/tab_home" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_main_v2_content"
|
||||||
|
android:icon="@drawable/ic_nav_content_tab"
|
||||||
|
android:title="@string/tab_content" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_main_v2_chat"
|
||||||
|
android:icon="@drawable/ic_nav_chat_tab"
|
||||||
|
android:title="@string/tab_chat" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_main_v2_my"
|
||||||
|
android:icon="@drawable/ic_nav_my_tab"
|
||||||
|
android:title="@string/tab_my" />
|
||||||
|
</menu>
|
||||||
@@ -137,6 +137,7 @@
|
|||||||
|
|
||||||
<!-- Main / Home -->
|
<!-- Main / Home -->
|
||||||
<string name="tab_home">Home</string>
|
<string name="tab_home">Home</string>
|
||||||
|
<string name="tab_content">Content</string>
|
||||||
<string name="tab_chat">Chat</string>
|
<string name="tab_chat">Chat</string>
|
||||||
<string name="tab_live">Live</string>
|
<string name="tab_live">Live</string>
|
||||||
<string name="tab_my">My</string>
|
<string name="tab_my">My</string>
|
||||||
|
|||||||
@@ -137,6 +137,7 @@
|
|||||||
|
|
||||||
<!-- Main / Home -->
|
<!-- Main / Home -->
|
||||||
<string name="tab_home">ホーム</string>
|
<string name="tab_home">ホーム</string>
|
||||||
|
<string name="tab_content">コンテンツ</string>
|
||||||
<string name="tab_chat">チャット</string>
|
<string name="tab_chat">チャット</string>
|
||||||
<string name="tab_live">ライブ</string>
|
<string name="tab_live">ライブ</string>
|
||||||
<string name="tab_my">マイ</string>
|
<string name="tab_my">マイ</string>
|
||||||
|
|||||||
@@ -136,6 +136,7 @@
|
|||||||
|
|
||||||
<!-- Main / Home -->
|
<!-- Main / Home -->
|
||||||
<string name="tab_home">홈</string>
|
<string name="tab_home">홈</string>
|
||||||
|
<string name="tab_content">콘텐츠</string>
|
||||||
<string name="tab_chat">채팅</string>
|
<string name="tab_chat">채팅</string>
|
||||||
<string name="tab_live">라이브</string>
|
<string name="tab_live">라이브</string>
|
||||||
<string name="tab_my">마이</string>
|
<string name="tab_my">마이</string>
|
||||||
|
|||||||
367
docs/plan-task/20260519_메인페이지하단내비게이션신규개발.md
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
# 메인 페이지 하단 내비게이션 신규 개발
|
||||||
|
|
||||||
|
## 작업 목표
|
||||||
|
- 메인 페이지를 `홈`, `콘텐츠`, `채팅`, `마이` 4개 하단 내비게이션 탭 구조로 구성한다.
|
||||||
|
- 기존 `MainActivity`에 신규 구조를 덧씌우지 않고 `kr.co.vividnext.sodalive.v2` 하위 신규 메인 Activity를 개발한다.
|
||||||
|
- 하단 내비게이션은 Material `BottomNavigationView`를 사용한다.
|
||||||
|
- `마이` 탭은 기존 `MyPageFragment`를 재사용한다.
|
||||||
|
- `홈`, `콘텐츠`, `채팅` 탭은 신규 빈 Fragment를 만들어 표시한다.
|
||||||
|
- 초기 문서 작성 완료 후, 사용자 승인에 따라 계획 문서 기준으로 앱 소스 구현까지 진행한다.
|
||||||
|
|
||||||
|
## 근거 문서
|
||||||
|
- PRD: `docs/prd/20260519_메인페이지하단내비게이션신규개발_prd.md`
|
||||||
|
- 문서 규칙: `docs/agent-guides/workflow-docs-commits.md`
|
||||||
|
- 빌드/테스트 규칙: `docs/agent-guides/build-test-style.md`
|
||||||
|
|
||||||
|
## 현재 구조 요약
|
||||||
|
- 기존 `MainActivity`는 `activity_main.xml`의 `FrameLayout#fl_container`에 탭별 Fragment를 표시한다.
|
||||||
|
- 기존 탭 전환은 `supportFragmentManager`의 `add`/`hide`/`show`/`commitNow()` 패턴을 사용한다.
|
||||||
|
- 기존 하단 탭 UI는 `activity_main.xml`의 `LinearLayout#ll_tab`과 `item_main_tab.xml` include 구조로 구성되어 있다.
|
||||||
|
- 기존 `MainViewModel.CurrentTab`은 `HOME`, `LIVE`, `MY`, `CHAT`을 가진다.
|
||||||
|
- 신규 구조는 기존 `MainActivity`를 직접 개편하지 않고 별도 v2 메인 Activity로 분리한다.
|
||||||
|
- 신규 요구 탭 순서는 `HOME`, `CONTENT`, `CHAT`, `MY`이다.
|
||||||
|
|
||||||
|
## 구현 계획
|
||||||
|
|
||||||
|
### Task 1: 신규 메인 Activity 골격 추가
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt`
|
||||||
|
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2ViewModel.kt`
|
||||||
|
- Create: `app/src/main/res/layout/activity_main_v2.xml`
|
||||||
|
- Modify: `app/src/main/AndroidManifest.xml`
|
||||||
|
- Reference: `app/src/main/java/kr/co/vividnext/sodalive/splash/SplashActivity.kt`
|
||||||
|
- Reference: `app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginActivity.kt`
|
||||||
|
- Reference: `app/src/main/java/kr/co/vividnext/sodalive/user/signup/SignUpActivity.kt`
|
||||||
|
- Reference: `app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt`
|
||||||
|
- Reference: `app/src/main/java/kr/co/vividnext/sodalive/audio_content/player/AudioContentPlayerService.kt`
|
||||||
|
|
||||||
|
- [x] **Step 1: 기존 MainActivity 책임 확인**
|
||||||
|
|
||||||
|
Run: `rg -n "class MainActivity|checkPermissions|showLoginActivity|getEventPopup|initAndVisibleMiniPlayer|executeDeeplink|setupBottomTabLayout|changeFragment" app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt`
|
||||||
|
|
||||||
|
Expected: 기존 인증, 이벤트 팝업, 미니 플레이어, 딥링크, 탭 전환 관련 메서드가 조회된다.
|
||||||
|
|
||||||
|
- [x] **Step 2: 신규 v2 Activity와 ViewModel 작성**
|
||||||
|
|
||||||
|
`MainV2Activity`와 `MainV2ViewModel`은 `kr.co.vividnext.sodalive.v2.main` 패키지 하위에 작성한다. 기존 `MainActivity` 파일은 수정 대상으로 삼지 않는다.
|
||||||
|
|
||||||
|
- [x] **Step 3: 신규 Activity 레이아웃 작성**
|
||||||
|
|
||||||
|
`activity_main_v2.xml`은 Fragment 컨테이너, 미니 플레이어 영역, `BottomNavigationView`를 분리해 배치한다. 미니 플레이어는 `BottomNavigationView` 위에 위치해야 한다.
|
||||||
|
|
||||||
|
- [x] **Step 4: AndroidManifest 등록**
|
||||||
|
|
||||||
|
신규 Activity를 `AndroidManifest.xml`에 등록한다. 런처 Activity는 기존처럼 `SplashActivity`를 유지한다.
|
||||||
|
|
||||||
|
- [x] **Step 5: MainActivity 진입점 전환 대상 확인**
|
||||||
|
|
||||||
|
Run: `rg -n "MainActivity::class|kr\.co\.vividnext\.sodalive\.main\.MainActivity" app/src/main/java app/src/main/AndroidManifest.xml`
|
||||||
|
|
||||||
|
Expected: `SplashActivity`, 로그인/회원가입 완료, 딥링크, 플레이어 서비스 알림 등 기존 `MainActivity` 진입점이 조회된다. 구현 시 신규 메인으로 전환할 대상과 레거시 유지 대상을 이 목록에서 명시적으로 분류한다.
|
||||||
|
|
||||||
|
### Task 2: 탭 상태와 라벨 정의 정리
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Tab.kt`
|
||||||
|
- Modify: `app/src/main/res/values/strings.xml`
|
||||||
|
|
||||||
|
- [x] **Step 1: 신규 탭 enum 정의**
|
||||||
|
|
||||||
|
`MainV2Tab`은 신규 메인 탭 기준으로 `HOME`, `CONTENT`, `CHAT`, `MY`를 가진다.
|
||||||
|
|
||||||
|
- [x] **Step 2: 기존 MainViewModel 탭 enum은 수정하지 않음 확인**
|
||||||
|
|
||||||
|
Run: `rg -n "enum class CurrentTab" app/src/main/java/kr/co/vividnext/sodalive/main/MainViewModel.kt`
|
||||||
|
|
||||||
|
Expected: 기존 `MainViewModel.CurrentTab`은 레거시 메인 화면용으로 유지한다.
|
||||||
|
|
||||||
|
- [x] **Step 3: 콘텐츠 탭 문자열 추가**
|
||||||
|
|
||||||
|
`strings.xml`의 Main/Home 문자열 영역에 `tab_content`를 추가한다.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<string name="tab_content">콘텐츠</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **Step 4: 문자열 리소스 확인**
|
||||||
|
|
||||||
|
Run: `rg -n 'name="tab_(home|content|chat|my)"' app/src/main/res/values/strings.xml`
|
||||||
|
|
||||||
|
Expected: `tab_home`, `tab_content`, `tab_chat`, `tab_my`가 모두 조회된다.
|
||||||
|
|
||||||
|
### Task 3: 신규 빈 페이지 Fragment 추가
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt`
|
||||||
|
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/ContentMainFragment.kt`
|
||||||
|
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/ChatMainFragment.kt`
|
||||||
|
- Create: `app/src/main/res/layout/fragment_v2_main_home.xml`
|
||||||
|
- Create: `app/src/main/res/layout/fragment_v2_main_content.xml`
|
||||||
|
- Create: `app/src/main/res/layout/fragment_v2_main_chat.xml`
|
||||||
|
|
||||||
|
- [x] **Step 1: 신규 패키지 위치 생성**
|
||||||
|
|
||||||
|
신규 Fragment는 저장소 규칙에 따라 `kr.co.vividnext.sodalive.v2.main` 패키지 하위에 둔다.
|
||||||
|
|
||||||
|
- [x] **Step 2: 홈 빈 Fragment 작성**
|
||||||
|
|
||||||
|
`HomeMainFragment`는 `BaseFragment`와 ViewBinding 패턴을 사용하고, 별도 로직 없이 빈 홈 페이지 레이아웃을 표시한다.
|
||||||
|
|
||||||
|
- [x] **Step 3: 콘텐츠 빈 Fragment 작성**
|
||||||
|
|
||||||
|
`ContentMainFragment`는 `BaseFragment`와 ViewBinding 패턴을 사용하고, 별도 로직 없이 빈 콘텐츠 페이지 레이아웃을 표시한다.
|
||||||
|
|
||||||
|
- [x] **Step 4: 채팅 빈 Fragment 작성**
|
||||||
|
|
||||||
|
`ChatMainFragment`는 `BaseFragment`와 ViewBinding 패턴을 사용하고, 별도 로직 없이 빈 채팅 페이지 레이아웃을 표시한다.
|
||||||
|
|
||||||
|
- [x] **Step 5: 빈 페이지 레이아웃 작성**
|
||||||
|
|
||||||
|
각 XML 레이아웃은 `match_parent` 크기의 최소 루트 뷰를 사용하고, 네트워크/리스트/상태 UI는 추가하지 않는다.
|
||||||
|
|
||||||
|
- [x] **Step 6: 신규 Fragment 참조 확인**
|
||||||
|
|
||||||
|
Run: `rg -n "class (HomeMainFragment|ContentMainFragment|ChatMainFragment)" app/src/main/java/kr/co/vividnext/sodalive/v2/main`
|
||||||
|
|
||||||
|
Expected: 신규 Fragment 3개가 모두 조회된다.
|
||||||
|
|
||||||
|
### Task 4: BottomNavigationView와 아이콘 연결
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `app/src/main/res/layout/activity_main_v2.xml`
|
||||||
|
- Create: `app/src/main/res/menu/menu_main_v2_bottom_navigation.xml`
|
||||||
|
- Create or Modify: tab icon selector drawable files under `app/src/main/res/drawable/`
|
||||||
|
- Create or Modify: bottom navigation text color selector under `app/src/main/res/color/`
|
||||||
|
- Reference: existing PNG resources under `app/src/main/res/drawable-mdpi/` and `app/src/main/res/drawable-xxhdpi/`
|
||||||
|
- Reference: `app/src/main/res/values/typography.xml`
|
||||||
|
|
||||||
|
- [x] **Step 1: BottomNavigationView 배치 확인**
|
||||||
|
|
||||||
|
Run: `rg -n "BottomNavigationView|bottom_navigation" app/src/main/res/layout/activity_main_v2.xml`
|
||||||
|
|
||||||
|
Expected: 신규 Activity 레이아웃에 `BottomNavigationView`가 조회된다.
|
||||||
|
|
||||||
|
- [x] **Step 2: menu 리소스 작성**
|
||||||
|
|
||||||
|
`menu_main_v2_bottom_navigation.xml`의 item은 `home`, `content`, `chat`, `my` 순서로 정의한다.
|
||||||
|
|
||||||
|
- [x] **Step 3: 탭 라벨과 아이콘 연결**
|
||||||
|
|
||||||
|
각 menu item은 `android:title`과 `android:icon`을 가진다. 접근성을 위해 title을 비워두지 않는다.
|
||||||
|
|
||||||
|
- [x] **Step 4: 탭 아이콘 selector 구성**
|
||||||
|
|
||||||
|
선택/미선택 상태 모두 아이콘 tint나 파란 선택 리소스를 사용하지 않고 다음 원본 리소스를 사용한다.
|
||||||
|
|
||||||
|
| 탭 | 미선택 | 선택 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 홈 | `ic_nav_home` | `ic_nav_home` |
|
||||||
|
| 콘텐츠 | `ic_nav_content` | `ic_nav_content` |
|
||||||
|
| 채팅 | `ic_nav_chat` | `ic_nav_chat` |
|
||||||
|
| 마이 | `ic_nav_my` | `ic_nav_my` |
|
||||||
|
|
||||||
|
- [x] **Step 5: 아이콘 리소스 존재 확인**
|
||||||
|
|
||||||
|
Run: `rg --files app/src/main/res | rg 'ic_nav_(home|content|chat|my)(_|\.)|ic_tabbar_my_selected'`
|
||||||
|
|
||||||
|
Expected: `ic_nav_home`, `ic_nav_content`, `ic_nav_chat`, `ic_nav_my`가 조회된다.
|
||||||
|
|
||||||
|
- [x] **Step 6: BottomNavigationView 시각 스타일 조정**
|
||||||
|
|
||||||
|
`activity_main_v2.xml`의 `BottomNavigationView`는 배경 `@color/black`, 아이콘 tint 없음, 라벨 색상 selector, `Typography.Caption3`, item padding 축소를 적용한다.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `app:itemIconTint="@null"`을 유지한다.
|
||||||
|
- 미선택 라벨은 `@color/gray_600`, 선택 라벨은 `@color/white`를 사용한다.
|
||||||
|
- `app:itemTextAppearanceActive`와 `app:itemTextAppearanceInactive`는 `@style/Typography.Caption3`을 사용한다.
|
||||||
|
- 과도한 내부 padding을 줄이기 위해 `app:itemPaddingTop`과 `app:itemPaddingBottom`을 `0dp`로 지정한다.
|
||||||
|
|
||||||
|
- [x] **Step 7: edge-to-edge 하단 inset과 아이콘 색상 원인 수정**
|
||||||
|
|
||||||
|
`MainV2Activity`는 `BaseActivity`의 루트 bottom system inset padding을 화면 범위에서만 제거한다. 탭 아이콘 selector는 checked 상태에서도 원본 아이콘 리소스를 사용해 `#3BB9F1` 선택 아이콘이 표시되지 않게 한다.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `BaseActivity`는 수정하지 않는다.
|
||||||
|
- `MainV2Activity`에서 root bottom padding을 `0`으로 재적용한다.
|
||||||
|
- `ic_nav_home_tab`, `ic_nav_content_tab`, `ic_nav_chat_tab`, `ic_nav_my_tab`은 checked/default 모두 원본 아이콘을 사용한다.
|
||||||
|
|
||||||
|
### Task 5: 신규 MainV2Activity 탭 연결과 Fragment 전환
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2ViewModel.kt`
|
||||||
|
|
||||||
|
- [x] **Step 1: BottomNavigationView 선택 리스너 연결**
|
||||||
|
|
||||||
|
`BottomNavigationView.setOnItemSelectedListener`에서 선택된 menu item id를 `MainV2Tab`으로 매핑한다.
|
||||||
|
|
||||||
|
- [x] **Step 2: 탭별 Fragment 매핑 변경**
|
||||||
|
|
||||||
|
`HOME`은 `HomeMainFragment`, `CONTENT`는 `ContentMainFragment`, `CHAT`은 `ChatMainFragment`, `MY`는 기존 `MyPageFragment`를 반환하도록 매핑한다.
|
||||||
|
|
||||||
|
- [x] **Step 3: 탭별 back stack 미사용 전환 구현**
|
||||||
|
|
||||||
|
Fragment 전환은 탭별 back stack을 만들지 않는다. `addToBackStack()`은 사용하지 않는다.
|
||||||
|
|
||||||
|
- [x] **Step 4: 채팅 배지 확장 지점 확보**
|
||||||
|
|
||||||
|
이번 구현에서 실제 배지 데이터는 붙이지 않더라도, `chat` menu item id는 향후 `BottomNavigationView.getOrCreateBadge()`로 확장할 수 있도록 안정적인 이름으로 정의한다.
|
||||||
|
|
||||||
|
### Task 6: 기존 필수 프로세스 선별 이관
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2ViewModel.kt`
|
||||||
|
|
||||||
|
- [x] **Step 1: 권한/인증 흐름 이관**
|
||||||
|
|
||||||
|
기존 `MainActivity`의 권한 요청과 로그인 진입 흐름을 신규 Activity에 필요한 범위로 이관한다.
|
||||||
|
|
||||||
|
- [x] **Step 2: 이벤트 팝업 흐름 이관**
|
||||||
|
|
||||||
|
로그인 상태에서 이벤트 팝업을 조회하고 표시하는 기존 사용자 경험을 신규 Activity에서도 유지한다.
|
||||||
|
|
||||||
|
- [x] **Step 3: 미니 플레이어 흐름 이관**
|
||||||
|
|
||||||
|
오디오 플레이어 서비스 상태를 관찰해 미니 플레이어를 표시/해제하는 흐름을 신규 Activity에서도 유지한다.
|
||||||
|
|
||||||
|
- [x] **Step 4: 딥링크 라우팅 진입점 분리**
|
||||||
|
|
||||||
|
딥링크 정리 계획을 고려해 신규 Activity 내부에 탭 라우팅 진입점을 분리한다. 상세 딥링크 정책 확장은 별도 작업에서 다룬다.
|
||||||
|
|
||||||
|
### Task 7: 검증
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/plan-task/20260519_메인페이지하단내비게이션신규개발.md`
|
||||||
|
|
||||||
|
- [x] **Step 1: 변경 파일 LSP 진단 실행**
|
||||||
|
|
||||||
|
Run: `lsp_diagnostics` on changed Kotlin/XML files.
|
||||||
|
|
||||||
|
Expected: 이번 변경으로 인한 오류가 없다. XML LSP가 환경에 없으면 검증 기록에 남긴다.
|
||||||
|
|
||||||
|
- [x] **Step 2: 리소스/문자열/Menu 조회 검증**
|
||||||
|
|
||||||
|
Run: `rg -n 'BottomNavigationView|menu_main_v2_bottom_navigation|tab_content|ic_nav_home|ic_nav_content|ic_nav_chat|ic_nav_my|ic_tabbar_my_selected|MainV2Activity' app/src/main/res app/src/main/java/kr/co/vividnext/sodalive`
|
||||||
|
|
||||||
|
Expected: 신규 Activity, BottomNavigationView, menu, 신규 탭 문자열과 아이콘 참조가 조회된다.
|
||||||
|
|
||||||
|
- [x] **Step 3: 디버그 빌드 실행**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:assembleDebug`
|
||||||
|
|
||||||
|
Expected: `BUILD SUCCESSFUL`
|
||||||
|
|
||||||
|
- [x] **Step 4: 검증 기록 누적**
|
||||||
|
|
||||||
|
이 문서 하단 `검증 기록`에 실행한 명령, 목적, 결과를 한국어로 누적 기록한다.
|
||||||
|
|
||||||
|
## 체크리스트
|
||||||
|
- [x] AC1: 하단 탭이 `홈`, `콘텐츠`, `채팅`, `마이` 순서로 표시된다.
|
||||||
|
- [x] AC2: 신규 메인 화면은 기존 `MainActivity`에 덧씌우지 않고 `kr.co.vividnext.sodalive.v2` 하위 신규 Activity로 구성된다.
|
||||||
|
- [x] AC3: 하단 내비게이션은 `BottomNavigationView`를 사용한다.
|
||||||
|
- [x] AC4: `마이` 탭은 기존 `MyPageFragment`를 표시한다.
|
||||||
|
- [x] AC5: `홈`, `콘텐츠`, `채팅` 탭은 신규 빈 Fragment를 표시한다.
|
||||||
|
- [x] AC6: 선택/미선택 아이콘은 tint 없이 원본 아이콘 리소스를 사용한다.
|
||||||
|
- [x] AC7: checked 상태에서 `#3BB9F1` 선택 아이콘 리소스로 바뀌지 않는다.
|
||||||
|
- [x] AC8: 탭별 back stack을 사용하지 않는다.
|
||||||
|
- [x] AC9: 인증, 이벤트 팝업, 미니 플레이어 흐름은 신규 Activity에서도 기존 사용자 경험을 유지한다.
|
||||||
|
- [x] AC10: `./gradlew :app:assembleDebug`가 성공한다.
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
- 2026-05-19
|
||||||
|
- 무엇/왜/어떻게: 구현 전 문서 작성을 위해 기존 메인 탭 구조와 문서 규칙을 조사했다.
|
||||||
|
- 실행 명령/도구:
|
||||||
|
- `rg --files docs app/src/main | rg '(^docs/|MainActivity|fragment_.*main|activity_.*main|menu|nav|tabbar|ic_nav|ic_tabbar)'`
|
||||||
|
- `read(app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt)`
|
||||||
|
- `read(app/src/main/java/kr/co/vividnext/sodalive/main/MainViewModel.kt)`
|
||||||
|
- `read(app/src/main/res/layout/activity_main.xml)`
|
||||||
|
- `read(app/src/main/res/layout/item_main_tab.xml)`
|
||||||
|
- `read(docs/agent-guides/workflow-docs-commits.md)`
|
||||||
|
- `read(docs/prd/sample-prd.md)`
|
||||||
|
- 결과:
|
||||||
|
- 현재 메인 화면은 커스텀 하단 탭바와 수동 `FragmentTransaction` 패턴을 사용한다.
|
||||||
|
- 현재 탭은 `HOME`, `CHAT`, `LIVE`, `MY` 구성이며 신규 요구는 `HOME`, `CONTENT`, `CHAT`, `MY`이다.
|
||||||
|
- 저장소 규칙에 따라 PRD는 `docs/prd/`, 계획/TASK 문서는 `docs/plan-task/`에 작성해야 한다.
|
||||||
|
- 사용자 지시에 따라 현재 단계에서는 문서만 작성하고 구현 파일은 변경하지 않는다.
|
||||||
|
- 2026-05-19
|
||||||
|
- 무엇/왜/어떻게: PRD 검토 중 결정된 `BottomNavigationView` 사용과 신규 v2 메인 Activity 개발 방향을 문서에 반영했다.
|
||||||
|
- 실행 명령/도구:
|
||||||
|
- `read(docs/prd/20260519_메인페이지하단내비게이션신규개발_prd.md)`
|
||||||
|
- `read(docs/plan-task/20260519_메인페이지하단내비게이션신규개발.md)`
|
||||||
|
- `rg -n "BottomNavigationView|커스텀|MainActivity|신규|v2|FragmentTransaction|tabbar|탭바" docs/prd/20260519_메인페이지하단내비게이션신규개발_prd.md docs/plan-task/20260519_메인페이지하단내비게이션신규개발.md`
|
||||||
|
- 결과:
|
||||||
|
- 기존 `MainActivity`에 신규 구조를 덧씌우지 않고 신규 v2 메인 Activity를 개발하는 방향으로 계획을 변경했다.
|
||||||
|
- 하단 내비게이션은 커스텀 탭바 대신 Material `BottomNavigationView`를 사용하는 것으로 정리했다.
|
||||||
|
- 탭별 back stack은 사용하지 않고, 인증/이벤트 팝업/미니 플레이어 흐름은 신규 Activity에 필요한 범위로 이관하는 것으로 정리했다.
|
||||||
|
- 2026-05-19
|
||||||
|
- 무엇/왜/어떻게: 계획 문서 기준으로 신규 v2 메인 Activity와 BottomNavigationView 기반 하단 내비게이션을 구현하고 검증했다.
|
||||||
|
- 실행 명령/도구:
|
||||||
|
- `task(subagent_type="explore", description="v2 메인 패턴 조사")`
|
||||||
|
- `task(subagent_type="librarian", description="BottomNavigationView API 조사")`
|
||||||
|
- `rg -n "com.google.android.material|viewBinding|koin|BaseActivity|BaseFragment|EventPopupDialogFragment|NotificationSettingsDialog|isPlayerServiceRunningFlow|MediaController|BottomNavigationView|MainActivity::class|class MainActivity|class MainViewModel" app/build.gradle build.gradle settings.gradle app/src/main/java app/src/main/res app/src/main/AndroidManifest.xml`
|
||||||
|
- `lsp_diagnostics(app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt)`
|
||||||
|
- `lsp_diagnostics(app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2ViewModel.kt)`
|
||||||
|
- `lsp_diagnostics(app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt)`
|
||||||
|
- `rg -n "class (HomeMainFragment|ContentMainFragment|ChatMainFragment)|MainV2Activity::class|MainActivity::class|showLoginActivity\(\)|menu_main_v2_|BottomNavigationView|getOrCreateBadge|addToBackStack" app/src/main/java app/src/main/res app/src/main/AndroidManifest.xml`
|
||||||
|
- `./gradlew :app:ktlintCheck`
|
||||||
|
- `./gradlew :app:assembleDebug`
|
||||||
|
- `adb devices`
|
||||||
|
- `test -f app/build/outputs/apk/debug/app-debug.apk`
|
||||||
|
- 결과:
|
||||||
|
- Kotlin LSP는 현재 환경에 설정되어 있지 않아 실행 불가(`No LSP server configured for extension: .kt`)였다.
|
||||||
|
- 신규 `MainV2Activity`, `MainV2ViewModel`, `MainV2Tab`, 홈/콘텐츠/채팅 빈 Fragment, `BottomNavigationView` menu/selector 리소스를 추가했다.
|
||||||
|
- `SplashActivity`, 로그인/회원가입 완료, `DeepLinkActivity`, 오디오 플레이어 알림 진입점을 `MainV2Activity`로 전환했다.
|
||||||
|
- 기존 `MyPageFragment`는 `MainActivity`와 `MainV2Activity` 양쪽 host에서 로그인 진입을 처리하도록 수정했다.
|
||||||
|
- `./gradlew :app:ktlintCheck`는 중간 fresh 실행에서 `BUILD SUCCESSFUL`로 완료됐으나, 이후 추가 확인 과정에서 최종 diff에 남지 않는 `LiveReservationCancelActivity`, `LiveRoomActivity`의 기존 ktlint 위반이 다시 보고되어 현재 명령은 실패한다.
|
||||||
|
- 최종 ktlint 보고서에서 이번 변경 파일 관련 위반은 조회되지 않았다.
|
||||||
|
- `./gradlew :app:assembleDebug`는 fresh 실행 기준 `BUILD SUCCESSFUL`로 완료됐다.
|
||||||
|
- `adb devices` 결과 연결된 기기/에뮬레이터가 없어 실제 앱 실행 수동 QA는 수행하지 못했다.
|
||||||
|
- 디버그 APK `app/build/outputs/apk/debug/app-debug.apk` 생성은 확인했다.
|
||||||
|
- 2026-05-19
|
||||||
|
- 무엇/왜/어떻게: 사용자 요청에 따라 `MainV2Activity`의 `BottomNavigationView` 시각 스타일을 조정하고 검증했다.
|
||||||
|
- 실행 명령/도구:
|
||||||
|
- `task(category="quick", description="BottomNavigation 패턴 조사")`
|
||||||
|
- `task(subagent_type="librarian", description="Material nav API 조사")`
|
||||||
|
- `rg -n "BottomNavigationView|bottom_navigation|menu_main_v2|itemIconTint|itemTextColor|itemTextAppearance|itemPadding|padding|fitsSystemWindows|WindowInsets|gray600|caption|TextAppearance|colorBlack|black" app/src/main/res app/src/main/java/kr/co/vividnext/sodalive/v2 app/src/main/java/kr/co/vividnext/sodalive/main app/src/main/AndroidManifest.xml`
|
||||||
|
- `lsp_diagnostics(app/src/main/res/layout/activity_main_v2.xml)`
|
||||||
|
- `rg -n "itemIconTint=\"@null\"|itemPadding(Bottom|Top)=\"0dp\"|itemTextAppearance(Active|Inactive)=\"@style/Typography.Caption3\"|itemTextColor=\"@color/color_main_v2_bottom_navigation_label\"|android:background=\"@color/black\"" app/src/main/res/layout/activity_main_v2.xml`
|
||||||
|
- `rg -n "state_checked=\"true\"|state_selected=\"true\"|gray_600|white" app/src/main/res/color/color_main_v2_bottom_navigation_label.xml`
|
||||||
|
- `./gradlew :app:assembleDebug`
|
||||||
|
- 결과:
|
||||||
|
- 과도한 하단 padding의 주요 후보는 `BottomNavigationView` 기본 item padding과 `BaseActivity`의 system bar inset padding 조합으로 확인했다.
|
||||||
|
- 전역 inset 처리는 변경하지 않고, `BottomNavigationView`의 item padding을 `0dp`로 줄였다.
|
||||||
|
- 하단 내비게이션 배경은 `@color/black`으로 변경했다.
|
||||||
|
- 아이콘은 `app:itemIconTint="@null"`을 유지해 원본 색상을 사용하도록 했다.
|
||||||
|
- 라벨 색상은 선택 `@color/white`, 미선택 `@color/gray_600` selector로 분리했다.
|
||||||
|
- 라벨 typography는 사용자 확정에 따라 active/inactive 모두 `@style/Typography.Caption3`을 적용했다.
|
||||||
|
- XML LSP는 현재 환경에 설정되어 있지 않아 실행 불가(`No LSP server configured for extension: .xml`)였다.
|
||||||
|
- `./gradlew :app:assembleDebug`는 `BUILD SUCCESSFUL`로 완료됐다.
|
||||||
|
- 2026-05-19
|
||||||
|
- 무엇/왜/어떻게: `BottomNavigationView` 하단 빈 공간과 아이콘의 `#3BB9F1` 색상 표시 원인을 추적하고 MainV2 범위에서 수정했다.
|
||||||
|
- 실행 명령/도구:
|
||||||
|
- `task(category="quick", description="Insets 원인 조사")`
|
||||||
|
- `task(category="quick", description="Icon tint 원인 조사")`
|
||||||
|
- `task(subagent_type="librarian", description="Material inset tint 조사")`
|
||||||
|
- `rg -n "setDecorFitsSystemWindows|setOnApplyWindowInsetsListener|WindowInsets|systemBars|ime\)|navigationBarColor|BottomNavigationView|itemIconTint|color_3bb9f1|3BB9F1|ic_nav_.*tab|ic_tabbar_my_selected|itemPadding|fitsSystemWindows|paddingBottom|state_checked|state_selected" app/src/main/java app/src/main/res`
|
||||||
|
- `read(app/src/main/java/kr/co/vividnext/sodalive/base/BaseActivity.kt)`
|
||||||
|
- `read(app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt)`
|
||||||
|
- `read(app/src/main/res/drawable/ic_nav_home_tab.xml)`
|
||||||
|
- `read(app/src/main/res/drawable/ic_nav_content_tab.xml)`
|
||||||
|
- `read(app/src/main/res/drawable/ic_nav_chat_tab.xml)`
|
||||||
|
- `read(app/src/main/res/drawable/ic_nav_my_tab.xml)`
|
||||||
|
- `rg -n "overrideRootWindowInsets|setOnApplyWindowInsetsListener|setPadding\(left, top, right, 0\)|requestApplyInsets" app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt`
|
||||||
|
- `rg -n "ic_nav_(home|content|chat|my)(_selected)?|ic_tabbar_my_selected|color_3bb9f1|#3BB9F1" app/src/main/res/drawable/ic_nav_home_tab.xml app/src/main/res/drawable/ic_nav_content_tab.xml app/src/main/res/drawable/ic_nav_chat_tab.xml app/src/main/res/drawable/ic_nav_my_tab.xml`
|
||||||
|
- `lsp_diagnostics(app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt)`
|
||||||
|
- `lsp_diagnostics(app/src/main/res/drawable/ic_nav_home_tab.xml)`
|
||||||
|
- `./gradlew :app:assembleDebug`
|
||||||
|
- `./gradlew :app:ktlintCheck`
|
||||||
|
- 결과:
|
||||||
|
- 하단 빈 공간의 원인은 `BaseActivity`가 edge-to-edge 상태에서 루트에 `systemBars.bottom` padding을 적용하는 구조로 확인했다.
|
||||||
|
- `BaseActivity`는 변경하지 않고 `MainV2Activity`에서 루트 inset listener를 재등록해 bottom padding만 `0`으로 재적용했다.
|
||||||
|
- 아이콘의 `#3BB9F1` 표시는 `BottomNavigationView` tint가 아니라 checked 상태 selector가 선택용 아이콘 리소스를 보여주는 구조에서 발생한 것으로 확인했다.
|
||||||
|
- `ic_nav_home_tab`, `ic_nav_content_tab`, `ic_nav_chat_tab`, `ic_nav_my_tab`은 checked/default 모두 원본 아이콘 리소스를 사용하도록 변경했다.
|
||||||
|
- Kotlin/XML LSP는 현재 환경에 설정되어 있지 않아 실행 불가였다.
|
||||||
|
- `./gradlew :app:assembleDebug`는 `BUILD SUCCESSFUL`로 완료됐다.
|
||||||
|
- `./gradlew :app:ktlintCheck`는 `BUILD SUCCESSFUL`로 완료됐다.
|
||||||
158
docs/prd/20260519_메인페이지하단내비게이션신규개발_prd.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# PRD: 메인 페이지 하단 내비게이션 신규 개발
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
메인 페이지를 4개 하단 내비게이션 탭 구조로 정리하고, 탭별 페이지를 표시한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Problem
|
||||||
|
- 현재 메인 화면은 커스텀 하단 탭바와 기존 `MainActivity`에 여러 레거시 흐름이 함께 묶여 있다.
|
||||||
|
- `BottomNavigationView`와 신규 페이지 구조를 기존 `MainActivity`에 덧씌우면 레거시 구조와 신규 구조가 섞여 복잡도가 커질 수 있다.
|
||||||
|
- 신규 메인 페이지는 `홈`, `콘텐츠`, `채팅`, `마이` 4개 탭을 기준으로 동작해야 한다.
|
||||||
|
- `마이` 탭은 기존 마이페이지 기능을 유지해야 하며, 나머지 탭은 아직 상세 기능이 없으므로 빈 페이지로 시작해야 한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
- 메인 페이지 하단 내비게이션을 `홈`, `콘텐츠`, `채팅`, `마이` 4개로 구성한다.
|
||||||
|
- 신규 메인 컨테이너는 기존 `MainActivity`를 수정해 덧씌우지 않고 `kr.co.vividnext.sodalive.v2` 하위 신규 Activity로 개발한다.
|
||||||
|
- 하단 내비게이션은 커스텀 탭바가 아니라 Material `BottomNavigationView`를 사용한다.
|
||||||
|
- 탭 선택 시 해당 탭에 대응하는 Fragment 페이지를 표시한다.
|
||||||
|
- `마이` 탭은 기존 `MyPageFragment`를 그대로 표시한다.
|
||||||
|
- `홈`, `콘텐츠`, `채팅` 탭은 신규 빈 페이지 Fragment를 만들어 표시한다.
|
||||||
|
- 탭 아이콘은 tint와 선택 상태용 파란 아이콘 리소스를 적용하지 않고 원본 아이콘 리소스를 그대로 사용한다.
|
||||||
|
- 하단 내비게이션 아이콘/타이틀 정렬은 가로 가운데, 세로 가운데를 우선 적용하고 타이틀은 아이콘 하단에 위치하도록 한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
- `홈`, `콘텐츠`, `채팅` 탭의 실제 콘텐츠/데이터/API 기능은 구현하지 않는다.
|
||||||
|
- 기존 `MyPageFragment` 내부 UI와 비즈니스 로직은 수정하지 않는다.
|
||||||
|
- 기존 `MainActivity`에 신규 `BottomNavigationView` 구조를 덧씌우지 않는다.
|
||||||
|
- 메인 페이지 외 다른 화면의 탭 구조를 일괄 변경하지 않는다.
|
||||||
|
- 탭별 back stack은 구현하지 않는다.
|
||||||
|
- Navigation Component 도입은 이번 범위에 포함하지 않는다.
|
||||||
|
- 누락된 `ic_nav_my_selected` 원본 리소스를 새로 제작하지 않는다.
|
||||||
|
- 기존 `MainActivity` 전체를 리팩터링하지 않는다. 신규 Activity에 필요한 인증, 이벤트 팝업, 미니 플레이어 등 필수 흐름만 선별 이관한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Target Users
|
||||||
|
- 앱 메인 화면에서 하단 탭으로 주요 섹션을 이동하는 일반 사용자.
|
||||||
|
- 메인 탭 구조를 기준으로 후속 홈/콘텐츠/채팅 기능을 개발할 Android 개발자.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. User Stories
|
||||||
|
- 사용자는 메인 화면 하단에서 `홈`, `콘텐츠`, `채팅`, `마이` 탭을 볼 수 있다.
|
||||||
|
- 사용자는 각 탭을 눌러 해당 페이지로 이동할 수 있다.
|
||||||
|
- 사용자는 `마이` 탭에서 기존 마이페이지 기능을 그대로 사용할 수 있다.
|
||||||
|
- 개발자는 아직 기능이 정해지지 않은 `홈`, `콘텐츠`, `채팅` 탭을 빈 페이지로 구분해 후속 개발의 시작점으로 사용할 수 있다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Core Features
|
||||||
|
|
||||||
|
### Feature A: 4개 하단 내비게이션 탭
|
||||||
|
신규 메인 Activity의 하단 내비게이션을 `홈`, `콘텐츠`, `채팅`, `마이` 순서로 구성한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- 탭 순서는 왼쪽부터 `홈`, `콘텐츠`, `채팅`, `마이`이다.
|
||||||
|
- 하단 내비게이션 UI는 Material `BottomNavigationView`와 menu 리소스로 구성한다.
|
||||||
|
- 탭 라벨은 기존 문자열 리소스가 있으면 재사용하고, 없는 경우 새 문자열 리소스를 추가한다.
|
||||||
|
- 현재 확인된 기존 문자열은 `tab_home`, `tab_chat`, `tab_my`이다.
|
||||||
|
- `콘텐츠` 탭 라벨은 신규 문자열 리소스 추가가 필요하다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 기존 `LIVE` 탭에 연결된 딥링크/진입 흐름은 신규 메인 Activity에서 그대로 탭으로 노출하지 않는다.
|
||||||
|
- 신규 메인 탭 상태는 기존 `MainViewModel.CurrentTab`을 수정하지 않고 v2 전용 탭 모델로 분리한다.
|
||||||
|
|
||||||
|
### Feature B: 탭별 Fragment 표시
|
||||||
|
탭 선택 시 신규 메인 Activity의 Fragment 컨테이너에 대응 Fragment를 표시한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- `마이` 탭은 `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt`를 표시한다.
|
||||||
|
- `홈`, `콘텐츠`, `채팅` 탭은 신규 빈 Fragment를 표시한다.
|
||||||
|
- 신규 `Activity`, `Fragment`, `ViewModel` 및 연결 하위 코드는 저장소 규칙에 따라 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
|
||||||
|
- 빈 페이지는 화면 영역을 차지하되, 별도 데이터 로딩이나 네트워크 호출을 하지 않는다.
|
||||||
|
- 탭별 back stack은 만들지 않는다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 신규 빈 Fragment는 `BaseFragment` 패턴과 ViewBinding 사용 여부를 기존 코드 스타일에 맞춘다.
|
||||||
|
- `MyPageFragment`는 기존 패키지에 있으므로 재사용만 하고 이동하지 않는다.
|
||||||
|
|
||||||
|
### Feature C: 신규 메인 Activity와 기존 필수 프로세스 이관
|
||||||
|
신규 메인 Activity는 기존 `MainActivity`를 덮어쓰지 않고 별도 진입점으로 구성하며, 사용자 경험 유지에 필요한 필수 프로세스만 이관한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- 신규 Activity는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
|
||||||
|
- 기존 인증/로그인 진입, 권한 요청, 이벤트 팝업, 미니 플레이어 표시/해제 흐름은 신규 Activity에서도 현재 사용자 경험과 동일하게 동작해야 한다.
|
||||||
|
- 미니 플레이어는 `BottomNavigationView` 위에 배치해 하단 내비게이션과 겹치지 않도록 한다.
|
||||||
|
- 기존 `MainActivity`의 모든 레거시 메서드를 무조건 복사하지 않고 신규 메인 화면에 필요한 흐름만 이관한다.
|
||||||
|
- 스플래시, 로그인/회원가입 완료, 딥링크, 플레이어 알림 등 기존 `MainActivity`로 이동하던 주요 진입점은 신규 Activity로 전환할지 구현 계획에서 명시적으로 검토한다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 딥링크는 향후 정리 계획이 있으므로 신규 Activity에서 탭 라우팅 진입점을 분리해 확장 가능하게 둔다.
|
||||||
|
- 채팅 배지는 향후 `BottomNavigationView`의 badge API로 확장할 수 있도록 menu item id를 안정적으로 정의한다.
|
||||||
|
|
||||||
|
### Feature D: 탭 아이콘 색상 유지
|
||||||
|
탭 선택/미선택 상태와 무관하게 아이콘 리소스 원본 색상을 그대로 표시한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- `홈`: `ic_nav_home`
|
||||||
|
- `콘텐츠`: `ic_nav_content`
|
||||||
|
- `채팅`: `ic_nav_chat`
|
||||||
|
- `마이`: `ic_nav_my`
|
||||||
|
- `BottomNavigationView`의 `itemIconTint`는 `null`로 두어 아이콘 tint를 적용하지 않는다.
|
||||||
|
- checked 상태에서도 선택용 파란 아이콘 리소스를 사용하지 않는다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 라벨 선택 상태는 텍스트 색상으로만 구분한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. UX / UI Expectations
|
||||||
|
- 하단 내비게이션은 화면 하단에 고정된다.
|
||||||
|
- 각 탭은 동일한 가로 영역을 가진다.
|
||||||
|
- 아이콘과 타이틀은 가로 가운데 정렬한다.
|
||||||
|
- 세로 정렬은 가운데 정렬을 우선으로 하고, 타이틀은 아이콘 하단에 배치한다.
|
||||||
|
- 선택된 탭은 선택 아이콘과 선택 텍스트 스타일로 구분한다.
|
||||||
|
- 미선택 탭은 미선택 아이콘과 미선택 텍스트 스타일로 구분한다.
|
||||||
|
- 하단 내비게이션 배경은 `black`을 사용한다.
|
||||||
|
- 하단 내비게이션 아이콘은 tint로 색을 변경하지 않고 아이콘 리소스 원본 색상을 그대로 사용한다.
|
||||||
|
- checked 상태에서 `#3BB9F1` 계열 선택 아이콘으로 바뀌지 않아야 한다.
|
||||||
|
- 하단 내비게이션 라벨 색상은 미선택 `gray_600`, 선택 `white`를 사용한다.
|
||||||
|
- 하단 내비게이션 라벨 typography는 `Typography.Caption3`을 사용한다.
|
||||||
|
- 하단 내비게이션의 과도한 내부 padding은 `BottomNavigationView` item padding 속성으로 줄인다.
|
||||||
|
- edge-to-edge로 인해 하단 내비게이션 아래에 생기는 system bar inset 빈 공간은 MainV2 화면 범위에서만 제거한다.
|
||||||
|
- 향후 채팅 배지와 Material UX 정리를 고려해 `BottomNavigationView`의 표준 item 상태, ripple, badge 확장성을 우선한다.
|
||||||
|
- 빈 페이지는 기능이 비어 있음을 개발자가 확인할 수 있는 최소 UI로 유지한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Technical Constraints
|
||||||
|
- Android XML 레이아웃과 Kotlin Fragment 기반 구현을 따른다.
|
||||||
|
- 신규 메인 화면은 기존 `MainActivity`, `activity_main.xml`, `item_main_tab.xml`에 덧씌우지 않는다.
|
||||||
|
- 신규 Activity, ViewModel, Fragment 코드는 `kr.co.vividnext.sodalive.v2` 하위에 둔다.
|
||||||
|
- 하단 내비게이션은 Material `BottomNavigationView`를 사용한다.
|
||||||
|
- 탭 전환은 탭별 back stack 없이 단일 Fragment 컨테이너에서 처리한다.
|
||||||
|
- 기존 `MyPageFragment`는 `kr.co.vividnext.sodalive.mypage` 패키지의 기존 파일을 재사용한다.
|
||||||
|
- 이번 문서 작성 단계에서는 구현 파일을 변경하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Metrics
|
||||||
|
- 하단 내비게이션 탭이 `홈`, `콘텐츠`, `채팅`, `마이` 4개로 표시된다.
|
||||||
|
- 신규 메인 화면이 `kr.co.vividnext.sodalive.v2` 하위 신규 Activity로 구성된다.
|
||||||
|
- 하단 내비게이션이 `BottomNavigationView`로 구성된다.
|
||||||
|
- 각 탭 선택 시 대응 Fragment가 표시된다.
|
||||||
|
- `마이` 탭 선택 시 기존 `MyPageFragment`가 표시된다.
|
||||||
|
- `홈`, `콘텐츠`, `채팅` 탭 선택 시 각각 신규 빈 페이지가 표시된다.
|
||||||
|
- 인증, 이벤트 팝업, 미니 플레이어 흐름이 신규 Activity에서도 기존 사용자 경험과 동일하게 동작한다.
|
||||||
|
- 요청된 선택/미선택 아이콘 리소스가 탭 상태에 맞게 연결된다.
|
||||||
|
- 디버그 빌드가 성공한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Open Questions
|
||||||
|
- 없음. 현재 구현 범위는 요청된 4탭 구성과 빈 페이지 표시로 한정한다.
|
||||||