diff --git a/feature/article/src/main/AndroidManifest.xml b/feature/article/src/main/AndroidManifest.xml index 5f5a50285..2ed45a026 100644 --- a/feature/article/src/main/AndroidManifest.xml +++ b/feature/article/src/main/AndroidManifest.xml @@ -4,21 +4,9 @@ - - - - - - - - - + android:windowSoftInputMode="adjustResize" /> diff --git a/feature/home/src/main/java/in/koreatech/koin/feature/home/profile/Constant.kt b/feature/home/src/main/java/in/koreatech/koin/feature/home/profile/Constant.kt index bbbc5f038..8b7bc9419 100644 --- a/feature/home/src/main/java/in/koreatech/koin/feature/home/profile/Constant.kt +++ b/feature/home/src/main/java/in/koreatech/koin/feature/home/profile/Constant.kt @@ -1,2 +1,2 @@ package `in`.koreatech.koin.feature.home.profile -const val DEEPLINK_NEW_MAIN_PROFILE = "koin://newmain/navigation" +const val DEEPLINK_MAIN_PROFILE = "koin://main/navigation" diff --git a/feature/home/src/main/java/in/koreatech/koin/feature/home/profile/ProfileScreen.kt b/feature/home/src/main/java/in/koreatech/koin/feature/home/profile/ProfileScreen.kt index 286dccf02..bdcaa4699 100644 --- a/feature/home/src/main/java/in/koreatech/koin/feature/home/profile/ProfileScreen.kt +++ b/feature/home/src/main/java/in/koreatech/koin/feature/home/profile/ProfileScreen.kt @@ -75,16 +75,14 @@ fun ProfileScreen( viewModel.onLogoutClick() } else { EventLogger.logCampusClickEvent(AnalyticsConstant.Label.LOGIN, "로그인") - navigator.navigateToSignIn(context, DEEPLINK_NEW_MAIN_PROFILE).let { context.startActivity(it) } + navigator.navigateToSignIn(context, DEEPLINK_MAIN_PROFILE).let { context.startActivity(it) } } }, onSettingClick = { - if (uiState.isLoggedIn) { - onNavigateToSetting() - } else { + if (!uiState.isLoggedIn) { EventLogger.logCampusClickEvent(AnalyticsConstant.Label.LOGIN_PROMPT, "설정(비로그인)") - navigator.navigateToSignIn(context, DEEPLINK_NEW_MAIN_PROFILE).let { context.startActivity(it) } } + onNavigateToSetting() }, onNotificationClick = { EventLogger.logCampusClickEvent(AnalyticsConstant.Label.NOTIFICATION, "알림 아이콘") diff --git a/koin/detekt-baseline.xml b/koin/detekt-baseline.xml index 1cbfa184e..6a89e57ce 100644 --- a/koin/detekt-baseline.xml +++ b/koin/detekt-baseline.xml @@ -15,12 +15,10 @@ LongMethod:KoinNavigationDrawerActivity.kt$KoinNavigationDrawerActivity$override fun onPostCreate(savedInstanceState: Bundle?) LongMethod:KoinNavigationDrawerActivity.kt$KoinNavigationDrawerActivity$private fun initDrawerViewModel() LongMethod:LandDetailActivity.kt$LandDetailActivity$private fun setRoomOption(landDetail: LandDetail) - LongMethod:MainActivity.kt$MainActivity$private fun initView() LongMethod:NotificationActivity.kt$NotificationActivity$private fun observeData() LongMethod:SettingActivity.kt$SettingActivity$private fun initListeners() LongMethod:TimetableActivity.kt$TimetableActivity$private fun initComposeView() LongMethod:TimetableSemesterActivity.kt$TimetableSemesterActivity$override fun onCreate(savedInstanceState: Bundle?) - PrintStackTrace:MainActivity.kt$MainActivity$e SpreadOperator:MainActivity.kt$MainActivity$( context = this, type = Pair(EXTRA_TYPE, type), *arrayOf( Pair(EXTRA_ID, targetId), Pair(EXTRA_BOARD_ID, targetBoardId), Pair(EXTRA_ARTICLE_ID, targetArticleId), Pair(EXTRA_CHAT_ROOM_ID, targetChatId), Pair(EXTRA_CLUB_ID, targetClubId), Pair(EXTRA_EVENT_ID, targetEventId) ) ) SpreadOperator:SchemeActivity.kt$SchemeActivity$( context = this, type = Pair(EXTRA_TYPE, url?.toHost()), *(getExtraIds(url ?: "").toTypedArray()) ) SpreadOperator:SchemeActivity.kt$SchemeActivity$( context = this, type = Pair(EXTRA_TYPE, url?.toHost()), navType = Pair(EXTRA_NAV_TYPE, NavigatorType.MAIN.type), *(getExtraIds(url ?: "").toTypedArray()) ) @@ -29,7 +27,7 @@ SwallowedException:KakaoTalkLinkExtensions.kt$e: NameNotFoundException TooGenericExceptionCaught:KoinFirebaseMessagingService.kt$KoinFirebaseMessagingService$e: Exception TooGenericExceptionCaught:KoinNavigationDrawerViewModel.kt$KoinNavigationDrawerViewModel$e: Exception - TooGenericExceptionCaught:MainActivity.kt$MainActivity$e: Exception + TooGenericExceptionCaught:MainActivityViewModel.kt$MainActivityViewModel$e: Exception TooManyFunctions:AuthNetworkModule.kt$AuthNetworkModule TooManyFunctions:KoinNavigationDrawerActivity.kt$KoinNavigationDrawerActivity : ActivityBaseOnNavigationItemSelectedListener TooManyFunctions:NotificationActivity.kt$NotificationActivity : ActivityBase diff --git a/koin/src/main/AndroidManifest.xml b/koin/src/main/AndroidManifest.xml index ee33b8819..ff61795e0 100644 --- a/koin/src/main/AndroidManifest.xml +++ b/koin/src/main/AndroidManifest.xml @@ -99,17 +99,15 @@ android:pathPrefix="/navigation" android:scheme="koin" /> - - - - - - + + + + + diff --git a/koin/src/main/java/in/koreatech/koin/navigation/SchemeType.kt b/koin/src/main/java/in/koreatech/koin/navigation/SchemeType.kt index f0bc5b7cc..b5aef7b26 100644 --- a/koin/src/main/java/in/koreatech/koin/navigation/SchemeType.kt +++ b/koin/src/main/java/in/koreatech/koin/navigation/SchemeType.kt @@ -1,6 +1,5 @@ package `in`.koreatech.koin.navigation -import `in`.koreatech.koin.feature.article.ArticleActivity import `in`.koreatech.koin.feature.callvan.CallvanActivity import `in`.koreatech.koin.feature.chat.ui.groupchat.GroupChatActivity import `in`.koreatech.koin.feature.chat.ui.room.ChatRoomActivity @@ -8,6 +7,7 @@ import `in`.koreatech.koin.feature.club.ui.ClubActivity import `in`.koreatech.koin.feature.dining.ui.DiningActivity import `in`.koreatech.koin.feature.lostandfound.ui.LostAndFoundActivity import `in`.koreatech.koin.feature.store.StoreActivity +import `in`.koreatech.koin.ui.main.activity.MainActivity enum class SchemeType( val type: String, @@ -15,7 +15,7 @@ enum class SchemeType( ) { SHOP("shop", StoreActivity::class.java), DINING("dining", DiningActivity::class.java), - ARTICLE("keyword", ArticleActivity::class.java), + ARTICLE("keyword", MainActivity::class.java), CHAT("chat", ChatRoomActivity::class.java), CLUB_RECRUIT("club-recruitment", ClubActivity::class.java), CLUB("club", ClubActivity::class.java), diff --git a/koin/src/main/java/in/koreatech/koin/ui/main/activity/MainActivity.kt b/koin/src/main/java/in/koreatech/koin/ui/main/activity/MainActivity.kt index feb2bc2c3..a0fc35dd8 100644 --- a/koin/src/main/java/in/koreatech/koin/ui/main/activity/MainActivity.kt +++ b/koin/src/main/java/in/koreatech/koin/ui/main/activity/MainActivity.kt @@ -1,35 +1,32 @@ package `in`.koreatech.koin.ui.main.activity +import android.Manifest import android.content.Intent -import android.net.Uri +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.graphics.Insets +import androidx.core.os.bundleOf import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updateLayoutParams +import androidx.core.view.isVisible +import androidx.core.view.updatePadding import androidx.lifecycle.Lifecycle -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.NavigationUI +import androidx.navigation.ui.setupWithNavController import dagger.hilt.android.AndroidEntryPoint -import `in`.koreatech.bus.BusSearchActivity -import `in`.koreatech.bus.BusTimetableActivity -import `in`.koreatech.bus.screen.MainEntryView import `in`.koreatech.koin.R import `in`.koreatech.koin.core.analytics.AnalyticsConstant -import `in`.koreatech.koin.core.analytics.EventAction import `in`.koreatech.koin.core.analytics.EventLogger -import `in`.koreatech.koin.core.analytics.EventUtils -import `in`.koreatech.koin.core.designsystem.theme.KoinTheme +import `in`.koreatech.koin.core.designsystem.util.enableEdgeToEdgeWithLightStatusBar import `in`.koreatech.koin.core.navigation.Navigator import `in`.koreatech.koin.core.navigation.utils.EXTRA_ARTICLE_ID import `in`.koreatech.koin.core.navigation.utils.EXTRA_BOARD_ID @@ -38,212 +35,120 @@ import `in`.koreatech.koin.core.navigation.utils.EXTRA_CLUB_ID import `in`.koreatech.koin.core.navigation.utils.EXTRA_EVENT_ID import `in`.koreatech.koin.core.navigation.utils.EXTRA_ID import `in`.koreatech.koin.core.navigation.utils.EXTRA_TYPE -import `in`.koreatech.koin.core.util.blueStatusBar -import `in`.koreatech.koin.core.util.dataBinding -import `in`.koreatech.koin.databinding.ActivityMainBinding -import `in`.koreatech.koin.domain.model.article.ArticleNotiType -import `in`.koreatech.koin.feature.article.ArticleActivity +import `in`.koreatech.koin.databinding.ActivityNewMainBinding import `in`.koreatech.koin.feature.banner.ui.BannerActivity -import `in`.koreatech.koin.feature.callvan.CallvanEntry -import `in`.koreatech.koin.feature.lostandfound.DEEP_LINK_LOST_AND_FOUND_BASE -import `in`.koreatech.koin.feature.lostandfound.ui.LostAndFoundActivity -import `in`.koreatech.koin.feature.lostandfound.ui.entry.LostAndFoundEntry -import `in`.koreatech.koin.feature.store.MainStoreWidget import `in`.koreatech.koin.navigation.SchemeType -import `in`.koreatech.koin.ui.main.compose.HotArticlePager +import `in`.koreatech.koin.ui.main.fragment.ArticleFragment import `in`.koreatech.koin.ui.main.viewmodel.MainActivityViewModel -import `in`.koreatech.koin.ui.main.widget.DiningWidget -import `in`.koreatech.koin.ui.navigation.KoinNavigationDrawerTimeActivity -import `in`.koreatech.koin.ui.navigation.state.MenuState -import `in`.koreatech.koin.ui.unibus.UnibusActivity -import `in`.koreatech.koin.util.ext.observeLiveData import javax.inject.Inject import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @AndroidEntryPoint -class MainActivity : KoinNavigationDrawerTimeActivity() { - override val menuState = MenuState.Main - private val binding by dataBinding(R.layout.activity_main) - override val screenTitle = "코인 - 메인" - private val viewModel by viewModels() +class MainActivity : AppCompatActivity() { - private var scrollPercentage = 0.0f + private lateinit var binding: ActivityNewMainBinding + private lateinit var navController: NavController + private val viewModel by viewModels() @Inject lateinit var navigator: Navigator - override fun onCreate(savedInstanceState: Bundle?) { - enableEdgeToEdge() - super.onCreate(savedInstanceState) - setContentView(binding.root) - - window.blueStatusBar() - - EventLogger.logScreenName("HomeActivity") // DA requirement - - initView() - initViewModel() - handleIntent() + private fun navigationLogValue(itemId: Int): Pair? = when (itemId) { + R.id.bottom_navigation_home -> AnalyticsConstant.Label.NAV_HOME to "홈" + R.id.bottom_navigation_category -> AnalyticsConstant.Label.NAV_CATEGORY to "카테고리" + R.id.bottom_navigation_article -> AnalyticsConstant.Label.NAV_BULLETIN to "게시판" + R.id.bottom_navigation_profile -> AnalyticsConstant.Label.NAV_PROFILE to "프로필" + else -> null } - override fun onResume() { - super.onResume() - viewModel.checkKeywordNotiContent() - viewModel.updateDining() - } - - private fun initView() = with(binding) { - viewModel.checkKeywordNotiContent() - - ViewCompat.setOnApplyWindowInsetsListener(binding.toolbarLayout) { v, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.updateLayoutParams { - leftMargin = systemBars.left - topMargin = systemBars.top - rightMargin = systemBars.right + private val requestMainPermissionLauncher = + registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permission -> + var permissionGranted = true + permission.entries.forEach { + if (it.key in MAIN_REQUIRED_PERMISSION && it.value == false) { + permissionGranted = false + } } - insets - } - binding.nestedScrollViewMain.setOnScrollChangeListener { _, _, _, _, _ -> - val offset = binding.nestedScrollViewMain.computeVerticalScrollOffset() - val extent = binding.nestedScrollViewMain.computeVerticalScrollExtent() - val range = binding.nestedScrollViewMain.computeVerticalScrollRange() - - val newScrollPercentage = 100.0f * offset / (range - extent) - if (EventUtils.didCrossedScrollThreshold( - scrollPercentage, - newScrollPercentage - ) && scrollPercentage.toDouble() != .0 - ) { - EventLogger.logScrollEvent( - EventAction.CAMPUS, - AnalyticsConstant.Label.MAIN_SCROLL, - "70%" - ) + if (!permissionGranted) { + // handle permission granted + } else { + // handle permission denied } - scrollPercentage = 100.0f * offset / (range - extent) } - buttonCategory.setOnClickListener { - toggleNavigationDrawer() - } + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdgeWithLightStatusBar() + super.onCreate(savedInstanceState) + binding = ActivityNewMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + EventLogger.logScreenName("HomeActivity") // DA requirement - initHotArticleView() + ViewCompat.setOnApplyWindowInsetsListener(binding.bottomNavigationMain) { view, insets -> + val navigationBars = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) - textSeeMoreArticle.setOnClickListener { - EventLogger.logClickEvent( - EventAction.CAMPUS, - AnalyticsConstant.Label.APP_MAIN_NOTICE_DETAIL, - getString(R.string.article_more) + view.updatePadding( + bottom = navigationBars.bottom ) - startActivity(Intent(this@MainActivity, ArticleActivity::class.java)) - } - busComposeView.apply { - setContent { - MainEntryView( - onShuttleTicketClicked = { - EventLogger.logCampusClickEvent( - "shuttle_ticket", - "셔틀 탑승권" - ) - val intent = Intent(this@MainActivity, UnibusActivity::class.java) - intent.putExtra("url", "https://koreatech.unibus.kr/") - startActivity(intent) - }, - onTimetableCardClicked = { - EventLogger.logCampusClickEvent( - "main_bus_timetable", - "버스 시간표 바로가기" - ) - val intent = Intent(this@MainActivity, BusTimetableActivity::class.java) - startActivity(intent) - }, - onSearchCardClicked = { - EventLogger.logCampusClickEvent( - "main_bus_search", - "가장 빠른 버스 조회하기" - ) - val intent = Intent(this@MainActivity, BusSearchActivity::class.java) - startActivity(intent) - }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - ) - } + WindowInsetsCompat.Builder(insets) + .setInsets(WindowInsetsCompat.Type.navigationBars(), Insets.NONE) + .build() } - callvanComposeView.setContent { - KoinTheme { - CallvanEntry() - } - } + val navHostFragment = supportFragmentManager.findFragmentById(binding.navHostFragmentMain.id) as NavHostFragment + navController = navHostFragment.navController - lostandfoundComposeView.apply { - setContent { - KoinTheme { - val lostAndFoundState by viewModel.lostAndFoundEntryState.collectAsStateWithLifecycle() - lostAndFoundState?.let { - LostAndFoundEntry( - postCount = it.postCount, - foundCount = it.foundCount, - onClick = { - EventLogger.logCampusClickEvent( - label = "lost_item_entry", - value = "분실물" - ) - val intent = Intent(this@MainActivity, LostAndFoundActivity::class.java) - startActivity(intent) - } - ) - } - } + binding.bottomNavigationMain.setupWithNavController(navController) + + // setOnItemReselectedListener를 따로 등록하면 재탭 시 기본 popUpTo/restoreState 동작이 사라지므로 등록하지 않는다. + // 리스너가 없으면 재탭도 이 setOnItemSelectedListener로 그대로 들어와 로깅과 기본 동작이 모두 보존된다. + binding.bottomNavigationMain.setOnItemSelectedListener { item -> + navigationLogValue(item.itemId)?.let { (label, value) -> + EventLogger.logCampusClickEvent(label, value) } + NavigationUI.onNavDestinationSelected(item, navController) } - shopComposeView.setContent { - val storeCategories by viewModel.storeCategories.collectAsState() + val topLevelDestinations = setOf( + R.id.bottom_navigation_home, + R.id.bottom_navigation_category, + R.id.bottom_navigation_article, + R.id.bottom_navigation_profile + ) - MainStoreWidget( - categories = storeCategories - ) { categoryId -> - gotoStoreActivity(categoryId) - } + navController.addOnDestinationChangedListener { _, destination, _ -> + binding.bottomNavigationMain.isVisible = destination.id in topLevelDestinations } - mainSwipeRefreshLayout.setOnRefreshListener { - viewModel.updateDining() - } + init() + handleIntent() + } - diningComposeView.apply { - setContent { - KoinTheme { - val diningData by viewModel.diningData.collectAsStateWithLifecycle() - val selectedPosition by viewModel.selectedPosition.collectAsStateWithLifecycle() - val selectedType by viewModel.selectedType.collectAsStateWithLifecycle() - - DiningWidget( - diningData = diningData, - selectedPosition = selectedPosition, - selectedType = selectedType - ) - } - } - } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + handleIntent() } - private fun initViewModel() = with(viewModel) { - observeLiveData(isLoading) { - binding.mainSwipeRefreshLayout.isRefreshing = it + private fun init() { + if (!checkMainPermission()) { + requestMainPermissionLauncher.launch( + MAIN_REQUIRED_PERMISSION + ) } + viewModel.updateDeviceToken() lifecycleScope.launch { - getStoreCategories() - binding.textViewStore.visibility = View.GONE - binding.shopComposeView.visibility = View.VISIBLE + repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel.showBottomNav.collectLatest { + binding.bottomNavigationMain.isVisible = it + } + } } } @@ -260,54 +165,9 @@ class MainActivity : KoinNavigationDrawerTimeActivity() { } } - private fun initHotArticleView() { - binding.composeViewHotArticle.setContent { - KoinTheme { - val articleMain by viewModel.articleMain.collectAsState() - - HotArticlePager( - articles = articleMain, - onNotiClick = { - EventLogger.logClickEvent( - EventAction.CAMPUS, - AnalyticsConstant.Label.TO_MANAGE_KEYWORD, - it.value - ) - val intent = - Intent(Intent.ACTION_VIEW).apply { - data = when (it.type) { - ArticleNotiType.KEYWORD -> Uri.parse("koin://article/navigation?fragment=article_keyword") - ArticleNotiType.LOST_AND_FOUND -> Uri.parse(DEEP_LINK_LOST_AND_FOUND_BASE) - } - } - intent.`package` = packageName - startActivity(intent) - }, - onArticleClick = { - EventLogger.logClickEvent( - EventAction.CAMPUS, - AnalyticsConstant.Label.POPULAR_NOTICE_BANNER, - it.title - ) - val intent = - Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse("koin://article/navigation?fragment=article_detail&article_id=${it.id}&board_id=${it.boardId}") - } - intent.`package` = packageName - startActivity(intent) - } - ) - } - } - } - - private fun gotoStoreActivity(id: Int) { - val bundle = Bundle() - bundle.putInt(STORE_CATEGORY, id) - callDrawerItem(R.id.navi_item_store, bundle) - } - private fun handleIntent() { + if (navigateToArticleFromDeepLink()) return + val targetId = intent.getIntExtra(EXTRA_ID, -1) val targetBoardId = intent.getIntExtra(EXTRA_BOARD_ID, -1) val targetArticleId = intent.getIntExtra(EXTRA_ARTICLE_ID, -1) @@ -317,9 +177,22 @@ class MainActivity : KoinNavigationDrawerTimeActivity() { val type = intent.getStringExtra(EXTRA_TYPE) ?: "" when (type) { + SchemeType.ARTICLE.type -> { + val articleId = if (targetId != -1) targetId else targetArticleId + if (articleId != -1) { + navController.navigate( + R.id.bottom_navigation_article, + bundleOf( + ArticleFragment.FRAGMENT to ArticleFragment.ARTICLE_DETAIL, + EXTRA_ARTICLE_ID to articleId, + EXTRA_BOARD_ID to targetBoardId + ) + ) + } + } + SchemeType.SHOP.type, SchemeType.DINING.type, - SchemeType.ARTICLE.type, SchemeType.CHAT.type, SchemeType.CLUB_RECRUIT.type, SchemeType.CLUB.type -> { @@ -344,7 +217,36 @@ class MainActivity : KoinNavigationDrawerTimeActivity() { } } + // koin://article/navigation?fragment=article_detail&article_id=..&board_id=.. 형태의 내부 딥링크 처리 + private fun navigateToArticleFromDeepLink(): Boolean { + val uri = intent.data ?: return false + if (uri.host != ARTICLE_DEEP_LINK_HOST) return false + + navController.navigate( + R.id.bottom_navigation_article, + bundleOf( + ArticleFragment.FRAGMENT to uri.getQueryParameter(ArticleFragment.FRAGMENT), + EXTRA_ARTICLE_ID to (uri.getQueryParameter("article_id")?.toIntOrNull() ?: -1), + EXTRA_BOARD_ID to (uri.getQueryParameter("board_id")?.toIntOrNull() ?: -1) + ) + ) + return true + } + + private fun checkMainPermission() = MAIN_REQUIRED_PERMISSION.all { + ContextCompat.checkSelfPermission( + this, it + ) == PackageManager.PERMISSION_GRANTED + } + companion object { - private const val STORE_CATEGORY = "STORE_CATEGORY" + private const val ARTICLE_DEEP_LINK_HOST = "article" + + private val MAIN_REQUIRED_PERMISSION = + mutableListOf().apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + add(Manifest.permission.POST_NOTIFICATIONS) + } + }.toTypedArray() } } diff --git a/koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/ArticleFragment.kt b/koin/src/main/java/in/koreatech/koin/ui/main/fragment/ArticleFragment.kt similarity index 74% rename from koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/ArticleFragment.kt rename to koin/src/main/java/in/koreatech/koin/ui/main/fragment/ArticleFragment.kt index 9e947db34..14020e277 100644 --- a/koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/ArticleFragment.kt +++ b/koin/src/main/java/in/koreatech/koin/ui/main/fragment/ArticleFragment.kt @@ -1,15 +1,16 @@ -package `in`.koreatech.koin.ui.newmain.fragment +package `in`.koreatech.koin.ui.main.fragment import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.bundleOf import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.doOnAttach import androidx.core.view.updatePadding import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -22,10 +23,15 @@ import `in`.koreatech.koin.core.analytics.AnalyticsConstant import `in`.koreatech.koin.core.analytics.EventAction import `in`.koreatech.koin.core.analytics.EventLogger import `in`.koreatech.koin.core.appbar.ToolbarMenu +import `in`.koreatech.koin.core.navigation.utils.EXTRA_ARTICLE_ID +import `in`.koreatech.koin.core.navigation.utils.EXTRA_BOARD_ID import `in`.koreatech.koin.databinding.FragmentArticleHostBinding import `in`.koreatech.koin.feature.article.R as ArticleR +import `in`.koreatech.koin.feature.article.enums.ArticleBoardType import `in`.koreatech.koin.feature.article.model.ArticleToolbarState -import `in`.koreatech.koin.ui.newmain.ArticleHostViewModel +import `in`.koreatech.koin.feature.article.ui.article.detail.ArticleDetailFragment.Companion.ARTICLE_ID +import `in`.koreatech.koin.feature.article.ui.article.detail.ArticleDetailFragment.Companion.NAVIGATED_BOARD_ID +import `in`.koreatech.koin.ui.main.viewmodel.MainActivityViewModel import kotlinx.coroutines.launch @AndroidEntryPoint @@ -34,7 +40,7 @@ class ArticleFragment : Fragment() { private var _binding: FragmentArticleHostBinding? = null private val binding get() = _binding!! - private val viewModel by viewModels() + private val viewModel by activityViewModels() private var destinationChangedListener: NavController.OnDestinationChangedListener? = null private var articleNavController: NavController? = null @@ -86,6 +92,7 @@ class ArticleFragment : Fragment() { val isArticleList = dest.id == ArticleR.id.articleListFragment binding.topbarArticleList.visibility = if (isArticleList) View.VISIBLE else View.GONE binding.toolbarArticle.visibility = if (isArticleList) View.GONE else View.VISIBLE + viewModel.updateShowBottomNav(isArticleList) if (!isArticleList) { when (dest.id) { @@ -97,6 +104,32 @@ class ArticleFragment : Fragment() { } destinationChangedListener = listener navController.addOnDestinationChangedListener(listener) + + if (savedInstanceState == null) { + navigateToDetailFragment(navController) + } + } + + private fun navigateToDetailFragment(navController: NavController) { + when (arguments?.getString(FRAGMENT)) { + ARTICLE_KEYWORD -> { + navController.navigate(ArticleR.id.articleKeywordFragment) + } + ARTICLE_DETAIL -> { + val articleId = arguments?.getInt(EXTRA_ARTICLE_ID, -1) ?: -1 + if (articleId == -1) return + val boardId = (arguments?.getInt(EXTRA_BOARD_ID, -1) ?: -1) + .takeIf { it != -1 } ?: ArticleBoardType.ALL.id + navController.navigate( + ArticleR.id.articleDetailFragment, + bundleOf( + ARTICLE_ID to articleId, + NAVIGATED_BOARD_ID to boardId + ) + ) + } + null -> Unit + } } private fun setToolbar(state: ArticleToolbarState, navController: NavController) { @@ -130,4 +163,10 @@ class ArticleFragment : Fragment() { articleNavController = null _binding = null } + + companion object { + const val FRAGMENT = "fragment" + const val ARTICLE_DETAIL = "article_detail" + const val ARTICLE_KEYWORD = "article_keyword" + } } diff --git a/koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/CategoryFragment.kt b/koin/src/main/java/in/koreatech/koin/ui/main/fragment/CategoryFragment.kt similarity index 96% rename from koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/CategoryFragment.kt rename to koin/src/main/java/in/koreatech/koin/ui/main/fragment/CategoryFragment.kt index 2d53506eb..140523a21 100644 --- a/koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/CategoryFragment.kt +++ b/koin/src/main/java/in/koreatech/koin/ui/main/fragment/CategoryFragment.kt @@ -1,4 +1,4 @@ -package `in`.koreatech.koin.ui.newmain.fragment +package `in`.koreatech.koin.ui.main.fragment import android.os.Bundle import android.view.LayoutInflater diff --git a/koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/HomeFragment.kt b/koin/src/main/java/in/koreatech/koin/ui/main/fragment/HomeFragment.kt similarity index 96% rename from koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/HomeFragment.kt rename to koin/src/main/java/in/koreatech/koin/ui/main/fragment/HomeFragment.kt index c631597ef..b73382f1f 100644 --- a/koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/HomeFragment.kt +++ b/koin/src/main/java/in/koreatech/koin/ui/main/fragment/HomeFragment.kt @@ -1,4 +1,4 @@ -package `in`.koreatech.koin.ui.newmain.fragment +package `in`.koreatech.koin.ui.main.fragment import android.os.Bundle import android.view.LayoutInflater diff --git a/koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/NotificationFragment.kt b/koin/src/main/java/in/koreatech/koin/ui/main/fragment/NotificationFragment.kt similarity index 95% rename from koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/NotificationFragment.kt rename to koin/src/main/java/in/koreatech/koin/ui/main/fragment/NotificationFragment.kt index 87b0f339e..d1fb65c53 100644 --- a/koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/NotificationFragment.kt +++ b/koin/src/main/java/in/koreatech/koin/ui/main/fragment/NotificationFragment.kt @@ -1,4 +1,4 @@ -package `in`.koreatech.koin.ui.newmain.fragment +package `in`.koreatech.koin.ui.main.fragment import android.os.Bundle import android.view.LayoutInflater diff --git a/koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/ProfileFragment.kt b/koin/src/main/java/in/koreatech/koin/ui/main/fragment/ProfileFragment.kt similarity index 96% rename from koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/ProfileFragment.kt rename to koin/src/main/java/in/koreatech/koin/ui/main/fragment/ProfileFragment.kt index 7fe823b12..887729551 100644 --- a/koin/src/main/java/in/koreatech/koin/ui/newmain/fragment/ProfileFragment.kt +++ b/koin/src/main/java/in/koreatech/koin/ui/main/fragment/ProfileFragment.kt @@ -1,4 +1,4 @@ -package `in`.koreatech.koin.ui.newmain.fragment +package `in`.koreatech.koin.ui.main.fragment import android.content.Intent import android.os.Bundle diff --git a/koin/src/main/java/in/koreatech/koin/ui/main/viewmodel/MainActivityViewModel.kt b/koin/src/main/java/in/koreatech/koin/ui/main/viewmodel/MainActivityViewModel.kt index 87f24f6b2..a31fbdc9f 100644 --- a/koin/src/main/java/in/koreatech/koin/ui/main/viewmodel/MainActivityViewModel.kt +++ b/koin/src/main/java/in/koreatech/koin/ui/main/viewmodel/MainActivityViewModel.kt @@ -3,115 +3,65 @@ package `in`.koreatech.koin.ui.main.viewmodel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import `in`.koreatech.koin.core.viewmodel.BaseViewModel -import `in`.koreatech.koin.domain.model.article.articleNotiContent import `in`.koreatech.koin.domain.model.dining.Dining -import `in`.koreatech.koin.domain.model.dining.DiningType -import `in`.koreatech.koin.domain.model.store.StoreCategories -import `in`.koreatech.koin.domain.model.user.User -import `in`.koreatech.koin.domain.repository.ArticleRepository -import `in`.koreatech.koin.domain.usecase.article.lostandfound.FetchArticleLostAndFoundStatsUseCase import `in`.koreatech.koin.domain.usecase.banner.CheckBannerRefusalUseCase import `in`.koreatech.koin.domain.usecase.dining.GetDiningUseCase -import `in`.koreatech.koin.domain.usecase.store.GetStoreCategoriesUseCase -import `in`.koreatech.koin.domain.usecase.user.GetUserStatusUseCase +import `in`.koreatech.koin.domain.usecase.notification.GetNotificationsFlowUseCase +import `in`.koreatech.koin.domain.usecase.user.UpdateDeviceTokenUseCase import `in`.koreatech.koin.domain.util.DiningUtil import `in`.koreatech.koin.domain.util.TimeUtil import `in`.koreatech.koin.domain.util.ext.arrange import `in`.koreatech.koin.domain.util.ext.typeFilter -import `in`.koreatech.koin.ui.main.state.ArticleMainState -import `in`.koreatech.koin.ui.main.state.LostAndFoundEntryState -import `in`.koreatech.koin.ui.main.state.toContent -import `in`.koreatech.koin.ui.main.state.toLostAndFoundEntryState -import `in`.koreatech.koin.ui.main.state.toNoti import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber @HiltViewModel class MainActivityViewModel @Inject constructor( + private val updateDeviceTokenUseCase: UpdateDeviceTokenUseCase, private val getDiningUseCase: GetDiningUseCase, - private val getStoreCategoriesUseCase: GetStoreCategoriesUseCase, private val checkBannerRefusalUseCase: CheckBannerRefusalUseCase, - private val articleRepository: ArticleRepository, - private val getUserStatusUseCase: GetUserStatusUseCase, - private val fetchArticleLostAndFoundStatsUseCase: FetchArticleLostAndFoundStatsUseCase + private val getNotificationsFlowUseCase: GetNotificationsFlowUseCase ) : BaseViewModel() { - val hotArticles: StateFlow> = - articleRepository.fetchHotArticleHeaders() - .map { - it.take(HOT_ARTICLE_COUNT).map { article -> article.toContent() } - }.catch { - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList() - ) - val articleNoti: StateFlow = - articleRepository.fetchKeywordNotiIndex() - .map { articleNotiContent[it].toNoti() } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = articleNotiContent.first().toNoti() - ) - - val articleMain: StateFlow> = - combine( - articleNoti, - hotArticles - ) { noti, articles -> - listOf(noti) + articles - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList() - ) - private val _selectedPosition = MutableStateFlow(0) - val selectedPosition: StateFlow get() = _selectedPosition private val _diningData = MutableStateFlow>(listOf()) - val diningData: StateFlow> get() = _diningData private val _selectedType = MutableStateFlow(DiningUtil.getCurrentType()) - val selectedType: StateFlow get() = _selectedType - - private val _userState: StateFlow = getUserStatusUseCase().stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = User.Anonymous - ) - - private val _storeCategories = MutableStateFlow>(emptyList()) - val storeCategories: StateFlow> get() = _storeCategories private val _isBannerRefusal = MutableStateFlow(null) val isBannerRefusal: StateFlow get() = _isBannerRefusal + private val _showBottomNav = MutableStateFlow(true) + val showBottomNav: StateFlow get() = _showBottomNav + + val hasUnreadNotification: StateFlow = getNotificationsFlowUseCase() + .map { notifications -> notifications.any { !it.isRead } } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = false + ) + init { checkBannerRefusal() updateDining() - getLostAndFoundState() } - fun checkKeywordNotiContent() { - viewModelScope.launchWithLoading { - articleRepository.saveKeywordNotiIndex().launchIn(viewModelScope) + fun updateDeviceToken() { + viewModelScope.launch { + try { + updateDeviceTokenUseCase() + } catch (e: Exception) { + Timber.e("Failed Update Fcm Token : ${e.message}") + } } } - fun setDiningType(diningType: DiningType) { - _selectedType.value = diningType - } - - fun setSelectedPosition(position: Int) { - _selectedPosition.value = position - } - fun updateDining() { viewModelScope.launchWithLoading { getDiningUseCase(TimeUtil.dateFormatToYYMMDD(DiningUtil.getCurrentDate())) @@ -130,14 +80,8 @@ class MainActivityViewModel @Inject constructor( } } - fun getStoreCategoriesWithBenefit(storeCategory: StoreCategories) = viewModelScope.launchWithLoading { - val categoryList = getStoreCategoriesUseCase().drop(1).toMutableList() - categoryList.add(0, storeCategory) - _storeCategories.value = categoryList - } - - fun getStoreCategories() = viewModelScope.launchWithLoading { - _storeCategories.value = getStoreCategoriesUseCase() + fun updateShowBottomNav(show: Boolean) { + _showBottomNav.update { show } } private fun checkBannerRefusal() { @@ -147,19 +91,4 @@ class MainActivityViewModel @Inject constructor( } } } - - private val _lostAndFoundEntryState = MutableStateFlow(null) - val lostAndFoundEntryState: StateFlow get() = _lostAndFoundEntryState - - private fun getLostAndFoundState() { - viewModelScope.launchWithLoading { - fetchArticleLostAndFoundStatsUseCase().onSuccess { stats -> - _lostAndFoundEntryState.value = stats.toLostAndFoundEntryState() - } - } - } - - companion object { - private const val HOT_ARTICLE_COUNT = 4 - } } diff --git a/koin/src/main/java/in/koreatech/koin/ui/navigation/KoinNavigationDrawerActivity.kt b/koin/src/main/java/in/koreatech/koin/ui/navigation/KoinNavigationDrawerActivity.kt index 5d11ebf67..44764fe2f 100644 --- a/koin/src/main/java/in/koreatech/koin/ui/navigation/KoinNavigationDrawerActivity.kt +++ b/koin/src/main/java/in/koreatech/koin/ui/navigation/KoinNavigationDrawerActivity.kt @@ -41,7 +41,6 @@ import `in`.koreatech.koin.core.util.blueStatusBar import `in`.koreatech.koin.core.util.whiteStatusBar import `in`.koreatech.koin.data.constant.URLConstant import `in`.koreatech.koin.domain.model.user.User -import `in`.koreatech.koin.feature.article.ArticleActivity import `in`.koreatech.koin.feature.callvan.CallvanActivity import `in`.koreatech.koin.feature.chat.ui.list.ChatListActivity import `in`.koreatech.koin.feature.club.ui.ClubActivity @@ -640,7 +639,10 @@ abstract class KoinNavigationDrawerActivity : } private fun goToArticleActivity() { - val intent = Intent(this, ArticleActivity::class.java) + val intent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse("koin://article/navigation") + `package` = packageName + } if (menuState != MenuState.Main) { goToActivityFinish(intent) diff --git a/koin/src/main/java/in/koreatech/koin/ui/newmain/ArticleHostViewModel.kt b/koin/src/main/java/in/koreatech/koin/ui/newmain/ArticleHostViewModel.kt deleted file mode 100644 index b635f358e..000000000 --- a/koin/src/main/java/in/koreatech/koin/ui/newmain/ArticleHostViewModel.kt +++ /dev/null @@ -1,25 +0,0 @@ -package `in`.koreatech.koin.ui.newmain - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import `in`.koreatech.koin.domain.usecase.notification.GetNotificationsFlowUseCase -import javax.inject.Inject -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -@HiltViewModel -class ArticleHostViewModel @Inject constructor( - getNotificationsFlowUseCase: GetNotificationsFlowUseCase -) : ViewModel() { - - val hasUnreadNotification: StateFlow = getNotificationsFlowUseCase() - .map { notifications -> notifications.any { !it.isRead } } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = false - ) -} diff --git a/koin/src/main/java/in/koreatech/koin/ui/newmain/NewMainActivity.kt b/koin/src/main/java/in/koreatech/koin/ui/newmain/NewMainActivity.kt deleted file mode 100644 index bf2ee9a95..000000000 --- a/koin/src/main/java/in/koreatech/koin/ui/newmain/NewMainActivity.kt +++ /dev/null @@ -1,76 +0,0 @@ -package `in`.koreatech.koin.ui.newmain - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.core.graphics.Insets -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.isVisible -import androidx.core.view.updatePadding -import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.NavigationUI -import androidx.navigation.ui.setupWithNavController -import dagger.hilt.android.AndroidEntryPoint -import `in`.koreatech.koin.R -import `in`.koreatech.koin.core.analytics.AnalyticsConstant -import `in`.koreatech.koin.core.analytics.EventLogger -import `in`.koreatech.koin.core.designsystem.util.enableEdgeToEdgeWithLightStatusBar -import `in`.koreatech.koin.databinding.ActivityNewMainBinding - -@AndroidEntryPoint -class NewMainActivity : AppCompatActivity() { - - private lateinit var binding: ActivityNewMainBinding - - private fun navigationLogValue(itemId: Int): Pair? = when (itemId) { - R.id.bottom_navigation_home -> AnalyticsConstant.Label.NAV_HOME to "홈" - R.id.bottom_navigation_category -> AnalyticsConstant.Label.NAV_CATEGORY to "카테고리" - R.id.bottom_navigation_article -> AnalyticsConstant.Label.NAV_BULLETIN to "게시판" - R.id.bottom_navigation_profile -> AnalyticsConstant.Label.NAV_PROFILE to "프로필" - else -> null - } - - override fun onCreate(savedInstanceState: Bundle?) { - enableEdgeToEdgeWithLightStatusBar() - super.onCreate(savedInstanceState) - binding = ActivityNewMainBinding.inflate(layoutInflater) - setContentView(binding.root) - - ViewCompat.setOnApplyWindowInsetsListener(binding.bottomNavigationMain) { view, insets -> - val navigationBars = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) - - view.updatePadding( - bottom = navigationBars.bottom - ) - - WindowInsetsCompat.Builder(insets) - .setInsets(WindowInsetsCompat.Type.navigationBars(), Insets.NONE) - .build() - } - - val navHostFragment = supportFragmentManager.findFragmentById(binding.navHostFragmentMain.id) as NavHostFragment - val navController = navHostFragment.navController - - binding.bottomNavigationMain.setupWithNavController(navController) - - // setOnItemReselectedListener를 따로 등록하면 재탭 시 기본 popUpTo/restoreState 동작이 사라지므로 등록하지 않는다. - // 리스너가 없으면 재탭도 이 setOnItemSelectedListener로 그대로 들어와 로깅과 기본 동작이 모두 보존된다. - binding.bottomNavigationMain.setOnItemSelectedListener { item -> - navigationLogValue(item.itemId)?.let { (label, value) -> - EventLogger.logCampusClickEvent(label, value) - } - NavigationUI.onNavDestinationSelected(item, navController) - } - - val topLevelDestinations = setOf( - R.id.bottom_navigation_home, - R.id.bottom_navigation_category, - R.id.bottom_navigation_article, - R.id.bottom_navigation_profile - ) - - navController.addOnDestinationChangedListener { _, destination, _ -> - binding.bottomNavigationMain.isVisible = destination.id in topLevelDestinations - } - } -} diff --git a/koin/src/main/java/in/koreatech/koin/ui/splash/SplashActivity.kt b/koin/src/main/java/in/koreatech/koin/ui/splash/SplashActivity.kt index 9b72c54fa..5d56f66f5 100644 --- a/koin/src/main/java/in/koreatech/koin/ui/splash/SplashActivity.kt +++ b/koin/src/main/java/in/koreatech/koin/ui/splash/SplashActivity.kt @@ -35,10 +35,9 @@ import `in`.koreatech.koin.core.navigation.utils.EXTRA_TYPE import `in`.koreatech.koin.core.onboarding.OnboardingManager import `in`.koreatech.koin.core.toast.ToastUtil import `in`.koreatech.koin.domain.state.version.VersionUpdatePriority -import `in`.koreatech.koin.feature.article.ArticleActivity import `in`.koreatech.koin.feature.lostandfound.ui.LostAndFoundActivity import `in`.koreatech.koin.ui.forceupdate.ForceUpdateActivity -import `in`.koreatech.koin.ui.newmain.NewMainActivity +import `in`.koreatech.koin.ui.main.activity.MainActivity import `in`.koreatech.koin.ui.splash.state.TokenState import `in`.koreatech.koin.ui.splash.viewmodel.SplashViewModel import `in`.koreatech.koin.util.FirebasePerformanceUtil @@ -210,7 +209,9 @@ class SplashActivity : ActivityBase() { private fun gotoArticleActivityOrDelay(state: TokenState) { lifecycleScope.launch { delay() - val intent = Intent(this@SplashActivity, ArticleActivity::class.java) + val intent = Intent(this@SplashActivity, MainActivity::class.java).apply { + data = "koin://article/navigation".toUri() + } startActivity(intent) overridePendingTransition(R.anim.fade, R.anim.hold) if (state == TokenState.Valid) { @@ -264,7 +265,7 @@ class SplashActivity : ActivityBase() { Pair(EXTRA_POST_ID, targetPostId) ) } else { - Intent(this@SplashActivity, NewMainActivity::class.java) + Intent(this@SplashActivity, MainActivity::class.java) } startActivity(intent) diff --git a/koin/src/main/res/navigation/nav_graph_main.xml b/koin/src/main/res/navigation/nav_graph_main.xml index 882561859..2b1dcb1bf 100644 --- a/koin/src/main/res/navigation/nav_graph_main.xml +++ b/koin/src/main/res/navigation/nav_graph_main.xml @@ -6,7 +6,7 @@ + android:name="in.koreatech.koin.ui.main.fragment.HomeFragment"> + android:name="in.koreatech.koin.ui.main.fragment.NotificationFragment" /> + android:name="in.koreatech.koin.ui.main.fragment.CategoryFragment"> + android:name="in.koreatech.koin.ui.main.fragment.ArticleFragment"> + + + + + + + + android:name="in.koreatech.koin.ui.main.fragment.ProfileFragment" />