콘텐츠 메인 단편 탭

- 태그별 추천 콘텐츠 영역 추가
This commit is contained in:
klaus 2025-02-18 23:44:39 +09:00
parent 304e6e166a
commit 7bb8f9c5af
11 changed files with 281 additions and 3 deletions

View File

@ -374,4 +374,10 @@ interface AudioContentApi {
@Query("creatorId") creatorId: Long, @Query("creatorId") creatorId: Long,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>> ): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/content/recommend-content-by-tag")
fun getRecommendedContentByTag(
@Query("tag") tag: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
} }

View File

@ -1,12 +1,12 @@
package kr.co.vividnext.sodalive.audio_content.all.by_theme package kr.co.vividnext.sodalive.audio_content.all.by_theme
import android.content.Intent import android.content.Intent
import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -20,7 +20,6 @@ import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentAllByThemeBinding import kr.co.vividnext.sodalive.databinding.ActivityAudioContentAllByThemeBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
class AudioContentAllByThemeActivity : BaseActivity<ActivityAudioContentAllByThemeBinding>( class AudioContentAllByThemeActivity : BaseActivity<ActivityAudioContentAllByThemeBinding>(
@ -52,6 +51,7 @@ class AudioContentAllByThemeActivity : BaseActivity<ActivityAudioContentAllByThe
viewModel.getContentList(themeId = themeId) viewModel.getContentList(themeId = themeId)
} }
@OptIn(UnstableApi::class)
override fun setupView() { override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater) loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.setOnClickListener { finish() } binding.toolbar.tvBack.setOnClickListener { finish() }

View File

@ -19,6 +19,7 @@ import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllActivity import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllActivity
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainContentAdapter import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainContentAdapter
@ -56,6 +57,8 @@ class AudioContentMainTabContentFragment : BaseFragment<FragmentAudioContentMain
private lateinit var contentRankCreatorAdapter: ContentRankCreatorAdapter private lateinit var contentRankCreatorAdapter: ContentRankCreatorAdapter
private lateinit var curationAdapter: AudioContentMainContentCurationAdapter private lateinit var curationAdapter: AudioContentMainContentCurationAdapter
private lateinit var popularContentByCreatorAdapter: PopularContentByCreatorAdapter private lateinit var popularContentByCreatorAdapter: PopularContentByCreatorAdapter
private lateinit var contentTagAdapter: AudioContentMainTabContentTagAdapter
private lateinit var contentByTagAdapter: AudioContentNewAllAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -76,6 +79,8 @@ class AudioContentMainTabContentFragment : BaseFragment<FragmentAudioContentMain
setupEventBanner() setupEventBanner()
setupPopularContentCreator() setupPopularContentCreator()
setupPopularContentByCreator() setupPopularContentByCreator()
setupContentTag()
setupContentByTag()
setupCuration() setupCuration()
} }
@ -506,6 +511,75 @@ class AudioContentMainTabContentFragment : BaseFragment<FragmentAudioContentMain
} }
} }
private fun setupContentTag() {
val spanCount = 4
val spacing = 6f.dpToPx()
contentTagAdapter = AudioContentMainTabContentTagAdapter {
viewModel.getRecommendContentByTag(it)
}
val recyclerView = binding.rvRecommendContentTag
recyclerView.layoutManager = GridLayoutManager(requireContext(), spanCount)
recyclerView.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing.toInt(), false))
recyclerView.adapter = contentTagAdapter
viewModel.tagListLiveData.observe(viewLifecycleOwner) {
contentTagAdapter.addItems(it)
if (contentTagAdapter.itemCount <= 0) {
binding.llRecommendContentByTag.visibility = View.GONE
} else {
binding.llRecommendContentByTag.visibility = View.VISIBLE
}
}
}
private fun setupContentByTag() {
val spanCount = 3
val horizontalSpacing = 13.3f.dpToPx().toInt()
val verticalSpacing = 26.7f.dpToPx().toInt()
val itemWidth = (screenWidth - horizontalSpacing * (spanCount + 1)) / spanCount
contentByTagAdapter = AudioContentNewAllAdapter(
itemWidth = itemWidth,
onClickItem = {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
val recyclerView = binding.rvRecommendContent
recyclerView.layoutManager = GridLayoutManager(requireContext(), spanCount)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
outRect.left = horizontalSpacing / 2
outRect.right = horizontalSpacing / 2
outRect.top = verticalSpacing / 2
outRect.bottom = verticalSpacing / 2
}
})
recyclerView.adapter = contentByTagAdapter
viewModel.tagCurationContentListLiveData.observe(viewLifecycleOwner) {
contentByTagAdapter.clear()
contentByTagAdapter.addItems(it)
}
}
private fun setupCuration() { private fun setupCuration() {
curationAdapter = AudioContentMainContentCurationAdapter( curationAdapter = AudioContentMainContentCurationAdapter(
onClickItem = { onClickItem = {

View File

@ -25,4 +25,9 @@ class AudioContentMainTabContentRepository(private val api: AudioContentApi) {
creatorId: Long, creatorId: Long,
token: String token: String
) = api.getContentMainContentPopularContentByCreator(creatorId, authHeader = token) ) = api.getContentMainContentPopularContentByCreator(creatorId, authHeader = token)
fun getRecommendedContentByTag(
tag: String,
token: String
) = api.getRecommendedContentByTag(tag = tag, authHeader = token)
} }

View File

@ -0,0 +1,78 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.content
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemContentMainTabContentTagBinding
class AudioContentMainTabContentTagAdapter(
private val onClick: (String) -> Unit
) : RecyclerView.Adapter<AudioContentMainTabContentTagAdapter.ViewHolder>() {
private val tagList = mutableListOf<String>()
private var selectedTag = ""
inner class ViewHolder(
private val context: Context,
private val binding: ItemContentMainTabContentTagBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(tag: String) {
if (tag == selectedTag) {
binding.tvTag.setBackgroundResource(
R.drawable.bg_round_corner_2_6_transparent_3bb9f1
)
binding.tvTag.setTextColor(
ContextCompat.getColor(context, R.color.color_3bb9f1)
)
} else {
binding.tvTag.setBackgroundResource(
R.drawable.bg_round_corner_2_6_transparent_777777
)
binding.tvTag.setTextColor(
ContextCompat.getColor(context, R.color.color_777777)
)
}
binding.tvTag.text = tag
binding.tvTag.setOnClickListener {
selectedTag = tag
onClick(tag)
notifyDataSetChanged()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemContentMainTabContentTagBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = tagList.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(tagList[position])
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(tagList: List<String>) {
this.tagList.clear()
this.tagList.addAll(tagList)
if (tagList.isNotEmpty()) {
selectedTag = tagList[0]
}
notifyDataSetChanged()
}
}

View File

@ -62,6 +62,14 @@ class AudioContentMainTabContentViewModel(
val salesCountRankContentListLiveData: LiveData<List<GetAudioContentRankingItem>> val salesCountRankContentListLiveData: LiveData<List<GetAudioContentRankingItem>>
get() = _salesCountRankContentListLiveData get() = _salesCountRankContentListLiveData
private val _tagListLiveData = MutableLiveData<List<String>>()
val tagListLiveData: LiveData<List<String>>
get() = _tagListLiveData
private val _tagCurationContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val tagCurationContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _tagCurationContentListLiveData
fun fetchData() { fun fetchData() {
_isLoading.value = true _isLoading.value = true
compositeDisposable.add( compositeDisposable.add(
@ -87,6 +95,9 @@ class AudioContentMainTabContentViewModel(
_salesCountRankContentListLiveData.value = _salesCountRankContentListLiveData.value =
data.salesCountRankContentList data.salesCountRankContentList
_curationListLiveData.value = data.curationList _curationListLiveData.value = data.curationList
_tagListLiveData.value = data.tagList
_tagCurationContentListLiveData.value = data.tagCurationContentList
} else { } else {
if (it.message != null) { if (it.message != null) {
_toastLiveData.postValue(it.message) _toastLiveData.postValue(it.message)
@ -211,4 +222,38 @@ class AudioContentMainTabContentViewModel(
) )
) )
} }
fun getRecommendContentByTag(tag: String) {
_isLoading.value = true
compositeDisposable.add(
repository.getRecommendedContentByTag(
tag = tag,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_tagCurationContentListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_isLoading.value = false
}
)
)
}
} }

View File

@ -19,5 +19,7 @@ data class GetContentMainTabContentResponse(
@SerializedName("contentRankCreatorList") val contentRankCreatorList: List<ContentCreatorResponse>, @SerializedName("contentRankCreatorList") val contentRankCreatorList: List<ContentCreatorResponse>,
@SerializedName("salesCountRankContentList") val salesCountRankContentList: List<GetAudioContentRankingItem>, @SerializedName("salesCountRankContentList") val salesCountRankContentList: List<GetAudioContentRankingItem>,
@SerializedName("eventBannerList") val eventBannerList: GetEventResponse, @SerializedName("eventBannerList") val eventBannerList: GetEventResponse,
@SerializedName("tagList") val tagList: List<String>,
@SerializedName("tagCurationContentList") val tagCurationContentList: List<GetAudioContentMainItem>,
@SerializedName("curationList") val curationList: List<GetContentCurationResponse> @SerializedName("curationList") val curationList: List<GetContentCurationResponse>
) )

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<corners android:radius="2.6dp" />
<stroke
android:width="1dp"
android:color="@color/color_3bb9f1" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<corners android:radius="2.6dp" />
<stroke
android:width="1dp"
android:color="@color/color_777777" />
</shape>

View File

@ -185,6 +185,40 @@
android:layout_marginTop="6.7dp" android:layout_marginTop="6.7dp"
android:visibility="gone" /> android:visibility="gone" />
<LinearLayout
android:id="@+id/ll_recommend_content_by_tag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="13.3dp"
android:fontFamily="@font/gmarket_sans_bold"
android:text="태그별 추천 콘텐츠"
android:textColor="@color/color_eeeeee"
android:textSize="18.3sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_recommend_content_tag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="13.3dp"
android:clipToPadding="false"
android:paddingHorizontal="13.3dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_recommend_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6.7dp"
android:clipToPadding="false"
android:paddingHorizontal="6.7dp"
android:paddingBottom="6.7dp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_curation" android:id="@+id/rv_curation"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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">
<TextView
android:id="@+id/tv_tag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/bg_round_corner_2_6_transparent_777777"
android:gravity="center"
android:paddingVertical="10dp"
android:textColor="@color/color_777777"
android:textSize="12sp"
tools:text="자작곡" />
</FrameLayout>