From 7ecb36a7be950a977c0530a002afcdc092472a24 Mon Sep 17 00:00:00 2001 From: klaus Date: Mon, 20 Oct 2025 18:57:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(home):=20=EC=9D=B8=EA=B8=B0=20=EC=BA=90?= =?UTF-8?q?=EB=A6=AD=ED=84=B0=20=EC=83=89=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/home/GetHomeResponse.kt | 1 + .../vividnext/sodalive/home/HomeFragment.kt | 136 ++++++++++++++++++ .../vividnext/sodalive/home/HomeViewModel.kt | 6 + .../vividnext/sodalive/main/MainActivity.kt | 4 + app/src/main/res/layout/fragment_home.xml | 45 ++++++ 5 files changed, 192 insertions(+) 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 bf62deaf..3f23c6aa 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 @@ -20,6 +20,7 @@ data class GetHomeResponse( @SerializedName("eventBannerList") val eventBannerList: GetEventResponse, @SerializedName("originalAudioDramaList") val originalAudioDramaList: List, @SerializedName("dayOfWeekSeriesList") val dayOfWeekSeriesList: List, + @SerializedName("popularCharacters") val popularCharacters: List, @SerializedName("contentRanking") val contentRanking: List, @SerializedName("recommendChannelList") val recommendChannelList: List, @SerializedName("freeContentList") val freeContentList: List, 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 0b01bec4..44988dd7 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 @@ -10,6 +10,7 @@ import android.os.Looper import android.text.SpannableString import android.text.Spanned import android.text.style.ForegroundColorSpan +import android.view.Gravity import android.view.View import android.widget.LinearLayout import android.widget.Toast @@ -19,6 +20,7 @@ import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.gson.Gson import com.zhpan.bannerview.BaseBannerAdapter import com.zhpan.indicator.enums.IndicatorSlideMode import com.zhpan.indicator.enums.IndicatorStyle @@ -34,6 +36,10 @@ import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity import kr.co.vividnext.sodalive.audition.AuditionActivity import kr.co.vividnext.sodalive.base.BaseFragment +import kr.co.vividnext.sodalive.base.SodaDialog +import kr.co.vividnext.sodalive.chat.character.CharacterAdapter +import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity +import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity.Companion.EXTRA_CHARACTER_ID import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.SharedPreferenceManager @@ -46,10 +52,15 @@ import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog import kr.co.vividnext.sodalive.main.MainActivity +import kr.co.vividnext.sodalive.mypage.MyPageViewModel +import kr.co.vividnext.sodalive.mypage.auth.Auth +import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest +import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity import kr.co.vividnext.sodalive.search.SearchActivity import kr.co.vividnext.sodalive.settings.event.EventDetailActivity import kr.co.vividnext.sodalive.settings.notification.MemberRole +import kr.co.vividnext.sodalive.splash.SplashActivity import org.koin.android.ext.android.inject import java.text.SimpleDateFormat import java.util.Date @@ -59,6 +70,7 @@ import java.util.Locale class HomeFragment : BaseFragment(FragmentHomeBinding::inflate) { private val viewModel: HomeViewModel by inject() private val liveViewModel: LiveViewModel by inject() + private val myPageViewModel: MyPageViewModel by inject() private lateinit var loadingDialog: LoadingDialog @@ -70,6 +82,7 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl private lateinit var contentBannerAdapter: AudioContentMainBannerAdapter private lateinit var originalSeriesAdapter: HomeSeriesAdapter private lateinit var seriesDayOfWeekAdapter: HomeSeriesAdapter + private lateinit var popularCharacterAdapter: CharacterAdapter private lateinit var weelyChartAdapter: HomeWeeklyChartAdapter private lateinit var recommendChannelAdapter: HomeRecommendChannelAdapter private lateinit var homeFreeContentAdapter: HomeContentAdapter @@ -173,6 +186,7 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl setupOriginalSeries() setupAudition() setupSeriesDayOfWeek() + setupPopularCharacters() setupWeelyChart() setupRecommendChannel() setupFreeContent() @@ -777,6 +791,67 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl rvDayOfWeek.adapter = dayOfWeekAdapter } + private fun setupPopularCharacters() { + // 인기 캐릭터 RecyclerView 설정 (순위 표시) + popularCharacterAdapter = CharacterAdapter( + showRanking = true + ) { + onCharacterClick(it) + } + + val recyclerView = binding.rvPopularCharacters + + recyclerView.layoutManager = LinearLayoutManager( + requireContext(), + LinearLayoutManager.HORIZONTAL, + false + ) + + recyclerView.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 = 8f.dpToPx().toInt() + } + + popularCharacterAdapter.itemCount - 1 -> { + outRect.left = 8f.dpToPx().toInt() + outRect.right = 0 + } + + else -> { + outRect.left = 8f.dpToPx().toInt() + outRect.right = 8f.dpToPx().toInt() + } + } + } + }) + + recyclerView.adapter = popularCharacterAdapter + + binding.tvPopularCharacterAll.setOnClickListener { + (requireActivity() as MainActivity).openChatTab() + } + + // 인기 캐릭터 LiveData 구독 + viewModel.popularCharacters.observe(viewLifecycleOwner) { + if (it.isNotEmpty()) { + binding.llPopularCharacters.visibility = View.VISIBLE + popularCharacterAdapter.updateCharacters(it) + } else { + binding.llPopularCharacters.visibility = View.GONE + } + } + } + private fun setupWeelyChart() { val spSectionTitle = SpannableString(binding.tvWeeklyChart.text) spSectionTitle.setSpan( @@ -1196,4 +1271,65 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl } } } + + private fun ensureLoginAndAuth(onAuthed: () -> Unit) { + if (SharedPreferenceManager.token.isBlank()) { + (requireActivity() as MainActivity).showLoginActivity() + return + } + + if (!SharedPreferenceManager.isAuth) { + SodaDialog( + activity = requireActivity(), + layoutInflater = layoutInflater, + title = "본인인증", + desc = "보이스온의 오픈월드 캐릭터톡은\n청소년 보호를 위해 본인인증한\n성인만 이용이 가능합니다.\n" + + "캐릭터톡 서비스를 이용하시려면\n본인인증을 하고 이용해주세요.", + confirmButtonTitle = "본인인증 하러가기", + confirmButtonClick = { startAuthFlow() }, + cancelButtonTitle = "취소", + cancelButtonClick = {}, + descGravity = Gravity.CENTER + ).show(screenWidth) + return + } + + onAuthed() + } + + private fun startAuthFlow() { + Auth.auth(requireActivity(), requireContext()) { json -> + val bootpayResponse = Gson().fromJson( + json, + BootpayResponse::class.java + ) + val request = AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId) + requireActivity().runOnUiThread { + myPageViewModel.authVerify(request) { + startActivity( + Intent( + requireContext(), + SplashActivity::class.java + ).apply { + addFlags( + Intent.FLAG_ACTIVITY_CLEAR_TASK or + Intent.FLAG_ACTIVITY_NEW_TASK + ) + } + ) + requireActivity().finish() + } + } + } + } + + private fun onCharacterClick(characterId: Long) { + ensureLoginAndAuth { + startActivity( + Intent(requireContext(), CharacterDetailActivity::class.java).apply { + putExtra(EXTRA_CHARACTER_ID, characterId) + } + ) + } + } } 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 3c3bda78..4ea49fab 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 @@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.chat.character.Character import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.explorer.GetExplorerSectionCreatorResponse import kr.co.vividnext.sodalive.live.GetRoomListResponse @@ -58,6 +59,11 @@ class HomeViewModel( val dayOfWeekSeriesListLiveData: LiveData> get() = _dayOfWeekSeriesListLiveData + // 인기 캐릭터 LiveData + private val _popularCharacters = MutableLiveData>(emptyList()) + val popularCharacters: LiveData> + get() = _popularCharacters + private var _contentRankingLiveData = MutableLiveData>() val contentRankingLiveData: LiveData> get() = _contentRankingLiveData diff --git a/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt index fda80138..37fc63a2 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt @@ -596,6 +596,10 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl } } + fun openChatTab() { + viewModel.clickTab(MainViewModel.CurrentTab.CHAT) + } + inner class AudioContentReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index b5862203..35f86e37 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -263,6 +263,51 @@ android:paddingHorizontal="24dp" /> + + + + + + + + + + + + + + +