feat(home): 홈 추천 콘텐츠 섹션 추가 및 API 연동
This commit is contained in:
@@ -24,5 +24,6 @@ data class GetHomeResponse(
|
|||||||
@SerializedName("contentRanking") val contentRanking: List<GetAudioContentRankingItem>,
|
@SerializedName("contentRanking") val contentRanking: List<GetAudioContentRankingItem>,
|
||||||
@SerializedName("recommendChannelList") val recommendChannelList: List<RecommendChannelResponse>,
|
@SerializedName("recommendChannelList") val recommendChannelList: List<RecommendChannelResponse>,
|
||||||
@SerializedName("freeContentList") val freeContentList: List<AudioContentMainItem>,
|
@SerializedName("freeContentList") val freeContentList: List<AudioContentMainItem>,
|
||||||
@SerializedName("pointAvailableContentList") val pointAvailableContentList: List<AudioContentMainItem>
|
@SerializedName("pointAvailableContentList") val pointAvailableContentList: List<AudioContentMainItem>,
|
||||||
|
@SerializedName("recommendContentList") val recommendContentList: List<AudioContentMainItem>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,4 +32,11 @@ interface HomeApi {
|
|||||||
@Query("contentType") contentType: ContentType,
|
@Query("contentType") contentType: ContentType,
|
||||||
@Header("Authorization") authHeader: String
|
@Header("Authorization") authHeader: String
|
||||||
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
|
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
|
||||||
|
|
||||||
|
@GET("/api/home/recommend-contents")
|
||||||
|
fun getRecommendContents(
|
||||||
|
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||||
|
@Query("contentType") contentType: ContentType,
|
||||||
|
@Header("Authorization") authHeader: String
|
||||||
|
): Single<ApiResponse<List<AudioContentMainItem>>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import kr.co.vividnext.sodalive.databinding.ItemHomeContentBinding
|
|||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
|
|
||||||
class HomeContentAdapter(
|
class HomeContentAdapter(
|
||||||
|
private val itemSquareSizePx: Int? = null,
|
||||||
private val onClickItem: (Long) -> Unit,
|
private val onClickItem: (Long) -> Unit,
|
||||||
) : RecyclerView.Adapter<HomeContentAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<HomeContentAdapter.ViewHolder>() {
|
||||||
private val items = mutableListOf<AudioContentMainItem>()
|
private val items = mutableListOf<AudioContentMainItem>()
|
||||||
@@ -40,6 +41,18 @@ class HomeContentAdapter(
|
|||||||
private val binding: ItemHomeContentBinding
|
private val binding: ItemHomeContentBinding
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(item: AudioContentMainItem) {
|
fun bind(item: AudioContentMainItem) {
|
||||||
|
// 아이템 크기(정사각형) 동적 적용: 추천 콘텐츠 섹션에서만 사용
|
||||||
|
itemSquareSizePx?.let { size ->
|
||||||
|
val rootLp = binding.root.layoutParams
|
||||||
|
rootLp.width = size
|
||||||
|
binding.root.layoutParams = rootLp
|
||||||
|
|
||||||
|
val imageLp = binding.ivContentCoverImage.layoutParams
|
||||||
|
imageLp.width = size
|
||||||
|
imageLp.height = size
|
||||||
|
binding.ivContentCoverImage.layoutParams = imageLp
|
||||||
|
}
|
||||||
|
|
||||||
binding.ivPoint.visibility = if (item.isPointAvailable) {
|
binding.ivPoint.visibility = if (item.isPointAvailable) {
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.home
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -16,6 +15,7 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.media3.common.util.UnstableApi
|
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
|
||||||
@@ -87,6 +87,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
|||||||
private lateinit var recommendChannelAdapter: HomeRecommendChannelAdapter
|
private lateinit var recommendChannelAdapter: HomeRecommendChannelAdapter
|
||||||
private lateinit var homeFreeContentAdapter: HomeContentAdapter
|
private lateinit var homeFreeContentAdapter: HomeContentAdapter
|
||||||
private lateinit var homePointContentAdapter: HomeContentAdapter
|
private lateinit var homePointContentAdapter: HomeContentAdapter
|
||||||
|
private lateinit var recommendContentAdapter: HomeContentAdapter
|
||||||
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
@@ -191,6 +192,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
|||||||
setupRecommendChannel()
|
setupRecommendChannel()
|
||||||
setupFreeContent()
|
setupFreeContent()
|
||||||
setupPointContent()
|
setupPointContent()
|
||||||
|
setupRecommendContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupLiveView() {
|
private fun setupLiveView() {
|
||||||
@@ -534,7 +536,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
|||||||
}
|
}
|
||||||
|
|
||||||
AudioContentBannerType.LINK -> {
|
AudioContentBannerType.LINK -> {
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
|
startActivity(Intent(Intent.ACTION_VIEW, it.link!!.toUri()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1156,6 +1158,76 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupRecommendContent() {
|
||||||
|
// 제목 설정: 로그인 여부에 따라 변경
|
||||||
|
val title = if (SharedPreferenceManager.token.isNotBlank()) {
|
||||||
|
"${SharedPreferenceManager.nickname}님을 위한 추천 콘텐츠"
|
||||||
|
} else {
|
||||||
|
"추천 콘텐츠"
|
||||||
|
}
|
||||||
|
binding.tvRecommendContent.text = title
|
||||||
|
|
||||||
|
// 아이템 정사각형 크기 계산: (screenWidth - (24*2) - 16) / 2
|
||||||
|
val itemSize = ((screenWidth - 24f.dpToPx() * 2 - 16f.dpToPx()) / 2f).toInt()
|
||||||
|
|
||||||
|
// 어댑터 설정 (아이템 클릭 동작은 무료/포인트 콘텐츠와 동일)
|
||||||
|
recommendContentAdapter = HomeContentAdapter(onClickItem = {
|
||||||
|
if (SharedPreferenceManager.token.isNotBlank()) {
|
||||||
|
startActivity(
|
||||||
|
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(requireActivity() as MainActivity).showLoginActivity()
|
||||||
|
}
|
||||||
|
}, itemSquareSizePx = itemSize)
|
||||||
|
|
||||||
|
val rv = binding.rvRecommendContent
|
||||||
|
// 한 줄에 아이템 2개인 Grid
|
||||||
|
rv.layoutManager = GridLayoutManager(context, 2)
|
||||||
|
rv.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||||
|
override fun getItemOffsets(
|
||||||
|
outRect: Rect,
|
||||||
|
view: View,
|
||||||
|
parent: RecyclerView,
|
||||||
|
state: RecyclerView.State
|
||||||
|
) {
|
||||||
|
super.getItemOffsets(outRect, view, parent, state)
|
||||||
|
outRect.top = 8f.dpToPx().toInt()
|
||||||
|
outRect.bottom = 8f.dpToPx().toInt()
|
||||||
|
|
||||||
|
val position = parent.getChildAdapterPosition(view)
|
||||||
|
// 좌/우 간격 설정: 각 컬럼 간 간격 유지
|
||||||
|
if (position % 2 == 0) {
|
||||||
|
// 왼쪽 컬럼
|
||||||
|
outRect.left = 0f.dpToPx().toInt()
|
||||||
|
outRect.right = 8f.dpToPx().toInt()
|
||||||
|
} else {
|
||||||
|
// 오른쪽 컬럼
|
||||||
|
outRect.left = 8f.dpToPx().toInt()
|
||||||
|
outRect.right = 0f.dpToPx().toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
rv.adapter = recommendContentAdapter
|
||||||
|
|
||||||
|
// LiveData observe
|
||||||
|
viewModel.recommendContentListLiveData.observe(viewLifecycleOwner) {
|
||||||
|
if (it.isNotEmpty() || recommendContentAdapter.itemCount > 0) {
|
||||||
|
binding.llRecommendContent.visibility = View.VISIBLE
|
||||||
|
recommendContentAdapter.addItems(it)
|
||||||
|
} else {
|
||||||
|
binding.llRecommendContent.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 새로고침 아이콘 클릭
|
||||||
|
binding.ivRecommendRefresh.setOnClickListener {
|
||||||
|
viewModel.refreshRecommendContents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun bindData() {
|
private fun bindData() {
|
||||||
viewModel.isLoading.observe(viewLifecycleOwner) {
|
viewModel.isLoading.observe(viewLifecycleOwner) {
|
||||||
if (it) {
|
if (it) {
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ class HomeRepository(private val api: HomeApi) {
|
|||||||
fun fetchData(token: String) = api.getHomeData(
|
fun fetchData(token: String) = api.getHomeData(
|
||||||
timezone = TimeZone.getDefault().id,
|
timezone = TimeZone.getDefault().id,
|
||||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||||
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
|
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||||
authHeader = token
|
authHeader = token
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getLatestContentByTheme(theme: String, token: String) = api.getLatestContentByTheme(
|
fun getLatestContentByTheme(theme: String, token: String) = api.getLatestContentByTheme(
|
||||||
theme = theme,
|
theme = theme,
|
||||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||||
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
|
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||||
authHeader = token
|
authHeader = token
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,7 +24,13 @@ class HomeRepository(private val api: HomeApi) {
|
|||||||
) = api.getDayOfWeekSeriesList(
|
) = api.getDayOfWeekSeriesList(
|
||||||
dayOfWeek = dayOfWeek,
|
dayOfWeek = dayOfWeek,
|
||||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||||
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
|
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||||
|
authHeader = token
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getRecommendContents(token: String) = api.getRecommendContents(
|
||||||
|
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||||
|
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||||
authHeader = token
|
authHeader = token
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ class HomeViewModel(
|
|||||||
val pointAvailableContentListLiveData: LiveData<List<AudioContentMainItem>>
|
val pointAvailableContentListLiveData: LiveData<List<AudioContentMainItem>>
|
||||||
get() = _pointAvailableContentListLiveData
|
get() = _pointAvailableContentListLiveData
|
||||||
|
|
||||||
|
private var _recommendContentListLiveData = MutableLiveData<List<AudioContentMainItem>>()
|
||||||
|
val recommendContentListLiveData: LiveData<List<AudioContentMainItem>>
|
||||||
|
get() = _recommendContentListLiveData
|
||||||
|
|
||||||
fun fetchData() {
|
fun fetchData() {
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
|
|
||||||
@@ -108,6 +112,7 @@ class HomeViewModel(
|
|||||||
_pointAvailableContentListLiveData.value =
|
_pointAvailableContentListLiveData.value =
|
||||||
data.pointAvailableContentList
|
data.pointAvailableContentList
|
||||||
_recommendChannelListLiveData.value = data.recommendChannelList
|
_recommendChannelListLiveData.value = data.recommendChannelList
|
||||||
|
_recommendContentListLiveData.value = data.recommendContentList
|
||||||
} else {
|
} else {
|
||||||
if (it.message != null) {
|
if (it.message != null) {
|
||||||
_toastLiveData.postValue(it.message)
|
_toastLiveData.postValue(it.message)
|
||||||
@@ -127,6 +132,28 @@ class HomeViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun refreshRecommendContents() {
|
||||||
|
_isLoading.value = true
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.getRecommendContents(token = "Bearer ${SharedPreferenceManager.token}")
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
_isLoading.value = false
|
||||||
|
if (it.success && it.data != null) {
|
||||||
|
_recommendContentListLiveData.value = it.data!!
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_isLoading.value = false
|
||||||
|
it.message?.let { message -> Logger.e(message) }
|
||||||
|
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun getLatestContentByTheme(theme: String) {
|
fun getLatestContentByTheme(theme: String) {
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
|
|
||||||
|
|||||||
@@ -434,6 +434,49 @@
|
|||||||
android:paddingHorizontal="24dp" />
|
android:paddingHorizontal="24dp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 추천 콘텐츠 섹션 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/ll_recommend_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="48dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_recommend_content"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:fontFamily="@font/pretendard_bold"
|
||||||
|
android:text="추천 콘텐츠"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="24sp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_recommend_refresh"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_refresh" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rv_recommend_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingHorizontal="24dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<io.github.glailton.expandabletextview.ExpandableTextView
|
<io.github.glailton.expandabletextview.ExpandableTextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
Reference in New Issue
Block a user