From 3353ebb777b8b4c32c7f43ce646f2f41c3ac5c2c Mon Sep 17 00:00:00 2001 From: klaus Date: Tue, 11 Nov 2025 16:21:53 +0900 Subject: [PATCH] =?UTF-8?q?feat(home):=20=ED=99=88=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EC=84=B9=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/home/GetHomeResponse.kt | 3 +- .../kr/co/vividnext/sodalive/home/HomeApi.kt | 7 ++ .../sodalive/home/HomeContentAdapter.kt | 13 ++++ .../vividnext/sodalive/home/HomeFragment.kt | 76 ++++++++++++++++++- .../vividnext/sodalive/home/HomeRepository.kt | 12 ++- .../vividnext/sodalive/home/HomeViewModel.kt | 27 +++++++ app/src/main/res/layout/fragment_home.xml | 43 +++++++++++ 7 files changed, 175 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/home/GetHomeResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/home/GetHomeResponse.kt index c92ae267..721e0e8b 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/home/GetHomeResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/home/GetHomeResponse.kt @@ -24,5 +24,6 @@ data class GetHomeResponse( @SerializedName("contentRanking") val contentRanking: List, @SerializedName("recommendChannelList") val recommendChannelList: List, @SerializedName("freeContentList") val freeContentList: List, - @SerializedName("pointAvailableContentList") val pointAvailableContentList: List + @SerializedName("pointAvailableContentList") val pointAvailableContentList: List, + @SerializedName("recommendContentList") val recommendContentList: List ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeApi.kt index e3317452..dd34b4b6 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeApi.kt @@ -32,4 +32,11 @@ interface HomeApi { @Query("contentType") contentType: ContentType, @Header("Authorization") authHeader: String ): Single>> + + @GET("/api/home/recommend-contents") + fun getRecommendContents( + @Query("isAdultContentVisible") isAdultContentVisible: Boolean, + @Query("contentType") contentType: ContentType, + @Header("Authorization") authHeader: String + ): Single>> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeContentAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeContentAdapter.kt index 62ed86a7..1709db0d 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeContentAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeContentAdapter.kt @@ -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() { private val items = mutableListOf() @@ -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 { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeFragment.kt index 04fb3fb2..587752d4 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeFragment.kt @@ -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::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::infl setupRecommendChannel() setupFreeContent() setupPointContent() + setupRecommendContent() } private fun setupLiveView() { @@ -534,7 +536,7 @@ class HomeFragment : BaseFragment(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::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) { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeRepository.kt index 7a381b08..13dd7f3c 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeRepository.kt @@ -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 ) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeViewModel.kt index 9d700f40..7afcac94 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeViewModel.kt @@ -79,6 +79,10 @@ class HomeViewModel( val pointAvailableContentListLiveData: LiveData> get() = _pointAvailableContentListLiveData + private var _recommendContentListLiveData = MutableLiveData>() + val recommendContentListLiveData: LiveData> + 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 diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 2499f1f4..44778198 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -434,6 +434,49 @@ android:paddingHorizontal="24dp" /> + + + + + + + + + + + + +