diff --git a/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt index 5d9def8c..7da7499d 100644 --- a/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -24,6 +24,8 @@ internal class AndroidFeatureConventionPlugin : Plugin { implementation(project(path = ":core:model")) implementation(project(path = ":core:ui")) + implementation(libs.compose.effects) + implementation(libs.bundles.circuit) api(libs.circuit.codegen.annotation) diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt new file mode 100644 index 00000000..3558568d --- /dev/null +++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt @@ -0,0 +1,7 @@ +package com.ninecraft.booket.core.data.api.repository + +import com.ninecraft.booket.core.model.UserProfileModel + +interface UserRepository { + suspend fun getUserProfile(): Result +} diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/RepositoryModule.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/RepositoryModule.kt index ba60c3b3..cf88cb48 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/RepositoryModule.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/RepositoryModule.kt @@ -1,7 +1,9 @@ package com.ninecraft.booket.core.data.impl.di import com.ninecraft.booket.core.data.api.repository.AuthRepository +import com.ninecraft.booket.core.data.api.repository.UserRepository import com.ninecraft.booket.core.data.impl.repository.DefaultAuthRepository +import com.ninecraft.booket.core.data.impl.repository.DefaultUserRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -15,4 +17,8 @@ internal abstract class RepositoryModule { @Binds @Singleton abstract fun bindAuthRepository(defaultAuthRepository: DefaultAuthRepository): AuthRepository + + @Binds + @Singleton + abstract fun bindUserRepository(defaultUserRepository: DefaultUserRepository): UserRepository } diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt index 17a78fa7..c4edaecc 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt @@ -1,7 +1,9 @@ package com.ninecraft.booket.core.data.impl.mapper import com.ninecraft.booket.core.model.LoginModel +import com.ninecraft.booket.core.model.UserProfileModel import com.ninecraft.booket.core.network.response.LoginResponse +import com.ninecraft.booket.core.network.response.UserProfileResponse internal fun LoginResponse.toModel(): LoginModel { return LoginModel( @@ -9,3 +11,12 @@ internal fun LoginResponse.toModel(): LoginModel { refreshToken = refreshToken, ) } + +internal fun UserProfileResponse.toModel(): UserProfileModel { + return UserProfileModel( + id = id, + email = email, + nickname = nickname, + provider = provider, + ) +} diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt new file mode 100644 index 00000000..3b74c5ce --- /dev/null +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt @@ -0,0 +1,15 @@ +package com.ninecraft.booket.core.data.impl.repository + +import com.ninecraft.booket.core.common.utils.runSuspendCatching +import com.ninecraft.booket.core.data.api.repository.UserRepository +import com.ninecraft.booket.core.data.impl.mapper.toModel +import com.ninecraft.booket.core.network.service.AuthService +import javax.inject.Inject + +internal class DefaultUserRepository @Inject constructor( + private val authService: AuthService, +) : UserRepository { + override suspend fun getUserProfile() = runSuspendCatching { + authService.getUserProfile().toModel() + } +} diff --git a/core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserProfileModel.kt b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserProfileModel.kt new file mode 100644 index 00000000..63216fa4 --- /dev/null +++ b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserProfileModel.kt @@ -0,0 +1,8 @@ +package com.ninecraft.booket.core.model + +data class UserProfileModel( + val id: String, + val email: String, + val nickname: String, + val provider: String, +) diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/UserProfileResponse.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/UserProfileResponse.kt new file mode 100644 index 00000000..dd462b7e --- /dev/null +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/UserProfileResponse.kt @@ -0,0 +1,16 @@ +package com.ninecraft.booket.core.network.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UserProfileResponse( + @SerialName("id") + val id: String, + @SerialName("email") + val email: String, + @SerialName("nickname") + val nickname: String, + @SerialName("provider") + val provider: String, +) diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/AuthService.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/AuthService.kt index 13312989..cb273798 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/AuthService.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/AuthService.kt @@ -1,8 +1,13 @@ package com.ninecraft.booket.core.network.service +import com.ninecraft.booket.core.network.response.UserProfileResponse +import retrofit2.http.GET import retrofit2.http.POST interface AuthService { @POST("api/v1/auth/signout") suspend fun logout() + + @GET("api/v1/auth/me") + suspend fun getUserProfile(): UserProfileResponse } diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt index d97ec6ab..5763714d 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt @@ -7,8 +7,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import com.ninecraft.booket.core.common.utils.handleException import com.ninecraft.booket.core.data.api.repository.AuthRepository +import com.ninecraft.booket.core.data.api.repository.UserRepository import com.ninecraft.booket.feature.login.LoginScreen import com.orhanobut.logger.Logger +import com.skydoves.compose.effects.RememberedEffect import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator @@ -21,7 +23,8 @@ import kotlinx.coroutines.launch class LibraryPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, - private val repository: AuthRepository, + private val authRepository: AuthRepository, + private val userRepository: UserRepository, ) : Presenter { @Composable @@ -29,6 +32,42 @@ class LibraryPresenter @AssistedInject constructor( val scope = rememberCoroutineScope() var isLoading by rememberRetained { mutableStateOf(false) } var sideEffect by rememberRetained { mutableStateOf(null) } + var nickname by rememberRetained { mutableStateOf("") } + var email by rememberRetained { mutableStateOf("") } + + fun getUserProfile() { + scope.launch { + try { + isLoading = true + userRepository.getUserProfile() + .onSuccess { user -> + nickname = user.nickname + email = user.email + } + .onFailure { exception -> + val handleErrorMessage = { message: String -> + Logger.e(message) + sideEffect = LibraryScreen.SideEffect.ShowToast(message) + } + + handleException( + exception = exception, + onServerError = handleErrorMessage, + onNetworkError = handleErrorMessage, + onLoginRequired = { + navigator.resetRoot(LoginScreen) + }, + ) + } + } finally { + isLoading = false + } + } + } + + RememberedEffect(Unit) { + getUserProfile() + } fun handleEvent(event: LibraryScreen.Event) { when (event) { @@ -40,9 +79,9 @@ class LibraryPresenter @AssistedInject constructor( scope.launch { try { isLoading = true - repository.logout() + authRepository.logout() .onSuccess { - repository.clearTokens() + authRepository.clearTokens() navigator.resetRoot(LoginScreen) } .onFailure { exception -> @@ -70,6 +109,8 @@ class LibraryPresenter @AssistedInject constructor( return LibraryScreen.State( isLoading = isLoading, + nickname = nickname, + email = email, sideEffect = sideEffect, eventSink = ::handleEvent, ) diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt index b9340679..83cc2251 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt @@ -3,6 +3,7 @@ package com.ninecraft.booket.feature.library import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -31,6 +32,8 @@ import kotlinx.parcelize.Parcelize data object LibraryScreen : Screen { data class State( val isLoading: Boolean = false, + val nickname: String = "", + val email: String = "", val sideEffect: SideEffect? = null, val eventSink: (Event) -> Unit, ) : CircuitUiState @@ -51,6 +54,11 @@ internal fun Library( state: LibraryScreen.State, modifier: Modifier = Modifier, ) { + HandleLibrarySideEffects( + state = state, + eventSink = state.eventSink, + ) + Column( modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, @@ -68,21 +76,23 @@ internal fun LibraryContent( state: LibraryScreen.State, modifier: Modifier = Modifier, ) { - HandleLibrarySideEffects( - state = state, - eventSink = state.eventSink, - ) - Column( modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { Box(modifier = modifier.fillMaxSize()) { - Text( - text = "내 서재", - modifier = Modifier.align(Alignment.Center), - ) + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(text = "내 서재") + Spacer(modifier = Modifier.height(16.dp)) + Text(text = state.nickname) + Spacer(modifier = Modifier.height(16.dp)) + Text(text = state.email) + } BooketButton( onClick = { state.eventSink(LibraryScreen.Event.OnLogoutButtonClick) @@ -118,6 +128,8 @@ private fun LibraryPreview() { BooketTheme { Library( state = LibraryScreen.State( + nickname = "홍길동", + email = "test@test.com", eventSink = {}, ), ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 73058d28..4d88e53d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ androidx-datastore = "1.1.7" androidx-compose-bom = "2025.06.00" androidx-compose-material3 = "1.4.0-alpha15" compose-stable-marker = "1.0.6" - +compose-effects = "0.1.1" coil-compose = "2.7.0" ## Kotlin Symbol Processing @@ -87,6 +87,7 @@ androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-te androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" } androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended"} compose-stable-marker = { group = "com.github.skydoves", name = "compose-stable-marker", version.ref = "compose-stable-marker" } +compose-effects = { group = "com.github.skydoves", name = "compose-effects", version.ref = "compose-effects" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }