Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions Prezel/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ dependencies {
implementation(projects.coreData)
implementation(projects.coreDesignsystem)
implementation(projects.coreDomain)
implementation(projects.coreModel)
implementation(projects.coreNavigation)
implementation(projects.coreUi)
implementation(projects.coreCommon)
Expand All @@ -87,6 +88,8 @@ dependencies {
implementation(projects.featureHistoryImpl)
implementation(projects.featureMyApi)
implementation(projects.featureMyImpl)
implementation(projects.featureBadgeApi)
implementation(projects.featureBadgeImpl)
implementation(projects.featureFeedbackApi)
implementation(projects.featureFeedbackImpl)

Expand Down
5 changes: 5 additions & 0 deletions Prezel/app/src/main/java/com/team/prezel/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.navigation3.runtime.NavKey
import com.team.prezel.core.common.event.GlobalEventBus
import com.team.prezel.core.data.NetworkMonitor
import com.team.prezel.core.designsystem.theme.PrezelTheme
import com.team.prezel.core.domain.usecase.badge.ConnectBadgeEventStreamUseCase
import com.team.prezel.ui.PrezelApp
import com.team.prezel.ui.rememberPrezelAppState
import dagger.hilt.android.AndroidEntryPoint
Expand All @@ -23,6 +24,9 @@ class MainActivity : ComponentActivity() {
@Inject
lateinit var globalEventBus: GlobalEventBus

@Inject
lateinit var connectBadgeEventStreamUseCase: ConnectBadgeEventStreamUseCase

@Inject
lateinit var entryBuilders: Set<@JvmSuppressWildcards EntryProviderScope<NavKey>.() -> Unit>

Expand All @@ -37,6 +41,7 @@ class MainActivity : ComponentActivity() {
PrezelApp(
appState = appState,
globalEventBus = globalEventBus,
connectBadgeEventStreamUseCase = connectBadgeEventStreamUseCase,
entryBuilders = entryBuilders.toImmutableSet(),
)
}
Expand Down
53 changes: 53 additions & 0 deletions Prezel/app/src/main/java/com/team/prezel/ui/PrezelApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,29 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.core.view.WindowCompat
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import com.team.prezel.R
import com.team.prezel.core.common.event.EdgeToEdgeStatusBarStyle
import com.team.prezel.core.common.event.GlobalEvent
import com.team.prezel.core.common.event.GlobalEventBus
import com.team.prezel.core.designsystem.component.PrezelNavigationScaffold
import com.team.prezel.core.designsystem.component.PrezelNavigationScope
import com.team.prezel.core.designsystem.component.feedback.snackbar.showPrezelSnackbar
import com.team.prezel.core.designsystem.theme.PrezelTheme
import com.team.prezel.core.domain.usecase.badge.ConnectBadgeEventStreamUseCase
import com.team.prezel.core.navigation.LocalNavigator
import com.team.prezel.core.navigation.Navigator
import com.team.prezel.core.navigation.ProvideSharedTransitionScope
Expand All @@ -51,6 +56,7 @@ import com.team.prezel.core.ui.state.LocalAppDimmerState
import com.team.prezel.core.ui.state.LocalSnackbarHostState
import com.team.prezel.core.ui.state.rememberAppDimmerState
import com.team.prezel.core.ui.util.noRippleClickable
import com.team.prezel.feature.badge.api.BadgeNavKey
import com.team.prezel.feature.splash.api.SplashNavKey
import com.team.prezel.navigation.MAIN_NAV_ITEMS
import kotlinx.collections.immutable.ImmutableSet
Expand All @@ -59,6 +65,7 @@ import kotlinx.collections.immutable.ImmutableSet
fun PrezelApp(
appState: PrezelAppState,
globalEventBus: GlobalEventBus,
connectBadgeEventStreamUseCase: ConnectBadgeEventStreamUseCase,
entryBuilders: ImmutableSet<EntryProviderScope<NavKey>.() -> Unit>,
) {
val navigator = remember(appState.navigationState) { Navigator(appState.navigationState) }
Expand All @@ -75,6 +82,7 @@ fun PrezelApp(
PrezelAppContent(
appState = appState,
globalEventBus = globalEventBus,
connectBadgeEventStreamUseCase = connectBadgeEventStreamUseCase,
entryBuilders = entryBuilders,
)
}
Expand All @@ -84,6 +92,7 @@ fun PrezelApp(
private fun PrezelAppContent(
appState: PrezelAppState,
globalEventBus: GlobalEventBus,
connectBadgeEventStreamUseCase: ConnectBadgeEventStreamUseCase,
entryBuilders: ImmutableSet<EntryProviderScope<NavKey>.() -> Unit>,
) {
val navigator = LocalNavigator.current
Expand All @@ -96,6 +105,13 @@ private fun PrezelAppContent(
onStatusBarStyleChange = { statusBarStyle = it },
)

ObserveBadgeEvents(
isAuthenticated = appState.isAuthenticated,
connectBadgeEventStreamUseCase = connectBadgeEventStreamUseCase,
shouldShowNavigationBar = appState.shouldShowNavigationBar,
navigateToBadge = { navigator.navigate(BadgeNavKey) },
)

Box(modifier = Modifier.fillMaxSize()) {
SharedTransitionLayout {
ProvideSharedTransitionScope(this@SharedTransitionLayout) {
Expand Down Expand Up @@ -242,6 +258,43 @@ private tailrec fun Context.findActivity(): Activity? =
else -> null
}

@Composable
private fun ObserveBadgeEvents(
isAuthenticated: Boolean,
connectBadgeEventStreamUseCase: ConnectBadgeEventStreamUseCase,
shouldShowNavigationBar: Boolean,
navigateToBadge: () -> Unit,
) {
val snackbarHostState = LocalSnackbarHostState.current
val resources = LocalResources.current
val currentShouldShowNavigationBar by rememberUpdatedState(shouldShowNavigationBar)

LaunchedEffect(isAuthenticated) {
if (!isAuthenticated) return@LaunchedEffect

connectBadgeEventStreamUseCase()
.collect { event ->
val message =
event.message
?.takeIf(String::isNotBlank)
?: event.badgeName
?.takeIf(String::isNotBlank)
?.let { badgeName ->
resources.getString(R.string.app_badge_event_message_with_name, badgeName)
}
?: resources.getString(R.string.app_badge_event_message)

snackbarHostState.currentSnackbarData?.dismiss()
snackbarHostState.showPrezelSnackbar(
message = message,
actionLabel = resources.getString(R.string.app_badge_event_action),
onAction = navigateToBadge,
useRaisedPosition = currentShouldShowNavigationBar,
)
}
}
Comment thread
moondev03 marked this conversation as resolved.
}

@Composable
private fun PrezelNavigationScope.AppNavigationItems(
appState: PrezelAppState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import com.team.prezel.core.data.NetworkMonitor
import com.team.prezel.core.navigation.NavigationState
import com.team.prezel.core.navigation.rememberNavigationState
import com.team.prezel.feature.login.api.LoginNavKey
import com.team.prezel.feature.splash.api.SplashNavKey
import com.team.prezel.navigation.MAIN_NAV_KEYS
import com.team.prezel.navigation.TOP_LEVEL_KEYS
Expand Down Expand Up @@ -45,6 +46,9 @@ class PrezelAppState(
coroutineScope: CoroutineScope,
networkMonitor: NetworkMonitor,
) {
val isAuthenticated
get() = navigationState.currentKey !in UNAUTHENTICATED_NAV_KEYS

val shouldShowNavigationBar
get() = navigationState.currentKey in MAIN_NAV_KEYS

Expand All @@ -57,3 +61,8 @@ class PrezelAppState(
initialValue = false,
)
}

private val UNAUTHENTICATED_NAV_KEYS = setOf(
SplashNavKey,
LoginNavKey,
)
3 changes: 3 additions & 0 deletions Prezel/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@
<string name="bottom_nav_profile">프로필</string>

<string name="double_back_to_exit_snackbar_message">한 번 더 누르면 앱을 종료합니다.</string>
<string name="app_badge_event_message">새 배지를 획득했어요.</string>
<string name="app_badge_event_message_with_name">%1$s 배지를 획득했어요.</string>
<string name="app_badge_event_action">보러가기</string>
</resources>
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.team.prezel.core.data.di

import com.team.prezel.core.data.repository.AuthRepositoryImpl
import com.team.prezel.core.data.repository.BadgeRepositoryImpl
import com.team.prezel.core.data.repository.PracticeRepositoryImpl
import com.team.prezel.core.data.repository.PresentationRepositoryImpl
import com.team.prezel.core.data.repository.TermsRepositoryImpl
import com.team.prezel.core.data.repository.UserRepositoryImpl
import com.team.prezel.core.domain.repository.auth.AuthRepository
import com.team.prezel.core.domain.repository.badge.BadgeRepository
import com.team.prezel.core.domain.repository.practice.PracticeRepository
import com.team.prezel.core.domain.repository.presentation.PresentationRepository
import com.team.prezel.core.domain.repository.profile.UserRepository
Expand All @@ -23,6 +25,10 @@ internal abstract class RepositoryModule {
@Singleton
abstract fun bindAuthRepository(impl: AuthRepositoryImpl): AuthRepository

@Binds
@Singleton
abstract fun bindBadgeRepository(impl: BadgeRepositoryImpl): BadgeRepository

@Binds
@Singleton
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.team.prezel.core.data.repository

import com.team.prezel.core.data.error.mapDomainFailure
import com.team.prezel.core.domain.repository.badge.BadgeRepository
import com.team.prezel.core.model.badge.Badge
import com.team.prezel.core.model.badge.BadgeDetail
import com.team.prezel.core.model.badge.BadgeEvent
import com.team.prezel.core.network.datasource.BadgeRemoteDataSource
import com.team.prezel.core.network.model.badge.BadgeEventResponse
import com.team.prezel.core.network.model.badge.GetBadgeDetailResponse
import com.team.prezel.core.network.model.badge.GetBadgeResponse
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

internal class BadgeRepositoryImpl @Inject constructor(
private val badgeRemoteDataSource: BadgeRemoteDataSource,
) : BadgeRepository {
override suspend fun getBadges(): Result<List<Badge>> =
runCatching {
badgeRemoteDataSource.getBadges()
}.mapCatching { response ->
response.map(GetBadgeResponse::toDomain)
}.mapDomainFailure()

override suspend fun getBadgeDetail(badgeCode: String): Result<BadgeDetail> =
runCatching {
badgeRemoteDataSource.getBadgeDetail(badgeCode = badgeCode)
}.mapCatching(GetBadgeDetailResponse::toDomain)
.mapDomainFailure()

override fun connectBadgeEventStream(): Flow<BadgeEvent> =
badgeRemoteDataSource
.connectBadgeEventStream()
.map(BadgeEventResponse::toDomain)
}

private fun GetBadgeResponse.toDomain(): Badge =
Badge(
badgeCode = badgeCode,
badgeName = badgeName,
isUnlocked = isUnlocked,
imageUrl = imageUrl,
unlockedAt = unlockedAt,
)

private fun GetBadgeDetailResponse.toDomain(): BadgeDetail =
BadgeDetail(
badgeCode = badgeCode,
badgeName = badgeName,
conditionText = conditionText,
detailDescription = detailDescription,
imageUrl = imageUrl,
isUnlocked = isUnlocked,
unlockedAt = unlockedAt,
)

private fun BadgeEventResponse.toDomain(): BadgeEvent =
BadgeEvent(
badgeCode = badgeCode,
badgeName = badgeName,
introduction = introduction,
imageUrl = imageUrl,
message = message,
rawData = rawData,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.team.prezel.core.domain.repository.badge

import com.team.prezel.core.model.badge.Badge
import com.team.prezel.core.model.badge.BadgeDetail
import com.team.prezel.core.model.badge.BadgeEvent
import kotlinx.coroutines.flow.Flow

interface BadgeRepository {
suspend fun getBadges(): Result<List<Badge>>

suspend fun getBadgeDetail(badgeCode: String): Result<BadgeDetail>

fun connectBadgeEventStream(): Flow<BadgeEvent>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.team.prezel.core.domain.usecase.badge

import com.team.prezel.core.domain.repository.badge.BadgeRepository
import com.team.prezel.core.model.badge.BadgeEvent
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class ConnectBadgeEventStreamUseCase @Inject constructor(
private val badgeRepository: BadgeRepository,
) {
operator fun invoke(): Flow<BadgeEvent> = badgeRepository.connectBadgeEventStream()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.team.prezel.core.domain.usecase.badge

import com.team.prezel.core.domain.repository.badge.BadgeRepository
import com.team.prezel.core.model.badge.BadgeDetail
import javax.inject.Inject

class FetchBadgeDetailUseCase @Inject constructor(
private val badgeRepository: BadgeRepository,
) {
suspend operator fun invoke(badgeCode: String): Result<BadgeDetail> = badgeRepository.getBadgeDetail(badgeCode = badgeCode)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.team.prezel.core.domain.usecase.badge

import com.team.prezel.core.domain.repository.badge.BadgeRepository
import com.team.prezel.core.model.badge.Badge
import javax.inject.Inject

class FetchBadgesUseCase @Inject constructor(
private val badgeRepository: BadgeRepository,
) {
suspend operator fun invoke(): Result<List<Badge>> = badgeRepository.getBadges()
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
package com.team.prezel.core.domain.usecase.user

import com.team.prezel.core.domain.usecase.badge.FetchBadgesUseCase
import com.team.prezel.core.model.badge.Badge
import com.team.prezel.core.model.badge.BadgeType
import javax.inject.Inject
import kotlin.random.Random

class FetchUserBadgesUseCase @Inject constructor() {
suspend operator fun invoke(): Result<List<Badge>> =
runCatching {
listOf(
BadgeType.FIRST_PRESENTATION,
BadgeType.SECOND_ANALYSIS,
BadgeType.FIRST_PRACTICE,
BadgeType.RETROSPECT_COMPLETED,
BadgeType.PERFECT_SCORE,
BadgeType.TEN_ANALYSIS,
).map { type -> Badge(type = type, isAchieved = Random.nextBoolean()) }
}
class FetchUserBadgesUseCase @Inject constructor(
private val fetchBadgesUseCase: FetchBadgesUseCase,
) {
suspend operator fun invoke(): Result<List<Badge>> = fetchBadgesUseCase()
}
Loading