Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fbe25e4
fix: Home 취향 추천 상태를 상단 콘텐츠와 분리
devfeijoa Sep 27, 2026
9c61410
test: 세션 복구 후 Home 상태 초기화를 검증
devfeijoa Sep 27, 2026
6e0c986
fix: 홈 화면 글로벌 에러 복구 로직 개선 및 상태 관리 최적화
devfeijoa Oct 2, 2026
d706b9e
fix: 세션 만료 판정 조건 수정(403 Forbidden 응답에 대한 에러 처리 로직 수정)
devfeijoa Oct 2, 2026
41eb884
feat: 홈 화면 취향 기반 추천 작품 갱신 실패 시 토스트 알림 및 예외 처리 로직 구현
devfeijoa Oct 2, 2026
bbbd5d4
feat: 홈 취향 추천 로드 실패 UI 개선 및 스타일 수정
devfeijoa Oct 2, 2026
e9a93e8
refactor: HomeSectionReleaseTest 내 코드 포맷팅 수정
devfeijoa Oct 2, 2026
45c8c7f
feat: 프로필 수정 시 장르 취향 변경 여부 전달 및 홈 화면 반영
devfeijoa Oct 3, 2026
5cc03d4
refactor: 홈 섹션의 에러 처리 로직 간소화 및 `isGlobalHomeFailure` 제거
devfeijoa Oct 3, 2026
c1d6968
refactor: fragment_home.xml 내 불필요한 리플 효과 제거
devfeijoa Oct 3, 2026
6dbb4c4
refactor: Home 화면 섹션 로딩 및 상태 관리 로직 개선
devfeijoa Oct 4, 2026
793b4a5
refactor: ProfileEditViewModelTest 코드 포맷팅 및 가독성 개선
devfeijoa Oct 4, 2026
0bd150e
fix: Home 선요청 전달과 수명 관리 추가
devfeijoa Oct 4, 2026
cd85e00
fix: Home 최초 조회에 선요청 재사용 적용
devfeijoa Oct 4, 2026
7785e94
fix: Splash 인증 후 Home 상단 데이터 선요청
devfeijoa Oct 4, 2026
e23e054
feat: 스플래시 화면 최소 노출 시간 보장 및 단위 테스트 추가
devfeijoa Oct 5, 2026
f0379cb
feat: 스플래시 화면 내 홈 화면 진입 시 데이터 사전 요청(HomeStartupRequest) 로직 구현
devfeijoa Oct 5, 2026
c4fe813
refactor: SplashActivity 코드 포맷팅 및 가독성 개선
devfeijoa Oct 5, 2026
93f5256
feat: 스플래시 화면 데이터 사전 요청 로직 개선 및 에지 케이스 대응
devfeijoa Oct 5, 2026
192ecb4
test: 홈 화면 초기화(StartupRequest) 진행 중 새로고침 시 중복 요청 방지 로직 검증 테스트 추가
devfeijoa Oct 5, 2026
8c574c9
refactor: 홈 화면 섹션 데이터 로드 시 사전 요청(startupRequests) 취소 시점 조정
devfeijoa Oct 5, 2026
2871a4e
fix: `HomeStartupRequest` 내 세션 조회 실패 시 예외 처리 및 폴백 로직 개선
devfeijoa Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion app/src/main/java/com/into/websoso/ui/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.annotation.IntegerRes
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import com.google.firebase.messaging.FirebaseMessaging
Expand All @@ -32,12 +33,18 @@ import com.into.websoso.ui.main.MainActivity.FragmentType.LIBRARY
import com.into.websoso.ui.main.MainActivity.FragmentType.MY_PAGE
import com.into.websoso.ui.main.feed.FeedFragment
import com.into.websoso.ui.main.home.HomeFragment
import com.into.websoso.ui.main.home.HomeStartupRequest
import com.into.websoso.ui.main.library.LibraryFragment
import com.into.websoso.ui.main.myPage.MyPageFragment
import dagger.Lazy
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : BaseActivity<ActivityMainBinding>(activity_main) {
@Inject
lateinit var homeStartupRequest: Lazy<HomeStartupRequest>

private val mainViewModel: MainViewModel by viewModels()
private var backPressedTime: Long = 0L
private var currentFragment: Fragment? = null
Expand All @@ -52,6 +59,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>(activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

if (intent.getAdaptedSerializableExtra<FragmentType>(DESTINATION_KEY)?.let { it != HOME } == true ||
intent.getLongExtra(CollectionDeepLink.PENDING_COLLECTION_ID, 0L) > 0L
) {
discardStartupRequest()
}
setupBackButtonListener()
setupBottomNavigationView()
setupObserver()
Expand Down Expand Up @@ -142,6 +154,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(activity_main) {

private fun replaceCurrentFragment(itemId: Int) {
val tag = fragmentTags[itemId] ?: return
if (itemId != menu_home) discardStartupRequest()
val isLibrary = itemId == menu_library
val existingFragment = supportFragmentManager.findFragmentByTag(tag)
val targetFragment = existingFragment ?: findOrCreateFragment(tag)
Expand Down Expand Up @@ -175,7 +188,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>(activity_main) {

private fun findOrCreateFragment(tag: String): Fragment =
supportFragmentManager.findFragmentByTag(tag) ?: when (tag) {
HomeFragment.TAG -> HomeFragment()
HomeFragment.TAG -> HomeFragment().apply {
arguments = bundleOf(HomeStartupRequest.KEY to intent.getStringExtra(HomeStartupRequest.KEY))
}
FeedFragment.TAG -> FeedFragment()
LibraryFragment.TAG -> LibraryFragment()
MyPageFragment.TAG -> MyPageFragment()
Expand Down Expand Up @@ -231,6 +246,16 @@ class MainActivity : BaseActivity<ActivityMainBinding>(activity_main) {
}
}

private fun discardStartupRequest() {
intent.getStringExtra(HomeStartupRequest.KEY)?.let { homeStartupRequest.get().discard(it) }
intent.removeExtra(HomeStartupRequest.KEY)
}

override fun onDestroy() {
if (!isChangingConfigurations) discardStartupRequest()
super.onDestroy()
}

companion object {
private const val DESTINATION_KEY = "destination"
const val IS_LOGIN_KEY = "isLogin"
Expand Down
66 changes: 53 additions & 13 deletions app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import com.google.firebase.messaging.FirebaseMessaging
import com.into.websoso.R.color.gray_200_949399
import com.into.websoso.R.color.gray_300_52515F
import com.into.websoso.R.layout.fragment_home
import com.into.websoso.core.common.ui.base.BaseFragment
import com.into.websoso.core.common.ui.model.ResultFrom.FeedDetailBack
Expand All @@ -21,8 +24,13 @@ import com.into.websoso.core.common.ui.model.ResultFrom.NovelDetailBack
import com.into.websoso.core.common.ui.model.ResultFrom.ProfileEditSuccess
import com.into.websoso.core.common.util.SingleEventHandler
import com.into.websoso.core.common.util.collectWithLifecycle
import com.into.websoso.core.common.util.showWebsosoToast
import com.into.websoso.core.common.util.tracker.Tracker
import com.into.websoso.core.resource.R.drawable.ic_novel_rating_alert
import com.into.websoso.core.resource.R.string.home_rising_feed_for_user
import com.into.websoso.core.resource.R.string.home_taste_load_failed
import com.into.websoso.core.resource.R.string.home_taste_refresh_failed
import com.into.websoso.core.resource.R.string.load_load_title
import com.into.websoso.databinding.FragmentHomeBinding
import com.into.websoso.ui.detailExplore.DetailExploreActivity
import com.into.websoso.ui.feedDetail.FeedDetailActivity
Expand All @@ -31,6 +39,8 @@ import com.into.websoso.ui.main.home.adpater.PopularFeedsAdapter
import com.into.websoso.ui.main.home.adpater.PopularNovelsAdapter
import com.into.websoso.ui.main.home.adpater.RecommendedNovelsByUserTasteAdapter
import com.into.websoso.ui.main.home.dialog.TermsAgreementDialogFragment
import com.into.websoso.ui.main.home.model.HomeTasteStatus
import com.into.websoso.ui.main.home.model.HomeUiState
import com.into.websoso.ui.normalExplore.NormalExploreActivity
import com.into.websoso.ui.notification.NotificationActivity
import com.into.websoso.ui.novelDetail.NovelDetailActivity
Expand All @@ -45,6 +55,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(fragment_home) {

private val homeViewModel: HomeViewModel by viewModels()
private val mainViewModel: MainViewModel by activityViewModels()
private var renderedState: HomeUiState? = null

private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() }

Expand Down Expand Up @@ -80,7 +91,12 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(fragment_home) {

ProfileEditSuccess.RESULT_OK -> {
mainViewModel.updateUserInfo()
homeViewModel.updateNovel()
homeViewModel.updateNovel(
preferencesChanged = result.data?.getBooleanExtra(
ProfileEditActivity.EXTRA_GENRE_PREFERENCES_CHANGED,
false,
) == true,
)
}
}
}
Expand All @@ -102,6 +118,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(fragment_home) {
bindViewModel()
setupAdapter()
setupItemDecoration()
binding.btnHomeTasteRetry.setOnClickListener { homeViewModel.retryTaste() }
setupObserver()
setupDotsIndicator()
onSettingPreferenceGenreClick()
Expand Down Expand Up @@ -162,15 +179,28 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(fragment_home) {

!uiState.loading -> {
binding.wllHome.setWebsosoLoadingVisibility(false)
popularNovelsAdapter.submitList(uiState.popularNovels)
popularFeedsAdapter.submitList(uiState.popularFeeds)
updateRecommendedNovelByUserTasteVisibility(uiState.recommendedNovelsByUserTaste.isEmpty())
recommendedNovelsByUserTasteAdapter.submitList(uiState.recommendedNovelsByUserTaste)
if (renderedState?.popularNovels !== uiState.popularNovels) {
popularNovelsAdapter.submitList(uiState.popularNovels)
}
if (renderedState?.popularFeeds !== uiState.popularFeeds) {
popularFeedsAdapter.submitList(uiState.popularFeeds)
}
if (renderedState?.recommendedNovelsByUserTaste !== uiState.recommendedNovelsByUserTaste) {
recommendedNovelsByUserTasteAdapter.submitList(uiState.recommendedNovelsByUserTaste)
}
renderTasteStatus(uiState)
updateHasNotificationUnread(uiState.isNotificationUnread)
renderedState = uiState
}
}
}

homeViewModel.tasteRefreshFailed.collectWithLifecycle(viewLifecycleOwner) {
val state = homeViewModel.uiState.value ?: return@collectWithLifecycle
if (state.loading || state.error) return@collectWithLifecycle
showWebsosoToast(requireContext(), getString(home_taste_refresh_failed), ic_novel_rating_alert)
}

homeViewModel.isNotificationPermissionFirstLaunched.observe(viewLifecycleOwner) { isFirstLaunch ->
if (isFirstLaunch) {
showNotificationPermissionDialog()
Expand All @@ -193,18 +223,28 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(fragment_home) {
}
}

private fun updateRecommendedNovelByUserTasteVisibility(isRecommendNovelByUserTasteEmpty: Boolean) {
private fun renderTasteStatus(state: HomeUiState) {
with(binding) {
if (isRecommendNovelByUserTasteEmpty) {
clHomeUserRecommendNovel.visibility = View.GONE
clHomeRecommendNovel.visibility = View.VISIBLE
} else {
clHomeUserRecommendNovel.visibility = View.VISIBLE
clHomeRecommendNovel.visibility = View.GONE
}
val empty = state.tasteStatus == HomeTasteStatus.EMPTY
val showContent = state.tasteStatus == HomeTasteStatus.CONTENT
clHomeRecommendNovel.visibility = if (empty) View.VISIBLE else View.GONE
clHomeUserRecommendNovel.visibility = if (empty) View.GONE else View.VISIBLE
rvRecommendNovelByUserTaste.visibility = if (showContent) View.VISIBLE else View.GONE
llHomeTasteStatus.visibility = if (showContent || empty) View.GONE else View.VISIBLE
val error = state.tasteStatus == HomeTasteStatus.ERROR
tvHomeTasteStatus.setText(if (error) home_taste_load_failed else load_load_title)
tvHomeTasteStatus.setTextColor(
ContextCompat.getColor(requireContext(), if (error) gray_300_52515F else gray_200_949399),
)
btnHomeTasteRetry.visibility = if (error) View.VISIBLE else View.GONE
}
}

override fun onDestroyView() {
renderedState = null
super.onDestroyView()
}

private fun updateHasNotificationUnread(hasUnread: Boolean) {
binding.ivHomeNotification.isSelected = hasUnread
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.into.websoso.ui.main.home

import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import retrofit2.HttpException

internal enum class HomeSection {
POPULAR,
FEEDS,
TASTE,
}

internal fun isSessionFailure(error: Throwable): Boolean = error is HttpException && error.code() == 401

/** Requests belong to the Home ViewModel; replacing one never cancels another section. */
internal class HomeSectionLoader(
private val scope: CoroutineScope,
) {
private val jobs = mutableMapOf<HomeSection, Job>()

fun <T> load(
section: HomeSection,
request: suspend () -> T,
success: (T) -> Unit,
failure: (Exception) -> Unit,
): Deferred<Boolean> {
jobs[section]?.cancel()
return scope
.async {
try {
val result = request()
currentCoroutineContext().ensureActive()
success(result)
true
} catch (cancelled: CancellationException) {
throw cancelled
} catch (error: Exception) {
currentCoroutineContext().ensureActive()
failure(error)
false
}
}.also { jobs[section] = it }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.into.websoso.ui.main.home

/** Main-thread only. Tracks when Home can open; content is owned by HomeViewModel. */
internal class HomeSectionRelease {
private val waitingForUpper = mutableSetOf(HomeSection.POPULAR, HomeSection.FEEDS)

fun update(section: HomeSection): Boolean {
waitingForUpper.remove(section)
return waitingForUpper.isEmpty()
}

fun recover(loading: Boolean) {
if (!loading) waitingForUpper.clear()
}
}
Loading
Loading