feat(pushnotification): 홈 알림 리스트 화면과 딥링크 라우팅을 추가한다
This commit is contained in:
@@ -167,6 +167,7 @@
|
|||||||
<activity android:name=".mypage.profile.nickname.NicknameUpdateActivity" />
|
<activity android:name=".mypage.profile.nickname.NicknameUpdateActivity" />
|
||||||
<activity android:name=".mypage.profile.password.ModifyPasswordActivity" />
|
<activity android:name=".mypage.profile.password.ModifyPasswordActivity" />
|
||||||
<activity android:name=".audio_content.all.AudioContentNewAllActivity" />
|
<activity android:name=".audio_content.all.AudioContentNewAllActivity" />
|
||||||
|
<activity android:name=".home.pushnotification.PushNotificationListActivity" />
|
||||||
<activity android:name=".audio_content.all.AudioContentRankingAllActivity" />
|
<activity android:name=".audio_content.all.AudioContentRankingAllActivity" />
|
||||||
<activity android:name=".audio_content.all.by_theme.AudioContentAllByThemeActivity" />
|
<activity android:name=".audio_content.all.by_theme.AudioContentAllByThemeActivity" />
|
||||||
<activity android:name=".live.roulette.config.RouletteConfigActivity" />
|
<activity android:name=".live.roulette.config.RouletteConfigActivity" />
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import kr.co.vividnext.sodalive.following.FollowingCreatorViewModel
|
|||||||
import kr.co.vividnext.sodalive.home.HomeApi
|
import kr.co.vividnext.sodalive.home.HomeApi
|
||||||
import kr.co.vividnext.sodalive.home.HomeRepository
|
import kr.co.vividnext.sodalive.home.HomeRepository
|
||||||
import kr.co.vividnext.sodalive.home.HomeViewModel
|
import kr.co.vividnext.sodalive.home.HomeViewModel
|
||||||
|
import kr.co.vividnext.sodalive.home.pushnotification.PushNotificationListViewModel
|
||||||
import kr.co.vividnext.sodalive.live.LiveApi
|
import kr.co.vividnext.sodalive.live.LiveApi
|
||||||
import kr.co.vividnext.sodalive.live.LiveRepository
|
import kr.co.vividnext.sodalive.live.LiveRepository
|
||||||
import kr.co.vividnext.sodalive.live.LiveViewModel
|
import kr.co.vividnext.sodalive.live.LiveViewModel
|
||||||
@@ -376,6 +377,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
|||||||
viewModel { SearchViewModel(get()) }
|
viewModel { SearchViewModel(get()) }
|
||||||
viewModel { PointStatusViewModel(get()) }
|
viewModel { PointStatusViewModel(get()) }
|
||||||
viewModel { HomeViewModel(get(), get()) }
|
viewModel { HomeViewModel(get(), get()) }
|
||||||
|
viewModel { PushNotificationListViewModel(get()) }
|
||||||
viewModel { CharacterTabViewModel(get()) }
|
viewModel { CharacterTabViewModel(get()) }
|
||||||
viewModel { CharacterDetailViewModel(get()) }
|
viewModel { CharacterDetailViewModel(get()) }
|
||||||
viewModel { CharacterGalleryViewModel(get()) }
|
viewModel { CharacterGalleryViewModel(get()) }
|
||||||
@@ -435,7 +437,6 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
|||||||
factory { SeriesMainRepository(get()) }
|
factory { SeriesMainRepository(get()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val moduleList = listOf(
|
private val moduleList = listOf(
|
||||||
networkModule,
|
networkModule,
|
||||||
viewModelModule,
|
viewModelModule,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.app.NotificationManager
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.media.RingtoneManager
|
import android.media.RingtoneManager
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
@@ -66,6 +67,14 @@ class SodaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
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 deepLinkUrl = messageData["deepLink"] ?: messageData["deep_link"]
|
||||||
|
if (!deepLinkUrl.isNullOrBlank()) {
|
||||||
|
runCatching {
|
||||||
|
intent.action = Intent.ACTION_VIEW
|
||||||
|
intent.data = Uri.parse(deepLinkUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val deepLinkExtras = android.os.Bundle().apply {
|
val deepLinkExtras = android.os.Bundle().apply {
|
||||||
messageData["room_id"]?.let { putString("room_id", it) }
|
messageData["room_id"]?.let { putString("room_id", it) }
|
||||||
messageData["message_id"]?.let { putString("message_id", it) }
|
messageData["message_id"]?.let { putString("message_id", it) }
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ class HomeContentThemeAdapter(
|
|||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun setSelectedTheme(theme: String) {
|
||||||
|
selectedTheme = theme
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||||
ItemHomeContentThemeBinding.inflate(
|
ItemHomeContentThemeBinding.inflate(
|
||||||
LayoutInflater.from(parent.context),
|
LayoutInflater.from(parent.context),
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
|
|||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentHomeBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentHomeBinding
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||||
|
import kr.co.vividnext.sodalive.home.pushnotification.PushNotificationListActivity
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import kr.co.vividnext.sodalive.live.LiveViewModel
|
import kr.co.vividnext.sodalive.live.LiveViewModel
|
||||||
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
|
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
|
||||||
@@ -154,6 +155,15 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.ivPushNotification.setOnClickListener {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
requireContext(),
|
||||||
|
PushNotificationListActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.llShortIcon.visibility = View.GONE
|
binding.llShortIcon.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package kr.co.vividnext.sodalive.home.pushnotification
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
data class GetPushNotificationCategoryResponse(
|
||||||
|
@SerializedName("categories") val categories: List<String>
|
||||||
|
)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package kr.co.vividnext.sodalive.home.pushnotification
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
data class GetPushNotificationListResponse(
|
||||||
|
@SerializedName("totalCount") val totalCount: Long,
|
||||||
|
@SerializedName("items") val items: List<PushNotificationListItem>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
data class PushNotificationListItem(
|
||||||
|
@SerializedName("id") val id: Long,
|
||||||
|
@SerializedName("senderNickname") val senderNickname: String,
|
||||||
|
@SerializedName("senderProfileImage") val senderProfileImage: String?,
|
||||||
|
@SerializedName("message") val message: String,
|
||||||
|
@SerializedName("category") val category: String,
|
||||||
|
@SerializedName("deepLink") val deepLink: String?,
|
||||||
|
@SerializedName("sentAt") val sentAt: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package kr.co.vividnext.sodalive.home.pushnotification
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
|
import kr.co.vividnext.sodalive.databinding.ActivityPushNotificationListBinding
|
||||||
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
|
import kr.co.vividnext.sodalive.home.HomeContentThemeAdapter
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
|
class PushNotificationListActivity : BaseActivity<ActivityPushNotificationListBinding>(
|
||||||
|
ActivityPushNotificationListBinding::inflate
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val viewModel: PushNotificationListViewModel by inject()
|
||||||
|
|
||||||
|
private lateinit var loadingDialog: LoadingDialog
|
||||||
|
private lateinit var categoryAdapter: HomeContentThemeAdapter
|
||||||
|
private lateinit var notificationAdapter: PushNotificationListAdapter
|
||||||
|
private var isInitialCategorySelected = false
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
bindData()
|
||||||
|
viewModel.getPushNotificationCategories()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupView() {
|
||||||
|
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||||
|
binding.tvBack.text = getString(R.string.screen_push_notification_title)
|
||||||
|
binding.tvBack.setOnClickListener { finish() }
|
||||||
|
|
||||||
|
setupCategoryList()
|
||||||
|
setupNotificationList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupCategoryList() {
|
||||||
|
categoryAdapter = HomeContentThemeAdapter("") { selectedCategory ->
|
||||||
|
viewModel.selectCategory(selectedCategory)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.rvCategory.layoutManager = LinearLayoutManager(
|
||||||
|
this,
|
||||||
|
LinearLayoutManager.HORIZONTAL,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.rvCategory.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||||
|
override fun getItemOffsets(
|
||||||
|
outRect: Rect,
|
||||||
|
view: View,
|
||||||
|
parent: RecyclerView,
|
||||||
|
state: RecyclerView.State
|
||||||
|
) {
|
||||||
|
super.getItemOffsets(outRect, view, parent, state)
|
||||||
|
|
||||||
|
when (parent.getChildAdapterPosition(view)) {
|
||||||
|
0 -> {
|
||||||
|
outRect.left = 0
|
||||||
|
outRect.right = 4f.dpToPx().toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryAdapter.itemCount - 1 -> {
|
||||||
|
outRect.left = 4f.dpToPx().toInt()
|
||||||
|
outRect.right = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
outRect.left = 4f.dpToPx().toInt()
|
||||||
|
outRect.right = 4f.dpToPx().toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.rvCategory.adapter = categoryAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupNotificationList() {
|
||||||
|
notificationAdapter = PushNotificationListAdapter(
|
||||||
|
onClickItem = { openDeepLink(it.deepLink) }
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.rvNotification.layoutManager = LinearLayoutManager(
|
||||||
|
this,
|
||||||
|
LinearLayoutManager.VERTICAL,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.rvNotification.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
|
||||||
|
if (dy <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recyclerView.canScrollVertically(1)) {
|
||||||
|
viewModel.getPushNotificationList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.rvNotification.adapter = notificationAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindData() {
|
||||||
|
viewModel.toastLiveData.observe(this) {
|
||||||
|
it?.let { message ->
|
||||||
|
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.isLoading.observe(this) {
|
||||||
|
if (it) {
|
||||||
|
loadingDialog.show(screenWidth)
|
||||||
|
} else {
|
||||||
|
loadingDialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.categoryListLiveData.observe(this) { categories ->
|
||||||
|
categoryAdapter.addItems(categories)
|
||||||
|
|
||||||
|
if (!isInitialCategorySelected && categories.isNotEmpty()) {
|
||||||
|
val initialCategory = categories.first()
|
||||||
|
categoryAdapter.setSelectedTheme(initialCategory)
|
||||||
|
viewModel.selectCategory(initialCategory)
|
||||||
|
isInitialCategorySelected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.notificationListLiveData.observe(this) { items ->
|
||||||
|
notificationAdapter.submitItems(items)
|
||||||
|
binding.llEmpty.visibility = if (items.isEmpty()) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openDeepLink(deepLink: String?) {
|
||||||
|
val deepLinkUrl = deepLink?.takeIf { it.isNotBlank() } ?: return
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(deepLinkUrl)))
|
||||||
|
}.onFailure {
|
||||||
|
Toast.makeText(applicationContext, getString(R.string.common_error_unknown), Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package kr.co.vividnext.sodalive.home.pushnotification
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import coil.load
|
||||||
|
import coil.transform.CircleCropTransformation
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.databinding.ItemPushNotificationListBinding
|
||||||
|
|
||||||
|
class PushNotificationListAdapter(
|
||||||
|
private val onClickItem: (PushNotificationListItem) -> Unit
|
||||||
|
) : RecyclerView.Adapter<PushNotificationListAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private val items = mutableListOf<PushNotificationListItem>()
|
||||||
|
|
||||||
|
inner class ViewHolder(
|
||||||
|
private val binding: ItemPushNotificationListBinding
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
fun bind(item: PushNotificationListItem) {
|
||||||
|
binding.ivProfile.load(item.senderProfileImage) {
|
||||||
|
transformations(CircleCropTransformation())
|
||||||
|
placeholder(R.drawable.ic_place_holder)
|
||||||
|
error(R.drawable.ic_place_holder)
|
||||||
|
crossfade(true)
|
||||||
|
}
|
||||||
|
binding.tvNickname.text = item.senderNickname
|
||||||
|
binding.tvTimeAgo.text = item.relativeTimeText(binding.root.context)
|
||||||
|
binding.tvMessage.text = item.message
|
||||||
|
binding.root.setOnClickListener { onClickItem(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun submitItems(items: List<PushNotificationListItem>) {
|
||||||
|
this.items.clear()
|
||||||
|
this.items.addAll(items)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
return ViewHolder(
|
||||||
|
ItemPushNotificationListBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package kr.co.vividnext.sodalive.home.pushnotification
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.orhanobut.logger.Logger
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.user.UserRepository
|
||||||
|
|
||||||
|
class PushNotificationListViewModel(
|
||||||
|
private val userRepository: UserRepository
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val _toastLiveData = MutableLiveData<String?>()
|
||||||
|
val toastLiveData: LiveData<String?>
|
||||||
|
get() = _toastLiveData
|
||||||
|
|
||||||
|
private val _isLoading = MutableLiveData(false)
|
||||||
|
val isLoading: LiveData<Boolean>
|
||||||
|
get() = _isLoading
|
||||||
|
|
||||||
|
private val _categoryListLiveData = MutableLiveData<List<String>>()
|
||||||
|
val categoryListLiveData: LiveData<List<String>>
|
||||||
|
get() = _categoryListLiveData
|
||||||
|
|
||||||
|
private val _notificationListLiveData = MutableLiveData<List<PushNotificationListItem>>()
|
||||||
|
val notificationListLiveData: LiveData<List<PushNotificationListItem>>
|
||||||
|
get() = _notificationListLiveData
|
||||||
|
|
||||||
|
private var page = 1
|
||||||
|
private val size = 20
|
||||||
|
private var totalCount = 0L
|
||||||
|
private var selectedCategory: String? = null
|
||||||
|
private var isLastPage = false
|
||||||
|
private val loadedItems = mutableListOf<PushNotificationListItem>()
|
||||||
|
|
||||||
|
fun getPushNotificationCategories() {
|
||||||
|
val unknownError = kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||||
|
.get()
|
||||||
|
.getString(R.string.common_error_unknown)
|
||||||
|
|
||||||
|
compositeDisposable.add(
|
||||||
|
userRepository.getPushNotificationCategories(token = "Bearer ${SharedPreferenceManager.token}")
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
if (it.success && it.data != null) {
|
||||||
|
_categoryListLiveData.value = it.data.categories
|
||||||
|
} else {
|
||||||
|
_toastLiveData.value = it.message ?: unknownError
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
it.message?.let { message -> Logger.e(message) }
|
||||||
|
_toastLiveData.value = unknownError
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectCategory(category: String?) {
|
||||||
|
val normalizedCategory = category?.takeIf { it.isNotBlank() }
|
||||||
|
if (selectedCategory == normalizedCategory && loadedItems.isNotEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedCategory = normalizedCategory
|
||||||
|
resetPaging()
|
||||||
|
getPushNotificationList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPushNotificationList() {
|
||||||
|
if (isLastPage || _isLoading.value == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading.value = true
|
||||||
|
val unknownError = kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||||
|
.get()
|
||||||
|
.getString(R.string.common_error_unknown)
|
||||||
|
|
||||||
|
compositeDisposable.add(
|
||||||
|
userRepository.getPushNotificationList(
|
||||||
|
page = page,
|
||||||
|
size = size,
|
||||||
|
category = selectedCategory,
|
||||||
|
token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
_isLoading.value = false
|
||||||
|
|
||||||
|
if (it.success && it.data != null) {
|
||||||
|
totalCount = it.data.totalCount
|
||||||
|
loadedItems.addAll(it.data.items)
|
||||||
|
_notificationListLiveData.value = loadedItems.toList()
|
||||||
|
|
||||||
|
isLastPage =
|
||||||
|
it.data.items.isEmpty() ||
|
||||||
|
totalCount == 0L ||
|
||||||
|
loadedItems.size.toLong() >= totalCount
|
||||||
|
|
||||||
|
if (!isLastPage) {
|
||||||
|
page += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_toastLiveData.value = it.message ?: unknownError
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_isLoading.value = false
|
||||||
|
it.message?.let { message -> Logger.e(message) }
|
||||||
|
_toastLiveData.value = unknownError
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetPaging() {
|
||||||
|
page = 1
|
||||||
|
totalCount = 0L
|
||||||
|
isLastPage = false
|
||||||
|
loadedItems.clear()
|
||||||
|
_notificationListLiveData.value = emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package kr.co.vividnext.sodalive.home.pushnotification
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
|
||||||
|
fun PushNotificationListItem.relativeTimeText(context: Context): String {
|
||||||
|
val pastMillis = parsePushNotificationUtcToMillis(sentAt)
|
||||||
|
?: return context.getString(R.string.character_comment_time_just_now)
|
||||||
|
|
||||||
|
val diff = (System.currentTimeMillis() - pastMillis).coerceAtLeast(0L)
|
||||||
|
val minute = 60_000L
|
||||||
|
val hour = 60 * minute
|
||||||
|
val day = 24 * hour
|
||||||
|
|
||||||
|
return when {
|
||||||
|
diff < minute -> context.getString(R.string.character_comment_time_just_now)
|
||||||
|
diff < hour -> context.getString(R.string.character_comment_time_minutes, (diff / minute).toInt())
|
||||||
|
diff < day -> context.getString(R.string.character_comment_time_hours, (diff / hour).toInt())
|
||||||
|
else -> context.getString(R.string.character_comment_time_days, (diff / day).toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parsePushNotificationUtcToMillis(sentAt: String?): Long? {
|
||||||
|
if (sentAt.isNullOrBlank()) return null
|
||||||
|
|
||||||
|
val value = sentAt.trim()
|
||||||
|
if (value.all { it.isDigit() }) {
|
||||||
|
return try {
|
||||||
|
val epoch = value.toLong()
|
||||||
|
if (value.length <= 10) epoch * 1000 else epoch
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val patterns = listOf(
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss.SSSX",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ssX",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss'Z'",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss.SSS",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss",
|
||||||
|
"yyyy-MM-dd HH:mm:ss",
|
||||||
|
"yyyy/MM/dd HH:mm:ss"
|
||||||
|
)
|
||||||
|
|
||||||
|
for (pattern in patterns) {
|
||||||
|
try {
|
||||||
|
val dateFormat = SimpleDateFormat(pattern, Locale.US)
|
||||||
|
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
val parsed: Date? = dateFormat.parse(value)
|
||||||
|
if (parsed != null) {
|
||||||
|
return parsed.time
|
||||||
|
}
|
||||||
|
} catch (_: ParseException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.common.Constants
|
|||||||
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
|
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
|
||||||
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 java.util.Locale
|
||||||
|
|
||||||
class DeepLinkActivity : AppCompatActivity() {
|
class DeepLinkActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -69,6 +70,12 @@ class DeepLinkActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val data = intent.data
|
val data = intent.data
|
||||||
|
|
||||||
|
fun putIfAbsent(key: String, value: String?) {
|
||||||
|
if (!value.isNullOrBlank() && !extras.containsKey(key)) {
|
||||||
|
extras.putString(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
fun putQuery(key: String) {
|
fun putQuery(key: String) {
|
||||||
val value = data.getQueryParameter(key)
|
val value = data.getQueryParameter(key)
|
||||||
@@ -135,6 +142,10 @@ class DeepLinkActivity : AppCompatActivity() {
|
|||||||
extras.putString("content_id", it.toString())
|
extras.putString("content_id", it.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data != null) {
|
||||||
|
applyPathDeepLink(data = data, putIfAbsent = ::putIfAbsent)
|
||||||
|
}
|
||||||
|
|
||||||
val deepLinkValue = extras.getString("deep_link_value")
|
val deepLinkValue = extras.getString("deep_link_value")
|
||||||
val deepLinkValueId = extras.getString("deep_link_sub5")
|
val deepLinkValueId = extras.getString("deep_link_sub5")
|
||||||
|
|
||||||
@@ -156,6 +167,14 @@ class DeepLinkActivity : AppCompatActivity() {
|
|||||||
extras.putString("audition_id", deepLinkValueId)
|
extras.putString("audition_id", deepLinkValueId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"community" -> if (!extras.containsKey(Constants.EXTRA_COMMUNITY_CREATOR_ID)) {
|
||||||
|
extras.putString(Constants.EXTRA_COMMUNITY_CREATOR_ID, deepLinkValueId)
|
||||||
|
}
|
||||||
|
|
||||||
|
"message" -> if (!extras.containsKey("message_id")) {
|
||||||
|
extras.putString("message_id", deepLinkValueId)
|
||||||
|
}
|
||||||
|
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,4 +185,62 @@ class DeepLinkActivity : AppCompatActivity() {
|
|||||||
extras
|
extras
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun applyPathDeepLink(
|
||||||
|
data: 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) {
|
||||||
|
"live" -> {
|
||||||
|
putIfAbsent("room_id", pathId)
|
||||||
|
putIfAbsent("deep_link_value", "live")
|
||||||
|
putIfAbsent("deep_link_sub5", pathId)
|
||||||
|
}
|
||||||
|
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ 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.AudioContentPlayerFragment
|
||||||
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
|
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
|
||||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
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.base.BaseActivity
|
||||||
import kr.co.vividnext.sodalive.chat.ChatFragment
|
import kr.co.vividnext.sodalive.chat.ChatFragment
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
@@ -45,6 +46,7 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
|||||||
import kr.co.vividnext.sodalive.databinding.ActivityMainBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityMainBinding
|
||||||
import kr.co.vividnext.sodalive.databinding.ItemMainTabBinding
|
import kr.co.vividnext.sodalive.databinding.ItemMainTabBinding
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import kr.co.vividnext.sodalive.home.HomeFragment
|
import kr.co.vividnext.sodalive.home.HomeFragment
|
||||||
import kr.co.vividnext.sodalive.live.LiveFragment
|
import kr.co.vividnext.sodalive.live.LiveFragment
|
||||||
@@ -318,6 +320,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
|||||||
?: bundle.getLong(Constants.EXTRA_MESSAGE_ID).takeIf { it > 0 }
|
?: bundle.getLong(Constants.EXTRA_MESSAGE_ID).takeIf { it > 0 }
|
||||||
val contentId = bundle.getString("content_id")?.toLongOrNull()
|
val contentId = bundle.getString("content_id")?.toLongOrNull()
|
||||||
?: bundle.getLong(Constants.EXTRA_AUDIO_CONTENT_ID).takeIf { it > 0 }
|
?: 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 isLiveReservation = bundle.getBoolean(Constants.EXTRA_LIVE_RESERVATION_RESPONSE)
|
val isLiveReservation = bundle.getBoolean(Constants.EXTRA_LIVE_RESERVATION_RESPONSE)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
@@ -355,12 +361,24 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
|||||||
startActivity(Intent(applicationContext, MessageActivity::class.java))
|
startActivity(Intent(applicationContext, MessageActivity::class.java))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
communityCreatorId != null && communityCreatorId > 0 -> {
|
||||||
|
val nextIntent = Intent(applicationContext, CreatorCommunityAllActivity::class.java)
|
||||||
|
nextIntent.putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, communityCreatorId)
|
||||||
|
startActivity(nextIntent)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
auditionId != null && auditionId > 0 -> {
|
||||||
|
startActivity(Intent(applicationContext, AuditionActivity::class.java))
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val deepLinkValue = bundle.getString("deep_link_value")
|
val deepLinkValue = bundle.getString("deep_link_value")
|
||||||
val deepLinkValueId = bundle.getString("deep_link_sub5")?.toLongOrNull()
|
val deepLinkValueId = bundle.getString("deep_link_sub5")?.toLongOrNull()
|
||||||
|
|
||||||
if (!deepLinkValue.isNullOrBlank() && deepLinkValueId != null && deepLinkValueId > 0) {
|
if (!deepLinkValue.isNullOrBlank()) {
|
||||||
return routeByDeepLinkValue(deepLinkValue = deepLinkValue, deepLinkValueId = deepLinkValueId)
|
return routeByDeepLinkValue(deepLinkValue = deepLinkValue, deepLinkValueId = deepLinkValueId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,16 +389,23 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
|||||||
val deepLinkValue = SharedPreferenceManager.marketingLinkValue
|
val deepLinkValue = SharedPreferenceManager.marketingLinkValue
|
||||||
val deepLinkValueId = SharedPreferenceManager.marketingLinkValueId
|
val deepLinkValueId = SharedPreferenceManager.marketingLinkValueId
|
||||||
|
|
||||||
if (deepLinkValue.isNotBlank() && deepLinkValueId > 0) {
|
if (deepLinkValue.isNotBlank()) {
|
||||||
routeByDeepLinkValue(deepLinkValue = deepLinkValue, deepLinkValueId = deepLinkValueId)
|
routeByDeepLinkValue(
|
||||||
|
deepLinkValue = deepLinkValue,
|
||||||
|
deepLinkValueId = deepLinkValueId.takeIf { it > 0 }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
clearDeferredDeepLink()
|
clearDeferredDeepLink()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun routeByDeepLinkValue(deepLinkValue: String, deepLinkValueId: Long): Boolean {
|
private fun routeByDeepLinkValue(deepLinkValue: String, deepLinkValueId: Long?): Boolean {
|
||||||
return when (deepLinkValue.lowercase(Locale.ROOT)) {
|
return when (deepLinkValue.lowercase(Locale.ROOT)) {
|
||||||
"series" -> {
|
"series" -> {
|
||||||
|
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(applicationContext, SeriesDetailActivity::class.java).apply {
|
Intent(applicationContext, SeriesDetailActivity::class.java).apply {
|
||||||
putExtra(Constants.EXTRA_SERIES_ID, deepLinkValueId)
|
putExtra(Constants.EXTRA_SERIES_ID, deepLinkValueId)
|
||||||
@@ -390,6 +415,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
|||||||
}
|
}
|
||||||
|
|
||||||
"content" -> {
|
"content" -> {
|
||||||
|
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(
|
Intent(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
@@ -402,6 +431,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
|||||||
}
|
}
|
||||||
|
|
||||||
"channel" -> {
|
"channel" -> {
|
||||||
|
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(applicationContext, UserProfileActivity::class.java).apply {
|
Intent(applicationContext, UserProfileActivity::class.java).apply {
|
||||||
putExtra(Constants.EXTRA_USER_ID, deepLinkValueId)
|
putExtra(Constants.EXTRA_USER_ID, deepLinkValueId)
|
||||||
@@ -411,6 +444,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
|||||||
}
|
}
|
||||||
|
|
||||||
"live" -> {
|
"live" -> {
|
||||||
|
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.clickTab(MainViewModel.CurrentTab.LIVE)
|
viewModel.clickTab(MainViewModel.CurrentTab.LIVE)
|
||||||
|
|
||||||
handler.postDelayed({
|
handler.postDelayed({
|
||||||
@@ -419,6 +456,29 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
|||||||
true
|
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
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.user
|
|||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.home.pushnotification.GetPushNotificationCategoryResponse
|
||||||
|
import kr.co.vividnext.sodalive.home.pushnotification.GetPushNotificationListResponse
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
|
import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
|
||||||
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
|
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
|
||||||
import kr.co.vividnext.sodalive.main.MarketingInfoUpdateRequest
|
import kr.co.vividnext.sodalive.main.MarketingInfoUpdateRequest
|
||||||
@@ -47,6 +49,19 @@ interface UserApi {
|
|||||||
@Header("Authorization") authHeader: String
|
@Header("Authorization") authHeader: String
|
||||||
): Single<ApiResponse<GetMemberInfoResponse>>
|
): Single<ApiResponse<GetMemberInfoResponse>>
|
||||||
|
|
||||||
|
@GET("/push/notification/categories")
|
||||||
|
fun getPushNotificationCategories(
|
||||||
|
@Header("Authorization") authHeader: String
|
||||||
|
): Single<ApiResponse<GetPushNotificationCategoryResponse>>
|
||||||
|
|
||||||
|
@GET("/push/notification/list")
|
||||||
|
fun getPushNotificationList(
|
||||||
|
@Query("page") page: Int,
|
||||||
|
@Query("size") size: Int,
|
||||||
|
@Query("category") category: String?,
|
||||||
|
@Header("Authorization") authHeader: String
|
||||||
|
): Single<ApiResponse<GetPushNotificationListResponse>>
|
||||||
|
|
||||||
@POST("/member/notification")
|
@POST("/member/notification")
|
||||||
fun updateNotificationSettings(
|
fun updateNotificationSettings(
|
||||||
@Body request: UpdateNotificationSettingRequest,
|
@Body request: UpdateNotificationSettingRequest,
|
||||||
@@ -180,5 +195,4 @@ interface UserApi {
|
|||||||
fun loginLine(
|
fun loginLine(
|
||||||
@Body request: SocialLoginRequest
|
@Body request: SocialLoginRequest
|
||||||
): Single<ApiResponse<LoginResponse>>
|
): Single<ApiResponse<LoginResponse>>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.user
|
|||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.home.pushnotification.GetPushNotificationCategoryResponse
|
||||||
|
import kr.co.vividnext.sodalive.home.pushnotification.GetPushNotificationListResponse
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
|
import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
|
||||||
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
|
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
|
||||||
import kr.co.vividnext.sodalive.main.MarketingInfoUpdateRequest
|
import kr.co.vividnext.sodalive.main.MarketingInfoUpdateRequest
|
||||||
@@ -36,6 +38,24 @@ class UserRepository(private val userApi: UserApi) {
|
|||||||
|
|
||||||
fun getMemberInfo(token: String) = userApi.getMemberInfo(authHeader = token)
|
fun getMemberInfo(token: String) = userApi.getMemberInfo(authHeader = token)
|
||||||
|
|
||||||
|
fun getPushNotificationCategories(token: String): Single<ApiResponse<GetPushNotificationCategoryResponse>> {
|
||||||
|
return userApi.getPushNotificationCategories(authHeader = token)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPushNotificationList(
|
||||||
|
page: Int,
|
||||||
|
size: Int,
|
||||||
|
category: String?,
|
||||||
|
token: String
|
||||||
|
): Single<ApiResponse<GetPushNotificationListResponse>> {
|
||||||
|
return userApi.getPushNotificationList(
|
||||||
|
page = page,
|
||||||
|
size = size,
|
||||||
|
category = category,
|
||||||
|
authHeader = token
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun getMyPage(token: String): Single<ApiResponse<MyPageResponse>> {
|
fun getMyPage(token: String): Single<ApiResponse<MyPageResponse>> {
|
||||||
return userApi.getMyPage(authHeader = token)
|
return userApi.getMyPage(authHeader = token)
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/src/main/res/drawable-xxhdpi/ic_bell.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_bell.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_bell_settings.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_bell_settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
92
app/src/main/res/layout/activity_push_notification_list.xml
Normal file
92
app/src/main/res/layout/activity_push_notification_list.xml
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout 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"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="51.7dp"
|
||||||
|
android:background="@color/color_131313"
|
||||||
|
android:paddingHorizontal="13.3dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_back"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:drawablePadding="6.7dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:fontFamily="@font/bold"
|
||||||
|
android:gravity="center"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:textColor="@color/color_eeeeee"
|
||||||
|
android:textSize="18.3sp"
|
||||||
|
app:drawableStartCompat="@drawable/ic_back"
|
||||||
|
tools:ignore="RelativeOverlap"
|
||||||
|
tools:text="@string/screen_push_notification_title" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_settings"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_bell_settings" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rv_category"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingHorizontal="13.3dp" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rv_notification"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="13.3dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/ll_empty"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_no_item" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:text="@string/screen_push_notification_empty"
|
||||||
|
android:textColor="@color/color_bbbbbb"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
android:id="@+id/iv_charge"
|
android:id="@+id/iv_charge"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginStart="24dp"
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
android:src="@drawable/ic_can" />
|
android:src="@drawable/ic_can" />
|
||||||
|
|
||||||
@@ -47,8 +47,17 @@
|
|||||||
android:id="@+id/iv_storage"
|
android:id="@+id/iv_storage"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
android:src="@drawable/ic_storage" />
|
android:src="@drawable/ic_storage" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_push_notification"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_bell" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
|||||||
84
app/src/main/res/layout/item_push_notification_list.xml
Normal file
84
app/src/main/res/layout/item_push_notification_list.xml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="13.3dp"
|
||||||
|
android:paddingVertical="13.3dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_profile"
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
tools:src="@drawable/ic_place_holder" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="13.3dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_nickname"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@color/color_bbbbbb"
|
||||||
|
android:textSize="13.3sp"
|
||||||
|
tools:text="란월" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_time_ago"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@color/color_909090"
|
||||||
|
android:textSize="10sp"
|
||||||
|
tools:text="・10분 전" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_message"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:lineSpacingExtra="2dp"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="@color/color_eeeeee"
|
||||||
|
android:textSize="13.3sp"
|
||||||
|
tools:text="라이브를 시작했습니다 ‘모여라 라이브 제목 1234567890’" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginHorizontal="13.3dp"
|
||||||
|
android:background="@color/color_555555" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -172,6 +172,8 @@
|
|||||||
<string name="dialog_unfollow_title">Unfollow</string>
|
<string name="dialog_unfollow_title">Unfollow</string>
|
||||||
<string name="dialog_unfollow_message">Are you sure you want to unfollow %1$s?</string>
|
<string name="dialog_unfollow_message">Are you sure you want to unfollow %1$s?</string>
|
||||||
<string name="screen_home_theme_all">All</string>
|
<string name="screen_home_theme_all">All</string>
|
||||||
|
<string name="screen_push_notification_title">Notifications</string>
|
||||||
|
<string name="screen_push_notification_empty">No notifications yet.</string>
|
||||||
<string name="screen_home_sort_revenue">Revenue</string>
|
<string name="screen_home_sort_revenue">Revenue</string>
|
||||||
<string name="screen_home_sort_sales_count">Units</string>
|
<string name="screen_home_sort_sales_count">Units</string>
|
||||||
<string name="screen_home_sort_comment_count">Comments</string>
|
<string name="screen_home_sort_comment_count">Comments</string>
|
||||||
|
|||||||
@@ -172,6 +172,8 @@
|
|||||||
<string name="dialog_unfollow_title">フォロー解除</string>
|
<string name="dialog_unfollow_title">フォロー解除</string>
|
||||||
<string name="dialog_unfollow_message">%1$sさんのフォロー를解除しますか?</string>
|
<string name="dialog_unfollow_message">%1$sさんのフォロー를解除しますか?</string>
|
||||||
<string name="screen_home_theme_all">全</string>
|
<string name="screen_home_theme_all">全</string>
|
||||||
|
<string name="screen_push_notification_title">通知</string>
|
||||||
|
<string name="screen_push_notification_empty">通知はありません。</string>
|
||||||
<string name="screen_home_sort_revenue">売上</string>
|
<string name="screen_home_sort_revenue">売上</string>
|
||||||
<string name="screen_home_sort_sales_count">販売数</string>
|
<string name="screen_home_sort_sales_count">販売数</string>
|
||||||
<string name="screen_home_sort_comment_count">コメント</string>
|
<string name="screen_home_sort_comment_count">コメント</string>
|
||||||
|
|||||||
@@ -171,6 +171,8 @@
|
|||||||
<string name="dialog_unfollow_title">팔로우 해제</string>
|
<string name="dialog_unfollow_title">팔로우 해제</string>
|
||||||
<string name="dialog_unfollow_message">%1$s님을 팔로우 해제 하시겠습니까?</string>
|
<string name="dialog_unfollow_message">%1$s님을 팔로우 해제 하시겠습니까?</string>
|
||||||
<string name="screen_home_theme_all">전체</string>
|
<string name="screen_home_theme_all">전체</string>
|
||||||
|
<string name="screen_push_notification_title">알림</string>
|
||||||
|
<string name="screen_push_notification_empty">받은 알림이 없습니다.</string>
|
||||||
<string name="screen_home_sort_revenue">매출</string>
|
<string name="screen_home_sort_revenue">매출</string>
|
||||||
<string name="screen_home_sort_sales_count">판매량</string>
|
<string name="screen_home_sort_sales_count">판매량</string>
|
||||||
<string name="screen_home_sort_comment_count">댓글</string>
|
<string name="screen_home_sort_comment_count">댓글</string>
|
||||||
|
|||||||
44
docs/20260312_알림리스트구현.md
Normal file
44
docs/20260312_알림리스트구현.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# 2026-03-12 알림 리스트 구현
|
||||||
|
|
||||||
|
## 체크리스트
|
||||||
|
- [x] 기존 패턴 분석 (Title bar, 카테고리 UI, Home 진입, 딥링크 라우팅)
|
||||||
|
- [x] 알림 카테고리/리스트 API 모델 및 네트워크 계층 구현
|
||||||
|
- [x] 알림 리스트 화면 UI 구현 (Title bar, Category List, Item List)
|
||||||
|
- [x] 무한 스크롤 및 마지막 페이지/빈 데이터 처리 구현
|
||||||
|
- [x] 알림 아이템 터치 시 deepLink 실행 및 Path 딥링크 보완
|
||||||
|
- [x] HomeFragment의 `ic_push_notification` 진입 연결
|
||||||
|
- [x] 검증 수행 및 결과 기록
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
- 2026-03-12
|
||||||
|
- 무엇/왜/어떻게: 작업 시작 전 기존 코드 패턴과 요구사항 반영 지점을 확인해 구현 범위를 고정했다.
|
||||||
|
- 실행 명령: `background_output(task_id="bg_0688c56d")`, `background_output(task_id="bg_d0442733")`, `background_output(task_id="bg_db96f80d")`
|
||||||
|
- 결과: Title bar/카테고리 패턴, 기존 딥링크 분기, Home 진입점 누락 상태를 확인했다.
|
||||||
|
- 2026-03-12
|
||||||
|
- 무엇/왜/어떻게: 알림 리스트 구현 변경분의 빌드/테스트/코드스타일 상태를 최종 확인해 배포 전 기본 안정성을 검증했다.
|
||||||
|
- 실행 명령: `./gradlew :app:assembleDebug :app:testDebugUnitTest :app:ktlintCheck`
|
||||||
|
- 결과: `BUILD SUCCESSFUL`.
|
||||||
|
- 2026-03-12
|
||||||
|
- 무엇/왜/어떻게: 수정 파일 정적 진단 수행 가능 여부를 확인했다.
|
||||||
|
- 실행 명령: `lsp_diagnostics(filePath="app/src/main/java/kr/co/vividnext/sodalive/home/pushnotification/PushNotificationListViewModel.kt")`, `lsp_diagnostics(filePath="app/src/main/res/layout/activity_push_notification_list.xml")`
|
||||||
|
- 결과: 현재 실행 환경에서 `.kt`, `.xml` LSP 서버가 미구성되어 진단을 수행할 수 없음을 확인했다.
|
||||||
|
- 2026-03-12
|
||||||
|
- 무엇/왜/어떻게: 작업 트리 상태를 확인해 리네임 과정의 인덱스 흔적 유무를 점검했다.
|
||||||
|
- 실행 명령: `git status --short`
|
||||||
|
- 결과: `home/push_notification` 경로에 `AD` 인덱스 흔적이 남아 있어 커밋 전 스테이징 정리가 필요함을 확인했다.
|
||||||
|
- 2026-03-12
|
||||||
|
- 무엇/왜/어떻게: 카테고리 리스트의 앱 내 `전체` 주입을 제거하고 서버 조회 카테고리만 사용하도록 로직을 축소했다.
|
||||||
|
- 실행 명령: `grep(pattern="allCategoryLabel|screen_home_theme_all|categoryAdapter.addItems\\(listOf", path="app/src/main/java/kr/co/vividnext/sodalive/home/pushnotification/PushNotificationListActivity.kt")`
|
||||||
|
- 결과: `PushNotificationListActivity`에서 `allCategoryLabel` 기반 주입/비교 로직이 제거되고, 서버 응답 `categories`를 그대로 바인딩하도록 반영되었다.
|
||||||
|
- 2026-03-12
|
||||||
|
- 무엇/왜/어떻게: 수정 파일 정적 진단과 빌드/테스트/코드스타일 검증을 재실행해 변경 안정성을 확인했다.
|
||||||
|
- 실행 명령: `lsp_diagnostics(filePath="app/src/main/java/kr/co/vividnext/sodalive/home/pushnotification/PushNotificationListActivity.kt")`, `lsp_diagnostics(filePath="docs/20260312_알림리스트구현.md")`, `./gradlew :app:assembleDebug :app:testDebugUnitTest :app:ktlintCheck`
|
||||||
|
- 결과: `.kt` LSP 서버 미구성으로 Kotlin 진단은 불가, Markdown 진단은 이슈 없음, Gradle 검증은 `BUILD SUCCESSFUL`.
|
||||||
|
- 2026-03-12
|
||||||
|
- 무엇/왜/어떻게: 서버 카테고리만 사용하면서 기본 선택을 첫 번째 카테고리로 고정하도록 초기 선택 흐름을 조정했다.
|
||||||
|
- 실행 명령: `grep(pattern="selectCategory\\(null\\)|isInitialCategorySelected|setSelectedTheme", path="app/src/main/java/kr/co/vividnext/sodalive/home/pushnotification/PushNotificationListActivity.kt")`, `./gradlew :app:assembleDebug :app:testDebugUnitTest :app:ktlintCheck`
|
||||||
|
- 결과: 앱 내 `전체` 기본 선택 호출을 제거하고 서버 카테고리 첫 항목 선택 로직을 반영했으며, Gradle 검증은 `BUILD SUCCESSFUL`.
|
||||||
|
- 2026-03-12
|
||||||
|
- 무엇/왜/어떻게: 최초 진입 시 카테고리 조회/선택 과정에서 알림 리스트 API가 2회 호출될 가능성이 있는지 호출 경로를 정적 분석으로 점검했다.
|
||||||
|
- 실행 명령: `background_output(task_id="bg_061507b7")`, `background_output(task_id="bg_23070c8e")`, `grep(pattern="getPushNotificationList\\(|selectCategory\\(|categoryListLiveData.observe\\(|onScrolled\\(", path="app/src/main/java/kr/co/vividnext/sodalive/home/pushnotification")`, `ast_grep_search(pattern="fun getPushNotificationList() { $$$ }", lang="kotlin", paths=["app/src/main/java/kr/co/vividnext/sodalive/home/pushnotification"])`
|
||||||
|
- 결과: 최초 진입 경로에서는 `categoryListLiveData` 수신 후 `isInitialCategorySelected` 가드로 첫 카테고리 선택이 1회만 실행되고, `getPushNotificationList`는 `_isLoading`/`isLastPage` 가드로 재진입이 차단되어 2회 호출 버그 재현 경로가 확인되지 않았다.
|
||||||
Reference in New Issue
Block a user