Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
faea8a4
Refactor: Home 레이어 Type-safe navigation 적용
wjdrjs00 Mar 26, 2026
822a734
Refactor: RoutineLocalDataSource 추가 및 DI 연결
wjdrjs00 Mar 26, 2026
ca58d92
Refactor: RoutineRepository를 Flow 기반으로 전환 및 Optimistic Update 책임 이동
wjdrjs00 Mar 26, 2026
efae7ac
Refactor: ToggleStrategy 도메인으로 이동 및 루틴 UseCase 재구성
wjdrjs00 Mar 26, 2026
a0ce38b
Refactor: HomeViewModel 단순화
wjdrjs00 Mar 26, 2026
1ddcf38
Refactor: RoutineListViewModel 단순화
wjdrjs00 Mar 26, 2026
ad48939
Refactor: OnBoarding 루틴 캐시 무효화 data 레이어 위임
wjdrjs00 Mar 26, 2026
3b674a6
Test: RoutineRepositoryImpl 단위 테스트 추가
wjdrjs00 Mar 26, 2026
d4abc0e
Refactor: syncTrigger의 BufferOverflow 전략 수정
wjdrjs00 Mar 26, 2026
836b342
Refactor: 펜딩 변경사항 동기화 및 동시성 제어 개선
wjdrjs00 Mar 27, 2026
baa4572
Refactor: syncError 노출 및 ObserveRoutineSyncErrorUseCase 추가
wjdrjs00 Mar 27, 2026
4ec0a5b
Refactor: BitnagilConfirmDialog 디자인시스템으로 추출
wjdrjs00 Mar 27, 2026
e7a6b02
Refactor: 롤백 에러 다이얼로그 HomeState로 관리
wjdrjs00 Mar 27, 2026
9e0fcaf
Refactor: 동기화 로직 배치 처리로 수정
wjdrjs00 Mar 29, 2026
7362cdf
Refactor: fetch 및 동기화 에러 핸들링 로직 수정
wjdrjs00 Mar 29, 2026
09eb7bb
Refactor: 루틴 동기화 요청 시 재시도(Retry) 로직 추가
wjdrjs00 Mar 29, 2026
0edc72f
Test: RoutineRepositoryImpl 동기화 재시도 및 디바운스 로직 테스트 수정
wjdrjs00 Mar 29, 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
18 changes: 18 additions & 0 deletions app/src/main/java/com/threegap/bitnagil/di/data/CoroutineModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.threegap.bitnagil.di.data

import com.threegap.bitnagil.data.di.IoDispatcher
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

@Module
@InstallIn(SingletonComponent::class)
object CoroutineModule {

@Provides
@IoDispatcher
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import com.threegap.bitnagil.data.recommendroutine.datasource.RecommendRoutineDa
import com.threegap.bitnagil.data.recommendroutine.datasourceImpl.RecommendRoutineDataSourceImpl
import com.threegap.bitnagil.data.report.datasource.ReportDataSource
import com.threegap.bitnagil.data.report.datasourceImpl.ReportDataSourceImpl
import com.threegap.bitnagil.data.routine.datasource.RoutineLocalDataSource
import com.threegap.bitnagil.data.routine.datasource.RoutineRemoteDataSource
import com.threegap.bitnagil.data.routine.datasourceImpl.RoutineLocalDataSourceImpl
import com.threegap.bitnagil.data.routine.datasourceImpl.RoutineRemoteDataSourceImpl
import com.threegap.bitnagil.data.user.datasource.UserLocalDataSource
import com.threegap.bitnagil.data.user.datasource.UserRemoteDataSource
Expand Down Expand Up @@ -52,7 +54,11 @@ abstract class DataSourceModule {

@Binds
@Singleton
abstract fun bindRoutineDataSource(routineDataSourceImpl: RoutineRemoteDataSourceImpl): RoutineRemoteDataSource
abstract fun bindRoutineRemoteDataSource(routineDataSourceImpl: RoutineRemoteDataSourceImpl): RoutineRemoteDataSource

@Binds
@Singleton
abstract fun bindRoutineLocalDataSource(impl: RoutineLocalDataSourceImpl): RoutineLocalDataSource

@Binds
@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.threegap.bitnagil.designsystem.BitnagilTheme
import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon

@Composable
fun HomeBottomNavigationBar(
navController: NavController,
selectedTab: HomeRoute,
onTabSelected: (HomeRoute) -> Unit,
modifier: Modifier = Modifier,
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route

Column {
Column(modifier = modifier) {
HorizontalDivider(
modifier = Modifier.fillMaxWidth(),
thickness = 1.dp,
Expand All @@ -47,19 +44,13 @@ fun HomeBottomNavigationBar(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
HomeRoute.entries.map { homeRoute ->
homeTabList.forEach { homeTab ->
HomeBottomNavigationItem(
modifier = Modifier.weight(1f),
icon = homeRoute.icon,
title = homeRoute.title,
onClick = {
if (currentRoute != homeRoute.route) {
navController.navigate(homeRoute.route) {
popUpTo(0) { inclusive = true }
}
}
},
selected = currentRoute == homeRoute.route,
icon = homeTab.icon,
title = homeTab.title,
selected = selectedTab == homeTab.route,
onClick = { onTabSelected(homeTab.route) },
)
}
}
Expand All @@ -71,8 +62,8 @@ private fun HomeBottomNavigationItem(
modifier: Modifier = Modifier,
icon: Int,
title: String,
onClick: () -> Unit,
selected: Boolean,
onClick: () -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
Expand Down Expand Up @@ -111,9 +102,8 @@ private fun HomeBottomNavigationItem(
@Composable
@Preview
private fun HomeBottomNavigationBarPreview() {
val navigator = rememberHomeNavigator()

HomeBottomNavigationBar(
navController = navigator.navController,
selectedTab = HomeRoute.Home,
onTabSelected = {},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,16 @@ fun HomeNavHost(
activity?.setStatusBarContentColor(isLightContent = isHomeTab)
}

val selectedBottomTab = navigator.currentHomeRoute ?: navigator.startDestination

Box(modifier = modifier.fillMaxSize()) {
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
HomeBottomNavigationBar(navController = navigator.navController)
HomeBottomNavigationBar(
selectedTab = selectedBottomTab,
onTabSelected = navigator::navigateTo,
)
},
contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
content = { innerPadding ->
Expand All @@ -73,7 +78,7 @@ fun HomeNavHost(
startDestination = navigator.startDestination,
modifier = modifier.padding(innerPadding),
) {
composable(HomeRoute.Home.route) {
composable<HomeRoute.Home> {
HomeScreenContainer(
navigateToGuide = navigateToGuide,
navigateToRegisterRoutine = {
Expand All @@ -86,14 +91,14 @@ fun HomeNavHost(
)
}

composable(HomeRoute.RecommendRoutine.route) {
composable<HomeRoute.RecommendRoutine> {
RecommendRoutineScreenContainer(
navigateToEmotion = navigateToEmotion,
navigateToRegisterRoutine = navigateToRegisterRoutine,
)
}

composable(HomeRoute.MyPage.route) {
composable<HomeRoute.MyPage> {
MyPageScreenContainer(
navigateToSetting = navigateToSetting,
navigateToOnBoarding = navigateToOnBoarding,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,39 @@ package com.threegap.bitnagil.navigation.home

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController

class HomeNavigator(
val navController: NavHostController,
) {
val startDestination = HomeRoute.Home.route
val startDestination: HomeRoute = HomeRoute.Home

val currentRoute: String?
@Composable get() = navController.currentBackStackEntryAsState().value?.destination?.route
val currentHomeRoute: HomeRoute?
@Composable get() {
val destination = navController.currentBackStackEntryAsState().value?.destination
return when {
destination?.hasRoute(HomeRoute.Home::class) == true -> HomeRoute.Home
destination?.hasRoute(HomeRoute.RecommendRoutine::class) == true -> HomeRoute.RecommendRoutine
destination?.hasRoute(HomeRoute.MyPage::class) == true -> HomeRoute.MyPage
else -> null
}
}

val isHomeRoute: Boolean
@Composable get() = currentRoute == HomeRoute.Home.route
@Composable get() = currentHomeRoute == HomeRoute.Home

@Composable
fun shouldShowFloatingAction(): Boolean =
currentRoute in setOf(HomeRoute.Home.route, HomeRoute.RecommendRoutine.route)
currentHomeRoute == HomeRoute.Home || currentHomeRoute == HomeRoute.RecommendRoutine

fun navigateTo(route: HomeRoute) {
navController.navigate(route) {
popUpTo(0) { inclusive = true }
}
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
package com.threegap.bitnagil.navigation.home

import com.threegap.bitnagil.R
import kotlinx.serialization.Serializable

enum class HomeRoute(
val route: String,
val title: String,
val icon: Int,
) {
Home(
route = "home/home",
title = "",
icon = R.drawable.ic_home,
),
@Serializable
sealed interface HomeRoute {
@Serializable
data object Home : HomeRoute

RecommendRoutine(
route = "home/recommend_routine",
title = "추천 루틴",
icon = R.drawable.ic_routine_recommend,
),
@Serializable
data object RecommendRoutine : HomeRoute

MyPage(
route = "home/my_page",
title = "마이페이지",
icon = R.drawable.ic_profile,
),
;
@Serializable
data object MyPage : HomeRoute
}

data class HomeTab(
val route: HomeRoute,
val title: String,
val icon: Int,
)

val homeTabList = listOf(
HomeTab(HomeRoute.Home, "", R.drawable.ic_home),
HomeTab(HomeRoute.RecommendRoutine, "추천 루틴", R.drawable.ic_routine_recommend),
HomeTab(HomeRoute.MyPage, "마이페이지", R.drawable.ic_profile),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.threegap.bitnagil.designsystem.component.block

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.threegap.bitnagil.designsystem.BitnagilTheme
import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BitnagilConfirmDialog(
title: String,
description: String,
confirmButtonText: String,
onConfirm: () -> Unit,
modifier: Modifier = Modifier,
dismissOnBackPress: Boolean = true,
dismissOnClickOutside: Boolean = true,
) {
BasicAlertDialog(
onDismissRequest = onConfirm,
modifier = modifier,
properties = DialogProperties(
dismissOnBackPress = dismissOnBackPress,
dismissOnClickOutside = dismissOnClickOutside,
),
) {
Column(
modifier = Modifier
.background(
color = BitnagilTheme.colors.white,
shape = RoundedCornerShape(12.dp),
)
.padding(vertical = 20.dp, horizontal = 24.dp),
) {
Text(
text = title,
color = BitnagilTheme.colors.coolGray10,
style = BitnagilTheme.typography.subtitle1SemiBold,
modifier = Modifier.padding(bottom = 6.dp),
)

Text(
text = description,
color = BitnagilTheme.colors.coolGray40,
style = BitnagilTheme.typography.body2Medium,
modifier = Modifier.padding(bottom = 18.dp),
)

Text(
text = confirmButtonText,
color = BitnagilTheme.colors.orange500,
style = BitnagilTheme.typography.body2Medium,
textAlign = TextAlign.End,
modifier = Modifier
.fillMaxWidth()
.clickableWithoutRipple { onConfirm() },
)
}
}
}

@Preview
@Composable
private fun Preview() {
BitnagilConfirmDialog(
title = "루틴 완료를 저장하지 못했어요",
description = "네트워크 연결에 문제가 있었던 것 같아요.\n다시 한 번 시도해 주세요.",
confirmButtonText = "확인",
onConfirm = {},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.threegap.bitnagil.data.di

import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,16 @@ import com.threegap.bitnagil.data.onboarding.datasource.OnBoardingDataSource
import com.threegap.bitnagil.data.onboarding.model.dto.OnBoardingDto
import com.threegap.bitnagil.data.onboarding.model.request.GetOnBoardingRecommendRoutinesRequest
import com.threegap.bitnagil.data.onboarding.model.request.RegisterOnBoardingRecommendRoutinesRequest
import com.threegap.bitnagil.data.routine.datasource.RoutineLocalDataSource
import com.threegap.bitnagil.domain.onboarding.model.OnBoarding
import com.threegap.bitnagil.domain.onboarding.model.OnBoardingAbstract
import com.threegap.bitnagil.domain.onboarding.model.OnBoardingRecommendRoutine
import com.threegap.bitnagil.domain.onboarding.model.OnBoardingRecommendRoutineEvent
import com.threegap.bitnagil.domain.onboarding.repository.OnBoardingRepository
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject

class OnBoardingRepositoryImpl @Inject constructor(
private val onBoardingDataSource: OnBoardingDataSource,
private val routineLocalDataSource: RoutineLocalDataSource,
) : OnBoardingRepository {
override suspend fun getOnBoardingList(): List<OnBoarding> {
val onBoardingDtos = onBoardingDataSource.getOnBoardingList()
Expand Down Expand Up @@ -59,7 +56,7 @@ class OnBoardingRepositoryImpl @Inject constructor(

return onBoardingDataSource.registerRecommendRoutineList(selectedRecommendRoutineIds = request.recommendedRoutineIds).also {
if (it.isSuccess) {
_onBoardingRecommendRoutineEventFlow.emit(OnBoardingRecommendRoutineEvent.AddRoutines(selectedRecommendRoutineIds))
routineLocalDataSource.clearCache()
}
}
}
Expand All @@ -73,10 +70,4 @@ class OnBoardingRepositoryImpl @Inject constructor(
)
}
}

private val _onBoardingRecommendRoutineEventFlow = MutableSharedFlow<OnBoardingRecommendRoutineEvent>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
override suspend fun getOnBoardingRecommendRoutineEventFlow(): Flow<OnBoardingRecommendRoutineEvent> = _onBoardingRecommendRoutineEventFlow.asSharedFlow()
}
Loading
Loading