feat(home): 홈 추천 콘텐츠 섹션 추가 및 API 연동
This commit is contained in:
@@ -24,5 +24,6 @@ data class GetHomeResponse(
|
||||
@SerializedName("contentRanking") val contentRanking: List<GetAudioContentRankingItem>,
|
||||
@SerializedName("recommendChannelList") val recommendChannelList: List<RecommendChannelResponse>,
|
||||
@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,
|
||||
@Header("Authorization") authHeader: String
|
||||
): 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
|
||||
|
||||
class HomeContentAdapter(
|
||||
private val itemSquareSizePx: Int? = null,
|
||||
private val onClickItem: (Long) -> Unit,
|
||||
) : RecyclerView.Adapter<HomeContentAdapter.ViewHolder>() {
|
||||
private val items = mutableListOf<AudioContentMainItem>()
|
||||
@@ -40,6 +41,18 @@ class HomeContentAdapter(
|
||||
private val binding: ItemHomeContentBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
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) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.home
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -16,6 +15,7 @@ import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -87,6 +87,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
||||
private lateinit var recommendChannelAdapter: HomeRecommendChannelAdapter
|
||||
private lateinit var homeFreeContentAdapter: HomeContentAdapter
|
||||
private lateinit var homePointContentAdapter: HomeContentAdapter
|
||||
private lateinit var recommendContentAdapter: HomeContentAdapter
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
@@ -191,6 +192,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
||||
setupRecommendChannel()
|
||||
setupFreeContent()
|
||||
setupPointContent()
|
||||
setupRecommendContent()
|
||||
}
|
||||
|
||||
private fun setupLiveView() {
|
||||
@@ -534,7 +536,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
||||
}
|
||||
|
||||
AudioContentBannerType.LINK -> {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
|
||||
startActivity(Intent(Intent.ACTION_VIEW, it.link!!.toUri()))
|
||||
}
|
||||
}
|
||||
} 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() {
|
||||
viewModel.isLoading.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
|
||||
@@ -8,14 +8,14 @@ class HomeRepository(private val api: HomeApi) {
|
||||
fun fetchData(token: String) = api.getHomeData(
|
||||
timezone = TimeZone.getDefault().id,
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getLatestContentByTheme(theme: String, token: String) = api.getLatestContentByTheme(
|
||||
theme = theme,
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
@@ -24,7 +24,13 @@ class HomeRepository(private val api: HomeApi) {
|
||||
) = api.getDayOfWeekSeriesList(
|
||||
dayOfWeek = dayOfWeek,
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,6 +79,10 @@ class HomeViewModel(
|
||||
val pointAvailableContentListLiveData: LiveData<List<AudioContentMainItem>>
|
||||
get() = _pointAvailableContentListLiveData
|
||||
|
||||
private var _recommendContentListLiveData = MutableLiveData<List<AudioContentMainItem>>()
|
||||
val recommendContentListLiveData: LiveData<List<AudioContentMainItem>>
|
||||
get() = _recommendContentListLiveData
|
||||
|
||||
fun fetchData() {
|
||||
_isLoading.value = true
|
||||
|
||||
@@ -108,6 +112,7 @@ class HomeViewModel(
|
||||
_pointAvailableContentListLiveData.value =
|
||||
data.pointAvailableContentList
|
||||
_recommendChannelListLiveData.value = data.recommendChannelList
|
||||
_recommendContentListLiveData.value = data.recommendContentList
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_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) {
|
||||
_isLoading.value = true
|
||||
|
||||
|
||||
@@ -434,6 +434,49 @@
|
||||
android:paddingHorizontal="24dp" />
|
||||
</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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
Reference in New Issue
Block a user