Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
98328fd
feat: 서재탭 스크롤 상태 ViewModel로 이동 및 scrollToTop 기능 추가
yeonjeen Jul 21, 2025
17689bd
refactor: 스크롤 초기화 함수 네이밍 변경
yeonjeen Jul 21, 2025
9037cb9
refactor: remember 대신 ViewModel의 스크롤 상태 사용
yeonjeen Jul 21, 2025
a018d99
feat: 스크롤 초기화 함수 resetScrollPosition 추가
yeonjeen Jul 21, 2025
f2c1b80
refactor: 프레그먼트 전환 방식 변경 및 서재탭 재클릭시 스크롤 초기화 구현
yeonjeen Jul 21, 2025
1776fe3
refactor: 린트 해결
yeonjeen Jul 21, 2025
b51fee3
refactor: 프레그먼트 각자 Tag관리하는 구조로 수정
yeonjeen Jul 22, 2025
4df8e23
feat: 바텀 탬 뷰들 배경컬러 추가
yeonjeen Jul 22, 2025
34336e4
refactor: setupInitialFragment()함수 fragment KTX DSL 적용
yeonjeen Jul 22, 2025
8ef02c0
refactor: replaceCurrentFragment 함수 fragment KTX DSL 적용
yeonjeen Jul 22, 2025
de29480
refactor: resetScrollPosition을 스크롤 이벤트 방식으로 변경
yeonjeen Jul 22, 2025
1582ebf
feat: scrollToTopEvent 수신하여 리스트 최상단 이동 처리
yeonjeen Jul 22, 2025
c6454ce
refactor: resetScrollPosition FragmentResultListener로 이벤트 전달
yeonjeen Jul 22, 2025
13bb841
refactor: 스크롤 이동 위치 매직 넘버 상수화 및 repeatOnLifecycle 적용
yeonjeen Jul 23, 2025
e5ae047
refactor: scrollToTopEvent를 Channel 기반 UI 이벤트로 전환
yeonjeen Jul 23, 2025
81a53c4
refactor: scrollToTopEvent 수집에 collectAsEventWithLifecycle 확장함수 적용
yeonjeen Jul 24, 2025
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
185 changes: 105 additions & 80 deletions app/src/main/java/com/into/websoso/ui/main/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package com.into.websoso.ui.main

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.annotation.IntegerRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import com.into.websoso.R.id.fcv_main
import com.into.websoso.R.id.menu_explore
import com.into.websoso.R.id.menu_feed
Expand Down Expand Up @@ -40,12 +38,22 @@ import dagger.hilt.android.AndroidEntryPoint
class MainActivity : BaseActivity<ActivityMainBinding>(activity_main) {
private val mainViewModel: MainViewModel by viewModels()
private var backPressedTime: Long = 0L
private var currentFragment: Fragment? = null
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c: 근데 이 프로퍼티는 어디에 사용하나요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaceCurrentFragment 함수에서 사용되고 있어요!

private var currentSelectedItemId: Int = menu_home

private val fragmentTags = mapOf(
menu_home to HomeFragment::class.java.name,
menu_explore to ExploreFragment::class.java.name,
menu_feed to FeedFragment::class.java.name,
menu_library to LibraryFragment::class.java.name,
menu_my_page to MyPageFragment::class.java.name,
)
Comment on lines +44 to +50
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r: 각 프래그먼트에서 관리하면 좋을 것 같아요!


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setupBackButtonListener()
setBottomNavigationView()
setupBottomNavigationView()
setupObserver()
onViewGuestClick()
handleNavigation(intent.getSerializableExtra(DESTINATION_KEY) as? FragmentType)
Expand Down Expand Up @@ -75,13 +83,74 @@ class MainActivity : BaseActivity<ActivityMainBinding>(activity_main) {
}
}

private fun setBottomNavigationView() {
@SuppressLint("CommitTransaction")
private fun setupBottomNavigationView() {
setupInitialFragment()
setupBottomNavListener()
binding.bnvMain.selectedItemId = menu_home
replaceFragment<HomeFragment>()
}

private fun setupInitialFragment() {
val initialItemId = menu_home
val initialTag = fragmentTags[initialItemId]!!
val initialFragment = findOrCreateFragment(initialTag)

if (!initialFragment.isAdded) {
supportFragmentManager
.beginTransaction()
.add(fcv_main, initialFragment, initialTag)
.commit()
}

currentFragment = initialFragment
}
Comment on lines +93 to +108
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r:
각 프래그먼트에 선언된 태그를 사용해 initialFragment를 가져오는 식으로 수정해보면 좋을 것 같아요
추가로 제공되는 fragment KTX DSL을 이용해주세요!

supportFragmentManager.commit {
        setReorderingAllowed(true)
        if (!initialFragment.isAdded) {
            add(R.id.fcv_main, initialFragment, initialTag)
            ...
            // 로직에 맞게
        }
    }

내부에 currentFragment를 직접 관리하는 것보다, supportFragmentManager에게 현재 프래그먼트를 요청해주세요


private fun setupBottomNavListener() {
binding.bnvMain.setOnItemSelectedListener { item ->
if (item.itemId == currentSelectedItemId && item.itemId == menu_library) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c: currentSelectedItemId 로 내부 상태를 이용하기보다 supportFragmentManager를 통해 조회 및 비교해보시죠
만약 현재 프래그먼트가 라이브러리가 맞다면, 굳이 아래에 libraryFragment를 가져오지 않고 바로 supportFragmentManager를 통해 불러와도 될 것 같아요

val libraryFragment = supportFragmentManager.findFragmentByTag(
LibraryFragment::class.java.name,
) as? LibraryFragment

libraryFragment?.resetScrollPosition()
Copy link
Copy Markdown
Member

@s9hn s9hn Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c: fragment로 함수를 직접 호출하는 것보단, 메시지를 전달하는 구조가 좋을 것 같아요.
FragmentManager를 통해 Bundle 메시지 전달을 위임해도 좋아보입니다!

// 송신
supportFragmentManager.setFragmentResult( 키값, 번들.Empty)

// 수신
parentFragmentManager.setFragmentResultListener (키값, viewLifecycleowner) {
 위로 스크롤
}

이렇게 구현하면, libraryFragment를 직접 find할 필요도 없겠네요 !

} else {
replaceCurrentFragment(item.itemId)
currentSelectedItemId = item.itemId
}

binding.bnvMain.setOnItemSelectedListener(::replaceFragment)
true
}
}

private fun replaceCurrentFragment(itemId: Int) {
val tag = fragmentTags[itemId]!!
val targetFragment = findOrCreateFragment(tag)

val transaction = supportFragmentManager.beginTransaction()

currentFragment?.let { transaction.hide(it) }

if (!targetFragment.isAdded) {
transaction.add(fcv_main, targetFragment, tag)
} else {
transaction.show(targetFragment)
}

transaction.commit()
currentFragment = targetFragment
}
Comment on lines +125 to +150
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r: 위랑 동일합니다! fragment dsl로 변경해주세요!


private fun findOrCreateFragment(tag: String): Fragment =
supportFragmentManager.findFragmentByTag(tag)
?: when (tag) {
HomeFragment::class.java.name -> HomeFragment()
ExploreFragment::class.java.name -> ExploreFragment()
FeedFragment::class.java.name -> FeedFragment()
LibraryFragment::class.java.name -> LibraryFragment()
MyPageFragment::class.java.name -> MyPageFragment()
else -> throw IllegalArgumentException("Unknown fragment tag: $tag")
}

Comment on lines +143 to +161
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c: 여기도 각 프래그먼트에 정의된 태그를 기반으로 !

private fun setupObserver() {
mainViewModel.isLogin.observe(this) { isLogin ->
when (isLogin) {
Expand All @@ -101,87 +170,22 @@ class MainActivity : BaseActivity<ActivityMainBinding>(activity_main) {
}
}

private fun replaceFragment(item: MenuItem): Boolean {
when (FragmentType.valueOf(item.itemId)) {
HOME -> replaceFragment<HomeFragment>()
EXPLORE -> replaceFragment<ExploreFragment>()
FEED -> replaceFragment<FeedFragment>()
LIBRARY -> replaceFragment<LibraryFragment>()
MY_PAGE -> replaceFragment<MyPageFragment>()
}
return true
}

private inline fun <reified T : Fragment> replaceFragment() {
supportFragmentManager.commit {
replace<T>(fcv_main)
setReorderingAllowed(true)
}
}

enum class FragmentType(
@IntegerRes private val resId: Int,
) {
LIBRARY(menu_library),
HOME(menu_home),
EXPLORE(menu_explore),
FEED(menu_feed),
MY_PAGE(menu_my_page),
;

companion object {
fun valueOf(id: Int): FragmentType =
entries.find { fragmentType -> fragmentType.resId == id }
?: throw IllegalArgumentException()

fun valueOf(fragmentName: String): FragmentType =
entries.find { fragmentType -> fragmentType.name == fragmentName }
?: throw IllegalArgumentException()
}
}

private fun showLoginRequestDialog() {
val dialog = LoginRequestDialogFragment.newInstance()
dialog.show(supportFragmentManager, LoginRequestDialogFragment.TAG)
}

private fun handleNavigation(destination: FragmentType?) {
when (destination) {
EXPLORE -> selectFragment(EXPLORE)
MY_PAGE -> selectFragment(MY_PAGE)
FEED -> selectFragment(FEED)
LIBRARY -> selectFragment(LIBRARY)
HOME, null -> selectFragment(HOME)
val menuId = when (destination) {
EXPLORE -> menu_explore
MY_PAGE -> menu_my_page
FEED -> menu_feed
LIBRARY -> menu_library
HOME, null -> menu_home
}
}

private fun selectFragment(fragmentType: FragmentType) {
when (fragmentType) {
HOME -> {
binding.bnvMain.selectedItemId = menu_home
replaceFragment<HomeFragment>()
}

EXPLORE -> {
binding.bnvMain.selectedItemId = menu_explore
replaceFragment<ExploreFragment>()
}

FEED -> {
binding.bnvMain.selectedItemId = menu_feed
replaceFragment<FeedFragment>()
}

LIBRARY -> {
binding.bnvMain.selectedItemId = menu_library
replaceFragment<LibraryFragment>()
}

MY_PAGE -> {
binding.bnvMain.selectedItemId = menu_my_page
replaceFragment<MyPageFragment>()
}
}
binding.bnvMain.selectedItemId = menuId
replaceCurrentFragment(menuId)
}

companion object {
Expand Down Expand Up @@ -210,4 +214,25 @@ class MainActivity : BaseActivity<ActivityMainBinding>(activity_main) {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}

enum class FragmentType(
@IntegerRes val resId: Int,
) {
LIBRARY(menu_library),
HOME(menu_home),
EXPLORE(menu_explore),
FEED(menu_feed),
MY_PAGE(menu_my_page),
;

companion object {
fun valueOf(id: Int): FragmentType =
entries.find { it.resId == id }
?: throw IllegalArgumentException()

fun valueOf(fragmentName: String): FragmentType =
entries.find { it.name == fragmentName }
?: throw IllegalArgumentException()
}
Comment on lines +237 to +244
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

예외 처리 시 의미 있는 메시지 추가

예외 생성 시 구체적인 오류 메시지를 포함하여 디버깅을 용이하게 하세요.

fun valueOf(id: Int): FragmentType =
    entries.find { it.resId == id }
-        ?: throw IllegalArgumentException()
+        ?: throw IllegalArgumentException("Unknown fragment resource id: $id")

fun valueOf(fragmentName: String): FragmentType =
    entries.find { it.name == fragmentName }
-        ?: throw IllegalArgumentException()
+        ?: throw IllegalArgumentException("Unknown fragment name: $fragmentName")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun valueOf(id: Int): FragmentType =
entries.find { it.resId == id }
?: throw IllegalArgumentException()
fun valueOf(fragmentName: String): FragmentType =
entries.find { it.name == fragmentName }
?: throw IllegalArgumentException()
}
fun valueOf(id: Int): FragmentType =
entries.find { it.resId == id }
?: throw IllegalArgumentException("Unknown fragment resource id: $id")
fun valueOf(fragmentName: String): FragmentType =
entries.find { it.name == fragmentName }
?: throw IllegalArgumentException("Unknown fragment name: $fragmentName")
🧰 Tools
🪛 detekt (1.23.8)

[warning] 231-231: A call to the default constructor of an exception was detected. Instead one of the constructor overloads should be called. This allows to provide more meaningful exceptions.

(detekt.exceptions.ThrowingExceptionsWithoutMessageOrCause)


[warning] 235-235: A call to the default constructor of an exception was detected. Instead one of the constructor overloads should be called. This allows to provide more meaningful exceptions.

(detekt.exceptions.ThrowingExceptionsWithoutMessageOrCause)

🤖 Prompt for AI Agents
In app/src/main/java/com/into/websoso/ui/main/MainActivity.kt around lines 229
to 236, the IllegalArgumentException thrown when no matching FragmentType is
found lacks a descriptive message. Update both valueOf functions to include
meaningful error messages in the exceptions, specifying which id or fragmentName
caused the error to improve debugging clarity.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.into.websoso.R
import com.into.websoso.core.common.navigator.NavigatorProvider
import com.into.websoso.core.designsystem.theme.WebsosoTheme
import com.into.websoso.feature.library.LibraryScreen
import com.into.websoso.feature.library.LibraryViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

Expand Down Expand Up @@ -43,4 +45,9 @@ class LibraryFragment : Fragment() {
}
return view
}

fun resetScrollPosition() {
val viewModel: LibraryViewModel by viewModels()
viewModel.resetScrollPosition()
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r: 코드래빗 말대로 함수호출마다 매번 뷰모델을 생성하고 있어요
Fragment API를 이용해봅시다

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 Fragment와 LibraryScreen이 같은 함수를 공유해야하니, viewModel을 매번 생성하는 것보다 Fragment에서 생성하고 Screen에 주입하는 구조가 좋을 것 같습니다!

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.material3.rememberModalBottomSheetState
Expand Down Expand Up @@ -56,8 +54,8 @@ fun LibraryScreen(
.map { it.map { novel -> novel.toUiModel() } }
.collectAsLazyPagingItems()
var isShowBottomSheet by remember { mutableStateOf(false) }
val listState = rememberLazyListState()
val gridState = rememberLazyGridState()
val listState = libraryViewModel.listState
val gridState = libraryViewModel.gridState
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
confirmValueChange = { false },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package com.into.websoso.feature.library

import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
Expand Down Expand Up @@ -35,6 +40,12 @@ class LibraryViewModel
private val _uiState = MutableStateFlow(LibraryUiState())
val uiState: StateFlow<LibraryUiState> = _uiState.asStateFlow()

var listState by mutableStateOf(LazyListState())
private set

var gridState by mutableStateOf(LazyGridState())
private set

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r: mutableStateOf는 Compose API입니다! 뷰모델보단 뷰에 어울리는 의존성이예요

init {
updateMyLibraryFilter()
}
Expand Down Expand Up @@ -88,4 +99,14 @@ class LibraryViewModel
it.copy(isInterested = !it.isInterested)
}
}

fun resetScrollPosition() {
viewModelScope.launch {
if (uiState.value.isGrid) {
gridState.scrollToItem(0)
} else {
listState.scrollToItem(0)
}
}
}
Comment on lines +103 to +101
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r: 이벤트를 전파하고, 뷰에서 이를 감지해 동작하도록 구현해도 좋겠어요

}